← Start of tutorial

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.

Manjaro rollingReviewed against official documentation ·

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
05

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.

bash

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 1
How do I know it worked?

The intended checksum publisher and the exact selected SOPS binary digest verify successfully.

06

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.

bash

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"
How do I know it worked?

The archive’s proof verifies under the approved publisher keys and Sigsum policy; its local evidence digest is recorded.

07

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.

bash

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" --version
How do I know it worked?

The private launchers report SOPS 3.13.3 and age/age-keygen 1.3.2 after both upstream verification paths passed.

08

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.

bash

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"
How do I know it worked?

Three distinct private identity files and their public recipients exist; file permissions are private and no identity was displayed.

09

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.

bash

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 1
How do I know it worked?

The original ciphertext opens separately with old and recovery identities under controlled discovery.

10

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.

bash

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 1
How do I know it worked?

The encrypted fixture now lists old, new and recovery recipients after the reviewed updatekeys change.

Explore all tutorials ↗