cheat-sheets

SOPS and age

SOPS encrypts the values of YAML, JSON, ENV and INI files while leaving keys readable, so secrets can live in Git and still produce sensible diffs. age is the small, modern file-encryption tool SOPS uses for keys (simpler than GPG). Typical homelab use: encrypted .env files for Docker-Compose and encrypted Kubernetes Secrets for Flux or Argo CD (Kubernetes, Argo CD).

Documentation: SOPS · age

Install #

bash
sudo apt install age                      # Ubuntu 22.04+ / Debian 12+
brew install sops age                     # macOS
# sops: download the .deb / binary from https://github.com/getsops/sops/releases

age Keys #

COMMANDDESCRIPTION
age-keygen -o key.txtGenerate a key pair (the public key is printed and stored as a comment in the file)
age-keygen -y key.txtPrint the public key (age1...) of an existing key file
age -r <PUBKEY> -o file.age fileEncrypt a file to a recipient
age -R recipients.txt -o file.age fileEncrypt to every key in a file
age -d -i key.txt file.age > fileDecrypt
age -p -o file.age fileEncrypt with a passphrase

Store the private key where SOPS looks by default and lock it down:

bash
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt

Back the private key up in a password manager (Bitwarden Cheat-Sheet). If it is lost, every file encrypted only to it is unrecoverable.

Environment Variables #

VARIABLEDESCRIPTION
SOPS_AGE_KEY_FILEPath to the age private key file (default ~/.config/sops/age/keys.txt)
SOPS_AGE_KEYThe private key itself (handy in CI)
SOPS_AGE_RECIPIENTSRecipients used when encrypting without a .sops.yaml

Configuration (.sops.yaml) #

Put this at the repo root so sops picks the right keys by path. The first matching rule wins.

yaml
creation_rules:
  - path_regex: ^kubernetes/.*\.sops\.ya?ml$
    encrypted_regex: ^(data|stringData)$      # only encrypt these top-level keys
    age: <PUBKEY_LAPTOP>,<PUBKEY_CLUSTER>
  - path_regex: \.env\.sops$
    age: <PUBKEY_LAPTOP>
  - path_regex: secrets/.*
    age: <PUBKEY_LAPTOP>

Multiple recipients are comma-separated: any one of the matching private keys can decrypt.

SOPS Commands #

COMMANDDESCRIPTION
sops encrypt secrets.yaml > secrets.sops.yamlEncrypt using .sops.yaml rules
sops encrypt -i secrets.yamlEncrypt in place
sops decrypt secrets.sops.yamlPrint plaintext to stdout
sops decrypt -i secrets.sops.yamlDecrypt in place (don’t commit the result)
sops edit secrets.sops.yamlDecrypt into $EDITOR, re-encrypt on save
sops --encrypt --age <PUBKEY> file.yamlEncrypt with an explicit recipient
sops --encrypted-regex '^(data|stringData)$' -e -i secret.yamlEncrypt only matching keys
sops decrypt --extract '["db"]["password"]' secrets.sops.yamlPrint a single value
sops set secrets.sops.yaml '["db"]["password"]' '"<VALUE>"'Set a value without opening an editor
sops updatekeys secrets.sops.yamlApply the current .sops.yaml recipients (add/remove keys)
sops rotate -i secrets.sops.yamlGenerate a new data key and re-encrypt
sops --input-type dotenv --output-type dotenv -d file.envForce format for odd file names
sops exec-env secrets.sops.yaml '<CMD>'Run a command with the values as environment variables
sops exec-file secrets.sops.yaml '<CMD> {}'Run a command with a temporary decrypted file ({})

Older versions use sops -e / sops -d; both spellings work.

Docker Compose .env #

bash
sops encrypt --input-type dotenv --output-type dotenv .env > .env.sops
git add .env.sops && echo .env >> .gitignore

# deploy: decrypt only into the process environment
sops exec-env .env.sops 'docker compose up -d'
# or write the file temporarily
sops decrypt --input-type dotenv --output-type dotenv .env.sops > .env

Kubernetes with Flux #

Create the key secret in the cluster, then tell the Kustomization to decrypt.

bash
kubectl -n flux-system create secret generic sops-age \
  --from-file=age.agekey=$HOME/.config/sops/age/keys.txt
yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps
  namespace: flux-system
spec:
  interval: 10m
  path: ./apps
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  decryption:
    provider: sops
    secretRef:
      name: sops-age

Argo CD has no native SOPS support; use a plugin such as KSOPS or helm-secrets.

Git Integration #

bash
# show a readable diff of encrypted files
git config diff.sops.textconv "sops decrypt"
echo '*.sops.yaml diff=sops' >> .gitattributes

Pair with Secret Scanning so an accidentally committed plaintext copy is caught.

Troubleshooting #

SYMPTOMFIX
no identity matched any of the recipients / Failed to get the data keyThe private key isn’t found. Set SOPS_AGE_KEY_FILE or check the file matches a recipient in the file’s sops: block
config file not found, or has no creation rulesRun from the repo (or a subfolder) with .sops.yaml, or pass --config; check path_regex matches the path you give sops
Whole file encrypted, including metadata.nameAdd encrypted_regex so Kubernetes metadata stays readable
MAC mismatchThe encrypted file was edited by hand. Restore from Git; always use sops edit
Removed a colleague’s key but they can still decryptsops updatekeys only affects future data keys of that file; run sops rotate -i and rotate the secrets themselves
Wrong format on .env filesAdd --input-type dotenv --output-type dotenv
Search all sheets

Tip: / or Ctrl/Cmd+K to open, Esc to close.