Managing Secrets in GitLab / Git

Let's say that you have to log in via ssh into an instance, and you work with GitLab, so you want to keep the private key in GitLab somewhere. Is it secure? Let's see!

Custom environment variables

You can use custom environment variables. Here you can read more about them (Developers cannot change them, only Maintainers and Owners can). There are two types of variables:

  • Variable (the runner creates an environment variable that uses the key for the name and the value for the value)
  • File (the runner creates an environment variable that uses the key for the name. For the value, the runner writes the variable value to a temporary file and uses this path)

It seems that we can use File type for our purpose. We can set up it via API or UI. So, let's do that! Go to project's Settings > CI/CD. There will be Variables section (btw, you can specify variables also per group and even for all projects (in admin panel)). Click Add Variable button and add a variable:

  • Key:  PRIV_KEY
  • Value: content of our private key
  • Type: File

There are also three other choices: Environment scope, Protect variable and Mask variable. With Environment scope you can decide that this variable will be available only on a specific environment (by default each environment has access to this variable). With Protect variable you can export the variable to pipelines running only on protected branches or tags. With Mask variable our variable will be masked in job logs (the variable is hidden in job logs). But, not all variables can be masked, and sadly our private key cannot be. Here is more info about it. And, even then... masking your variables is not so secure as you think.

Here is an example. I have $TEST_SECRET variable, and the content is a secret for you! If you try to "get into" this variable, GitLab will prevent you for this, and it is great:

 

As you can see, you cannot use echo or cat command and get the content of this variable. I even tried to encode the content into base64, save this to a file, and then try to read the file; still without a success 🤔. But, we are able to echo the variable and pipe into base64 program. And if you copy the output (c3VwZXItc2VjcmV0LWl0LXNob3VsZC1iZS1pbnZpc2libGUK) to your local machine and type echo "c3VwZXItc2VjcmV0LWl0LXNob3VsZC1iZS1pbnZpc2libGUK" | base64 -d, you get: super-secret-it-should-be-invisible. It was supposed to be super secret!

I do not know how it works under the hood, but it seems that GitLab try to mask the content everywhere in the log file, so if you just echo the variable, you will not see the content, but when you also encode it into base64 for example, GitLab does not know anything about this new string (I mean, it is different than the masked content, right?), so it cannot be masked.

Anyway. If someone has access to your project and can edit jobs (like me, above), they can do what they want, so giving appropriate permissions is a must. Spawning such variables only on protected branches/tags is also a good idea. This way, you can perform such jobs only on master and staging branches for example and do code review before, and block the suspicious code 👮. You can also use rules keyword and give access to run some jobs to only a small subset of people.

Here is an example, how you can use custom environment variable to keep a private key:

test-ssh:
script:
- cp $PRIV_KEY gitlab-blog-post.pem
- chmod 400 gitlab-blog-post.pem
# create the SSH directory and give it the right permissions
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
# add the instance to the list of authorized hosts
- ssh-keyscan -t rsa my-instance.on.aws.of.course >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
# ssh
- ssh -i "gitlab-blog-post.pem" user@my-instance.on.aws.of.course
- echo 👍
view raw ssh-gitlab.yaml hosted with ❤ by GitHub

It is a quite good solution. But, it has a flaw. Your variables are not versioned. If there are more than two maintainers/owners, it might be a problem. Someone can change a variable and you will not be notified about it. Maybe, we can keep our secrets in git repository? Then, we will have versioning by default. Such approach would be useful for storing passwords for example.

SOPS: Secrets OPerationS

We can use sops for this:

sops is an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault and PGP.

Using sops, we can keep our "secrets" secret 🤫. We are going to use AWS KMS for encrypting and decrypting, but please, keep in mind that it is only for demo purpose. If you want to use it to keep some secret data, it makes sense to create more than one master key. According to sops documentation:

Multiple master keys allow for sharing encrypted files without sharing master keys, and provide a disaster recovery solution. The recommended way to use sops is to have two KMS master keys in different regions and one PGP public key with the private key stored offline. If, by any chance, both KMS master keys are lost, you can always recover the encrypted data using the PGP private key.

By default, sops encrypts the data key with each of the master keys. So, the file can be decrypted by each master key. You need only one master key for decrypting.

We need to create a symmetric key in AWS KMS. Then we have to set up sops by creating a file called .sops.yaml and putting the info about our ARN(s) (Amazon Resource Name(s)). Here is an example:

---
creation_rules:
- kms: 'arn:aws:kms:eu-central-1:818612439639:key/c0806b36-037a-4986-b895-27b984ca9a3f'
view raw .sops.yaml hosted with ❤ by GitHub

After that we have to create a file where we want to keep our secrets. Let's say that we want to keep there our private key and some passwords. And, we are going to keep all of this in YAML file. So, type sops secrets.yaml, you will see an example in your default editor, delete it and put there this (no, it is not my private key, do not worry):

dev:
redis: our-secret-password
priv_key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAwd/Z4BQH3Btw+qHPAqhTFvRSTlQaBy10ZOvRRrpU2840Ht98
Su9l6obFOxYIx42N+Hk0IyPOM+b3G2v9hxJOTanQRWrmmRwHjSJ5Ugw0l0JpD1H5
SkWfjn7eaJzidX/klubkNvIBb69bOStUwfEszRFUQy0kohUtTcLTGoXnuoxNOxmc
Z8zf1q8cavKrtc/NwNS5WWI4vBirn8oB2X2UCNICiHZLQAdadZnXhstOPk9R3hcU
eE1/BC1tO7Jm3IWFo10O1APbtlfuz47AtKM9Hkf5qVdDDthyjuOLtLe1sKFPHqme
EJdSiZbRWKLftKM8CzHTGD6/FLbXCDoz0Bxf9QIDAQABAoIBAF7+wFL7fZ5sE7+6
6SP0NkJZFLssmlRKAW4x3ik5w7qwHvnBp7VP+DAiOSHqegLNaXMGcHWHZdIoqYvW
LjOw1I1ZV6Or7bnic+tutaj+nie+ma+Rd4Tc+IFpyLCZrpzEPc1y9P+3NNz0n04z
3SPqeHlCtHb1960zky829xlX42OT4LIPsBmsvQN4j4wNvUqx8D7zYOhklhG3Clxa
HOAH0aSkefnt6go76X4BOuMeKqbCLHxjOuAiqL6YXAikyk1BB1YFtG75bbEq7uK1
Z0sPlH3FZRb8UCXkLW7pUMzUHKreo+gw38/N6mQZ9Gt+TrIF0Gv727BJ2aGxsZV/
YdSo4gECgYEA7lTc6Q7TtwM5t8L4gZjR3Z4aD6Qg+LJHScMIVw5s62+AlxPipgpb
QFrZZE7vctI49r0clwtnctE2KtVl/JU+d6RXCt0LwGEWyVt8T75fpWuePjzBYmcn
V1Hww5i0d1lq+tUYrR8OLa7gjVXTGAikppL4tpaozwAjj/zhCDKk3m0CgYEA0D9E
NYFOLzO3kcX5Ux5GC87FH5nqJLEQTCazsLUWo+haO2FhfDMziL3vUaMPJsRhNDyI
RYe0W8hSgMPqjt64im6oDX5ytlqK/9gKMrZODjdeEe8wamUzsdqtpmLkrrk19NKl
7HlFVO0Lh/QfCXyi2nY2PndWVklKzQu5KOd9cqkCgYBpUNbNpd+oX6SBr4Zgvkb+
x358qupm+2DFF4n37kfzQbJxEDh3Ztwr8/lcegUVmA/T+H1JwaBU/F4TT3lhxBF2
jQhppIQs3rpTADpOheeeeeeeellllllooooooooo5TeA3+mnlrGNPlm8GtDNLgSU
Fx4QkULHNiiA2B4YoUh5gQKBgChhXDST0jlM6feWT/ZSFHsNqSOrkL90phheGNHX
C4DU+UoyY9jVhNSrH9DQsYtu4PpkEniJC+wQOA9H7h+uCFKvil6zekLp47Igjjmv
KAuRqOgJXXYEEbYXFT0CSB2pRFEo14u3KD6DiIzgRXRtepap/XK3aUJqC9sevtSz
rpuhAoGAfDucW0PX2AVJvIhaIA7cUwzD4cQwdsr87blKzRjHetk+ckQNXRcmEX8e
tn+WI+be5LfvSuhfOzJ/SjUOPPU7gzdgJP8Nc27wKYz85khMNxApIU96RGWuEIWH
mgwNLWaFW4YGY81slvYTnIy6Lo3PZc0PkAGP4VTwJpM6vu7+yrU=
-----END RSA PRIVATE KEY-----

When you close the editor, sops will save the file with encoded values:

dev:
redis: ENC[AES256_GCM,data:HZOWhuWflL3FnymLkEdx8mP7Cw==,iv:O1/lU0znXYnUUBNwQ/7PKZD8YBqgCghJgS9/h4q5tBo=,tag:MVVLJk2KLxq6YA5Gx9BtJw==,type:str]
priv_key: ENC[AES256_GCM,data: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,iv:RDwcGu7n+Y1FCYTqjsB9zKpbDIQDoM6ZSUyq95VyiIY=,tag:bxl44YVU7r7q2TXEjViG+Q==,type:str]
sops:
kms:
- arn: arn:aws:kms:eu-central-1:818612439639:key/c0806b36-037a-4986-b895-27b984ca9a3f
created_at: '2021-01-15T17:32:32Z'
enc: AQICAHi4ToVYzC3Ho8RWe+4pFpQSydq+xA7nkA78dp94V8ZtEAETlVnmLwRgvWpsgg8ScnfgAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKUvO6szWeZSQVgHHAgEQgDuQJJk3+AeBjsP3c7PP/yKh03gderzThpIAL/iAI1u/bh7wm1jjaBoC1zWrKw8kRcd3YC9pIXxRD93NqA==
aws_profile: ""
gcp_kms: []
azure_kv: []
hc_vault: []
lastmodified: '2021-01-15T17:41:01Z'
mac: ENC[AES256_GCM,data:pNO04DYVzeHqlNQK3Z/goJEMj/2KZlM0k9aZ4pLxU1qBdOViVu+popAukkzVetkC1nBALUIqeVitVtkxqG6N+zmkvafHPtgwOqQNv/PrQ/3SD4AGCaTLQL45oFNXeqzKlxyFBAx7fVFeJENe9S2FZPVe7vfUMGsfYESnhXRu0nQ=,iv:4oKXM5LJwpdkgXvqxPy6gO+ujG3iewbk/+IWWOuLjW0=,tag:aksstTXvg2BnwjQlc58abw==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.6.1
view raw secrets.yaml hosted with ❤ by GitHub

Such file without master key is useless for others, so we can keep this guy in git repository without worry. We can also renew the data key on regular basis. You can also use key groups. This way you can tell sops that some data must be decrypted by providing two (or more; there also might be some thresholds like, at least 3 of 5 keys for instance) keys (AWS KMS and PGP for example).

If someone wants to get the content of our file without master key, they will get error like:

Now, we want to get secrets from our file in a job. If we use AWS KMS, we need to provide somehow credentials for it (AWS Access Key ID, AWS Secret Access Key). In most cases we can keep them as custom environment variables (like in the first example). But, if you think that someone evil can do very bad things, you can provide these environment variables during spawning pipeline or job:

You should also remember about enabling Protect variable option. It is very important. Without this, everyone who can create a new branch, will get an access to these environment variables; then they can modify a job, and echo some passwords for example. We have seen that we can mask our variable, but you can still unmask it via base64 for instance (if someone does such things in your team, I think you have other problems than that 😅).

How to get values from this file then? You can just type sops -d secrets.yaml, but this will give you all the data. What if we want to get only a password for redis? You can do that easily by sops -d --extract '["dev"]["redis"]' secrets.yaml:

So, how would look like our gitlab job if we wanted to use sops for getting a private key and logging into our instance? It might be something like that:

test-ssh-sops:
script:
- curl -sSfL "https://github.com/mozilla/sops/releases/download/v3.6.1/sops-v3.6.1.linux" > /usr/local/bin/sops
- chmod +x /usr/local/bin/sops
# extract the priv key and save it to gitlab-blog-post.pem file
- sops -d --extract '["priv_key"]' secrets.yaml > gitlab-blog-post.pem
- chmod 400 gitlab-blog-post.pem
# create the SSH directory and give it the right permissions
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
# add the instance to the list of authorized hosts
- ssh-keyscan -t rsa ec2-18-185-112-126.eu-central-1.compute.amazonaws.com >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
# ssh
- ssh -i "gitlab-blog-post.pem" ec2-user@ec2-18-185-112-126.eu-central-1.compute.amazonaws.com
- echo 👍
view raw .gitlab-ci.yml hosted with ❤ by GitHub
Can I use it with Helm?

Yes! There is a plugin for Helm called helm-secrets. Instead of deploying your service by helm upgrade ..., you must use helm secrets upgrade -f secrets.yaml .... Let's look for an example!

Let's say that we have two environments: dev and prod. On dev environment we are fine with storing passwords in repository, but on prod we do not want to do that. We want to deploy grafana both on dev and prod environments. Here is our root catalogue:

And here is a code:

.deploy:
# we trust this image, of course
image: dtzar/helm-kubectl:3.4.2
environment:
name: $ENV_NAME
variables:
HELM_COMMAND: helm upgrade
HELM_SECRETS_FILE: ""
before_script:
# install sops and helm-secrets
- curl -sSfL "https://github.com/mozilla/sops/releases/download/v3.6.1/sops-v3.6.1.linux" > /usr/local/bin/sops
- chmod +x /usr/local/bin/sops
- SKIP_SOPS_INSTALL=true helm plugin install https://github.com/jkroepke/helm-secrets --version v3.3.4
# add bitnami repo
- helm repo add bitnami https://charts.bitnami.com/bitnami
# search secrets.yaml
# if exists we want to include them
- |
if [[ -f "$ENV_NAME/grafana/secrets.yaml" ]] ;
then export HELM_SECRETS_FILE="-f $ENV_NAME/grafana/secrets.yaml" && export HELM_COMMAND="helm secrets upgrade" ;
fi
script:
# deploy changes
- $HELM_COMMAND
grafana
bitnami/grafana
--namespace grafana
--version 5.0.1
-f $ENV_NAME/grafana/values.yaml
$HELM_SECRETS_FILE
dev:
extends: .deploy
variables:
ENV_NAME: dev
prod:
extends: .deploy
variables:
ENV_NAME: prod
view raw .gitlab-ci.yml hosted with ❤ by GitHub

How it works then? We have two jobs: dev and prod. We install sops and helm-secrets plugin, add bitnami repository to helm and check if there is a secrets.yaml file. If it exists, then we want to use it, so we must change the command from helm upgrade to helm secrets upgrade and use both values.yaml and secrets.yaml files. Here is a content of these files:

admin:
user: "admin"
persistence:
enabled: true
view raw values.yaml hosted with ❤ by GitHub
admin:
password: ENC[AES256_GCM,data:ezm4hCEnNktaacPZOI4y3tPk+qcLMvoI8D2ln5wsyS98m2t3GcJS,iv:+DE4/3wUFacVBc0xdM4lNaTl5plbwKCqcrniXU8haBQ=,tag:w/B489UnI1gFQm9fO48mag==,type:str]
sops:
kms:
- arn: arn:aws:kms:eu-central-1:818612439639:key/c0806b36-037a-4986-b895-27b984ca9a3f
created_at: '2021-01-16T12:52:38Z'
enc: AQICAHi4ToVYzC3Ho8RWe+4pFpQSydq+xA7nkA78dp94V8ZtEAGQwBu/9iuFPA0GAOC2mVshAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMK/s1BRh54JH0bdDhAgEQgDujtbgQ7J5hcq319HXSBWcwMZ4C+V9a0FUSYD4ZSKfxLxMAzXN6KX6dBZ+kAhuCauXShZJHUS+DJkDe0g==
aws_profile: ""
gcp_kms: []
azure_kv: []
hc_vault: []
lastmodified: '2021-01-16T12:53:20Z'
mac: ENC[AES256_GCM,data:8+LR/n+MH4DLPj8hqDshq4wVFdpi+T88ZKk2oTltzmojCsk+pMVPyTw6Gf+LTAFZWAR6XOHYV9rElrQqqwXyx4jRUIsyBcr+LD+tA3nHWNXJ82lo5HRuP6Mb0rxuY0Zs9gp905rvaC90x8NeUJE5Nnf0T4/rQn6zMArmjM5syU0=,iv:0KbAJg0j1AQztGJ5a55pg70sD/9yXYi+NhsYHHOTh7k=,tag:/gGIo/LImxuInACnjl7lYA==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.6.1
view raw secrets.yaml hosted with ❤ by GitHub

Here is a link to the repository, so you can see the whole picture how it might work. I hope it was useful! Of course, there are many other tools for storing secret data (Vault by HashiCorp for instance), but I think that sops and custom environment variables are enough in most cases.

Here is a video if you don't want to read:



Comments

Popular posts from this blog

GitLab - extends keyword

GitLab - trigger keyword