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?


Prerequisites

Install Tools

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:


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:


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:


Security Best Practices

✅ Do

  1. Use age keys, not PGP
  2. 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...'
    
  3. Minimize encrypted surface area
  4. Audit access
    1
    2
    
    # See who can decrypt a file
    sops --show-master-keys .secrets.env.sops
    
  5. Use separate keys per environment

❌ Don’t

  1. Never commit private keys
    1
    2
    3
    4
    
    # Add to .gitignore (already configured)
    ~/.config/sops/age/keys.txt
    *.key
    *.pem
    
  2. Don’t encrypt entire files unnecessarily
  3. Don’t share private keys in Slack/email
  4. Don’t lose your private key

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:

  1. Have someone with access decrypt and re-encrypt with new key
  2. 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)