All articles

Managing Secrets With SOPS

A framework-agnostic way to encrypt secrets — shown here with a Rails app


Secrets management is a problem in every stack, not just Rails. SOPS (Secrets OPerationS) is a framework-agnostic tool that encrypts just the values in a YAML/JSON/ENV file, supports multiple keys and cloud KMS, and produces diffs you can actually review — so you can safely commit your secrets to git. It works with anything, but since I reach for it most often on Rails, that’s the example we’ll use here. Rails’ own encrypted credentials are a fine starting point, but they have limits: one shared key, an all-or-nothing file, and noisy diffs — exactly what SOPS fixes.

Secrets Plaintext values
SOPS Encrypt with an age key
Git Safe to commit
Rails Decrypted into ENV
SOPS encrypts values at rest; only the decrypt key can turn them back into ENV vars at runtime.
Why SOPS over Rails credentials?
  • Reviewable diffs: Only values are encrypted, so keys and structure stay readable. A PR shows which secret changed, not a wall of ciphertext.
  • Many keys: Encrypt to several recipients at once — each developer’s key, plus a CI key. Revoke one without re-sharing a master key.
  • KMS-ready: Back it with AWS KMS, GCP KMS, Azure Key Vault, age, or PGP.
  • Per-environment files: Keep staging and production secrets in separate, independently-encrypted files.
Install SOPS and age

We’ll use age for keys — it’s simpler than PGP and perfect for a small team.

brew install sops age

Generate a key pair. The public key encrypts; the private key (kept off git) decrypts.

age-keygen -o config/sops/age.key
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8r
Tell SOPS what to encrypt

A .sops.yaml at the repo root defines creation rules — which files to encrypt and to which recipients:

# .sops.yaml
creation_rules:
  - path_regex: config/secrets/.*\.yml$
    age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8r
Encrypt a secrets file

Write your secrets as plain YAML:

# config/secrets/production.yml
DATABASE_URL: postgres://user:pass@db.internal/myapp
STRIPE_SECRET_KEY: sk_live_abc123
RAILS_MASTER_KEY: 0a1b2c3d4e5f

Then encrypt it in place. SOPS rewrites the file so every value becomes ciphertext while the keys stay legible:

sops --encrypt --in-place config/secrets/production.yml

The result is safe to commit — DATABASE_URL is still visible, but its value is now ENC[AES256_GCM,data:...] plus a sops: metadata block.

Load secrets into Rails

The cleanest runtime approach is sops exec-env, which decrypts into the process environment and runs your command — no plaintext ever touches disk:

SOPS_AGE_KEY_FILE=config/sops/age.key \
  sops exec-env config/secrets/production.yml 'bin/rails server'

Prefer to decrypt inside the app? Read and parse it in an initializer:

# config/initializers/sops.rb
secrets_file = Rails.root.join("config/secrets/#{Rails.env}.yml")

if secrets_file.exist?
  decrypted = `sops --decrypt #{secrets_file}`
  YAML.safe_load(decrypted).each { |key, value| ENV[key] ||= value.to_s }
end

Now ENV["STRIPE_SECRET_KEY"] is available throughout the app, just like any other environment variable.

Editing secrets later

Never edit the encrypted file by hand. sops opens the decrypted version in your editor and re-encrypts on save:

sops config/secrets/production.yml
In CI/CD

Store the private age key as a single CI secret (e.g. SOPS_AGE_KEY), and let your pipeline decrypt at deploy time. One secret in your CI provider unlocks everything else — and that one key never lands in the repo.

Wrapping up

SOPS turns secrets management into normal version control: encrypted files live next to your code, diffs are reviewable, and access is controlled by who holds a key. For teams that have outgrown a single shared master.key, it’s a clean, auditable upgrade — and it plays nicely with whatever Rails version you’re on.

More articles