Keeping secrets out of Git

Keeping secrets out of Git

In a previous post I wrote about using per stage environment variables with the Serverless Framework. That article showed how you could set different values for environment variables depending on the stage you were deploying to.

There still seems to be a lot of misunderstaning about how powerful the environment variable implementation in Serverless is. Today I want to show you how to keep your secrets out of version control.

Before I show you how it's important to understand why. Secrets like API keys, database passwords, etc should never be committed into version control. There are two main reasons for this.

  1. Once they're in version control they're available to everyone who has access to version control. In many environments you don't want to provide developers with access to production systems.
  2. It forces everyone and every environment to share the same credentials. Typically you want developers to build in their own environment and your staging/production environments will have their own credentials.

There's a really simple way to handle this with Serverless.

It all starts by creating a secrets.yml file to hold your secrets for each environment. I've included an example file below.

default: &default
  <<: *default
  COMMON_API_KEY: "AN API KEY COMMON TO ALL ENVIRONMENTS"
  COMMON_API_SECRET: "AN API KEY COMMON TO ALL ENVIRONMENTS"

dev:
  <<: *default
  API_KEY: "YOUR DEVELOPMENT API KEY"
  API_SECRET: "YOUR DEVELOPMENT API SECRET"

stage:
  <<: *default
  API_KEY: "YOUR STAGING API KEY"
  API_SECRET: "YOUR STAGING API SECRET"

prod:
  <<: *default
  API_KEY: "YOUR PRODUCTION API KEY"
  API_SECRET: "YOUR PRODUCTION API SECRET"

The default section at the beginning sets default values that will be used by all environment. For example - you may be using an external service to lookup IP addresses and you only have one key that is used by all environments.

Below that is one section for each stage. The name should match the stage you're deploying to. Each of these environments inherits values from default because of the line <<: *default. You can override any values set in default by setting a different value for that stage.

Now we have a secrets.yml file we don't want it commited to version control or uploaded to AWS with our code.

Start by adding secrets.yml to your .gitignore file.

echo "secrets.yml" >> .gitignore

You can check that this is working by running git status and if the file doesn't show up you're good to go.

Next you want to add secrets.yml to the list of files excluded from your package that is uploaded to Lambda. You do this inside your serverless.yml.

package:
  exclude:
    - secrets.yml

Finally you need to include the secrets.yml file in your serverless.yml file. I've been doing this in the custom section by using.

custom:
  stage: ${opt:stage, self:provider.stage}
  secrets: ${file(secrets.yml):${self:custom.stage}}

This allows you to set your environment varilables to use the values imported from the secrets.yml. Below is an example.

provider:
  environment:
    API_KEY: "${self:custom.secrets.API_KEY}"
    API_SECRET: "${self:custom.secrets.API_SECRET}"
    COMMON_API_KEY: "${self:custom.secrets.COMMON_API_KEY}"
    COMMON_API_SECRET: "${self:custom.secrets.COMMON_API_SECRET}"

When you deploy your application using Serverless it will now load your secrets from secrets.yml that won't be commited to version or uploaded to Lambda.

To make it easy for new developers I tend to maintain a secrets-example.ymlfile that has all of the keys required by secrets.yml with dummy values. This makes it easy for them to create their own secrets.yml and tells them which secrets they need to acquire.

PS: There's going to be one more part to this series explaining how to combine env.yml and secrets.yml. Join my mailing list today to get updates when that's released.