Managing Secrets with SOPS
How-To Guide: Securely store and manage secrets in the SEA-Forge™ repository using SOPS + age encryption.
Overview
SEA-Forge™ uses SOPS (Secrets OPerationS) with age encryption for managing sensitive data like API keys, tokens, and credentials in version control.
Why SOPS?
- ✅ Encrypt specific values while keeping keys visible for review
- ✅ Git-friendly: merge/diff encrypted files safely
- ✅ Team-based access via age public keys
- ✅ Audit trail: track who changed what in git history
Prerequisites
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # Install SOPS
# macOS
brew install sops
# Linux
curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
# Install age
# macOS
brew install age
# Linux
sudo apt install age # Debian/Ubuntu
# or
curl -LO https://github.com/FiloSottile/age/releases/download/v1.1.1/age-v1.1.1-linux-amd64.tar.gz
tar xzf age-v1.1.1-linux-amd64.tar.gz
sudo mv age/age* /usr/local/bin/
|
Generate Your Age Key (First Time Only)
1
2
3
4
5
6
| # Generate new key pair
age-keygen -o ~/.config/sops/age/keys.txt
# View your public key
grep "public key:" ~/.config/sops/age/keys.txt
# Output: public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
Important:
- Keep
~/.config/sops/age/keys.txt safe and backed up
- Never commit your private key to git
- Share only your public key with team members
Repository Configuration
SOPS Configuration (.sops.yaml)
The repository root contains .sops.yaml which defines encryption rules:
1
2
3
4
5
6
7
8
9
10
11
12
13
| creation_rules:
# Rule 1: Generic secret files and env vars
- path_regex: '.*\.sops$|.*\.env$|^\.secrets.*'
encrypted_regex: '^(APP_|OPENAI|SECRET|TOKEN|KEY|PASSWORD)'
key_groups:
- age: [] # Empty = uses SOPS_AGE_RECIPIENTS env var
# Rule 2: Team shared secrets
- path_regex: '^\.secrets\.env\.sops$'
encrypted_regex: '^(.*)$'
key_groups:
- age:
- 'age1mq5sj8gj4k5vqtgefkuvs05nghanzhgmcqkxxspk0vffq9hxm5ssnj93a5'
|
Rule Explanation:
- path_regex: Files matching this pattern use this rule
- encrypted_regex: Variable names matching this pattern get encrypted
- key_groups: Age public keys that can decrypt (recipients)
Common Workflows
1. Create a New Secret File
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Set your age recipient (your public key from age-keygen)
export SOPS_AGE_RECIPIENTS="age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
# Create and edit encrypted file
sops .secrets.env.sops
# SOPS opens your $EDITOR with plaintext
# Add secrets:
OPENAI_API_KEY=sk-proj-xxx
DATABASE_PASSWORD=supersecret123
APP_SECRET_KEY=random-uuid-here
# Save and exit - SOPS encrypts automatically
|
2. Edit Existing Encrypted File
1
2
3
4
| # SOPS decrypts → opens editor → re-encrypts on save
sops .secrets.env.sops
# Make changes, save, exit
|
3. View Encrypted File (Without Editing)
1
2
3
4
5
6
| # Decrypt to stdout (doesn't modify file)
sops -d .secrets.env.sops
# Or use in scripts
source <(sops -d .secrets.env.sops)
export $(sops -d .secrets.env.sops | xargs)
|
4. Encrypt Existing Plaintext File
1
2
3
4
5
6
7
| # You have: secrets.env (plaintext)
# You want: secrets.env.sops (encrypted)
sops -e secrets.env > secrets.env.sops
# Then delete plaintext version
rm secrets.env
|
5. Add Team Member to Encrypted File
1
2
3
4
5
6
7
| # Get their public key
TEAMMATE_KEY="age1xyz..."
# Update file to include new recipient
sops updatekeys --add-age "$TEAMMATE_KEY" .secrets.env.sops
# Now they can decrypt with their private key
|
Just Recipes (Repository-Specific)
The justfile provides convenience commands:
1
2
3
4
5
6
7
| # Rotate encryption keys (re-encrypt with new key)
just sops-rotate RECIPIENT='age1newkey...'
# This command:
# 1. Re-encrypts all .sops files with new key
# 2. Updates .sops.yaml
# 3. Commits changes
|
File Naming Conventions
| Pattern |
Purpose |
Example |
*.sops |
Generic encrypted file |
config.json.sops |
.secrets.*.sops |
Secret environment files |
.secrets.env.sops |
.env.*.sops |
Environment-specific secrets |
.env.production.sops |
Do NOT commit:
*.env (without .sops suffix)
.secrets (without .sops suffix)
~/.config/sops/age/keys.txt (your private key)
Security Best Practices
✅ Do
- Use age keys, not PGP
- Simpler, more secure, designed for file encryption
- Rotate keys regularly
1
2
3
4
5
| # Generate new key
age-keygen -o ~/.config/sops/age/keys-2026.txt
# Update all secrets
just sops-rotate RECIPIENT='age1newkey...'
|
- Minimize encrypted surface area
- Only encrypt sensitive values (via
encrypted_regex)
- Keep structure visible for code review
- Audit access
1
2
| # See who can decrypt a file
sops --show-master-keys .secrets.env.sops
|
- Use separate keys per environment
- Dev key: wider team access
- Prod key: restricted to ops/SRE
❌ Don’t
- Never commit private keys
1
2
3
4
| # Add to .gitignore (already configured)
~/.config/sops/age/keys.txt
*.key
*.pem
|
- Don’t encrypt entire files unnecessarily
- Use
encrypted_regex to target specific values
- Allows git diff/merge to work
- Don’t share private keys in Slack/email
- Use secure channels (1Password, Bitwarden, encrypted email)
- Don’t lose your private key
- Back up to password manager
- Store offline copy in safe location
Troubleshooting
Error: “no key could be found to decrypt”
Cause: Your public key isn’t in the file’s recipient list.
Fix:
1
2
| # Ask someone with access to add you
sops updatekeys --add-age "YOUR_PUBLIC_KEY" .secrets.env.sops
|
Error: “failed to get the data key”
Cause: SOPS can’t find your private key.
Fix:
1
2
3
4
5
| # Check key exists
ls -la ~/.config/sops/age/keys.txt
# Set correct path if needed
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
|
How to recover if you lost your private key
Bad news: You cannot decrypt existing files without the private key.
Recovery:
- Have someone with access decrypt and re-encrypt with new key
- Or regenerate secrets and update encrypted files
Prevention: Always back up ~/.config/sops/age/keys.txt
Integration with CI/CD
GitHub Actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # .github/workflows/deploy.yml
- name: Install SOPS
run: |
curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops
- name: Decrypt secrets
env:
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }} # Store private key in GitHub Secrets
run: |
echo "$SOPS_AGE_KEY" > /tmp/age-key.txt
export SOPS_AGE_KEY_FILE=/tmp/age-key.txt
sops -d .secrets.env.sops > .env
source .env
|
Docker
1
2
3
4
5
6
7
8
9
10
11
| # Dockerfile
FROM ubuntu:22.04
# Install SOPS
RUN curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64 \
&& mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops \
&& chmod +x /usr/local/bin/sops
# Runtime: mount age key and decrypt
# docker run -v ~/.config/sops/age:/root/.config/sops/age myapp
ENTRYPOINT ["sh", "-c", "sops -d .secrets.env.sops > .env && exec \"$@\"", "--"]
|
Advanced: Multiple Recipients
For team-wide secrets, add multiple age public keys:
1
2
3
4
5
6
7
8
9
| # .sops.yaml
creation_rules:
- path_regex: '^\.secrets\.team\.sops$'
encrypted_regex: '^(.*)$'
key_groups:
- age:
- 'age1alice...' # Alice's key
- 'age1bob...' # Bob's key
- 'age1carol...' # Carol's key
|
Now Alice, Bob, and Carol can all decrypt:
1
2
3
4
5
| # Alice decrypts
sops -d .secrets.team.sops
# Bob decrypts (using his own private key)
sops -d .secrets.team.sops
|
Reference
Quick Reference Card
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # Create/Edit
sops .secrets.env.sops
# View (decrypt to stdout)
sops -d .secrets.env.sops
# Encrypt existing file
sops -e plaintext.env > encrypted.env.sops
# Add recipient
sops updatekeys --add-age "age1..." file.sops
# Remove recipient
sops updatekeys --rm-age "age1..." file.sops
# Rotate keys (re-encrypt)
sops -r -i file.sops
# Use in shell
export $(sops -d .secrets.env.sops | xargs)
|