Illustrative people-free WIUNIX Academy security workspace with a board separating SOPS recipients, data-key rotation and recovery access

WIUNIX ACADEMY

SOPS and age: rotate recipients, data keys and recovery access

Rehearse a staged age-recipient migration, isolate decryption tests and distinguish rewrapping from data-key rotation without pretending old ciphertext has been revoked.

Intermediate to advanced45 min readUpdated
Make this guide yours

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Ubuntu 26.04 LTSReviewed 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

THE OUTCOME

What will you have at the end?

Rehearse a staged age-recipient migration, isolate decryption tests and distinguish rewrapping from data-key rotation without pretending old ciphertext has been revoked.

Supported operating systems and versions

Documented preparation for Ubuntu 26.10/26.04/24.04, Debian 13/12, RHEL/Rocky/AlmaLinux 10/9 and Arch/Manjaro rolling, reviewed 2026-11-05. Private SOPS 3.13.3/age 1.3.2 Linux x86_64/aarch64 binaries and existing trusted verifiers required; no all-environment key/decryption execution is claimed.

Before you begin

  • An isolated administration lab, unprivileged Bash account, a new private path outside Git and approved access to public release artifacts.
  • Existing trusted Cosign and sigsum-verify, including sigsum-generic-2025-1 and independently approved publisher-verification keys.
  • curl, CA certificates, coreutils, tar and Git base tools; package preparation only in an agreed lab maintenance window.
  • Only the public fixture value; no real secret, KMS/plugin/SSH recipient or production consumer enters this exercise.

Rotate the correct key, and define what retirement can mean

SOPS encrypts file values with a data key and stores encrypted copies of that key for approved recipients. An age recipient is public; its matching identity is private. Changing the recipient list, generating a different data key and replacing an age identity are three distinct operations.

We rehearse an old-to-new recipient migration with an independent recovery recipient using fresh local identities and a deliberately non-secret YAML value. No real secret or production deployment is needed. The fixture stays outside Git and no private identity is placed in a browser field, command argument or pasted file panel.

updatekeys changes the file’s key wrappers using the selected policy without rotating its data key. rotate makes a new data key and reencrypts the values. Neither action revokes access to an old ciphertext copy, data key or plaintext that somebody already retained. A compromised application credential must also be replaced at its actual issuer.

Choose only public lab names

Apply a new absolute lab directory and a short document basename in the values form. The directory must not exist or sit inside a Git working tree. The basename controls the encrypted filename and matching policy rule together; it is not a secret name or customer identifier.

Recipients are public keys, but their actual value and ownership still require authenticated exchange. This form intentionally does not accept keys. The lab derives public recipients from its new private files locally; real migrations must verify the recipient’s owner and recovery custody through the existing protected process. Tab-local values are not access authorization.

01

LET’S BUILD

Record distribution tools and trusted verifiers

Use an unprivileged Bash account on an isolated administration lab. The actual SOPS/age executables will be pinned inside this new lab, so an existing system package or another PATH entry is never silently replaced. The reviewed release pair is SOPS 3.13.3 and age 1.3.2, checked 2026-11-05, for Linux x86_64/aarch64.

A trusted existing Cosign and sigsum-verify (with the sigsum-generic-2025-1 policy, available in the documented v0.13.1 path) are prerequisites. Obtain missing verifiers through the organization’s already approved installation route before this recipe. Do not bootstrap a verifier from an unchecked binary and call the following verification independent.

Use this Ubuntu/Debian release’s official packages for base tools. Debian 12 LTS has package/architecture-specific coverage. Upstream pinned private SOPS/age binaries avoid silently changing installed distribution versions; AppArmor and permissions remain active.

bash

Same unprivileged Bash shell on the isolated administration lab

cat /etc/os-release
uname -m
bash --version
dpkg-query -W curl ca-certificates coreutils tar git util-linux
command -v curl sha256sum awk tar install env git findmnt cosign sigsum-verify
cosign version
sigsum-verify --help
How do I know it worked?

The host/build and trusted verifier availability are known. Missing prerequisites stop preparation before downloads or key creation.

02

LET’S BUILD

Prepare base tools through this distribution’s repositories

Skip installation when the base tools already exist. Otherwise review the native package transaction in a suitable lab maintenance window. DEB uses apt, the RHEL family uses its entitled/vendor dnf repositories, and Arch/Manjaro use their own rolling repositories with a planned full upgrade.

SOPS also publishes DEB/RPM packages and Arch currently packages SOPS/age. This exact recipe instead uses the upstream binary verification path below, with private version-pinned launchers. The SOPS signed checksum configuration covers the binary artifacts; do not assume that the same checksum signature authenticates every DEB/RPM asset. No daemon, firewall, MAC or credential-store policy is changed.

bash

Same unprivileged Bash shell on the isolated administration lab

sudo apt-get update
sudo apt-get install curl ca-certificates coreutils tar git util-linux
How do I know it worked?

Base tools are present from the matching official repository; trusted verifiers remain separately required.

03

LET’S BUILD

Reserve a private lab outside Git

The commands disable shell tracing, set a private creation mask and refuse an existing path, including a dangling symlink. They create separate key, ciphertext, empty-discovery and tool directories under one new private lab. No Git repository is initialized.

Keep this Bash shell for the remaining panels. A failure can leave the already-created private directory; inspect it and choose a fresh unused path rather than deleting unrelated files. The recovery identity is co-located only to demonstrate the mechanics; real recovery custody and backup must be independent.

bash

Same unprivileged Bash shell on the isolated administration lab

set +x
umask 077
set -o pipefail
lab='/var/tmp/academy-sops-age'
doc='rotation-demo'
case "$lab" in /*) ;; *) printf '%s\n' 'STOP: require an absolute lab path.'; exit 1 ;; esac
test ! -e "$lab" && test ! -L "$lab" || exit 1
mkdir -m 0700 -- "$lab" || exit 1
probe=$(readlink -f -- "$lab") || exit 1
while :; do
    test -r "$probe" && test -x "$probe" || exit 1
    if test -e "$probe/.git" || test -L "$probe/.git"; then
        printf '%s\n' 'STOP: an ancestor contains a Git working-tree marker.'
        exit 1
    fi
    test "$probe" != / || break
    probe=$(dirname -- "$probe") || exit 1
done
if git -C "$lab" rev-parse --show-toplevel >/dev/null 2>&1; then
    printf '%s\n' 'STOP: the lab must be outside every Git working tree.'
    exit 1
fi
mkdir -m 0700 -- "$lab/keys" "$lab/work" "$lab/empty-config" "$lab/downloads" "$lab/vendor" "$lab/bin" || exit 1
printf 'Private non-production lab: %s\n' "$lab"
How do I know it worked?

A new private lab exists outside Git, with no production files touched.

04

LET’S BUILD

Download the exact reviewed release artifacts

Select the Linux architecture from the actual host and fetch version-pinned public artifacts into the private download directory. Unsupported architectures stop explicitly. These files are still untrusted input: do not run or extract the executables before the next two verification steps.

The age archive has a Sigsum proof; SOPS has a signed checksum bundle. HTTPS transport alone and a checksum taken from the same unauthenticated source are not substitutes for checking the intended publisher. Retain the releases, verifier versions and verification results with the lab evidence.

bash

Same unprivileged Bash shell on the isolated administration lab

case "$(uname -m)" in
    x86_64) arch=amd64 ;;
    aarch64) arch=arm64 ;;
    *) printf '%s\n' 'STOP: this binary path covers only Linux x86_64/aarch64.'; exit 1 ;;
esac
cd "$lab/downloads" || exit 1
sops_artifact="sops-v3.13.3.linux.$arch"
age_archive="age-v1.3.2-linux-$arch.tar.gz"
curl --fail --location --proto '=https' --tlsv1.2 --output "$sops_artifact" "https://github.com/getsops/sops/releases/download/v3.13.3/$sops_artifact" || exit 1
curl --fail --location --proto '=https' --tlsv1.2 --output sops-v3.13.3.checksums.txt https://github.com/getsops/sops/releases/download/v3.13.3/sops-v3.13.3.checksums.txt || exit 1
curl --fail --location --proto '=https' --tlsv1.2 --output sops-v3.13.3.checksums.sigstore.json https://github.com/getsops/sops/releases/download/v3.13.3/sops-v3.13.3.checksums.sigstore.json || exit 1
curl --fail --location --proto '=https' --tlsv1.2 --output "$age_archive" "https://github.com/FiloSottile/age/releases/download/v1.3.2/$age_archive" || exit 1
curl --fail --location --proto '=https' --tlsv1.2 --output "$age_archive.proof" "https://github.com/FiloSottile/age/releases/download/v1.3.2/$age_archive.proof" || exit 1
How do I know it worked?

The selected SOPS binary/checksum bundle and age archive/proof are downloaded but not yet installed or executed.

Explore all tutorials ↗