Using SOPS for Secrets
Set up Mozilla SOPS as Gordon's secrets backend for encrypted file-based secret storage.
What You'll Learn
- Installing SOPS and age (or GPG)
- Creating encrypted secrets files
- Configuring Gordon to use SOPS
- Using secrets in environment files
- Team workflows and key rotation
Prerequisites
- Linux or macOS system
- Basic understanding of encryption concepts
Why SOPS?
SOPS (Secrets OPerationS) encrypts specific values in YAML/JSON files while keeping keys readable:
- Git-friendly: Encrypted files can be version controlled
- Multiple backends: age, GPG, AWS KMS, GCP KMS, Azure Key Vault
- Selective encryption: Only values are encrypted, keys remain visible
- Team support: Multiple recipients can decrypt the same file
Installation
SOPS
# Ubuntu/Debian
curl -LO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
sudo mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
# macOS
brew install sops
# Arch Linux
sudo pacman -S sops
age (Recommended)
age is simpler than GPG and recommended for new setups:
# Ubuntu/Debian
sudo apt install age
# macOS
brew install age
# Arch Linux
sudo pacman -S age
GPG (Alternative)
# Ubuntu/Debian
sudo apt install gnupg
# macOS
brew install gnupg
Setup with age
1. Generate an age Key
# Generate key pair
age-keygen -o ~/.config/sops/age/keys.txt
# Output shows your public key:
# Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Note your public key - you'll need it for encryption.
2. Configure SOPS
Create .sops.yaml in your project or home directory:
creation_rules:
- age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Or for multiple recipients:
creation_rules:
- age: >-
age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
age1yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
3. Set age Key Location
# Add to ~/.bashrc or ~/.zshrc
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
Setup with GPG
1. Generate or Use Existing GPG Key
# Generate new key
gpg --gen-key
# List keys to get fingerprint
gpg --list-keys
2. Configure SOPS
creation_rules:
- pgp: ABCD1234EFGH5678... # Your GPG fingerprint
Creating Encrypted Secrets
Create secrets.yaml
# Create and encrypt in one step
sops edit secrets.yaml
This opens your editor. Add secrets in YAML format:
auth:
password_hash: "$2y$10$..."
token_secret: "your-random-32-char-string-here"
database:
password: "db-password-here"
api:
key: "api-key-here"
Save and exit. SOPS encrypts the values automatically.
View Encrypted File
cat secrets.yaml
Output shows encrypted values:
auth:
password_hash: ENC[AES256_GCM,data:...,iv:...,tag:...]
token_secret: ENC[AES256_GCM,data:...,iv:...,tag:...]
database:
password: ENC[AES256_GCM,data:...,iv:...,tag:...]
api:
key: ENC[AES256_GCM,data:...,iv:...,tag:...]
sops:
age:
- recipient: age1xxx...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-01-15T12:00:00Z"
version: 3.9.0
Decrypt and View
# View entire file
sops decrypt secrets.yaml
# Extract specific value
sops decrypt --extract '["registry"]["token_secret"]' secrets.yaml
Edit Existing File
sops edit secrets.yaml
Gordon Configuration
Enable SOPS Backend
[auth]
enabled = true
secrets_backend = "sops"
token_secret = "secrets.yaml:auth.token_secret"
# Optional: enable password auth for interactive login
# username = "admin"
# password_hash = "secrets.yaml:auth.password_hash"
The path format is file:key.path where:
fileis the SOPS-encrypted file pathkey.pathis dot-notation to the value
Route secrets remain in .env files with ${sops:...} references. The SOPS backend is used for auth secrets (like token_secret) and provider lookups.
Using Secrets in Environment Files
Reference SOPS secrets in your app's environment files:
# ~/.gordon/env/app_mydomain_com.env
DATABASE_URL=postgresql://user:${sops:secrets.yaml:database.password}@postgres:5432/app
API_KEY=${sops:secrets.yaml:api.key}
File Organization
Recommended structure:
project/
├── .sops.yaml # SOPS configuration
├── secrets.yaml # Encrypted secrets (commit this)
├── secrets.dev.yaml # Dev environment secrets
├── secrets.prod.yaml # Production secrets
└── gordon.toml # Gordon config (references secrets)
Environment-Specific Rules
# .sops.yaml
creation_rules:
# Dev secrets - developer keys
- path_regex: \.dev\.yaml$
age: age1devkey...
# Prod secrets - production keys only
- path_regex: \.prod\.yaml$
age: age1prodkey...
# Default
- age: age1defaultkey...
Running in Containers
Custom Dockerfile
FROM ghcr.io/bnema/gordon:latest
USER root
RUN apk add --no-cache sops age
USER gordon
Mount Secrets and Keys
docker run -d \
--name gordon \
-p 80:8080 \
-p 5000:5000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gordon-data:/data \
-v $(pwd)/secrets.yaml:/app/secrets.yaml:ro \
-v $(pwd)/.sops.yaml:/app/.sops.yaml:ro \
-v ~/.config/sops/age/keys.txt:/home/gordon/.config/sops/age/keys.txt:ro \
-e SOPS_AGE_KEY_FILE=/home/gordon/.config/sops/age/keys.txt \
-v $(pwd)/gordon.toml:/etc/gordon/gordon.toml:ro \
gordon-with-sops
Docker Compose
services:
gordon:
build:
context: .
dockerfile: Dockerfile.gordon
container_name: gordon
restart: unless-stopped
ports:
- "80:8080"
- "5000:5000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- gordon-data:/data
- ./gordon.toml:/etc/gordon/gordon.toml:ro
- ./secrets.yaml:/app/secrets.yaml:ro
- ./.sops.yaml:/app/.sops.yaml:ro
- ~/.config/sops/age/keys.txt:/home/gordon/.config/sops/age/keys.txt:ro
environment:
- SOPS_AGE_KEY_FILE=/home/gordon/.config/sops/age/keys.txt
- GORDON_SECRETS_BACKEND=sops
volumes:
gordon-data:
Team Workflows
Adding Team Members
- Get their age public key
- Add to
.sops.yaml:
creation_rules:
- age: >-
age1yourkey...,
age1teammemberkey...
- Re-encrypt existing files:
sops updatekeys secrets.yaml
Key Rotation
- Generate new key
- Update
.sops.yaml - Re-encrypt all files:
sops updatekeys secrets.yaml
sops updatekeys secrets.prod.yaml
Git Workflow
Encrypted files are safe to commit:
git add .sops.yaml secrets.yaml
git commit -m "Add encrypted secrets"
git push
Team members with valid keys can decrypt after pulling.
Troubleshooting
"failed to get the data key"
Your key isn't in the recipients list or key file is missing:
# Check if age key is set
echo $SOPS_AGE_KEY_FILE
cat $SOPS_AGE_KEY_FILE
# Check recipients in encrypted file
grep -A5 "age:" secrets.yaml
"sops: command not found"
Install SOPS:
# Check if installed
which sops
# Install if missing (see Installation section)
"could not decrypt data key"
Wrong key or corrupted file:
# Verify your public key matches a recipient
age-keygen -y ~/.config/sops/age/keys.txt
# Compare output to recipients in .sops.yaml
"no matching creation_rules"
Create or fix .sops.yaml:
# Check if .sops.yaml exists
cat .sops.yaml
# Ensure path_regex matches your file
Permission Denied in Container
Ensure key file is readable:
docker exec gordon cat /home/gordon/.config/sops/age/keys.txt
Check mount permissions:
ls -la ~/.config/sops/age/keys.txt
# Should be readable by container user
Security Best Practices
- Never commit unencrypted secrets: Always use
sops edit, never edit decrypted files directly - Protect age/GPG keys: Store private keys securely, never commit them
- Use separate keys per environment: Production keys should be isolated
- Rotate keys regularly: Update keys and re-encrypt periodically
- Audit access: Track who has decryption keys for each environment
- Use .sops.yaml: Enforce encryption rules via configuration