SOPS

SOPS encrypts secrets in the YAML configuration while leaving the file safe to store in Git. restic-backups asks SOPS to decrypt the file in memory and parses the result; it does not create a decrypted copy.

Install SOPS and age

make install-deps installs SOPS. Install age when age-keygen is not already available:

brew install age

Generate an age identity outside the repository:

mkdir -p "$HOME/.config/sops/age"
age-keygen -o "$HOME/.config/sops/age/keys.txt"
age-keygen -y "$HOME/.config/sops/age/keys.txt"

The final command prints the public age1... recipient. It is safe to put that recipient in .sops.yaml. Never commit the private AGE-SECRET-KEY-1... identity or the identity file.

Add .sops.yaml

The SOPS configuration file must be named .sops.yaml, not .sops.yml. Put it in the repository root so SOPS finds it when commands run anywhere below that directory.

This template encrypts only the credential fields and restic repository passwords used by restic-backups:

creation_rules:
  - path_regex: config\.sops\.yaml$
    age: age1REPLACE_WITH_YOUR_PUBLIC_RECIPIENT
    encrypted_regex: '^(access-key-id|secret-access-key|password|token)$'
    mac_only_encrypted: true

Storage and repository IDs, descriptions, endpoints, paths, and other operational settings remain readable. SOPS encrypts matching leaf values—including inline job authentication tokens—anywhere in the YAML tree. mac_only_encrypted: true allows those readable values to be edited directly without invalidating the SOPS message authentication code (MAC).

Warning

With mac_only_encrypted: true, SOPS authenticates encrypted values but no longer detects changes to plaintext IDs, endpoints, paths, descriptions, or job mappings. Review those changes through Git. Omit this setting when SOPS should protect the integrity of every value, accepting that all edits must then go through SOPS.

SOPS also encrypts comments, so use the configuration’s description fields for readable notes.

To encrypt every value instead, omit encrypted_regex:

creation_rules:
  - path_regex: config\.sops\.yaml$
    age: age1REPLACE_WITH_YOUR_PUBLIC_RECIPIENT

The public recipient can instead come from SOPS_AGE_RECIPIENTS. When that variable is always set for encryption, omit age from the creation rule:

creation_rules:
  - path_regex: config\.sops\.yaml$
    encrypted_regex: '^(access-key-id|secret-access-key|password|token)$'
    mac_only_encrypted: true

See the official SOPS configuration format for rule matching and other key providers.

Create the encrypted config

Use config.sops.yaml as the conventional name. The CLI accepts another name, but it must match path_regex and be passed through --config or RESTIC_BACKUPS_CONFIG.

Copy the template, then edit it through SOPS so secrets are encrypted when the editor closes:

cp config.template.yaml config.sops.yaml
sops config.sops.yaml

Confirm the file is encrypted and validate it without contacting remote storage:

sops filestatus config.sops.yaml
uv run restic-backups --config config.sops.yaml --sops check-config

Commit .sops.yaml and the encrypted config.sops.yaml. Do not commit a plain config.yaml, the age identity, or decrypted command output.

Environment variables

These variables avoid repeating the global CLI options:

Variable Purpose Sensitive
RESTIC_BACKUPS_CONFIG Path to the encrypted application config No
RESTIC_BACKUPS_SOPS=1 Tell restic-backups to decrypt through SOPS No
SOPS_AGE_KEY_FILE Path to a file containing private age identities Yes: protect the file
SOPS_AGE_KEY Private age identities supplied directly by a secret manager Yes
SOPS_AGE_KEY_CMD Command that prints private age identities Depends on the command
SOPS_AGE_RECIPIENTS Public recipients used when encrypting new files No
SOPS_CONFIG Non-default path to .sops.yaml No

Normally, set the identity file and the two restic-backups variables:

export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"
export RESTIC_BACKUPS_CONFIG="$PWD/config.sops.yaml"
export RESTIC_BACKUPS_SOPS=1

uv run restic-backups check-config
uv run restic-backups generic repository list

SOPS_AGE_KEY_FILE, SOPS_AGE_KEY, and SOPS_AGE_KEY_CMD are alternatives; set only the one appropriate for the environment. If the identity is stored in SOPS’s operating-system default age location, none of them is required.

Edit and rotate

With mac_only_encrypted: true, ordinary text editors may change plaintext leaf values directly:

$EDITOR config.sops.yaml
uv run restic-backups check-config

Do not replace an ENC[...] value manually. Use SOPS when changing a password, access key, or secret key, and for structural edits that add, remove, reorder, or rename branches containing encrypted values:

sops edit config.sops.yaml

For a single value, sops set avoids opening the complete decrypted document:

sops set config.sops.yaml \
  '["storage"][0]["description"]' '"Off-site object storage"'

The encrypted file already records its public recipients. When recipients in .sops.yaml change, synchronize them with:

sops updatekeys config.sops.yaml

Adding or changing encrypted_regex or mac_only_encrypted in .sops.yaml does not alter an existing encrypted document. Decrypt and re-encrypt it once so the new rule is stored in the file. Keep plaintext in the pipe and replace the original only after encryption succeeds:

umask 077
sops decrypt config.sops.yaml |
  sops encrypt --filename-override config.sops.yaml \
    --input-type yaml --output-type yaml /dev/stdin > config.sops.yaml.new
mv config.sops.yaml.new config.sops.yaml

Validate again after every key or rule change:

uv run restic-backups check-config