← All articles
a computer chip is shown in front of a black background

Managing Homelab Secrets with SOPS and age Encryption

Security 2026-02-14 · 6 min read security secrets encryption sops age git ci-cd ansible
By HomeLab Starter Editorial TeamHome lab enthusiasts covering hardware setup, networking, and self-hosted services for home and small office environments.

Every homelab has secrets: database passwords, API keys, SMTP credentials, VPN private keys. The common approaches to managing these secrets range from bad (hardcoded in config files committed to git) to slightly less bad (stored in a .env file you hope you remember to gitignore). Both fail eventually -- you accidentally commit a secret, you lose the .env file when rebuilding a machine, or you can't remember which version of which password is deployed where.

Photo by Steve Johnson on Unsplash

SOPS (Secrets OPerationS) solves this by encrypting only the values in your config files while leaving the keys readable. Combined with age -- a modern, simple encryption tool -- you get a secrets workflow that's safe to commit to git, easy to use, and doesn't require running a separate secrets server like HashiCorp Vault.

SOPS logo

Why SOPS + age

The Problem with Other Approaches

.env files: Not version-controlled. Easy to lose. No audit trail. Different versions on different machines with no way to tell which is correct.

HashiCorp Vault: Powerful but massive overkill for a homelab. Running a highly-available Vault cluster to store 20 secrets is like driving a semi truck to pick up groceries.

Git-crypt: Encrypts entire files, making diffs useless. You can't see which config values changed -- just that "something changed in this encrypted blob."

Ansible Vault: Works, but ties you to Ansible. If you stop using Ansible (or never started), Ansible Vault is dead weight.

What SOPS Does Differently

SOPS encrypts values but preserves keys and structure. A SOPS-encrypted YAML file looks like this:

database:
    host: homelab-db.local        # Not encrypted -- not a secret
    port: 5432                    # Not encrypted
    password: ENC[AES256_GCM,data:kD8k3j2...,iv:...,tag:...,type:str]
    username: ENC[AES256_GCM,data:a8Dk2p...,iv:...,tag:...,type:str]
smtp:
    server: smtp.fastmail.com     # Not encrypted
    api_key: ENC[AES256_GCM,data:pQ9z7m...,iv:...,tag:...,type:str]

You can see the structure. You can see which keys exist. You can review PRs and see that someone changed database.password without seeing the actual password. Git diffs are meaningful. The file is safe to commit because only the sensitive values are encrypted.

Installation

Install age

age is the encryption backend. It generates keys and handles the actual cryptographic operations.

# Debian/Ubuntu
sudo apt install age

# Fedora
sudo dnf install age

# macOS
brew install age

# From source (any platform)
go install filippo.io/age/cmd/...@latest

Install SOPS

# Download the latest binary (check GitHub releases for current version)
curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
sudo mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops

# Or via package manager
# macOS
brew install sops

# Arch Linux
sudo pacman -S sops

Generate an age Key Pair

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

# This creates a file containing both the public and private key:
# public key: age1abc123...
# AGE-SECRET-KEY-1xyz789...

# Note the public key -- you'll need it for SOPS configuration
cat ~/.config/sops/age/keys.txt | grep "public key"

Back up the private key. If you lose keys.txt, you permanently lose access to every file encrypted with it. Store a copy in a password manager, on a USB drive in a safe, or printed as a QR code.

Encrypting Config Files

Create a .sops.yaml Configuration

In your project or homelab repo root, create .sops.yaml to define which files get encrypted and with which keys:

# .sops.yaml
creation_rules:
  # Encrypt all files in secrets/ directory
  - path_regex: secrets/.*\.yaml$
    age: age1abc123def456...  # Your public key

  # Encrypt .env files
  - path_regex: \.env\.encrypted$
    age: age1abc123def456...

  # Multiple recipients (e.g., you + your deploy server)
  - path_regex: deploy/.*\.yaml$
    age: >-
      age1abc123def456...,
      age1second789key...

Encrypt a File

# Create a plaintext config
cat > secrets/database.yaml << 'EOF'
database:
  host: 10.0.0.50
  port: 5432
  name: homelab
  username: admin
  password: super-secret-password-123
  ssl: true
EOF

# Encrypt it in place
sops -e -i secrets/database.yaml

# Or encrypt to a new file
sops -e secrets/database.yaml > secrets/database.enc.yaml

Selective Encryption

By default, SOPS encrypts all values. You can control this with --encrypted-regex or --unencrypted-regex:

# Only encrypt keys matching "password", "secret", "key", or "token"
sops -e --encrypted-regex '^(password|secret|key|token|api_key)$' config.yaml

# Or set this in .sops.yaml:
creation_rules:
  - path_regex: config/.*\.yaml$
    age: age1abc123...
    encrypted_regex: '^(password|secret|key|token)$'

This is useful when most of your config is non-sensitive and you want to keep it readable without the noise of ENC[...] everywhere.

Edit Encrypted Files

SOPS decrypts the file, opens it in your editor, and re-encrypts when you save:

# Edit an encrypted file (uses $EDITOR)
sops secrets/database.yaml

# The file opens decrypted in your editor
# Make changes, save, and quit -- SOPS re-encrypts automatically

Decrypt for Use

# Decrypt to stdout (for piping into other commands)
sops -d secrets/database.yaml

# Decrypt to a specific file
sops -d secrets/database.yaml > /tmp/database-decrypted.yaml
# Don't forget to clean up: rm /tmp/database-decrypted.yaml

# Extract a single value
sops -d --extract '["database"]["password"]' secrets/database.yaml
# Output: super-secret-password-123

Like what you're reading? Subscribe to HomeLab Starter — free weekly guides in your inbox.

Git Integration

Gitignore Plaintext, Commit Encrypted

The beauty of SOPS is that encrypted files are safe to commit. Your .gitignore only needs to exclude plaintext secrets and the age private key:

# .gitignore
# Never commit the private key
.config/sops/
keys.txt

# Never commit decrypted output
*.decrypted.yaml
*.decrypted.json
/tmp/

Pre-commit Hook

Add a pre-commit hook to prevent accidentally committing unencrypted secrets:

#!/bin/bash
# .git/hooks/pre-commit

# Check for unencrypted files in secrets/ directory
for file in $(git diff --cached --name-only | grep -E '^secrets/.*\.(yaml|json)$'); do
  if ! grep -q "sops" "$file" 2>/dev/null; then
    echo "ERROR: $file appears to be unencrypted. Run 'sops -e -i $file' first."
    exit 1
  fi
done

Diff Encrypted Files

Configure git to show meaningful diffs of encrypted files:

# .gitattributes
secrets/*.yaml diff=sopsdiffer

# .gitconfig (local or global)
[diff "sopsdiffer"]
  textconv = sops -d

Now git diff will show the decrypted content, making code reviews actually useful.

CI/CD Integration

GitHub Actions

Store the age private key as a GitHub Actions secret, then decrypt during deployment:

# .github/workflows/deploy.yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install SOPS and age
        run: |
          curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
          sudo mv sops-v3.9.4.linux.amd64 /usr/local/bin/sops && sudo chmod +x /usr/local/bin/sops
          sudo apt install -y age

      - name: Decrypt secrets
        env:
          SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
        run: |
          sops -d secrets/production.yaml > /tmp/secrets.yaml
          # Use the decrypted secrets in your deployment

      - name: Deploy
        run: |
          # Your deployment commands here
          # Reference /tmp/secrets.yaml as needed

Gitea Actions / Self-hosted CI

Same approach -- store the age private key as a CI secret and decrypt at deploy time. The key never touches disk in plaintext (it's passed via environment variable).

SOPS vs Ansible Vault

If you're already using Ansible, here's how SOPS compares:

Feature SOPS + age Ansible Vault
Structure preservation Keys visible, values encrypted Entire file encrypted
Git diffs Meaningful (see which keys changed) Useless (encrypted blob)
Editor integration sops file.yaml opens decrypted ansible-vault edit file.yaml
Partial encryption Yes (regex-based) No (entire file or per-variable)
Tool dependency Standalone (works with anything) Requires Ansible
Key management age keys (simple files) Password-based (or vault-id files)
Multiple recipients Yes (multiple age public keys) Yes (multiple vault IDs)
Performance Fast (age is very fast) Slower (Python-based)

Choose SOPS + age if: you use multiple tools (Docker Compose, Kubernetes manifests, custom scripts) and want a universal solution. SOPS works with YAML, JSON, ENV, and INI files regardless of what tool consumes them.

Choose Ansible Vault if: your entire infrastructure is managed by Ansible and you don't need tool-agnostic secrets.

Best Practices

One key per environment. Generate separate age keys for development, staging, and production. This limits blast radius -- compromising the dev key doesn't expose production secrets.

Rotate keys periodically. SOPS makes key rotation straightforward:

# Add a new key to .sops.yaml, then re-encrypt all files
sops updatekeys secrets/database.yaml

Use encrypted_regex for mixed files. When a config file is mostly non-sensitive, encrypt only the sensitive fields. This keeps the file readable and diffs clean.

Never decrypt to disk in production. Use sops -d to pipe decrypted values directly into your application's environment or stdin. Decrypted files on disk are a liability.

Test your recovery process. At least once, pretend you've lost your machine and try to decrypt your secrets using only your backed-up age key. If you can't, fix your backup process before you actually need it.

Get free weekly tips in your inbox. Subscribe to HomeLab Starter