WIUNIX ACADEMY
SOPS and age: rotate recipients, data keys and recovery access
Page 2 of 4. Review the earlier steps and prerequisites before continuing.
YOUR SYSTEM. YOUR STEPS.
Where are you working?
Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.
USE YOUR OWN VALUES
Configure this tutorial
Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.
Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.
Set values for my environment2 editable values
LET’S BUILD
Authenticate the SOPS checksum, then the selected binary
Cosign verifies the checksum bundle against GitHub OIDC and the getsops/sops repository identity. Then select exactly the downloaded binary’s entry and check its SHA256. An absent or duplicate checksum entry stops the panel; a successful check of some unrelated downloaded file is insufficient.
Keep verifier errors and stop on any signature, identity, issuer or digest mismatch. Do not disable transparency or signature checks to make an unavailable verification service look successful. Resolve the trusted-tool/network issue before continuing.
Same unprivileged Bash shell on the isolated administration lab
cosign verify-blob sops-v3.13.3.checksums.txt \
--bundle sops-v3.13.3.checksums.sigstore.json \
--certificate-identity-regexp='^https://github.com/getsops/sops/' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com || exit 1
awk -v file="$sops_artifact" '$2==file {print; n++} END {if(n!=1) exit 1}' sops-v3.13.3.checksums.txt > sops-selected.sha256 || exit 1
sha256sum --check sops-selected.sha256 || exit 1The intended checksum publisher and the exact selected SOPS binary digest verify successfully.
LET’S BUILD
Verify the age archive’s Sigsum proof
The two lines below are public publisher-verification keys from age’s version-pinned SIGSUM.md, not decryption identities. Confirm them through that authoritative reference and the organization’s trust process. Sigsum verifies the signed, logged archive proof using its named trust policy; the later SHA256 is an evidence fingerprint, not an independent publisher signature.
Stop if the trusted verifier lacks this policy or reports a proof failure. Never replace these public verification keys with a key obtained from the untrusted archive itself.
Same unprivileged Bash shell on the isolated administration lab
cat > age-sigsum-key.pub <<'PUBLIC_KEYS'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1WpnEswJLPzvXJDiswowy48U+G+G1kmgwUE2eaRHZG
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAz2WM5CyPLqiNjk7CLl4roDXwKhQ0QExXLebukZEZFS
PUBLIC_KEYS
sigsum-verify -k age-sigsum-key.pub -P sigsum-generic-2025-1 "$age_archive.proof" < "$age_archive" || exit 1
sha256sum "$age_archive"The archive’s proof verifies under the approved publisher keys and Sigsum policy; its local evidence digest is recorded.
LET’S BUILD
Install only private, verified launchers
Inspect the authenticated age archive members, then extract only the two named executables. Copy SOPS and age into this lab’s private bin directory. No system executable, package-owned path or service is overwritten.
Run version output only after verification. Expect the pinned release pair and retain the absolute launcher paths. These are reviewed tools for this isolated exercise; this does not upgrade a production client or certify every future release.
Same unprivileged Bash shell on the isolated administration lab
tar -tzf "$age_archive" || exit 1
tar -xzf "$age_archive" -C "$lab/vendor" --no-same-owner --no-same-permissions age/age age/age-keygen || exit 1
install -m 0755 -- "$sops_artifact" "$lab/bin/sops" || exit 1
install -m 0755 -- "$lab/vendor/age/age" "$lab/bin/age" || exit 1
install -m 0755 -- "$lab/vendor/age/age-keygen" "$lab/bin/age-keygen" || exit 1
sops_bin="$lab/bin/sops"
age_bin="$lab/bin/age"
keygen_bin="$lab/bin/age-keygen"
"$sops_bin" --disable-version-check --version
"$age_bin" --version
"$keygen_bin" --versionThe private launchers report SOPS 3.13.3 and age/age-keygen 1.3.2 after both upstream verification paths passed.
LET’S BUILD
Create separate old, new and recovery identities
Generate fresh classic X25519 identities into three private files, never to terminal output. age-keygen prints the public recipient when writing a key file, and -y derives public recipient-only files. Inspect permissions without printing private contents. Do not copy identity material to the browser, shell variables, a repository or a chat.
This classic-key fixture explains migration mechanics and interoperability; it makes no post-quantum protection claim. age 1.3 also supports hybrid keys, which require compatible consumers and a separately reviewed key-type policy. A new production identity should be created and held by its actual custodian, not generated on a shared host for convenience.
Same unprivileged Bash shell on the isolated administration lab
"$keygen_bin" -o "$lab/keys/old.txt" || exit 1
"$keygen_bin" -o "$lab/keys/new.txt" || exit 1
"$keygen_bin" -o "$lab/keys/recovery.txt" || exit 1
"$keygen_bin" -y "$lab/keys/old.txt" > "$lab/keys/old-recipient.txt" || exit 1
"$keygen_bin" -y "$lab/keys/new.txt" > "$lab/keys/new-recipient.txt" || exit 1
"$keygen_bin" -y "$lab/keys/recovery.txt" > "$lab/keys/recovery-recipient.txt" || exit 1
old_recipient=$(cat "$lab/keys/old-recipient.txt")
new_recipient=$(cat "$lab/keys/new-recipient.txt")
recovery_recipient=$(cat "$lab/keys/recovery-recipient.txt")
stat -c '%a %n' "$lab/keys/old.txt" "$lab/keys/new.txt" "$lab/keys/recovery.txt"Three distinct private identity files and their public recipients exist; file permissions are private and no identity was displayed.
LET’S BUILD
Build a non-secret encrypted control and isolate key discovery
The fixture contains only the public marker non-secret-rotation-demo. Its first ciphertext is encrypted for old plus recovery. The helper runs SOPS with an empty inherited environment, an empty XDG configuration directory and only the selected identity file. Merely setting SOPS_AGE_KEY_FILE is insufficient for isolation: SOPS can also discover default identities and key commands.
The helper does not change HOME or the user’s configuration. This file uses only newly generated native age recipients and no KMS/plugin/SSH recipients. Successful old and recovery decryptions go directly to /dev/null; they validate key access and the normal SOPS MAC without writing or displaying plaintext.
Same unprivileged Bash shell on the isolated administration lab
with_identity() {
selected_identity=$1
shift
env -i PATH=/usr/bin:/bin LANG=C \
XDG_CONFIG_HOME="$lab/empty-config" \
SOPS_AGE_KEY_FILE="$selected_identity" \
"$sops_bin" "$@"
}
cd "$lab/work" || exit 1
test ! -e "$doc.enc.yaml" || exit 1
cat > .sops.yaml <<POLICY
creation_rules:
- path_regex: ^rotation-demo\.enc\.yaml$
age:
- $old_recipient
- $recovery_recipient
POLICY
printf 'message: non-secret-rotation-demo\n' | \
with_identity "$lab/keys/old.txt" --config "$lab/work/.sops.yaml" \
encrypt --filename-override "$doc.enc.yaml" --input-type yaml --output-type yaml > "$doc.enc.yaml" || exit 1
cp -- "$doc.enc.yaml" "$lab/before-old.enc.yaml" || exit 1
with_identity "$lab/keys/old.txt" decrypt --output /dev/null "$doc.enc.yaml" || exit 1
with_identity "$lab/keys/recovery.txt" decrypt --output /dev/null "$doc.enc.yaml" || exit 1The original ciphertext opens separately with old and recovery identities under controlled discovery.
LET’S BUILD
Add the new recipient without removing the recovery path
Generate the lab’s one-file policy from the three public recipients, inspect it, and run updatekeys with the old identity. The explicit config path avoids accidentally selecting a parent project policy. Read the proposed recipient changes and confirm only this fixture’s intended addition.
This is a rewrap of the existing data key, not data-key rotation. Changing a policy file alone does not update an already encrypted document; this selected-file command does. No repository-wide loop or live Git write is part of the exercise.
Same unprivileged Bash shell on the isolated administration lab
cd "$lab/work" || exit 1
cat > .sops.yaml <<POLICY
creation_rules:
- path_regex: ^rotation-demo\.enc\.yaml$
age:
- $old_recipient
- $new_recipient
- $recovery_recipient
POLICY
cat .sops.yaml
with_identity "$lab/keys/old.txt" --config "$lab/work/.sops.yaml" updatekeys "$doc.enc.yaml" || exit 1The encrypted fixture now lists old, new and recovery recipients after the reviewed updatekeys change.