Chances are your application depends on valuable secrets such as API keys, OAuth tokens, passwords, and passphrases. Storing these secrets in Git in plaintext is a bad idea (what if your GitHub account gets compromised?), but passing around .env files via Slack or Email also gets old quickly.
In this article we'll take a look at how you can leverage encryption to securely store sensitive data inside your Git repo.
Passing around .env files via Slack or Email gets old quickly.
Committing encrypted secrets to your Git repository offers multiple advantages:
- The encrypted files are version controlled, we can see when they are added, edited and removed.
- The encrypted files are close to the code, which makes them more easily accessible to CI pipelines.
There are multiple tools you can use for encryption/decryption, we will focus on the senv package because I like its simplicity, it can be installed from the NPM registry and has no binary dependencies.
Ignoring sensitive files
We start with a project that has two env files: .env.development and .env.production.
# .env.production
DB_NAME=production_password
# .env.development
DB_NAME=development_password
Let's update the .gitignore file to specify that we don't want to track the plaintext version of these files:
# .gitignore
.env.development
.env.production
Check out this article if you have already committed the sensitive data to your repository.
Creating the encryption secret
We are going to need a key to encrypt and decrypt our files.
senv will look for a file called .env.pass by default.
We can use the openssl rand
command to generate a random number and write the output to the file.
openssl rand -base64 32 > .env.pass
Since this file is highly sensitive (it contains a plaintext password) we want to make sure it is not versioned, update your .gitignore:
# .gitignore
.env.pass
.env.development
.env.production
Keep a securely stored copy of this key, I recommend storing it in your password manager. If you lose this key, you may lose permanent access to your secrets.
Encrypting and decrypting secrets
Install senv:
npm install --save-dev senv
Create the following scripts:
env-encrypt.sh
#!/bin/bash
set -e
npx senv encrypt .env.development > .env.development.encrypted
npx senv encrypt .env.production > .env.production.encrypted
env-decrypt.sh
#!/bin/bash
set -e
npx senv decrypt .env.development.encrypted > .env.development
npx senv decrypt .env.production.encrypted > .env.production
{
"scripts": {
"env:decrypt": "bash scripts/env-decrypt.sh",
"env:encrypt": "bash scripts/env-encrypt.sh"
}
}
Note: I prefer bash scripts with package.json aliases over inline package.json scripts but inline package.json scripts will work just fine too.
Testing the setup
Execute the following command:
npm run env:encrypt
Two files are created:
- .env.development.encrypted
- .env.production.encrypted
They should look something like this:
DB_NAME=encrypted-string
SENV_AUTHENTICATION=string
SENV_SALT=string
It is safe to commit these files, we don't need to update our .gitignore file this time.
Delete the unencrypted files and execute the following command:
npm run env:decrypt
The decrypted versions of your env files should re-appear.
Your final directory should look like this:
.gitignore
.env.pass # plaintext encryption secret, not versioned
.env.development # plaintext secrets, not versioned
.env.development.encrypted # encrypted secrets, versioned
.env.production # plaintext secrets, not versioned
.env.production.encrypted # encrypted secret, versioned
Usage with CI pipelines
Keep in mind that we are not versioning the .env.pass file.
Luckily, senv also supports a DOTENV_PASS
environment variable.
- Create a
DOTENV_PASS
secret environment variable (instructions for GitHub actions) and set its value to your secret key. - Add a step to decrypt your secrets, the example below works for GitHub Actions:
name: Continuous deployment
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: "Install dependencies"
run: npm install
- name: "Decrypt env files"
run: npm run env:decrypt
timeout-minutes: 1
env:
DOTENV_PASS: ${{ secrets.DOTENV_PASS }}