SOPS + Age: Modern Secrets Management for Teams

Encrypt secrets directly in Git without complex infrastructure. Learn how Mozilla SOPS with Age encryption provides simple, secure, and auditable credential management for cloud-native teams.

Why SOPS + Age?

Managing secrets in version control has always been a dilemma. You want infrastructure as code, but you can't commit plaintext credentials. Traditional solutions force you into complex trade-offs:

Mozilla SOPS takes a different approach: encrypt values, not files. It integrates with your existing workflows—Git, diff tools, CI/CD—while keeping secrets encrypted at rest. Paired with Age, the modern encryption tool by Filippo Valsorda, you get a secrets management solution that's simple, secure, and maintainable by small teams.

Traditional Tools

  • Requires running infrastructure
  • Operational complexity
  • Network dependency
  • Vendor lock-in risks
  • Higher costs

SOPS + Age

  • No infrastructure to run
  • Works offline
  • Git-native workflow
  • Vendor independent
  • Free and open source

How SOPS Works

SOPS uses a technique called envelope encryption. Here's the elegant simplicity:

  1. Data Key Generation: When encrypting, SOPS generates a random 256-bit data key
  2. Content Encryption: Your actual secrets are encrypted with this data key using AES-256-GCM
  3. Key Encryption: The data key itself is encrypted with your Age (or other) public keys
  4. Metadata Storage: Encrypted data keys are stored in the file's metadata

This means multiple people can decrypt the same file using their individual private keys. Rotate a team member out? Just remove their encrypted data key from the metadata—no re-encryption of actual secrets needed.

💡 Key Insight

SOPS only encrypts values, not keys. Your YAML/JSON structure remains visible, making diffs meaningful and code review possible. You can see that database.password changed, just not what it changed to.

Installation

Install Age

Linux/macOS
# macOS
brew install age

# Linux (Debian/Ubuntu)
sudo apt-get install age

# Linux (Arch)
sudo pacman -S age

# Or download binary
curl -LO https://github.com/FiloSottile/age/releases/latest/download/age-linux-amd64.tar.gz
tar xf age-linux-amd64.tar.gz
sudo mv age/age /usr/local/bin/
sudo mv age/age-keygen /usr/local/bin/

Install SOPS

Install SOPS
# macOS
brew install sops

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

# Verify
sops --version

Generate Your Age Key

Create Age Identity
# Generate a new key pair
age-keygen -o ~/.config/sops/age/keys.txt

# View your public key (used for encryption)
age-keygen -y ~/.config/sops/age/keys.txt
# Output: age1ql3z7... (your public key)

Basic Usage

Configure SOPS for Age

Create a SOPS configuration file in your repo root:

.sops.yaml
# Define creation rules - who can decrypt new files
creation_rules:
  - age: |
      # Add all team member public keys here
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3pj2etdfg...  # Alice
      age1g47h9dy...  # Bob
      age1f8h2k...    # CI/CD key

  # Path-specific rules (optional)
  - path_regex: production/.*
    age: |
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3pj2etdfg...  # Senior team only

Encrypt Your First File

1

Create a Plaintext Secret

# secrets.yaml
database:
  host: prod-db.internal
  port: 5432
  username: app_user
  password: SuperSecret123!
api:
  key: sk_live_abcdef123456
  endpoint: https://api.example.com
2

Encrypt It

sops encrypt secrets.yaml > secrets.enc.yaml

# Or edit in place with automatic encryption
sops secrets.yaml  # Opens in $EDITOR, encrypts on save
3

View Encrypted Output

# Result: secrets.enc.yaml
database:
    host: prod-db.internal
    port: 5432
    username: app_user
    password: ENC[AES256_GCM,data:abc...,iv:def...,tag:ghi...,type:str]
api:
    key: ENC[AES256_GCM,data:jkl...,iv:mno...,tag:pqr...,type:str]
    endpoint: https://api.example.com
sops:
    age:
        - recipient: age1ql3z7hjy54pw3...
          enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            ...
            -----END AGE ENCRYPTED FILE-----
    lastmodified: "2026-03-18T08:00:00Z"
    version: 3.9.0

Decrypt and Use

Decrypt for use
# Decrypt to stdout
sops decrypt secrets.enc.yaml

# Decrypt to file
sops decrypt secrets.enc.yaml > secrets.yaml

# Extract specific value
sops decrypt --extract '["database"]["password"]' secrets.enc.yaml

Working with Different Formats

SOPS supports multiple file formats with intelligent value detection:

Format Extension Encrypted Fields
YAML .yaml, .yml All values by default, keys visible
JSON .json All values by default
ENV .env Values only, variable names visible
INI .ini Values only
Binary any Entire file encrypted

Partial Encryption

Not everything needs encryption. Configure SOPS to only encrypt specific paths:

.sops.yaml with selective encryption
creation_rules:
  - path_regex: config/.*\.yaml
    encrypted_regex: '^(password|secret|key|token)$'
    age: |
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3pj2etdfg...

  - path_regex: production/.*\.env
    age: |
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3pj2etdfg...

Encrypted Comments

SOPS preserves and encrypts comments too:

Before encryption
database:
  # This is the production password - DO NOT SHARE
  password: SecretPassword123
After encryption
database:
    #ENC[AES256_GCM,data:abc...,iv:def...,type:comment]
    password: ENC[AES256_GCM,data:ghi...,iv:jkl...,type:str]

Key Groups & Multiple Keys

SOPS supports sophisticated key management scenarios:

Shamir Secret Sharing

Require multiple keys to decrypt (M-of-N threshold):

Threshold decryption
creation_rules:
  - shamir_threshold: 2
    key_groups:
      - age:
          - age1ql3z7hjy54pw3...  # Alice
      - age:
          - age1g47h9dy...       # Bob
      - age:
          - age1f8h2k...         # Charlie

# Any 2 of 3 can decrypt

Multiple KMS Providers

Mix Age with cloud KMS for recovery scenarios:

Hybrid encryption
creation_rules:
  - key_groups:
      - age:
          - age1ql3z7hjy54pw3...  # Team key
      - kms:
          - arn: arn:aws:kms:us-east-1:123456789:key/abc123
            role: arn:aws:iam::123456789:role/sops-role

Git Integration

Meaningful Diffs

SOPS integrates with Git for encrypted diffs:

Configure Git diff
# Add to .gitattributes
*.yaml diff=sopsdiffer
*.yml diff=sopsdiffer
*.json diff=sopsdiffer
*.env diff=sopsdiffer

# Configure git diff driver
git config diff.sopsdiffer.textconv "sops decrypt"

Pre-commit Hooks

Prevent committing plaintext secrets:

.pre-commit-config.yaml
repos:
  - repo: https://github.com/yuvipanda/pre-commit-hook-ensure-sops
    rev: v1.0
    hooks:
      - id: sops-encryption
        # Ensure all files matching these patterns are encrypted
        files: ^(secrets|config|production)/.*\.(yaml|yml|json|env)$
        exclude: ".sops.yaml|.*\.dec\.*"
⚠️ Important

Even with pre-commit hooks, always review your diffs before committing. Once a plaintext secret hits Git history, it's there forever—consider it compromised.

CI/CD Integration

GitHub Actions

.github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install SOPS and Age
        run: |
          wget -qO /usr/local/bin/sops \
            "https://github.com/getsops/sops/releases/latest/download/sops-linux-amd64"
          chmod +x /usr/local/bin/sops
          sudo apt-get install age

      - name: Setup Age key
        env:
          AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
        run: |
          mkdir -p ~/.config/sops/age
          echo "$AGE_SECRET_KEY" > ~/.config/sops/age/keys.txt

      - name: Decrypt secrets
        run: |
          sops decrypt secrets.enc.yaml > secrets.yaml
          export DB_PASSWORD=$(sops decrypt --extract '["database"]["password"]' secrets.enc.yaml)

      - name: Deploy
        run: |
          # Your deployment commands
          kubectl apply -f k8s/

GitLab CI

.gitlab-ci.yml
stages:
  - deploy

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache age sops
    - mkdir -p ~/.config/sops/age
    - echo "$AGE_SECRET_KEY" > ~/.config/sops/age/keys.txt
  script:
    - export DB_PASSWORD=$(sops decrypt --extract '["database"]["password"]' secrets.enc.yaml)
    - deploy-script.sh

Flux/GitOps

Flux has native SOPS support:

Flux SOPS configuration
# Add Age public key to cluster
kubectl create secret generic sops-age \
  --namespace=flux-system \
  --from-file=age.agekey=keys.txt

# Kustomization with SOPS decryption
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: app
  namespace: flux-system
spec:
  interval: 10m
  path: ./k8s
  sourceRef:
    kind: GitRepository
    name: app
  decryption:
    provider: sops

Cloud KMS Integration

Age is the recommended approach, but SOPS supports cloud KMS for hybrid scenarios:

AWS KMS

AWS KMS setup
# Create KMS key
aws kms create-key --description "SOPS key"

# Add to .sops.yaml
creation_rules:
  - kms: arn:aws:kms:us-east-1:123456789:key/abc123

Azure Key Vault

Azure Key Vault setup
creation_rules:
  - azure_keyvault: https://myvault.vault.azure.net/keys/sops-key

GCP KMS

GCP KMS setup
creation_rules:
  - gcp_kms: projects/my-project/locations/global/keyRings/sops/cryptoKeys/my-key

Best Practices

Key Management

  1. Generate separate CI/CD keys: Don't use personal keys for automation
  2. Rotate keys quarterly: Re-encrypt files with new keys periodically
  3. Store Age private keys securely: Password managers, HSMs, or cloud secret stores—not in repos
  4. Use key groups for teams: Each team member gets their own key

File Organization

Recommended structure
.
├── .sops.yaml              # Root config
├── secrets/
│   ├── production/
│   │   ├── database.enc.yaml
│   │   └── api-keys.enc.yaml
│   └── staging/
│       ├── database.enc.yaml
│       └── api-keys.enc.yaml
├── k8s/
│   └── secrets/            # Kubernetes Secrets, encrypted
│       └── app-secrets.enc.yaml
└── terraform/
    └── terraform.enc.tfvars

Rotation Strategies

✓ Rotation Checklist
  • Update the actual secret value in the target system
  • Encrypt new value with SOPS
  • Commit and deploy
  • Verify application uses new secret
  • Revoke old secret in target system

Auditing

Git provides your audit trail:

Audit commands
# Who changed secrets last?
git log --oneline -10 -- secrets/production/

# What changed?
git log -p -- secrets/production/database.enc.yaml

# Blame (with SOPS diff)
git blame -L 1,50 secrets/production/database.enc.yaml

Conclusion

SOPS with Age provides a pragmatic middle ground for secrets management. You don't need to run Vault infrastructure, yet you get encryption, access control, and Git-native workflows. It's particularly well-suited for:

The simplicity is the point. When secrets management is easy, teams actually use it. When it's complex, they find workarounds. SOPS + Age removes the friction while maintaining security best practices.

Start with Age for simple setups, add cloud KMS when you need enterprise features, and evolve your approach as requirements grow. The foundation you build with SOPS will scale with your organization.