Managing Git Secrets Safely with Mozilla SOPS and Age: A Lightweight HashiCorp Vault Alternative

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Every team eventually makes the same mistake: a database password, API key, or cloud credential gets committed to a Git repository. Sometimes it’s a junior developer who doesn’t know better. Sometimes it’s a rushed hotfix at 2 AM. Either way, once a secret hits Git history, it’s compromised — even if you delete it in the next commit.

The obvious answer is “just don’t commit secrets.” But that doesn’t solve the real problem: where do those secrets live instead? For small teams without a dedicated secrets manager, the answer usually devolves into shared Google Docs, Slack DMs, or a .env file sitting on someone’s laptop. That’s arguably worse.

HashiCorp Vault is the industry-standard solution. But running it isn’t free — you need a dedicated server, policy management, token renewal, and integrations for every tool in your stack. For a three-person team managing a handful of microservices, that’s a lot of infrastructure to babysit.

I’ve been in exactly this situation. Mozilla SOPS combined with Age is the tool that got us out of it. Encrypted secrets live inside your Git repository — version-controlled, diff-able, and reviewable — with zero external infrastructure to maintain.

Quick Start — Up and Running in 5 Minutes

Install Age and SOPS

Age is a modern encryption tool designed to replace PGP for most practical purposes. SOPS (Secrets OPerationS) is Mozilla’s tool that uses Age (or PGP/KMS) to encrypt structured files like YAML, JSON, and .env — while keeping field names in plaintext so diffs stay readable.

On macOS:

brew install age
brew install sops

On Linux:

# Install Age
curl -Lo age.tar.gz https://github.com/FiloSottile/age/releases/latest/download/age-v1.1.1-linux-amd64.tar.gz
tar -xf age.tar.gz
sudo mv age/age age/age-keygen /usr/local/bin/

# Install SOPS
curl -Lo sops https://github.com/getsops/sops/releases/latest/download/sops-v3.9.0.linux.amd64
chmod +x sops
sudo mv sops /usr/local/bin/

Generate Your Age Key

age-keygen -o ~/.config/sops/age/keys.txt
# Output:
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Your public key is safe to share with teammates. The private key in keys.txt stays on your machine — back it up somewhere secure (more on this later).

Encrypt Your First Secret File

Create a file called secrets.yaml:

database:
  password: "supersecret123"
  host: "prod-db.internal"
api_key: "sk-live-abc123xyz"

Encrypt it with SOPS:

sops --encrypt \
  --age age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
  secrets.yaml > secrets.enc.yaml

The resulting file looks like this — values are encrypted, keys remain readable:

database:
    password: ENC[AES256_GCM,data:xyz123...,tag:abc==,type:str]
    host: ENC[AES256_GCM,data:def456...,tag:ghi==,type:str]
api_key: ENC[AES256_GCM,data:jkl789...,tag:mno==,type:str]
sops:
    age:
        - recipient: age1ql3z7...
          enc: |
            -----BEGIN AGE ENCRYPTED FILE-----
            ...

Commit secrets.enc.yaml to Git. Add secrets.yaml to .gitignore. Decrypt when needed:

sops --decrypt secrets.enc.yaml > secrets.yaml
# Or decrypt in-place and edit:
sops secrets.enc.yaml

Deep Dive — How SOPS and Age Work Together

Understanding the Encryption Model

SOPS uses hybrid encryption. When you encrypt a file, it generates a random data encryption key (DEK), encrypts the file contents with AES-256-GCM using that DEK, then encrypts the DEK itself with your Age public key. Add multiple recipients and each gets their own encrypted copy of the DEK — no need to re-encrypt all the data when the team grows.

Field names stay in plaintext by design. You can see what secrets exist and review diffs in pull requests without exposing actual values. For compliance workflows and code review, this turns out to matter more than most teams expect when they first set it up.

Configuring .sops.yaml for Your Project

Passing the Age recipient on every command gets old fast. Drop a .sops.yaml at your project root instead:

creation_rules:
  # Encrypt all files matching *secrets* or *.enc.yaml
  - path_regex: .*(secrets|enc)\.yaml$
    age: >
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
      age1another_team_member_public_key_here
  # Tighter rules for production
  - path_regex: environments/prod/.*\.yaml$
    age: age1prod_key_only_here

Now sops --encrypt secrets.yaml automatically picks the right keys based on the file path. No flags, no copy-pasting key strings.

Advanced Usage

Team Setup with Multiple Recipients

This is where SOPS earns its keep. Each developer generates their own Age keypair and shares their public key. Add all of them to .sops.yaml:

creation_rules:
  - path_regex: .*secrets.*
    age: >
      age1alice_public_key,
      age1bob_public_key,
      age1carol_public_key

When Alice encrypts a file, Bob and Carol can decrypt it using their own private keys. When someone leaves the team, remove their key from .sops.yaml and run sops updatekeys secrets.enc.yaml. Their encrypted copy in Git history is useless without the matching private key.

For onboarding, I keep a keys/ directory at the repo root with all team members’ public keys as plain text files. New joiners add their key, open a PR, and a maintainer re-encrypts shared secrets with the updated recipient list. The whole process takes about ten minutes.

Integrating with CI/CD Pipelines

For GitHub Actions, store the Age private key as a repository secret and inject it during the workflow:

- name: Decrypt secrets
  env:
    SOPS_AGE_KEY: ${{ secrets.AGE_PRIVATE_KEY }}
  run: |
    sops --decrypt secrets.enc.yaml > secrets.yaml
    source <(sops --decrypt --output-type dotenv app.enc.env)

Kubernetes users: Flux CD has a SOPS integration that decrypts secrets at deploy time using a key stored in a Kubernetes secret. Your manifests stay encrypted in Git all the way to the cluster.

Key Rotation

Update .sops.yaml with the new key, then re-encrypt:

# Update keys for a single file
sops updatekeys secrets.enc.yaml

# Re-encrypt everything after a team change
find . -name "*.enc.yaml" -exec sops updatekeys {} \;

Practical Tips

Always add unencrypted files to .gitignore. The cleanest pattern: name encrypted files secrets.enc.yaml and unencrypted ones secrets.yaml, then gitignore the unencrypted version only.

SOPS handles dotenv format too. You don’t need to convert to YAML first:

sops --encrypt --input-type dotenv --output-type dotenv app.env > app.enc.env

Back up your Age private key. SOPS has no recovery mechanism. Lose the private key when it’s the only recipient on a file, and those secrets are gone permanently. A password manager (1Password, Bitwarden) is the right place for this backup.

On that note — before encrypting any secret, generate something strong enough to be worth protecting. I keep ToolCraft’s Password Generator open for this: it runs entirely in the browser with no data sent anywhere, which means I can generate high-entropy database credentials or API tokens without second-guessing the privacy implications. When transferring keys between machines, the Hash Generator on the same site handles SHA-256 checksums client-side as well.

Audit recipients regularly. The sops: block at the bottom of every encrypted file lists all recipients in plaintext. Review it periodically — especially on secrets used in automated pipelines — to confirm no unexpected keys have been added.

Add a pre-commit hook. Prevent unencrypted secrets from getting staged in the first place:

# .git/hooks/pre-commit
#!/bin/bash
for file in $(git diff --cached --name-only | grep -E 'secrets\.yaml$|app\.env$'); do
  echo "ERROR: Unencrypted secret file staged: $file"
  echo "Run: sops --encrypt $file > ${file%.yaml}.enc.yaml"
  exit 1
done

SOPS vs Vault — choosing the right tool. SOPS + Age fits when your secrets rotate monthly rather than per-request, your team is under 20 people, and you want no additional infrastructure to run. HashiCorp Vault (or its open-source fork, OpenBao) is the better fit when you need dynamic secrets, per-service access policies, or lease-based credentials that expire automatically. Most teams start with SOPS and switch to Vault only when they’ve genuinely hit its limits.

Secret sprawl — credentials scattered across Slack threads, email, and local dotfiles — is the kind of debt that stays invisible until something breaks badly. SOPS gives every team member a workflow that’s secure by default and requires no server to babysit. It won’t cover every use case Vault handles, but it handles the 80% of teams that don’t need Vault’s complexity yet.

Share: