Skip to main content

SOPS Secret Management Guide

This guide explains how we manage secrets using SOPS with SSH keys for the Open Service Portal project.

Why SOPS?

Problems it Solves

  • No plaintext secrets in Git - All sensitive data is encrypted before committing
  • Version control for secrets - Track changes to secrets alongside code
  • Team collaboration - Multiple team members can decrypt using their SSH keys
  • Local development - Seamless secret management without external services
  • GitOps compatible - Works with Flux, ArgoCD, and other GitOps tools

Key Benefits

  • Uses existing SSH keys - No need to generate new encryption keys
  • Automatic decryption - direnv integration for seamless local development
  • Multiple formats - Supports YAML, JSON, ENV, INI, BINARY files
  • Partial encryption - Only encrypts values, keys remain readable
  • Works offline - No external key management service required

Quick Start

For New Team Members

# 1. Share your SSH public key with the team
cat ~/.ssh/id_ed25519.pub # or ~/.ssh/id_rsa.pub

# 2. Wait for team lead to add your key to .sops.yaml

# 3. Test decryption
cd app-portal
sops -d --input-type dotenv --output-type dotenv .env.enc

# 4. Enable direnv
direnv allow

For Team Leads

# 1. Add new member's SSH public key to .sops.yaml
vim app-portal/.sops.yaml # Add their ssh-ed25519 or ssh-rsa key

# 2. Update existing encrypted files with the new key
cd app-portal
sops updatekeys .env.enc
sops updatekeys github-app-key.pem.enc

# 3. Commit and push changes
git add .sops.yaml *.enc
git commit -m "chore: add [Name]'s SSH key to SOPS"
git push

Understanding SOPS

What is SOPS?

SOPS (Secrets OPerationS) is a tool that encrypts files so they can be safely stored in Git. It only encrypts the values, not the keys, making diffs readable.

How SSH Keys Work with SOPS

  1. SOPS uses the age encryption library which natively supports SSH keys
  2. SSH keys are automatically found in ~/.ssh/
  3. Multiple SSH public keys can be specified (team members)
  4. Any corresponding private key can decrypt
  5. Encrypted files are safe to commit to Git

Key Architecture

┌─────────────────────────────────────────────────────────────┐
│ Repository │
│ │
│ .sops.yaml .env.enc │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ age: │ │ data: │ │
│ │ - ssh-ed25519 │ ──────> │ ENC[AES256...] │ │
│ │ Alice... │ │ ENC[AES256...] │ │
│ │ - ssh-ed25519 │ └──────────────────┘ │
│ │ Bob... │ │
│ │ - ssh-rsa │ github-app-key.pem.enc │
│ │ CI... │ ┌──────────────────┐ │
│ └──────────────────┘ │ ENC[BINARY...] │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘

│ Can decrypt with ANY of:

┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Alice's │ │ Bob's │ │ CI/CD │
│ SSH Key │ │ SSH Key │ │ SSH Key │
│ (~/.ssh/) │ │ (~/.ssh/) │ │ (GitHub) │
└──────────────┘ └──────────────┘ └──────────────┘

SSH Key Usage

Personal SSH Keys

  • Location: ~/.ssh/id_ed25519 or ~/.ssh/id_rsa
  • Purpose: Individual developer access
  • Usage: Encrypt/decrypt during local development
  • Security: Private key never shared, public key in .sops.yaml

CI/CD SSH Keys

  • Location: GitHub Secrets, GitLab Variables
  • Purpose: Automated decryption in pipelines
  • Usage: Deploy secrets, validate encryption
  • Security: Restricted access, rotated regularly

Why SSH Keys?

  • Already exist - Developers already have SSH keys for Git
  • Automatically found - SOPS finds them in ~/.ssh/
  • No extra setup - No need to generate age keys
  • Secure - Same security as Git authentication

File Structure

app-portal Repository

app-portal/
├── .sops.yaml # SSH public keys configuration
├── .env.enc # Encrypted environment variables
├── github-app-key.pem.enc # Encrypted GitHub App private key
├── .envrc # direnv auto-decryption
└── .gitignore # Excludes plaintext secrets

Common Operations

Creating a New Secret

Environment Variables (.env)

# Create plaintext .env
cat > .env << 'EOF'
AUTH_GITHUB_CLIENT_ID=Iv1.xxxxx
AUTH_GITHUB_CLIENT_SECRET=xxxxx
DATABASE_URL=postgresql://user:pass@localhost/db
EOF

# Encrypt it (SSH keys automatically found)
sops -e --input-type dotenv --output-type dotenv .env > .env.enc

# Remove plaintext
rm .env

Binary Files (certificates, keys)

# Encrypt a private key or certificate
sops -e --input-type binary --output-type binary github-app-key.pem > github-app-key.pem.enc

# Remove plaintext
rm github-app-key.pem

Kubernetes Secrets

cat > secret.yaml <<EOF
apiVersion: v1
kind: Secret
metadata:
name: my-secret
namespace: backstage
stringData:
key: value
EOF

# Encrypt it
sops -e secret.yaml > secret.enc.yaml

# Clean up
rm secret.yaml

Editing Existing Secrets

# Method 1: Direct edit (opens in $EDITOR)
sops .env.enc
EDITOR=vim sops github-app-key.pem.enc

# Method 2: Decrypt, edit, re-encrypt
sops -d --input-type dotenv --output-type dotenv .env.enc > .env
vim .env
sops -e --input-type dotenv --output-type dotenv .env > .env.enc
rm .env

Viewing Secrets

# View decrypted .env file
sops -d --input-type dotenv --output-type dotenv .env.enc

# View decrypted binary file
sops -d --input-type binary --output-type binary github-app-key.pem.enc

# View specific value from YAML
sops -d --extract '["data"]["AUTH_GITHUB_CLIENT_ID"]' secret.enc.yaml

Rotating Secrets

# 1. Decrypt and update values
sops -d --input-type dotenv --output-type dotenv .env.enc > .env
vim .env # Update passwords, tokens, etc.

# 2. Re-encrypt with same recipients
sops -e --input-type dotenv --output-type dotenv .env > .env.enc
rm .env

# 3. Commit and push
git add .env.enc
git commit -m "Rotate GitHub App credentials"
git push

# 4. Restart application or re-run direnv
direnv reload

Team Workflows

Adding a Team Member

graph LR
A[New Member] -->|1. Share SSH key| B[Public SSH Key]
B -->|2. Send to team lead| C[Team Lead]
C -->|3. Add to .sops.yaml| D[Repository]
C -->|4. Run updatekeys| E[Re-encrypt secrets]
E -->|5. Push changes| F[Team can decrypt]
  1. New member shares SSH public key:

    # Get SSH public key
    cat ~/.ssh/id_ed25519.pub # or id_rsa.pub
    # Send output to team lead
  2. Team lead updates .sops.yaml:

    creation_rules:
    - path_regex: \.env$
    age: >-
    ssh-ed25519 EXISTING_KEY,
    ssh-ed25519 NEW_MEMBER_KEY # New member
  3. Team lead updates all encrypted files:

    cd app-portal
    sops updatekeys .env.enc
    sops updatekeys github-app-key.pem.enc
  4. Commit and push:

    git add .sops.yaml .env.enc github-app-key.pem.enc
    git commit -m "Add [Name]'s SSH key to SOPS"
    git push

Removing a Team Member

  1. Remove their SSH public key from .sops.yaml
  2. IMPORTANT: Rotate all secret values (not just re-encrypt!)
    # Decrypt
    sops -d --input-type dotenv --output-type dotenv .env.enc > .env

    # Update actual values (passwords, tokens, etc.)
    vim .env

    # Re-encrypt with remaining team members only
    sops -e --input-type dotenv --output-type dotenv .env > .env.enc
    rm .env
  3. Update all encrypted files:
    sops updatekeys .env.enc
    sops updatekeys github-app-key.pem.enc
  4. Commit and deploy:
    git add .sops.yaml .env.enc github-app-key.pem.enc
    git commit -m "Remove [Name] and rotate secrets"
    git push

Environment-Specific Secrets

Different Keys per Environment

You can configure different SSH keys for different environments or file patterns:

# .sops.yaml
creation_rules:
# Development secrets - all developers
- path_regex: .*\.dev\.enc$
age: ssh-ed25519 AAAA...,ssh-ed25519 BBBB...

# Production secrets - ops team only
- path_regex: .*\.prod\.enc$
age: ssh-ed25519 CCCC...,ssh-ed25519 DDDD...

# Default - everyone
- age: ssh-ed25519 AAAA...,ssh-ed25519 BBBB...,ssh-ed25519 CCCC...

CI/CD Integration

GitHub Actions

Using SSH Key

- name: Setup SOPS with SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
# Setup SSH key
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519

# Decrypt secrets (SSH key automatically found)
sops -d --input-type dotenv --output-type dotenv .env.enc > .env
sops -d --input-type binary --output-type binary github-app-key.pem.enc > github-app-key.pem

# Clean up
rm -rf ~/.ssh

Using Age Key (Alternative)

- name: Setup SOPS with Age
env:
SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
run: |
# Decrypt secrets
sops -d --input-type dotenv --output-type dotenv .env.enc > .env
sops -d --input-type binary --output-type binary github-app-key.pem.enc > github-app-key.pem

GitLab CI

decrypt-secrets:
before_script:
# Setup SSH key
- mkdir -p ~/.ssh
- echo "$CI_SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
script:
# Decrypt and use secrets
- sops -d --input-type dotenv --output-type dotenv .env.enc > .env
- source .env
- deploy.sh
after_script:
- rm -rf ~/.ssh

Security Best Practices

DO's ✅

  • Generate unique keys per person
  • Store project keys in secure vaults
  • Rotate secrets when team members leave
  • Use different keys for different environments
  • Audit .sops.yaml changes in PR reviews
  • Keep private keys secure and never commit them

DON'Ts ❌

  • Never commit unencrypted secrets
  • Never share private keys via insecure channels
  • Don't use the same key for local and production
  • Don't forget to remove departed members' keys
  • Never store keys in the repository

Troubleshooting

"Cannot decrypt" Error

Check SSH Key

# Verify SSH key exists
ls -la ~/.ssh/id_ed25519 # or id_rsa

# Check if your SSH key is in .sops.yaml
cat .sops.yaml | grep "$(cat ~/.ssh/id_ed25519.pub | cut -d' ' -f1-2)"

# Test decryption
sops -d --input-type dotenv --output-type dotenv .env.enc

# If using non-standard SSH key location
SSH_AUTH_SOCK= SSH_AGENT_PID= sops -d .env.enc

SSH Agent Issues

# Check if SSH agent is interfering
ssh-add -l

# Try without SSH agent
SSH_AUTH_SOCK= sops -d .env.enc

# Or add your key to agent
ssh-add ~/.ssh/id_ed25519

"No matching keys" Error

# Your SSH key isn't in .sops.yaml
# Ask team lead to:
1. Add your SSH public key to .sops.yaml
2. Run: sops updatekeys .env.enc

Format Detection Issues

# Always specify format for .env files
sops -e --input-type dotenv --output-type dotenv .env > .env.enc

# For binary files
sops -e --input-type binary --output-type binary key.pem > key.pem.enc

# Wrong (SOPS might misinterpret format):
sops -e .env > .env.enc

direnv Not Loading Secrets

# Check if direnv is allowed
direnv status

# Re-allow if needed
direnv allow

# Check .envrc for errors
cat .envrc

# Test manually
source .envrc

Corrupted Encrypted File

# Restore from Git
git checkout -- .env.enc
git checkout -- github-app-key.pem.enc

# If you need the latest changes
# Ask team member to decrypt and share securely
# Then re-encrypt with team keys

Advanced Topics

Multiple Key Files

# Personal key (default)
~/.config/sops/age/keys.txt

# Project keys
~/.config/sops/age/project-prod.txt
~/.config/sops/age/project-dev.txt

# Switch between keys
export SOPS_AGE_KEY_FILE=~/.config/sops/age/project-prod.txt
sops -d secret.enc.yaml

Key Rotation

# 1. Generate new key
age-keygen -o new-key.txt

# 2. Add new key to .sops.yaml
# 3. Remove old key from .sops.yaml
# 4. Update all secrets
find . -name "*.enc.yaml" -exec sops updatekeys {} \;

# 5. Update cluster secret
kubectl create secret generic sops-age \
--namespace=flux-system \
--from-file=age.agekey=new-key.txt \
--dry-run=client -o yaml | kubectl apply -f -

Audit Trail

# See who can decrypt (public keys in .sops.yaml)
cat deploy-backstage/.sops.yaml

# See when secrets were last modified
git log --oneline -- "*.enc.yaml"

# See who modified secrets
git blame app-portal/.env.enc

Quick Reference

Essential Commands

CommandPurpose
cat ~/.ssh/id_ed25519.pubGet your SSH public key
sops -e --input-type dotenv --output-type dotenv .env > .env.encEncrypt .env file
sops -d --input-type dotenv --output-type dotenv .env.encDecrypt .env file
sops -e --input-type binary --output-type binary key.pem > key.pem.encEncrypt binary
sops -d --input-type binary --output-type binary key.pem.enc > key.pemDecrypt binary
sops .env.encEdit encrypted file
sops updatekeys .env.encUpdate after .sops.yaml change
direnv allowEnable auto-decryption

File Types

FilePurposeCommit to Git?
.sops.yamlSSH public keys config✅ Yes
.env.encEncrypted secrets✅ Yes
*.pem.encEncrypted keys/certs✅ Yes
.envrcdirenv config✅ Yes
.envPlaintext secrets❌ No
*.pemPlaintext keys❌ No

Installation

# macOS
brew install sops age direnv

# Linux
curl -LO https://github.com/getsops/sops/releases/latest/download/sops-linux-amd64
sudo mv sops-linux-amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops

# Verify
sops --version
age --version