As I previously discussed in my post on secret management in macOS and Linux, the real challenge of managing keys and tokens isn’t the encryption itself, but reducing accidental leakage without turning the sysadmin’s daily routine into a bureaucratic nightmare. Over the last few years, however, a duo of tools has gained significant traction and completely changed this dynamic: Mozilla SOPS and age. Together, they enable a declarative, GitOps-friendly, and extremely secure approach with virtually zero friction. This post is a detailed look at how these tools work and how to integrate them practically into your daily workflow.

age: Modern Encryption for the Real World#

age (designed by Filippo Valsorda) was built with a simple premise: to be a modern, secure, and above all, uncomplicated file encryption tool. It was created specifically to replace GPG in everyday infrastructure use cases.

While GPG is a monolith with decades of legacy and support for dozens of obsolete algorithms, age uses state-of-the-art cryptography by default (like X25519 and ChaCha20-Poly1305) and doesn’t try to manage your identity. It has no “trust networks” or local key databases.

An age key is simply a text file containing two strings:

  • The public key (which starts with age1...), used for encrypting.
  • The private key (which starts with AGE-SECRET-KEY-1...), used for decrypting.

age in Practice#

Installing age is trivial on any system. On macOS, you can use brew install age, and on most Linux distributions, it’s already in the official repositories (apt install age).

To generate a key pair, simply run:

age-keygen -o key.txt

The generated file will look something like this:

# created: 2026-05-26T07:15:00-03:00
# public key: age1y3d8q2u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlyspqyv2pns
AGE-SECRET-KEY-1R4X2QYLWMVPTQYP...

Encrypting a file using the public key is straightforward:

age -r age1y3d8q2u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlyspqyv2pns -o secrets.enc secrets.txt

And to decrypt it using the private key:

age -d -i key.txt secrets.enc

That’s it. No background daemons, no expired keys locking your scripts, no public keys imported into a local database. Just plain text files and standard input/output streams.

Mozilla SOPS: Smart Partial Encryption#

While age solves file encryption brilliantly, it still operates on the entire file. If you have a 50-line YAML configuration file where only two lines are secrets (like the database password), encrypting the whole file with age introduces two classic version control issues:

  1. You lose all visibility into what changed in Git (the diff becomes just an opaque binary block).
  2. Resolving merge conflicts in encrypted binary files is virtually impossible.

This is where Mozilla SOPS (Secrets Operations) comes in.

SOPS is an encrypted file editor that supports YAML, JSON, TOML, .env files, and binaries. Its killer feature is partial encryption: it parses the structure of your file and encrypts only the values, keeping the structural keys in plain text.

SOPS in Action#

Imagine you have the following configuration file (secrets.yaml):

database:
  host: db.internal
  username: app_user
  password: "my-super-secret-password"
api:
  token: "sk_live_abcdef123456"

Running the SOPS command configured to use your age key:

sops --encrypt --age age1y3d8q2u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlyspqyv2pns secrets.yaml > secrets.enc.yaml

The resulting secrets.enc.yaml file will look like this:

database:
  host: db.internal
  username: app_user
  password: ENC[AES256_GCM,data:lTqgB4e5aY1G8pQ=,iv:...,tag:...]
api:
  token: ENC[AES256_GCM,data:2u7gB4a...,iv:...,tag:...]
sops:
  age:
    - recipient: age1y3d8q2u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlyspqyv2pns
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2026-05-26T07:22:00Z"
  version: 3.9.0

Notice how elegant this is:

  • The keys database.host and database.username remain readable. Anyone viewing the repo knows the host is db.internal.
  • Only the confidential values (password and token) are encrypted with AES-256-GCM.
  • The metadata required to decrypt the file (including the recipient’s public key) is attached under the sops key.

In Git, if you change the database host, the diff will show exactly that line changing. If you rotate the password, only the encrypted value line will change. The diff remains clean, readable, and useful.

Furthermore, editing the file is completely transparent. If you set the SOPS_AGE_KEY_FILE=/path/to/key.txt environment variable and run:

sops secrets.enc.yaml

SOPS will read your private key, decrypt the file temporarily in memory, open your default editor (defined in $EDITOR), wait for your changes, re-encrypt the file, and save it back to disk once the editor is closed. The private key never touches the saved file, and the plain text never touches the disk.

The Power of .sops.yaml#

Passing public keys on the command line every time you encrypt a file is tedious and error-prone. SOPS solves this with a declarative configuration file placed at the root of your repository: .sops.yaml.

This file defines rules on which keys should encrypt which files based on regular expressions.

Here is a real-world infrastructure example:

creation_rules:
  # Rule for production secrets
  - path_regex: secrets/production/.*\.yaml$
    key_groups:
      - age:
          - age1y3d8q2u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlyspqyv2pns # Main Dev (Janio)
          - age1r78k4u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlq928asl0212 # Prod Server Key
          - age1backup9u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlyspqkey00 # Cold Backup Key

  # Rule for staging/development secrets
  - path_regex: secrets/staging/.*\.yaml$
    key_groups:
      - age:
          - age1y3d8q2u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlyspqyv2pns # Main Dev
          - age1dev01u9vx8zux5mshq8387hws55v7pyll6zdfp4slx2p8nlyspqyvdev01 # Dev Team Key

With this structure, when anyone on the team runs sops secrets/production/app.yaml, SOPS automatically reads the configuration and encrypts the file so that any one of the three recipients listed in the production rule can decrypt it using their respective age private key.

This eliminates the need to share private keys. The developer uses their key, the server uses its key, and the backup uses its key.

Adding New Machines and Rotating Keys#

If you need to authorize a new server to read production secrets, the workflow is simple:

  1. Generate an age key on the new server.
  2. Add the public key to the recipients list in .sops.yaml.
  3. On your admin machine, apply the new configuration to existing files by running:
sops updatekeys secrets/production/app.yaml

SOPS will decrypt the file using your private key and re-encrypt it, adding the new server to the recipients list. Then, simply commit the changes to Git.

How to Consume Secrets in Practice (Friction-Free)#

While the sops + age ecosystem is incredibly robust for storage, you still need to read these secrets to use them in your scripts, deploys, and applications. The good news is that SOPS offers several ways to do this with minimal friction.

1. Injecting Secrets into the Environment via CLI (sops exec-env)#

If you have a script, container, or application that expects to find API keys and passwords as standard environment variables, you don’t need to store them in plain text .env files. SOPS has a dedicated command to decrypt secrets in memory and inject them directly into your application’s process:

sops exec-env secrets.enc.yaml 'npm run start'

The npm run start command will launch with access to all variables defined in the decrypted file. As soon as the process exits, those variables disappear. No secrets are ever persisted to disk in plain text.

2. Reading Structured Secrets in Shell Scripts#

If you only need to extract a specific value from an encrypted YAML file in an automation script, you can combine SOPS with yq (or use the native SOPS extractor):

# Using the native SOPS extractor
DB_PASS=$(sops -d --extract '["database"]["password"]' secrets.enc.yaml)

This is ideal for backup scripts or automated deploys over SSH, allowing you to fetch exactly the secret you need at execution time.

3. Direct Integration in Your Code (Python)#

In programming languages, you can read SOPS encrypted files by calling the binary as a subprocess, returning a structured dictionary directly in memory.

Here is a simple Python helper to load secrets without ever saving them to disk in readable format:

import json
import subprocess

def load_sops_secrets(filepath: str) -> dict:
    # Decrypt the file directly in memory as JSON
    result = subprocess.run(
        ["sops", "-d", "--output-type", "json", filepath],
        capture_output=True,
        text=True,
        check=True
    )
    return json.loads(result.stdout)

# Practical usage
secrets = load_sops_secrets("secrets.enc.yaml")
db_password = secrets["database"]["password"]

This approach ensures that even if the server is compromised and the disk is copied, the secrets remain secure (since the age key can be isolated with strict permissions under /etc/ or loaded only in the server’s key agent).

Conclusion: Real Security is the One You Actually Use#

One of the greatest lessons in systems administration is that security is inversely proportional to friction. If a security method requires 10 complex manual steps, people will find a shortcut—and that shortcut is usually a plain text password in an unprotected file.

The combination of SOPS + age shines because it removes the “security theater.” It allows you to maintain a 100% declarative, versioned, and secure infrastructure in Git without managing complex GPG keys or provisioning heavy secrets servers for small and medium environments.

Once you integrate this foundation into your automation routine, secrets management stops being an operational burden and becomes a natural, fluid, and transparent part of your development process.

What about you? How do you manage secrets in your infrastructure today? Have you given age a shot, or are you still wrestling with GPG keys? Share your thoughts in the comments below.