Skip to content
← Blog

Managing Secrets with SOPS, AGE, and 1Password

• 7 min read
devopssecuritytools

I’ve been using .env files for years. They work fine until you need to share secrets with a team, work across multiple machines, or worry about accidentally committing API keys to git. After researching and experimenting with different tools, I finally made a setup that works well for me: SOPS with AGE encryption.

The secrets are encrypted and commited to git, decrypted at runtime, and team members can be added by sharing public keys. You don’t need any cloud services or infrastructure for this setup, you do everything in your command line.

Quick Start

If you just want to get running:

# Install tools
brew install sops age  # macOS
# or use your package manager

# Generate your key
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt

# Get your public key (starts with age1...)
grep "public key:" ~/.config/sops/age/keys.txt

Create .sops.yaml in your project:

creation_rules:
  - path_regex: secrets/.*\.yaml$
    age: >-
      age1yourpublickey

Create and edit your first secret:

mkdir secrets
sops secrets/dev.yaml

This opens your editor with an empty file. Add secrets in YAML format:

DATABASE_URL: postgresql://localhost/mydb
API_KEY: sk_live_abc123

Save and close. SOPS automatically encrypts the file when you close your editor. The file on disk is now encrypted - you’ll see something like:

DATABASE_URL: ENC[AES256_GCM,data:0wGVsVZ/...,type:str]
API_KEY: ENC[AES256_GCM,data:8xQmN2k/...,type:str]

Use them in commands:

# SOPS decrypts in memory and injects as environment variables
sops exec-env secrets/dev.yaml 'echo $DATABASE_URL'
sops exec-env secrets/dev.yaml 'npm run migrate'

That’s the basics. Let’s dive into the details.

Why SOPS and AGE?

SOPS (Secrets OPerationS) is Mozilla’s tool for encrypting files. It’s battle-tested, widely adopted, integration with so many tools and interesting features that fits my use cases: it only encrypts the values infiles, not the keys. This means you can see what secrets exist without seeing their values, making code review and git diffs actually simple.

AGE is a modern encryption tool with a simple key format. Unlike GPG (which has a reputation for complexity), AGE is straightforward: you generate a keypair, share the public key with collaborators, and that’s it.

The combination gives us:

  • Encrypted secrets in files (safe to commit)
  • Decryption at runtime
  • Team member management (add public keys, re-encrypt)
  • No cloud or infrastructure dependencies

Project Structure

Here’s what a typical setup looks like:

your-project/
├── .sops.yaml           # SOPS configuration
├── Makefile             # Commands that handle decryption
└── secrets/
    └── dev.yaml        # Encrypted secrets (committed)

SOPS Configuration

The .sops.yaml file defines which files to encrypt and with which keys:

# SOPS configuration - https://github.com/getsops/sops
#
# Adding a team member:
#   1. age-keygen -o ~/.config/sops/age/keys.txt
#   2. They share the public key
#   3. Add it below and run: sops updatekeys secrets/dev.yaml
#   4. Commit - they can now decrypt

creation_rules:
  - path_regex: secrets/.*\.yaml$
    age: >-
      age195pjehrjy5ry8hl5s6jc9zys2gkuyp4mrc2fprs7yy704p8hufjq2d9vu4

Basically this file is the main configuration for SOPS inside your project. the path_regex specify which files it will be encrypted. From the regex above, it will be every file under secrets/ folder with .yaml extension. This setup can be improved and extended based on your needs, but this is a good starting point.

the age field contains a list of public keys that can decrypt the files matching the path_regex. You can add multiple keys there, including team member’s keys, CI/CD keys or server keys. Basically any public key that you want to allow decryption.

Makefile Integration

Use sops exec-env to decrypt secrets at runtime and inject them as environment variables:

SOPS_RUN = sops exec-env secrets/dev.yaml

dev:
	$(SOPS_RUN) 'exec iex -S mix phx.server'

server:
	$(SOPS_RUN) 'mix phx.server'

setup:
	$(SOPS_RUN) 'mix setup'

This example uses Elixir, but the same pattern works with any command.

When you run make dev, SOPS:

  1. Decrypts secrets/dev.yaml in memory
  2. Exports each key as an environment variable
  3. Executes the command with those variables available
  4. Never writes plaintext to disk

Basically you will not expose your secrets in your terminal or .env files. They are only in memory for the duration of the command.

The Encrypted Secrets File

Here’s what secrets/dev.yaml looks like committed to git:

DATABASE_URL: ENC[AES256_GCM,data:0wGVsVZ/...,type:str]
API_KEY: ENC[AES256_GCM,data:8xQmN2k/...,type:str]
sops:
  age:
    - recipient: age195pjehrjy5ry8hl5s6jc9zys2gkuyp4mrc2fprs7yy704p8hufjq2d9vu4
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2025-12-02T18:12:52Z"
  version: 3.11.0

The file is a bit verbose, but it allow you to check the keys (DATABASE_URL, API_KEY) that are encrypted. If anyone changes a secret, the encrypted value changes but the keys stay the same. This helps to understand the structure of the secrets, while the values are not exposed.

Editing Secrets

To add or modify secrets in an already-encrypted file:

sops secrets/dev.yaml

SOPS decrypts the file, opens your $EDITOR with the plaintext values, lets you make changes, then automatically re-encrypts when you save and close. You never manually encrypt - SOPS handles it.

Common workflows:

# Create new encrypted file (first time)
sops secrets/dev.yaml  # Opens empty editor, encrypts on save

# Edit existing encrypted file (anytime after)
sops secrets/dev.yaml  # Decrypts, shows plaintext, re-encrypts on save

# Use encrypted secrets in commands
sops exec-env secrets/dev.yaml 'npm start'

Backing Up Your AGE Key

AGE keys are just text files at ~/.config/sops/age/keys.txt. If you lose this file, you can’t decrypt your secrets. Back it up securely.

I store my AGE private key in 1Password as a secure note. When setting up a new machine:

# Retrieve from 1Password and save to the standard location
op read "op://Private/SOPS AGE Key/private_key" > ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt

You can use any password manager or secure backup method. The important part is having a copy somewhere safe that isn’t your laptop’s hard drive. Follow the SOPS which explain other options for key management and backup.

Team Workflows

Adding a New Team Member

  1. They generate their key:

    age-keygen -o ~/.config/sops/age/keys.txt
  2. They share their public key (starts with age1...)

  3. You add it to .sops.yaml:

    creation_rules:
      - path_regex: secrets/.*\.yaml$
        age: >-
          age195pjehrjy5ry8hl5s6jc9zys2gkuyp4mrc2fprs7yy704p8hufjq2d9vu4,
          age1abc...theirnewkey
  4. Re-encrypt the secrets for the new key:

    sops updatekeys secrets/dev.yaml

    This command re-encrypts the file so that both your key and their key can decrypt it. The secrets themselves don’t change, only who can access them.

  5. Commit and push - They can now decrypt

When to use sops updatekeys: Only when adding/removing team members in .sops.yaml. For normal secret editing, use sops secrets/dev.yaml which handles encryption automatically.

This setup is very simple and works well for small teams. For larger teams or more complex workflows, I recommend using SOPS with a KMS provider. This way you can manage access centrally without sharing keys directly.

Rotating Keys

If a team member leaves or a key is compromised:

  1. Remove their public key from .sops.yaml
  2. Run sops updatekeys secrets/dev.yaml

The removed key can no longer decrypt new versions.

Production Secrets

For production environments, use separate files with different keys:

creation_rules:
  - path_regex: secrets/dev\.yaml$
    age: >-
      age1developer1,age1developer2

  - path_regex: secrets/prod\.yaml$
    age: >-
      age1cicdkey,age1productiononly

Developers can decrypt dev.yaml but not prod.yaml. CI/CD has its own AGE key stored in your CI system.

Deployment Example with Docker Compose

Here’s one way to use SOPS in production (this example uses Docker Compose, but the same concepts apply to Kubernetes, Ansible, or other orchestration tools):

# docker-compose.yml
services:
  app:
    image: myapp:latest
    env_file:
      - secrets/.env.prod

Create a decrypted env file at deployment time:

# In your deploy script or CI/CD
sops -d --output-type dotenv secrets/prod.yaml > secrets/.env.prod
docker-compose up -d

Or use sops exec-env directly without creating files:

sops exec-env secrets/prod.yaml 'docker-compose up -d'

The AGE private key needs to be available on the production server at ~/.config/sops/age/keys.txt or via the SOPS_AGE_KEY environment variable.

As mentioned before, you may need more advanced features like audit trails or automated rotation. SOPS, with the supports of KMS providers (AWS KMS, GCP KMS, Azure Key Vault, and HashiCorp Vault), can help with that.

Wrapping Up

SOPS with AGE encryption handles encrypted secrets in git, runtime decryption, and team member management. It’s a straightforward setup that doesn’t require external services or infrastructure.

Further Reading