The docs still described Cloudflare as DNS + CDN in front of helsinki-a, but that was dropped in #90 - pez.sh lives on Hetzner DNS via Terraform now and records point straight at the origin. Updated README, architecture, networking, getting-started and the nuremberg-a host doc to match, and noted that pez.solutions still resolves via Cloudflare outside Terraform. Also fixed while I was in there: - terraform/README: PagerDuty provider is ~> 3.32 (table said ~> 2.2), and the B2 secret keys are backblaze_keyID/backblaze_applicationKey - secrets docs: group_vars secrets file is .enc.yaml, dropped the FreeBSD install steps, the long-gone .sops.yaml placeholder note and the ANSIBLE_VAULT_PASS migration note, swapped the cloudflare_record example for hcloud - getting-started referenced ansible/scripts/sops-setup.sh which doesn't exist - added naveen.pez.sh to the subdomain tables and a note about the DNS-only records (mail, minecraft, wow, public)
4.8 KiB
Secrets Management
This repo uses SOPS with age encryption for secrets. Encrypted files live in the repo alongside the configs they belong to — only the secret values are encrypted, so diffs remain useful.
Why SOPS + age?
- age over GPG: No key expiry, no keyservers, no UID headaches. A single static public key per recipient.
- SOPS over git-crypt: Encrypts values, not whole files. You can see the structure of a secrets file without decrypting it. Works with YAML, JSON, ENV, and INI.
- SOPS over Ansible Vault: Ansible Vault only works with Ansible. SOPS works everywhere — Terraform (via
terraform-provider-sops), Docker env files, CI pipelines, scripts.
File naming convention
Encrypted files use .enc. in their extension:
services/authelia/config.enc.yml # encrypted YAML
services/<service>/<file>.enc.env # encrypted env file (convention)
terraform/secrets.enc.yaml # encrypted Terraform vars
ansible/group_vars/all/secrets.enc.yaml
Plaintext files MUST NOT contain secrets. The .gitignore blocks common secret filenames (secrets.yml, vault.yml, secret.env, etc.) as a safety net.
Setup (one-time)
Install tools
# macOS
brew install sops age
# Debian/Ubuntu
apt install age
# SOPS: download from https://github.com/getsops/sops/releases
wget https://github.com/getsops/sops/releases/download/v3.9.4/sops_3.9.4_amd64.deb
dpkg -i sops_3.9.4_amd64.deb
Generate your age key
age-keygen -o ~/.config/sops/age/keys.txt
# Output: public key: age1abc123...
This file is your private key. Never commit it. The .gitignore already blocks keys.txt and *.agekey.
SOPS automatically looks for keys in ~/.config/sops/age/keys.txt (Linux/macOS) or you can set SOPS_AGE_KEY_FILE to point elsewhere.
Add your public key to .sops.yaml
Add your public key to the creation_rules in .sops.yaml, re-encrypt the affected files (see "Add a new recipient" below), and commit the updated .sops.yaml.
Day-to-day usage
Create a new encrypted file
# SOPS picks the right age keys from .sops.yaml based on file path
sops services/authelia/config.enc.yml
# Opens your $EDITOR with a decrypted view. Save and quit to encrypt.
Edit an existing encrypted file
sops services/authelia/config.enc.yml
Decrypt to stdout (for scripts/debugging)
sops -d services/authelia/config.enc.yml
Encrypt an existing plaintext file
# If you have a plaintext file you want to encrypt in-place:
sops -e -i services/<service>/<file>.enc.env
Add a new recipient
When someone new needs access (or a new CI key is generated):
- Get their age public key
- Add it to the relevant
creation_rulesin.sops.yaml - Re-encrypt all affected files:
# Update keys on all encrypted files
find . -name '*.enc.*' -exec sops updatekeys {} \;
CI / GitHub Actions
The CI runner needs to decrypt secrets during deploys. Store the age secret key as a GitHub Actions secret:
- Generate a CI-specific age key:
age-keygen - Add the private key (the
AGE-SECRET-KEY-1...line) as a GitHub repository secret namedAGE_SECRET_KEY - Add the public key to
.sops.yaml(the CI recipient)
In the workflow:
- name: Decrypt secrets
env:
SOPS_AGE_KEY: ${{ secrets.AGE_SECRET_KEY }}
run: |
sops -d ansible/group_vars/all/secrets.enc.yaml > ansible/group_vars/all/secrets.yml
Terraform integration
Use the terraform-provider-sops to read encrypted values directly:
provider "sops" {}
data "sops_file" "secrets" {
source_file = "secrets.enc.yaml"
}
# Use decrypted values
provider "hcloud" {
token = data.sops_file.secrets.data["hetzner_token"]
}
What gets encrypted
These are the types of secrets expected in this repo:
| Category | Example | Location |
|---|---|---|
| Ansible group vars | SSH keys, API tokens, passwords | ansible/group_vars/all/secrets.enc.yaml |
| Docker env files | DB passwords, app secrets | services/*/service.enc.env |
| Terraform vars | Hetzner token, Grafana Cloud tokens, B2 keys | terraform/secrets.enc.yaml |
| Service configs | Authelia JWT secret, LLDAP admin pass | services/*/config.enc.yml |
Security notes
- Never commit
keys.txtor any file containingAGE-SECRET-KEY. The.gitignoreblocks these. - Rotate keys if a machine is compromised: generate new key, update
.sops.yaml, re-encrypt all files, revoke the old key from.sops.yaml. - CI key is separate from personal keys so it can be rotated independently.
- SOPS encrypted files contain metadata about which keys can decrypt them — this is intentional and not a secret.