Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Manage Secrets with SOPS and age

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.

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.

SOPS + age Secrets in GitPlaintext.env / YAMLSOPS + ageEncrypt valuesGit repoSafe commitsCI / DeploySOPS_AGE_KEY_FILEProductionDecrypted .env
Manage secrets with SOPS and age: encrypt at commit time, store ciphertext in Git, decrypt only on trusted hosts.

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.

age Encryption Flowage-keygenKey pair.sops.yamlPublic key rulePlaintext file.env / YAMLsops -eEncryptEncrypted output in GitKeys visible, values ENC[AES256_GCM,...]Safe to commit and review diffs
age key generation and SOPS creation rules turn plaintext env files into reviewable encrypted artifacts.

Encrypt and edit secrets

Run these commands from your project root after exporting the age key path.

  1. Set export SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt in your shell profile.
  2. Create plaintext secrets/.env.production with real values locally.
  3. Encrypt: sops -e secrets/.env.production > secrets/.env.production.enc
  4. Commit only secrets/.env.production.enc and .sops.yaml.
  5. Edit in place: sops secrets/.env.production.enc opens 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.

CI/CD Decrypt PipelineGit pushCI runnerAge keyFrom vault varsops -dPlaintext .envEphemeral onlyDeploy / FPMShared .env pathNever echo decrypted output or commit plaintext .env
CI runners decrypt SOPS age files ephemerally, deploy to shared paths, then remove keys and plaintext artifacts.

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.

CriteriaSOPS + ageAnsible VaultAWS Secrets ManagerHashiCorp Vault
Secrets in GitYes, encryptedYes, encryptedNo, external storeOptional via agent
Setup costLow, two binariesLow, built into AnsiblePer-secret monthly feeHigh, cluster to run
Key rotationRe-encrypt filesRe-encrypt filesAutomatic versionsDynamic secrets possible
Best fitGitOps, Laravel deploys, small teamsAnsible playbooks onlyAWS-native appsMulti-team, PKI, DB creds
Local dev UXsops file.enc opens editoransible-vault editAWS CLI / SDKVault 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.

Choose Your Secrets ToolSecrets in Git?SOPS + ageSmall team, DeployerSealed SecretsKubernetes GitOpsCloud managerAWS / Azure nativeHashiCorp VaultDynamic DB creds, PKI, many teamsYes, SSH deployK8s clusterCloud-onlyEnterprise scale
Decision guide: manage secrets with SOPS and age when Git-backed config and simple deploy pipelines are your primary model.

Common mistakes to avoid

  • Committing keys.txt or exporting the private key into a public gist.
  • Logging sops -d output in CI — mask scripts and disable set -x around 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 .env copies.
  • Generate age keys once, define .sops.yaml creation rules, and never commit private key material.
  • Decrypt ephemerally in CI with SOPS_AGE_KEY_FILE, then write shared .env during 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

SOPS (Secrets OPerationS) encrypts structured files like YAML, JSON, or .env while keeping key names readable so you can diff commits. Age is a small modern encryption tool with one-line public keys and a single private key file. Together they let you commit encrypted ciphertext to Git and decrypt only on trusted laptops, CI runners, or deploy hosts.

SOPS and age are free open-source static binaries. No per-secret monthly fees, no secrets server required on day one.

Choose SOPS plus age when secrets belong in Git-backed config and your deploy pipeline is simple — Laravel with Deployer 7, GitLab CI, or small Ubuntu servers. Vault or AWS Secrets Manager fit better when compliance demands central audit logs, automatic rotation, or dynamic database credentials. SOPS sits closer to the repo; cloud managers sit closer to enterprise audit requirements.

Download static binaries from the official getsops/sops and FiloSottile/age GitHub releases. The article pins age v1.2.1 and SOPS v3.9.4 for linux-amd64. Extract age and age-keygen to /usr/local/bin, install the sops binary with execute permissions, then verify with sops --version. Pin the same versions in your team runbook so laptops, CI runners, and servers stay aligned. Verify checksums if your organisation requires it.

Run age-keygen -o ~/.config/sops/age/keys.txt after installing age. The output includes a public key line starting with age1 that is safe to share in .sops.yaml. Keep keys.txt at permissions 600, never commit it, and back up the private key offline. Store CI copies in GitLab masked variables or a password manager. Losing the private key means every encrypted file becomes unreadable until you re-encrypt with a new key pair.

SOPS reads creation_rules to pick age recipients automatically, removing long command lines and wrong-key mistakes. Define path_regex patterns matching files you encrypt — for example .env, .yaml, .yml, .json extensions or secrets/.yaml paths — and list the age public key under each rule. For multi-environment setups, point production and staging path patterns at different public keys so staging runners cannot decrypt production ciphertext unless you deliberately share both keys in one file.

Create plaintext secrets/.env.production locally with real values, keep it in .gitignore, then run sops -e to produce secrets/.env.production.enc for commit alongside .sops.yaml. Export SOPS_AGE_KEY_FILE pointing at your keys.txt before any SOPS command. Edit in place with sops secrets/.env.production.enc, which opens your editor and re-encrypts on save. Update a single variable with sops --set. SOPS supports dotenv format natively; Laravel reads a normal decrypted .env and never calls SOPS at runtime.

Decrypt during deploy, not on every web request. Add a deploy:secrets task in deploy.php that runs sops -d on the encrypted file in the release path and writes shared/.env before the symlink swap. Hook it with before('deploy:symlink', 'deploy:secrets'). PHP-FPM and opcache see a standard env file with no runtime crypto overhead. This mirrors the pattern used on sister sites sharing a GitLab CI pipeline on shared EC2 infrastructure.

Store the full contents of keys.txt in a protected CI variable named SOPS_AGE_KEY, restricted to protected branches. In before_script, install pinned SOPS and age binaries, write the key to ~/.config/sops/age/keys.txt with chmod 600, export SOPS_AGE_KEY_FILE, then run sops -d to produce .env before dep deploy. In after_script, delete both the key file and plaintext .env. Never log sops -d output or enable set -x around decrypt steps. Audit who can read protected variables.

Encrypted files become unreadable. Generate a new key pair, update .sops.yaml with the new public key, recover plaintext from an offline backup or a live server that still has the values, then re-encrypt every secrets file. Treat key backup as part of your disaster plan from day one, not an afterthought when someone leaves or a laptop dies.

Age uses modern primitives with a minimal API and fewer configuration footguns than long GPG workflows. SOPS supports both recipients. New projects should default to age unless your organisation mandates GPG smart cards or existing PKI infrastructure. In practice, one-line public keys and a single private key file reduce the misconfigured-key problems I have seen with GPG-based setups on small teams.

Both keep encrypted secrets in Git. Ansible Vault is built into Ansible and suits playbook-heavy repos where nothing else needs structured YAML secrets. SOPS wins when the same Laravel app, Docker Compose stack, and CI pipeline all read one encrypted .env or JSON file. SOPS also leaves key names readable for cleaner diffs. Setup cost for both is low; SOPS adds two binaries while Vault needs no extra install for Ansible-only shops.

Ignoring .env keeps secrets out of Git but also removes config from history. New developers guess values, staging drifts from production, and nobody documents which keys exist. Encrypted files in-repo show every variable name while hiding only values. Pair SOPS with Gitleaks in CI because plaintext accidents still happen — encryption is not a substitute for leak prevention, and a bad commit can still expose secrets before you adopt SOPS fully.

Use different age key pairs per environment. Production ciphertext should decrypt only with a production private key on the production runner. Staging uses another pair with matching path_regex rules in .sops.yaml — for example secrets/production/. versus secrets/staging/.*. Avoid one shared key across every developer laptop; rotate per person or use short-lived CI keys when someone leaves. Splitting files by environment is cleaner than adding multiple public keys to one file unless the same encrypted file must serve both.

Yes. SOPS runs outside PHP as a CLI step before or during deploy. Laravel 13 on PHP 8.3 or higher consumes a normal decrypted .env file. No PHP extension is required. Install SOPS on the deploy runner and target Ubuntu server only; your web workers never touch the encryption tooling. Keep SOPS and age versions pinned consistently across those hosts.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: