
September 12, 2026
11 min read
By Kokil Thapa | Last reviewed: September 2026
Plaintext API keys in Git remain one of the fastest ways to lose money and client trust. To manage secrets with SOPS and age, you encrypt YAML, JSON, or .env files before commit and decrypt only where keys exist — your laptop, a CI runner, or a deploy host. On production Laravel stacks I maintain with GitLab CI and Deployer 7, SOPS plus age replaced ad-hoc .env copies emailed between developers. The workflow is boring, auditable, and cheap to run on a single Ubuntu server.
.sops.yaml creation rule, encrypting secret files into Git-safe ciphertext, and setting SOPS_AGE_KEY_FILE in CI or on servers so only authorised environments can decrypt.What is SOPS and why should you manage secrets with SOPS and age?
SOPS (Secrets OPerationS) encrypts structured files while leaving keys readable. You can diff encrypted commits and spot which variable changed. Age is a small modern encryption tool. It replaces long GPG workflows with one-line public keys and a single private key file.
Together they solve a narrow problem well: secrets that belong in version control, but never in plaintext. Think Laravel .env.production, Deployer shared configs, Ansible vars, or Kubernetes manifests with embedded credentials. For full runtime secret stores, see HashiCorp Vault or AWS Secrets Manager. SOPS sits closer to the repo.
The model fits teams that already use Git for infrastructure. You do not need a secrets server on day one. You do need discipline around private keys. Never commit an age secret key. Store it in your CI variable store or a password manager. Rotate when people leave.
What problems SOPS solves that .gitignore does not
Ignoring .env keeps secrets out of Git. It also keeps config out of history. New developers guess values. Staging drifts from production. Encrypted files in-repo document every key name. Only values stay hidden. Pair this with Gitleaks in CI so plaintext never slips through a bad commit.
How do you install SOPS and age on Ubuntu?
Both tools ship as static binaries. On Ubuntu 22.04 or 24.04 servers I use for Linux system administration, installation takes minutes. Verify checksums when your org requires it.
Install age and generate a key pair
# Download age (check latest release on GitHub)
curl -LO https://github.com/FiloSottile/age/releases/download/v1.2.1/age-v1.2.1-linux-amd64.tar.gz
tar xzf age-v1.2.1-linux-amd64.tar.gz
sudo mv age/age age/age-keygen /usr/local/bin/
# Generate a key pair for SOPS
age-keygen -o ~/.config/sops/age/keys.txt
# Public key line looks like: # public key: age1xxxxxxxx...
Keep keys.txt permissions at 600. Back up the private key offline. Losing it means re-encrypting every file with a new public key. The public key is safe to share in .sops.yaml.
Install SOPS
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
sops --version
Official docs live at getsops/sops on GitHub. Age documentation is at FiloSottile/age on GitHub. Pin versions in your team runbook so laptops and CI stay aligned.
How do you create and encrypt secret files with SOPS and age?
Start with a creation rules file at the repo root. SOPS reads it to pick recipients automatically. This removes long command lines and prevents encrypting to the wrong key.
Configure .sops.yaml
# .sops.yaml at repository root
creation_rules:
- path_regex: \.(env|yaml|yml|json)$
age: age1yourpublickeyherexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- path_regex: secrets/.*\.yaml$
age: age1yourpublickeyherexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
For Laravel projects, I store secrets/.env.production.enc or encrypt .env.production directly. The plaintext file stays in .gitignore. Only the .enc sibling gets committed. On a legal-tech portal like Mijar Law Associates, payment gateway keys and SMS tokens live in that encrypted file — not in Slack messages.
Encrypt and edit secrets
Run these commands from your project root after exporting the age key path.
- Set
export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txtin your shell profile. - Create plaintext
secrets/.env.productionwith real values locally. - Encrypt:
sops -e secrets/.env.production > secrets/.env.production.enc - Commit only
secrets/.env.production.encand.sops.yaml. - Edit in place:
sops secrets/.env.production.encopens your$EDITOR, re-encrypts on save.
# In-place edit (decrypts to temp file, re-encrypts on save)
sops secrets/.env.production.enc
# Decrypt to stdout for inspection (do not pipe to logs in CI)
sops -d secrets/.env.production.enc
# Update one key without opening an editor
sops --set '["DB_PASSWORD"] "new-secret"' secrets/.env.production.enc
SOPS uses age to wrap a data encryption key. Each value gets AES-256-GCM. That design lets you add multiple age or PGP recipients later without restructuring files. Use a strong generated password from the password generator tool for database credentials, then store them through SOPS — not the other way around.
Laravel .env patterns that work in production
On Deployer-based releases, decrypt during deploy — not on every web request. A pre-deploy hook writes shared .env from the encrypted source. PHP reads a normal env file. Opcache and FPM see no runtime crypto overhead.
# deploy.php snippet (Deployer 7)
task('deploy:secrets', function () {
run('cd {{deploy_path}}/shared && sops -d {{release_path}}/secrets/.env.production.enc > .env');
});
before('deploy:symlink', 'deploy:secrets');
This mirrors how sister sites on shared EC2 — including Notary Kathmandu — share a GitLab CI pipeline. One encrypted secrets file per app. Same age key on the runner and server, scoped by file path rules.
How do you decrypt SOPS secrets in CI/CD pipelines safely?
CI needs the age private key, never the plaintext secrets as a variable dump. GitLab CI masked variables work. GitHub Actions secret stores work too. The pattern: write the key to a temp file, decrypt, use the output, delete the key file.
GitLab CI example
stages: [deploy]
deploy_production:
stage: deploy
image: ubuntu:24.04
before_script:
- apt-get update && apt-get install -y curl
- curl -LO https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64
- install -m 755 sops-v3.9.4.linux.amd64 /usr/local/bin/sops
- curl -LO https://github.com/FiloSottile/age/releases/download/v1.2.1/age-v1.2.1-linux-amd64.tar.gz
- tar xzf age-v1.2.1-linux-amd64.tar.gz && install -m 755 age/age /usr/local/bin/age
- mkdir -p ~/.config/sops/age
- echo "$SOPS_AGE_KEY" > ~/.config/sops/age/keys.txt
- chmod 600 ~/.config/sops/age/keys.txt
- export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt
script:
- sops -d secrets/.env.production.enc > .env
- dep deploy production
after_script:
- rm -f ~/.config/sops/age/keys.txt .env
only: [main]
Store the full contents of keys.txt in a protected CI variable named SOPS_AGE_KEY. Restrict job visibility to protected branches. Audit who can read protected variables. Read CI/CD secrets management best practices and handling secrets in pipelines safely for broader guardrails.
Multi-environment key separation
Use different age keys per environment. Production ciphertext should decrypt only with a production private key stored on the production runner. Staging uses another key pair. Add both public keys to .sops.yaml only when the same file must serve both — otherwise split files.
creation_rules:
- path_regex: secrets/production/.*$
age: age1prodpublickey...
- path_regex: secrets/staging/.*$
age: age1stagingpublickey...
For Kubernetes workloads, combine SOPS with Flux or Argo CD decryption hooks. See Sealed Secrets for GitOps and Kubernetes secrets done right when you need cluster-native delivery instead of SSH deploys.
How does SOPS with age compare to Ansible Vault and cloud secret managers?
Pick the tool that matches where secrets live. Repo-bound config favors SOPS. Ansible-only shops may stay on Vault. Large orgs with audit requirements often need AWS or Azure backends.
| Criteria | SOPS + age | Ansible Vault | AWS Secrets Manager | HashiCorp Vault |
|---|---|---|---|---|
| Secrets in Git | Yes, encrypted | Yes, encrypted | No, external store | Optional via agent |
| Setup cost | Low, two binaries | Low, built into Ansible | Per-secret monthly fee | High, cluster to run |
| Key rotation | Re-encrypt files | Re-encrypt files | Automatic versions | Dynamic secrets possible |
| Best fit | GitOps, Laravel deploys, small teams | Ansible playbooks only | AWS-native apps | Multi-team, PKI, DB creds |
| Local dev UX | sops file.enc opens editor | ansible-vault edit | AWS CLI / SDK | Vault CLI / UI |
I still use Ansible Vault when the repo is playbook-heavy and nothing else needs structured YAML secrets. SOPS wins when the same Laravel app, Docker Compose stack, and CI pipeline all read one encrypted .env. Cloud managers win when compliance demands central audit logs and automatic rotation — topics covered in multi-cloud secrets management.
Common mistakes to avoid
- Committing
keys.txtor exporting the private key into a public gist. - Logging
sops -doutput in CI — mask scripts and disableset -xaround decrypt steps. - Encrypting without
.sops.yaml, then wondering why teammates cannot edit files. - Using one age key for every developer laptop — rotate per person or use short-lived CI keys.
- Skipping pre-commit secret scanning because "we use SOPS now". Plaintext accidents still happen.
For Docker Swarm or Compose stacks, compare with Docker secrets in Compose. For dotfiles and server configs under Git, see managing dotfiles with Git. External Secrets Operator patterns appear in External Secrets Operator with Vault when you outgrow file-based decrypt.
Key Takeaways
- Manage secrets with SOPS and age by committing encrypted structured files, not plaintext
.envcopies. - Generate age keys once, define
.sops.yamlcreation rules, and never commit private key material. - Decrypt ephemerally in CI with
SOPS_AGE_KEY_FILE, then write shared.envduring Deployer or SSH deploy hooks. - Separate age keys per environment so staging runners cannot decrypt production ciphertext.
- Pair SOPS with Gitleaks scanning and protected CI branches — encryption is not a substitute for leak prevention.
- Move to Vault or cloud secret managers when you need dynamic credentials, central audit, or strict compliance beyond Git.
People Also Ask
Can SOPS encrypt .env files for Laravel?
Yes. SOPS supports dotenv format natively. Encrypt .env.production, commit the .enc variant, and decrypt to the shared path your PHP-FPM pool reads. Laravel never calls SOPS at runtime.
Is age more secure than GPG for SOPS?
Age uses modern primitives with a minimal API. Fewer footguns mean fewer misconfigured keys in practice. SOPS supports both. New projects should default to age unless your org mandates GPG smart cards.
What happens if I lose the age private key?
Encrypted files become unreadable. You must generate a new key pair, update .sops.yaml, re-create plaintext from a backup or live server, and re-encrypt every secrets file. Treat key backup as part of your disaster plan.
Does SOPS work with PHP 8.5 and Laravel 13 deployments?
SOPS runs outside PHP as a CLI step before or during deploy. Laravel 13 on PHP 8.3+ consumes a normal decrypted .env. No PHP extension is required. Keep SOPS on the deploy runner and target server only.
Ship encrypted secrets without a heavyweight vault
You can manage secrets with SOPS and age on a single Ubuntu box, a GitLab runner, and a Git repo — no paid secrets service required on day one. The workflow scales from a solo freelancer to a small agency running multiple Laravel and WordPress clients. When you need help wiring SOPS into Deployer, GitLab CI, or a multi-site hosting setup, see support and maintenance services or web development services. For a production audit of how your team stores payment keys and API tokens today, contact us with your stack details.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

