Secrets in Code with Mozilla SOPS
Secrets presents a challenging dilemma for infrasture-as-a-code. Solution today converge mostly on storing these secrets in some external trusted system (Kube Secrets, Docker Secrets, Build System Secrest, Vault) outside of the code.
Using SOPS, we can check in the encrypted secrets (e.g. connection passwords) along with the code. The only thing out of sight is the encryption key wrapping these passwords.
The added value also is that SOPS understand structured file (JSON,YAML) and encrypt only values, leaving the keys intact to easily inspect.
Example – This file could be checked into github along with the code.
{
"postgres_password": "ENC[AES256_GCM,data:PsyN..,type:str]",
"aws_access_key": "ENC[AES256_GCM,data:PsyN..,type:str]",
}
Installation
MacOS
$ brew install sops
Windows
- Go to the latest release page: https://github.com/mozilla/sops/releases/latest
- Download sops-v3.5.0.exe (or whatever the latest version is)
- Rename the file from
sops-v3.5.0.exe
to justsops.exe
- Copy the file
sops.exe
toC:\Windows\System32
. - Or – Alternately, put the file in any directory and set the Path environment variable accordingly
Encryption Key Configuration
The most important configuration for SOPS is what encryption key to use. In many case this is the only configuration needed.
Create a .sops.yaml
at the root directory of the project with one of the configuration outlined below.
The key could be any PGP key. Alternately the key could also be provided by a Key Management Service (KMS) in AWS or Google Cloud. A good choice especially if the code eventually would be deployed to these cloud providers.
AWS KMS
A master key can be created in hardware security modules via AWS Key Management Service (KMS). We need arn
of the key with a corresponding AWS Profile that has a permission to use the key.
$ aws --profile myprofile configure
AWS Access Key ID [None]: KEY_ID
AWS Secret Access Key [None]: SECRET_ACCESS_KEY
# Create .sops.yaml At project root
$ vi .sops.yaml
creation_rules:
# If assuming roles for another account use "arn+role_arn".
# See Advanced usage
- kms: "arn:aws:kms:..."
aws_profile: myprofile
The permission needed for key operations are:
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
For slightly more advanced use cases. We could also access master key from another account using AWS AssumeRole mechanism. This is particularly useful when we have one master key in production, but wanted to also access it from our staging AWS account. See https://github.com/mozilla/sops#assuming-roles-and-using-kms-in-various-aws-accounts
PGP (optionally via Keybase)
For personal use cases, using a PGP key probably suffice. If you are a keybase user, you already have a PGP key. We needed to do the followings
- Install GPG
- Export private keys from Keybase
- Import the keys to local machine
- (optional) Remove passphrase from the key
- Create
.sops.yaml
at the project
$ brew install gpg ## or apt install gpg
$ gpg --import private_key.asc
... (take note of the key ID: 0701C740FB8D24E9)
...
gpg: key 0701C740FB8D24E9: secret key imported
...
...
# This is important for the passphrase screen to show up in console
$ export GPG_TTY=$(tty)
$ gpg --edit-key 0701C740FB8D24E9
gpg> passwd
# 1. Type current passphrase
# 2. Type "" (Blank)
# 3. Type "" (Confirm Blank)
$ vi .sops.yaml
creation_rules:
- pgp: 0701C740FB8D24E9
Generate new GPG Key (And export)
Sometimes we want to generate and managed the GPG key without keybase
$ gpg --full-generate-key
Kind of key : (1) RSA and RSA (default)
keysize: 3072
Key is valid for? : 0 (Do not expire)
Real name: <name>
Email: <email>
Comment: (blank)
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
Enter Passphrase 2 times (cannot be blank)
public and secret key created and signed.
pub rsa3072 2020-11-26 [SC]
3F73E4821B848420CDEAEAD585E2DE2374D6377A ---> Note this value
uid test1 <[email protected]>
sub rsa3072 2020-11-26 [E]
# Remove the passphrase if wanted
# This is important for the passphrase screen to show up in console
$ export GPG_TTY=$(tty)
$ gpg --edit-key 3F73E4821B848420CDEAEAD585E2DE2374D6377A
gpg> passwd
# 1. Type current passphrase
# 2. Type "" (Blank)
# 3. Type "" (Confirm Blank)
Encrypt / Decrypt files
Create
Create a new file via sops will launch an editor
$ sops secret.enc.json
{
"example_key": "example_value",
}
The resulting json would retain the same keys, with values encrypted. An extra metadata is added as an extra key in JSON
$ cat secret.enc.json
{
"example_key": "ENC[AES256_GCM,data:PsyNr6jRJLIPN3P0tA==,iv:Ne63tk8f6uD9GLiHQoyrS/BrK4WL2I6+9Ul8nO6PkDw=,tag:rF8Hm4gm0+xlA3BqKivY7w==,type:str]",
"sops": {
...
... (metadata here)
}
}%
Edit
Editing an existing files would launch and editor and encrypt the file.
$ sops secret.enc.json
Encrypt / Decrypt Existing Files
$ sops -e secret.json > secret.sops.json
$ sops -d secret.sops.json > secret.sops
⚠️ Important - If the PGP key has passphrase, make sure this environment variables is set or you will run into problems
GPG_TTY=$(tty)
Integration Recipes
Many tools provide a plugin to directly read and write encrypted SOPS file.
Terraform
Download the following provider plugin from github
$ mkdir -p ~/.terraform.d/plugins
$ curl -L -o ~/.terraform.d/plugins/terraform-provider-sops_v0.5.0_darwin_amd64.zip \
"https://github.com/carlpett/terraform-provider-sops/releases/download/v0.5.0/terraform-provider-sops_v0.5.0_darwin_amd64.zip"
$ unzip ~/.terraform.d/plugins/terraform-provider-sops_v0.5.0_darwin_amd64.zip \
-d ~/.terraform.d/plugins
$ terraform init
Then we could define a data
resource that automatically decrypt SOPS json.
provider "sops" {}
data "sops_file" "secrets" {
source_file = "secrets.enc.json"
}
## Using
provider "aws" {
region = "us-west-2"
access_key = data.sops_file.secrets.data["aws_access_key"]
secret_key = data.sops_file.secrets.data["aws_secret_key"]
}
Encrypted Private Keys
SOPs works with any unstrutured files as well. The data will get encoded into a data
key in a resulting json automatically.
$ sops -e private_key > private_key.sops
$ cat private_key.sops
{
"data": "ENC[AES256_GCM,data:bVr....."
"sops:: { ... }
}
$ rm private_key
Using the keys, we could just pipe the decrypted result to ssh-add
without writing to file first.
ssh-add - <<< $(sops -d private_key.sops)
Python Script
SOPS used to be written in python, but reimplemented in golang. The pip package exists but with many features missing. It is probably better to call it via subprocess.
import subprocess
b = subprocess.check_output(['sops', "-d", "private_key.sops"])
Kubernetes Secrets
Create a new yaml file, but indicates to SOPs that only data
and stringData
are keys to encrypt
$ sops --encrypted-regex '^(data|stringData)$' secrets-mysecret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
stringData:
mySecret: hello123
Apply by pipe the decrypted output to K8s
sops -d secrets-mysecret.yaml | kubectl -n workflow apply -f -