Storing encrypted .env files inside your Git repo

Bjorn Krolsavatar

Bjorn Krols

Published on
17 May 2021

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.

  1. Create a DOTENV_PASS secret environment variable (instructions for GitHub actions) and set its value to your secret key.
  2. 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 }}

Subscribe to our newsletter

The latest news, articles, and resources, sent to your inbox weekly.

More like this