The eternal problem: you need secrets in your repo for deployment, but you can’t commit plaintext credentials. SOPS solves this elegantly by encrypting only the values while leaving keys readable.

Why SOPS?

Traditional approaches:

  • Environment variables: Work, but no version control
  • Vault: Great, but complex for small teams
  • AWS Secrets Manager: Vendor lock-in, API calls at runtime
  • .env files in .gitignore: Hope nobody commits them

SOPS encrypts secrets in place. You commit encrypted files. CI/CD decrypts at deploy time. Full audit trail in git.

Installation

1
2
3
4
5
6
7
# macOS
brew install sops

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

Basic Usage with Age

Age is the simplest encryption backend:

1
2
3
4
5
# Generate a key
age-keygen -o ~/.sops/key.txt

# Note the public key (age1...)
cat ~/.sops/key.txt | grep "public key"

Create .sops.yaml in your repo root:

1
2
3
creation_rules:
  - path_regex: \.enc\.yaml$
    age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Now encrypt a file:

1
2
# Create secrets.enc.yaml
sops secrets.enc.yaml

SOPS opens your editor. Add secrets in plain YAML:

1
2
3
4
database:
  password: super-secret-password
  host: db.example.com
api_key: sk-1234567890

Save and close. The file is now encrypted:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
database:
    password: ENC[AES256_GCM,data:hKL...,type:str]
    host: ENC[AES256_GCM,data:vB2...,type:str]
api_key: ENC[AES256_GCM,data:sK9...,type:str]
sops:
    age:
        - recipient: age1ql3z7hjy...
          enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            ...

Keys are visible. Values are encrypted. You can grep for database.password without seeing the secret.

AWS KMS Integration

For teams, KMS is better than age (key rotation, IAM policies):

1
2
3
4
5
6
7
# .sops.yaml
creation_rules:
  - path_regex: environments/prod/.*\.enc\.yaml$
    kms: arn:aws:kms:us-east-1:123456789:key/abc-123
    
  - path_regex: environments/dev/.*\.enc\.yaml$
    kms: arn:aws:kms:us-east-1:123456789:key/dev-456

Different keys for different environments. Prod secrets stay locked down.

Editing Encrypted Files

1
2
3
4
5
6
7
8
# Edit in place (decrypts, opens editor, re-encrypts on save)
sops secrets.enc.yaml

# Decrypt to stdout
sops -d secrets.enc.yaml

# Decrypt to file (don't commit this!)
sops -d secrets.enc.yaml > secrets.yaml

In CI/CD

GitHub Actions example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install SOPS
        run: |
          curl -LO https://github.com/getsops/sops/releases/download/v3.8.1/sops-v3.8.1.linux.amd64
          chmod +x sops-v3.8.1.linux.amd64
          sudo mv sops-v3.8.1.linux.amd64 /usr/local/bin/sops
      
      - name: Decrypt secrets
        env:
          SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
        run: |
          sops -d secrets.enc.yaml > secrets.yaml
      
      - name: Deploy
        run: ./deploy.sh

The only secret in GitHub is SOPS_AGE_KEY. Everything else lives in encrypted files in the repo.

Kubernetes Integration

Use with Helm:

1
2
3
4
5
6
7
# values.enc.yaml (encrypted)
database:
  password: ENC[AES256_GCM,data:...]

# helm-wrapper.sh
#!/bin/bash
sops -d values.enc.yaml | helm upgrade myapp ./chart -f -

Or use the SOPS secrets operator:

1
2
3
4
5
6
7
8
9
apiVersion: isindir.github.com/v1alpha3
kind: SopsSecret
metadata:
  name: app-secrets
spec:
  secretTemplates:
    - name: db-credentials
      stringData:
        password: ENC[AES256_GCM,data:...]

Partial Encryption

Only encrypt sensitive fields:

1
2
3
4
5
# .sops.yaml
creation_rules:
  - path_regex: \.enc\.yaml$
    encrypted_regex: "^(password|secret|key|token)$"
    age: age1...

Now only matching keys get encrypted:

1
2
3
4
5
database:
  host: db.example.com          # plaintext
  port: 5432                     # plaintext
  password: ENC[AES256_GCM...]   # encrypted
  username: app                  # plaintext

Rotation

When you need to re-encrypt with a new key:

1
2
3
4
5
# Update .sops.yaml with new key, then:
sops updatekeys secrets.enc.yaml

# Or rotate all files
find . -name "*.enc.yaml" -exec sops updatekeys {} \;

Git Diff for Encrypted Files

Add to .gitattributes:

.enc.yamldiff=sopsdiffer

And .git/config:

1
2
[diff "sopsdiffer"]
    textconv = sops -d

Now git diff shows decrypted content (if you have the key).

My Setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# .sops.yaml
creation_rules:
  # Production: KMS with strict IAM
  - path_regex: ^prod/.*\.enc\.(yaml|json)$
    kms: arn:aws:kms:us-east-1:PROD_ACCOUNT:key/prod-key
    
  # Staging: KMS with looser access
  - path_regex: ^staging/.*\.enc\.(yaml|json)$
    kms: arn:aws:kms:us-east-1:STAGING_ACCOUNT:key/staging-key
    
  # Local dev: age key on my machine
  - path_regex: \.enc\.(yaml|json)$
    age: age1...

Secrets in git. Full history. Environment isolation. No plaintext anywhere.

That’s the dream.