Sep 16, 2026
10 mins read
In a past blog post I covered using Bitwarden for home lab automation secrets (Secrets Manager for Home Lab Automation). That approach works well if you already use a password manager and want a CLI to pull credentials into environment variables. This follow-up covers SOPS as an alternative when you want encrypted secrets in the repo without relying on a password manager or paying for a subscription.
The goal is the same: get secrets into environment variables for Terraform, Ansible, and cloud CLIs at runtime. The difference is where those secrets live. With SOPS you keep them versioned next to your infrastructure code as ciphertext. That means you are putting secrets in git — encrypted, but still in the repo — which is a tradeoff worth accepting for many solo home lab setups because it removes login/session/sync choreography and avoids another hosted service.
This post is opinionated toward SOPS with age keys. Age is simpler than GPG for a personal lab: generate a keypair, keep the private key off the repo, and commit a small config that points at your public key.
Hashicorp Vault and cloud secret managers are great when you need fine-grained access control, audit logs, and team workflows. For a personal home lab they often feel like operational burden. A password manager CLI (like Bitwarden) is a good middle ground if you already pay for it or use the free tier and sync across phone and laptop.
SOPS sits in a different spot:
sops secrets.yaml) instead of app UI + syncThe cost of that simplicity is the threat model. Encrypted secrets live forever in git history. If someone steals your age private key, they can decrypt every revision that was encrypted to that key. Treat the private key like a master password: back it up carefully, never commit it, and do not leave it on shared or untrusted machines.
You need two tools: sops and age. Install them with your package manager of choice (examples below; versions change, so check current docs if needed).
# macOS with Homebrew
$ brew install sops age
# Or on a Debian/Ubuntu-like system (package names may vary by distro)
$ sudo apt install age
# Install sops from the GitHub release for your arch if the distro package is old
The private key decrypts secrets. The public key encrypts them. Only the public key belongs in the repo.
# Create the default directory SOPS looks for age keys
$ mkdir -p ~/.config/sops/age
# Generate a keypair and write the private key file (do NOT commit this file)
$ age-keygen -o ~/.config/sops/age/keys.txt
Public key: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SOPS will find the private key automatically when that path exists. You can also set SOPS_AGE_KEY_FILE if you store the key somewhere else.
Keep a backup of keys.txt somewhere safe (encrypted USB, password manager note for the key file itself, etc.). Losing the private key means losing the ability to decrypt your secrets file.
In the root of your automation repo, create a .sops.yaml that tells SOPS which age public key to use and which files to encrypt. Only the public key goes here.
# .sops.yaml — safe to commit
creation_rules:
- path_regex: secrets\.yaml$
age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
You can widen the regex later (.*secrets\.ya?ml$, secrets/.*, etc.). For this post we keep a single secrets.yaml.
Create a plain YAML file with the same kind of credentials used in the Bitwarden post: cloud provider API keys and S3-compatible backend credentials for Terraform state.
$ cat > secrets.yaml <<'EOF'
vultr:
api_key: "vultr-api-key-example-1234567890"
cloudflare:
api_token: "cloudflare-api-token-example-abcdef"
aws:
access_key_id: "AKIAEXAMPLEKEYID1234"
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
EOF
Encrypt the file in place. After this, secrets.yaml contains ciphertext and is safe to commit.
$ sops -e -i secrets.yaml
If you open the file you will see encrypted values and a sops metadata block. Keys stay readable; values are encrypted. That is intentional so diffs stay somewhat reviewable.
To edit secrets later, open them through SOPS so they decrypt into your editor and re-encrypt on save:
$ sops secrets.yaml
To decrypt to stdout (useful for scripting):
$ sops -d secrets.yaml
vultr:
api_key: vultr-api-key-example-1234567890
cloudflare:
api_token: cloudflare-api-token-example-abcdef
aws:
access_key_id: AKIAEXAMPLEKEYID1234
secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Commit .sops.yaml and the encrypted secrets.yaml. Do not commit plaintext copies, backup *.dec.yaml files, or your age private key.
Same pattern as the Bitwarden post: load credentials into environment variables so Terraform providers, cloud CLIs, and Ansible lookups pick them up from the process environment. Variables live for the shell session and disappear when the shell exits.
You can parse decrypted YAML with yq, or use SOPS helpers that avoid dumping secrets to intermediate files.
# Decrypt and assign fields to the env vars providers expect
$ export VULTR_API_KEY=$(sops -d secrets.yaml | yq -r '.vultr.api_key')
$ export CLOUDFLARE_API_TOKEN=$(sops -d secrets.yaml | yq -r '.cloudflare.api_token')
$ export AWS_ACCESS_KEY_ID=$(sops -d secrets.yaml | yq -r '.aws.access_key_id')
$ export AWS_SECRET_ACCESS_KEY=$(sops -d secrets.yaml | yq -r '.aws.secret_access_key')
If you shape the file as a flat map of environment variable names to values, sops exec-env can inject them for a single command without exporting them in your interactive shell history the same way.
# Alternative shape for exec-env (still encrypted with SOPS)
VULTR_API_KEY: "vultr-api-key-example-1234567890"
CLOUDFLARE_API_TOKEN: "cloudflare-api-token-example-abcdef"
AWS_ACCESS_KEY_ID: "AKIAEXAMPLEKEYID1234"
AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# Run terraform with secrets injected only for that process tree
$ sops exec-env secrets.yaml 'terraform apply'
I prefer nested YAML for readability when many services share one file, and yq exports when I need the same env vars for several tools in one shell. Use exec-env when you want a tighter scope around a single command.
Export first, then look up from the environment:
$ export SSH_PUB_KEY=$(sops -d secrets.yaml | yq -r '.ssh.public_key')
- name: Print SSH Pub Key
ansible.builtin.debug:
msg: "'{{ lookup('ansible.builtin.env', 'SSH_PUB_KEY') }}'"
Same credential chain idea as in the Bitwarden post. After exporting, the AWS, Cloudflare, Vultr, and similar providers read from the usual environment variables. No secrets need to live in .tf files.
Context: Terraform creates a VM on Vultr, DNS records on Cloudflare, and uses an S3-compatible backend for state. Terraform code itself is out of scope; the point is loading credentials the same way as the Bitwarden example, but from SOPS.
Because those secrets live encrypted in git, the ideal scenario is to use credentials that are purpose-built for this automation and narrowly scoped to the function they serve. That limits blast radius if a key leaks and makes revocation easy later — you rotate or delete one automation-specific credential instead of a full personal or admin key.
In this example that would mean:
How far you can go depends on each platform’s IAM or token model. Not every provider exposes the same granularity, but aiming for least privilege per automation is still the right default when secrets are stored in the repo.
# Private age key must already be available, e.g. ~/.config/sops/age/keys.txt
# Get the cloud provider api key
$ export VULTR_API_KEY=$(sops -d secrets.yaml | yq -r '.vultr.api_key')
# Get S3 storage backend keys
$ export AWS_ACCESS_KEY_ID=$(sops -d secrets.yaml | yq -r '.aws.access_key_id')
$ export AWS_SECRET_ACCESS_KEY=$(sops -d secrets.yaml | yq -r '.aws.secret_access_key')
# Get cloudflare api token
$ export CLOUDFLARE_API_TOKEN=$(sops -d secrets.yaml | yq -r '.cloudflare.api_token')
After those exports you can run the Terraform stack. No bw login, no session token, no vault sync — just decrypt with the local age key.
Both solve “do not hardcode secrets in plaintext next to the code.” They differ in where secrets live and how much day-to-day friction you accept.
Where secrets live. Bitwarden keeps them in a vault (cloud or self-hosted). SOPS keeps encrypted files in git. With SOPS you are deliberately putting secrets in the repository; with Bitwarden the repo stays free of secret material.
Cost and operations. Bitwarden has a solid free tier, paid plans for more features, and optional self-hosting. SOPS is free and local: age keys on your machine, no subscription. For a home lab that wants to avoid another bill or another service to patch, that matters.
Multi-device UX. Bitwarden wins if you need phone apps, browser extensions, and easy sync across laptops. SOPS is CLI-first. Sharing an age private key across machines is manual and easy to do wrong; it is fine for one or two trusted lab hosts, awkward as a general password manager replacement.
Automation friction. Bitwarden needs login, unlock, BW_SESSION, and often bw sync before reads. SOPS needs the age private key present and a decrypt command. For repo-bound automation, SOPS is usually less ceremony once the key is installed.
History and audit. Bitwarden has vault history and a product UX around items. SOPS leans on git: every encrypted change is in history. That is useful for IaC review and painful if you ever need to rotate after a private-key leak, because old ciphertext may still decrypt.
Risk model. Bitwarden risks are vault account access, session material left on disk, and sync/trust of the service. SOPS risks are private-key compromise and the long life of secrets-in-git. Encrypted-in-git is not the same as “not in git.”
Best fit.
Using SOPS with age is a practical way to handle home lab automation secrets without a password manager or a self-hosted Vault. You encrypt a YAML file, commit the ciphertext, keep the age private key off the repo, and decrypt into environment variables when you run Terraform, Ansible, or cloud CLIs.
Compared to the Bitwarden approach in my earlier post, SOPS is simpler for solo, repo-centric labs and costs nothing beyond caring for a private key. The tradeoff is real: secrets live in git forever as ciphertext, and key hygiene becomes your whole security story. For enterprise or team environments with shared access needs, purpose-built secret managers are still a better fit.
For my own home lab automations that live in git, SOPS is often the path of least resistance. When I already need Bitwarden on every device for personal passwords, reusing it for automation remains a good option. Pick the model that matches where you want the secrets to live and how much operational surface you want to maintain.
Sharing is caring!