Archived
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2661f6d271 | ||
|
|
0532c3a282 | ||
|
|
ac8c9a20e3 | ||
|
|
0e66cdabc9 | ||
|
|
9c892ce1c2 | ||
|
|
bfeea90597 | ||
|
|
cafeb8853b | ||
|
|
2c2d464503 | ||
|
|
5ec7033439 | ||
|
|
9133afd444 | ||
|
|
eeec9ce302 | ||
|
|
a62c4fc023 | ||
|
|
97ede62f6d | ||
|
|
c939454983 |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
keys:
|
||||
- &admin age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
- &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
|
||||
@@ -7,7 +7,7 @@ servers and workstation.
|
||||
|
||||
The flake exposes NixOS configurations named `<platform>-<buildtype>`
|
||||
(platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`,
|
||||
`server`, `docker`, `gui`, `pxe-boot`), generated from `modules/platforms/*`
|
||||
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`), generated from `modules/platforms/*`
|
||||
and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
|
||||
every combination is built — `pxe-boot` has no `linode` variant. See
|
||||
`README.md` for the full current target list; treat `flake.nix` as the
|
||||
|
||||
@@ -62,8 +62,9 @@ There is no test suite — "correctness" here means the flake evaluates and
|
||||
sweeps: after editing one or two hosts/modules, evaluate just the
|
||||
`nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball`
|
||||
/`diskoImagesScript`/package output affected) rather than looping over every
|
||||
host — `codex-maintenance.sh` evaluates 18 hosts plus every package/tarball/
|
||||
image variant now and is slow to run after each small change. Reserve a full
|
||||
host — `codex-maintenance.sh` evaluates every `nixosConfigurations` host plus
|
||||
every package/tarball/image variant and is slow to run after each small
|
||||
change. Reserve a full
|
||||
`codex-maintenance.sh` run for changes that plausibly affect every host
|
||||
(`modules/common/*`, `flake.nix`, `variables.nix`) or as a final check before
|
||||
committing. This is a session-workflow preference only — it does not apply to
|
||||
@@ -105,13 +106,38 @@ Beyond `codex-setup.sh`/`codex-maintenance.sh` above, `scripts/` also has:
|
||||
reference `variables.nix` (confirmed empirically — `nix flake metadata`
|
||||
errors on it), so this is the closest equivalent to a single source of
|
||||
truth for the tracked release.
|
||||
- `scripts/rotate-admin-key.sh <backup-admin-key> [--new-key-file <path>]
|
||||
[--dry-run]` — rotates `.sops.yaml`'s `&admin` age key: decrypts with a
|
||||
backed-up copy of the key currently trusted as `&admin` (verified by
|
||||
deriving its public key and comparing, not taken on faith), replaces the
|
||||
`&admin` line with a new key already present in the environment
|
||||
(defaults to wherever sops/age itself would look), and runs
|
||||
`sops updatekeys` on every `secrets/*.yaml`. One-way: the old key can no
|
||||
longer decrypt anything re-encrypted this way. This is the automation
|
||||
for the manual steps `sync-host-keys.sh`/`create-proxmox-resource.sh`
|
||||
print when they bootstrap a brand-new, not-yet-trusted key on a machine
|
||||
with no prior admin access.
|
||||
- `scripts/backup-admin-key.sh <dest-path> [--key-file <path>] [--force]
|
||||
[--dry-run]` — copies the local sops age key (source resolution matches
|
||||
sops/age itself: `$SOPS_AGE_KEY` inline, then `--key-file`, then
|
||||
`$SOPS_AGE_KEY_FILE`, then the XDG default) to an arbitrary destination
|
||||
path with `0600` permissions, validating it's a real age identity and
|
||||
round-tripping the public key before and after the write. Refuses to
|
||||
overwrite an existing `<dest-path>` without `--force`. Purely a local
|
||||
filesystem copy — never touches `.sops.yaml`/`secrets/*.yaml` or the
|
||||
repo at all. The resulting file is exactly what `rotate-admin-key.sh`
|
||||
expects as its backup-key argument.
|
||||
|
||||
`sync-host-keys.sh` and `create-proxmox-resource.sh` genuinely mutate real
|
||||
state when run for real (not `--dry-run`): real `secrets/*.yaml`
|
||||
recipients, real Proxmox VMs/containers. They require the operator's own
|
||||
SSH/sops access, which an agent session doesn't have — but don't suggest
|
||||
running either non-dry-run without the operator's explicit go-ahead even
|
||||
if it becomes technically reachable.
|
||||
`sync-host-keys.sh`, `create-proxmox-resource.sh`, and
|
||||
`rotate-admin-key.sh` genuinely mutate real state when run for real (not
|
||||
`--dry-run`): real `secrets/*.yaml` recipients, real Proxmox VMs/
|
||||
containers, real revocation of decrypt access. They require the
|
||||
operator's own SSH/sops access, which an agent session doesn't have — but
|
||||
don't suggest running any of them non-dry-run without the operator's
|
||||
explicit go-ahead even if it becomes technically reachable.
|
||||
`backup-admin-key.sh` only writes a key copy to a path the operator gives
|
||||
it — lower-stakes than the others, but it still handles a real private
|
||||
key, so treat its destination path choice as the operator's call too.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -164,7 +190,7 @@ removing a host.
|
||||
`vzdump` backup-archive metadata this doesn't have), no install step —
|
||||
see `docs/auto-installer.md`.
|
||||
- `modules/build-types/*.nix` — what a system is for:
|
||||
minimal/server/docker/gui/pxe-boot/nix-cache.
|
||||
minimal/server/docker/gui/pxe-boot/nix-cache/tailscale-exit-node.
|
||||
- `modules/common/configuration.nix` — base NixOS config imported by every
|
||||
host: locale, users, nix settings, git.
|
||||
- `modules/common/home.nix` / `hosts/nixos/home.nix` — Home Manager config for
|
||||
|
||||
@@ -18,22 +18,23 @@ list:
|
||||
|
||||
| Target | Purpose |
|
||||
| --- | --- |
|
||||
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS (real, deployed) |
|
||||
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox (real, deployed — previously the flat `nix-minimal` target) |
|
||||
| `linode-minimal` | Minimal NixOS host profile on a Linode VPS |
|
||||
| `proxmox-minimal` | Minimal NixOS host profile on Proxmox — previously the flat `nix-minimal` target |
|
||||
| `lxc-minimal` | Minimal NixOS host profile in a Proxmox LXC container |
|
||||
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder (`lxc-nix-cache` is the real, deployed one — previously `proxmox-nix-cache`, itself previously the flat `nix-cache` target) |
|
||||
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host (`proxmox-server` is the real, deployed one — previously the flat `server` target) |
|
||||
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack (`proxmox-docker` is the real, deployed one — previously the flat `docker` target) |
|
||||
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation (`proxmox-gui` is the real, deployed one — previously the flat `nixos` target) |
|
||||
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host (`proxmox-pxe-boot` is the real, deployed one — previously the flat `pxe-boot` target) |
|
||||
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node (no deployed target yet; `lxc-tailscale-exit-node` is the one planned for actual use) |
|
||||
| `linode-nix-cache` / `proxmox-nix-cache` / `lxc-nix-cache` | Local Nix binary cache and remote builder — previously the flat `nix-cache` target |
|
||||
| `linode-server` / `proxmox-server` / `lxc-server` | Storage, NFS, backup, and monitoring exporter host — previously the flat `server` target |
|
||||
| `linode-docker` / `proxmox-docker` / `lxc-docker` | Docker host for the main container stack — previously the flat `docker` target |
|
||||
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation — previously the flat `nixos` target |
|
||||
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host — previously the flat `pxe-boot` target |
|
||||
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node |
|
||||
|
||||
This table is the only place "(real, deployed)" status is tracked — there's
|
||||
no separate machine-readable copy to keep in sync. `scripts/create-proxmox-resource.sh`
|
||||
guards against creating a same-identity duplicate of an already-deployed host
|
||||
by checking the Proxmox node itself (live `qm`/`pct` state) rather than any
|
||||
file in this repo, since a static list can't track whether a resource still
|
||||
actually exists.
|
||||
Which variant of a given buildtype is actually deployed isn't tracked
|
||||
anywhere in this repo — that's live infrastructure state, not something a
|
||||
committed file can keep accurate, and it changes independently of the code.
|
||||
Check the Proxmox node itself, or `/etc/flake-target` on a running host (see
|
||||
below), if you need to know what's really out there right now.
|
||||
`scripts/create-proxmox-resource.sh`'s duplicate-host guard works the same
|
||||
way: it checks the Proxmox node directly rather than any file here.
|
||||
|
||||
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
|
||||
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
|
||||
|
||||
@@ -19,7 +19,7 @@ see "LXC hosts" immediately below for why those are different.**
|
||||
## LXC hosts
|
||||
|
||||
`lxc-*` targets (`lxc-minimal`, `lxc-nix-cache`, `lxc-server`, `lxc-docker`,
|
||||
`lxc-gui`, `lxc-pxe-boot`) are **not** installed via `auto-install.sh` — the
|
||||
`lxc-gui`, `lxc-pxe-boot`, `lxc-tailscale-exit-node`) are **not** installed via `auto-install.sh` — the
|
||||
interactive menu deliberately excludes them. Don't try to select one there;
|
||||
`nixos-install` would bind-mount `/` onto `/mnt` (LXC containers have no raw
|
||||
disk to partition) and then refuse to touch the filesystem it's currently
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Spec: Refactor Flake Targets into Platform × Build-Type Matrix
|
||||
|
||||
**Status: implemented.** `flake.nix`'s `generatedTargets`/`mkTarget` and
|
||||
`modules/platforms/*`/`modules/build-types/*` are the result of this spec —
|
||||
kept here for historical rationale only (referenced from `CLAUDE.md`'s
|
||||
"Composition pattern" section), not as an active or open plan. The "Open
|
||||
Questions" below were resolved during implementation; don't treat them as
|
||||
outstanding. A `tailscale-exit-node` build type was added later, beyond this
|
||||
spec's original scope.
|
||||
|
||||
## Context
|
||||
|
||||
The flake at `~/nixos` currently defines these output targets (flat, ad-hoc naming):
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
_:
|
||||
|
||||
{
|
||||
imports = [ ./enable-service.nix ];
|
||||
|
||||
services.tailscale = {
|
||||
enable = true;
|
||||
# Enables the sysctl forwarding settings exit nodes/subnet routers need;
|
||||
# without this, --advertise-exit-node has no effect.
|
||||
useRoutingFeatures = "server";
|
||||
|
||||
# Lets peers reach this node directly over the tailscale UDP port
|
||||
# instead of relaying through DERP.
|
||||
openFirewall = true;
|
||||
|
||||
# extraSetFlags (tailscale set, via the always-on tailscaled-set
|
||||
# service), not extraUpFlags -- extraUpFlags is only ever applied by
|
||||
|
||||
@@ -122,13 +122,25 @@ Add a pre-commit hook (or a `nix flake check` step) running `gitleaks protect --
|
||||
|
||||
## Definition of done
|
||||
|
||||
- [ ] Milestone 1 inventory complete and reviewed
|
||||
- [ ] All hosts have per-host age keys; admin key backed up outside the repo
|
||||
- [ ] Every inventoried secret migrated to sops-nix, referenced via `*File`/`sops.secrets.*.path`, nothing plaintext in the working tree
|
||||
- [ ] `nixos-rebuild dry-build` and at least one real `switch` verified per host
|
||||
- [ ] Working-tree scanner sweep clean
|
||||
- [ ] History rewritten with `git-filter-repo`, force-pushed, full-history scanner sweep clean
|
||||
- [ ] All other clones deleted and re-cloned from the rewritten history
|
||||
- [ ] Every credential in the original inventory rotated (not just re-encrypted)
|
||||
- [ ] Pre-commit secret scanning hook added
|
||||
- [ ] `secrets-inventory.md` deleted from the working directory (never committed)
|
||||
**Status as of 2026-07-20:** Milestones 1–3 are done — sops-nix is fully
|
||||
wired (`.sops.yaml`, `secrets/*.yaml`, referenced via `hashedPasswordFile`/
|
||||
`*File`/`sops.secrets.*.path` throughout), and history has been scrubbed
|
||||
with `git-filter-repo` + force-push (this removed a GitHub fine-grained PAT
|
||||
that had been committed in plaintext in `flake.nix`/`common/home.nix`
|
||||
between 2025-07-16 and 2026-02-09, later migrated to sops but never scrubbed
|
||||
from history until now). **Milestone 4 is not confirmed** — whether that PAT
|
||||
(or any other historically-plaintext credential) was actually rotated, not
|
||||
just re-encrypted, isn't something this repo can attest to; that's an
|
||||
operator action against the issuing service (GitHub, etc.), not a repo
|
||||
change. Do that before considering this fully closed.
|
||||
|
||||
- [x] Milestone 1 inventory complete and reviewed
|
||||
- [x] All hosts have per-host age keys; admin key backed up outside the repo
|
||||
- [x] Every inventoried secret migrated to sops-nix, referenced via `*File`/`sops.secrets.*.path`, nothing plaintext in the working tree
|
||||
- [x] `nixos-rebuild dry-build` and at least one real `switch` verified per host
|
||||
- [x] Working-tree scanner sweep clean
|
||||
- [x] History rewritten with `git-filter-repo`, force-pushed, full-history scanner sweep clean
|
||||
- [ ] All other clones deleted and re-cloned from the rewritten history — every clone that existed before 2026-07-20's rewrite (any other machine, WSL instance, or CI checkout) needs this
|
||||
- [ ] Every credential in the original inventory rotated (not just re-encrypted) — **the GitHub PAT found in history specifically still needs this**
|
||||
- [x] Pre-commit secret scanning hook added (`.githooks/pre-commit`, `gitleaks protect --staged`)
|
||||
- [x] `secrets-inventory.md` deleted from the working directory (never committed)
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
# Backs up the local sops age key (the private key that decrypts
|
||||
# secrets/*.yaml -- normally the one trusted as &admin) to an arbitrary
|
||||
# destination path, e.g. a USB drive or other offline storage, so it can
|
||||
# later be restored and handed to rotate-admin-key.sh if this machine's
|
||||
# copy is ever lost, or to run either script from a different machine.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/backup-admin-key.sh <dest-path> [--key-file <path>] [--force] [--dry-run]
|
||||
#
|
||||
# Source key resolution matches sops/age's own default order:
|
||||
# $SOPS_AGE_KEY (inline identity text) if set, else
|
||||
# --key-file if given, else
|
||||
# $SOPS_AGE_KEY_FILE if set, else
|
||||
# ${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
sops_yaml="${repo_root}/.sops.yaml"
|
||||
|
||||
# shellcheck source=env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
|
||||
# Pin cwd for the same reason rotate-admin-key.sh does: age/sops calls
|
||||
# below should never depend on wherever the caller's shell happened to be.
|
||||
cd "$repo_root"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <dest-path> [--key-file <path>] [--force] [--dry-run]
|
||||
|
||||
<dest-path> Where to write the backup. Parent directories are
|
||||
created as needed. Written with 0600 permissions.
|
||||
--key-file <path> Read the key from here instead of the default
|
||||
sops/age resolution (\$SOPS_AGE_KEY_FILE, then
|
||||
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt).
|
||||
Ignored if \$SOPS_AGE_KEY is set (that always wins,
|
||||
same precedence sops/age itself uses).
|
||||
--force Overwrite <dest-path> if it already exists.
|
||||
--dry-run Print what would happen; write nothing.
|
||||
EOF
|
||||
}
|
||||
|
||||
dry_run=0
|
||||
force=0
|
||||
key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
||||
args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
force=1
|
||||
shift
|
||||
;;
|
||||
--key-file)
|
||||
key_file="${2:?--key-file requires a path}"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${#args[@]}" -ne 1 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
dest="${args[0]}"
|
||||
|
||||
nix_extra_opts
|
||||
|
||||
if [[ -n "${SOPS_AGE_KEY:-}" ]]; then
|
||||
echo "==> Source: \$SOPS_AGE_KEY (inline identity from the environment)."
|
||||
src_content="$SOPS_AGE_KEY"
|
||||
else
|
||||
[[ -s "$key_file" ]] || {
|
||||
echo "ERROR: no key found. \$SOPS_AGE_KEY is unset and ${key_file} doesn't exist or is empty." >&2
|
||||
exit 1
|
||||
}
|
||||
echo "==> Source: ${key_file}"
|
||||
src_content="$(cat "$key_file")"
|
||||
fi
|
||||
|
||||
# Round-trip through a private scratch file (rather than trusting the
|
||||
# source string as-is) so age-keygen -y validates it's a real identity
|
||||
# before anything is written to <dest-path>.
|
||||
scratch="$(mktemp)"
|
||||
trap 'rm -f "$scratch"' EXIT
|
||||
( umask 077; printf '%s\n' "$src_content" > "$scratch" )
|
||||
|
||||
src_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$scratch'")" || {
|
||||
echo "ERROR: source doesn't look like a valid age identity (age-keygen -y failed)." >&2
|
||||
exit 1
|
||||
}
|
||||
echo " public key: ${src_pub}"
|
||||
|
||||
current_admin_pub="$(grep -E '^ - &admin age1' "$sops_yaml" 2>/dev/null | awk '{print $NF}' || true)"
|
||||
if [[ -n "$current_admin_pub" && "$current_admin_pub" != "$src_pub" ]]; then
|
||||
echo "NOTE: this key does not match .sops.yaml's current &admin entry (${current_admin_pub})."
|
||||
echo " Backing it up anyway -- this script doesn't require it to be the admin key."
|
||||
fi
|
||||
|
||||
if [[ -e "$dest" && "$force" -ne 1 ]]; then
|
||||
echo "ERROR: ${dest} already exists. Pass --force to overwrite." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo
|
||||
echo "[dry-run] would write $(wc -c <"$scratch" | tr -d ' ') bytes to ${dest} (mode 0600)"
|
||||
[[ -e "$dest" ]] && echo "[dry-run] would overwrite existing file (--force given)"
|
||||
echo "[dry-run] Nothing was written. Re-run without --dry-run to apply this."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
install -m 600 "$scratch" "$dest"
|
||||
|
||||
dest_pub="$(nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$dest'")"
|
||||
if [[ "$dest_pub" != "$src_pub" ]]; then
|
||||
echo "ERROR: ${dest} was written but its public key doesn't match the source -- investigate before relying on this backup." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done. Backed up to: ${dest}
|
||||
public key: ${dest_pub}
|
||||
|
||||
This is a private key -- store it somewhere offline/secure, not in this
|
||||
repo or anywhere it'd get committed. Restore it with:
|
||||
scripts/rotate-admin-key.sh ${dest}
|
||||
EOF
|
||||
@@ -324,6 +324,7 @@ for id in $(pct list 2>/dev/null | awk 'NR>1{print $1}'); do
|
||||
n="$(pct config "$id" 2>/dev/null | grep -oP '^hostname:\s*\K\S+' || true)"
|
||||
[[ "$n" == "$target" ]] && echo "lxc ${id} ${n}"
|
||||
done
|
||||
exit 0
|
||||
REMOTE_SCRIPT
|
||||
)" || ssh_check_status=$?
|
||||
if [[ "$ssh_check_status" -ne 0 ]]; then
|
||||
|
||||
Executable
+190
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rotates the &admin sops age key: decrypts with a backed-up copy of the
|
||||
# key CURRENTLY trusted as &admin, replaces .sops.yaml's &admin entry with
|
||||
# a new key already present in this environment, and re-encrypts every
|
||||
# secrets/*.yaml for the new recipient set. After this runs, the old key
|
||||
# can no longer decrypt anything -- this is a real, one-way handoff of
|
||||
# trust, not a preview.
|
||||
#
|
||||
# This is the automation for the manual steps create-proxmox-resource.sh /
|
||||
# sync-host-keys.sh print when they bootstrap a brand-new, not-yet-trusted
|
||||
# age key on a machine that's never had admin access before:
|
||||
#
|
||||
# scripts/rotate-admin-key.sh /path/to/backed-up/admin/keys.txt
|
||||
#
|
||||
# The backup key's *public* key must match .sops.yaml's current &admin
|
||||
# entry -- this script verifies that by deriving it, it doesn't just trust
|
||||
# the filename or take it on faith. The new key defaults to wherever sops
|
||||
# itself would already look ($SOPS_AGE_KEY_FILE, then the XDG default), so
|
||||
# the common case is just pointing this at the restored backup.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
sops_yaml="${repo_root}/.sops.yaml"
|
||||
|
||||
# shellcheck source=env.sh
|
||||
source "${repo_root}/scripts/env.sh"
|
||||
|
||||
# sops resolves .sops.yaml by walking up from the process's cwd, not from
|
||||
# the target file's own path -- if this script were invoked from somewhere
|
||||
# other than the repo root (or from inside another checkout/worktree that
|
||||
# happens to have its own .sops.yaml), `sops updatekeys` would silently
|
||||
# re-encrypt against the WRONG config's recipient list instead of this
|
||||
# repo's. Pin cwd here so every sops/age call below is unambiguous
|
||||
# regardless of where the caller's shell started out.
|
||||
cd "$repo_root"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <path-to-backed-up-admin-key> [--new-key-file <path>] [--dry-run]
|
||||
|
||||
<path-to-backed-up-admin-key> age identity file for the key CURRENTLY
|
||||
trusted as &admin. Only ever read -- never
|
||||
copied or modified.
|
||||
--new-key-file <path> age identity file for the key to promote
|
||||
to &admin. Defaults to \$SOPS_AGE_KEY_FILE,
|
||||
then
|
||||
\${XDG_CONFIG_HOME:-\$HOME/.config}/sops/age/keys.txt
|
||||
(sops/age's own default resolution order).
|
||||
--dry-run Print what would change; touches nothing
|
||||
(.sops.yaml untouched, no sops updatekeys
|
||||
calls).
|
||||
EOF
|
||||
}
|
||||
|
||||
dry_run=0
|
||||
new_key_file="${SOPS_AGE_KEY_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/sops/age/keys.txt}"
|
||||
args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
--new-key-file)
|
||||
new_key_file="${2:?--new-key-file requires a path}"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${#args[@]}" -ne 1 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
backup_key="${args[0]}"
|
||||
|
||||
[[ -s "$backup_key" ]] || { echo "ERROR: backup key file not found or empty: ${backup_key}" >&2; exit 1; }
|
||||
[[ -s "$new_key_file" ]] || { echo "ERROR: new key file not found or empty: ${new_key_file}" >&2; exit 1; }
|
||||
|
||||
nix_extra_opts
|
||||
|
||||
age_pub() {
|
||||
nix-shell "${NIX_OPTS[@]}" -p age --run "age-keygen -y '$1'"
|
||||
}
|
||||
|
||||
echo "==> Deriving public keys..."
|
||||
old_pub="$(age_pub "$backup_key")"
|
||||
new_pub="$(age_pub "$new_key_file")"
|
||||
echo " backup (old admin) key: ${old_pub}"
|
||||
echo " new admin key: ${new_pub}"
|
||||
|
||||
if [[ "$old_pub" == "$new_pub" ]]; then
|
||||
echo "ERROR: backup key and new key are identical -- nothing to rotate." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_admin_line="$(grep -E '^ - &admin age1' "$sops_yaml" || true)"
|
||||
if [[ -z "$current_admin_line" ]]; then
|
||||
echo "ERROR: couldn't find a '&admin age1...' line in ${sops_yaml}." >&2
|
||||
exit 1
|
||||
fi
|
||||
current_admin_pub="$(awk '{print $NF}' <<<"$current_admin_line")"
|
||||
|
||||
if [[ "$current_admin_pub" != "$old_pub" ]]; then
|
||||
echo "ERROR: ${backup_key} doesn't match the current &admin key in .sops.yaml." >&2
|
||||
echo " .sops.yaml &admin: ${current_admin_pub}" >&2
|
||||
echo " backup key pubkey: ${old_pub}" >&2
|
||||
echo "Wrong backup file, or .sops.yaml has already moved on -- not touching anything." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t secrets_files < <(find "${repo_root}/secrets" -maxdepth 1 -name '*.yaml' | sort)
|
||||
if [[ "${#secrets_files[@]}" -eq 0 ]]; then
|
||||
echo "ERROR: no secrets/*.yaml files found under ${repo_root}/secrets." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Confirming the backup key can actually decrypt..."
|
||||
if ! SOPS_AGE_KEY_FILE="$backup_key" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||
"sops -d '${secrets_files[0]}'" >/dev/null; then
|
||||
echo "ERROR: backup key failed to decrypt $(basename "${secrets_files[0]}") -- aborting." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " OK: decrypted $(basename "${secrets_files[0]}")"
|
||||
|
||||
if [[ "$dry_run" -eq 1 ]]; then
|
||||
echo
|
||||
echo "[dry-run] would replace .sops.yaml's &admin line:"
|
||||
echo "[dry-run] - ${current_admin_pub}"
|
||||
echo "[dry-run] + ${new_pub}"
|
||||
echo "[dry-run] would then re-encrypt (sops updatekeys --yes) for the new recipient set:"
|
||||
for f in "${secrets_files[@]}"; do
|
||||
echo "[dry-run] secrets/$(basename "$f")"
|
||||
done
|
||||
echo
|
||||
echo "[dry-run] Nothing was changed. Re-run without --dry-run to apply this."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> Rotating .sops.yaml's &admin key..."
|
||||
sed -i "s|^ - &admin age1[a-z0-9]*| - \&admin ${new_pub}|" "$sops_yaml"
|
||||
grep -qF "$new_pub" "$sops_yaml" || {
|
||||
echo "ERROR: sed edit didn't take -- .sops.yaml left unchanged, check it by hand." >&2
|
||||
exit 1
|
||||
}
|
||||
echo " Updated."
|
||||
|
||||
echo "==> Re-encrypting secrets/*.yaml for the new recipient set..."
|
||||
for f in "${secrets_files[@]}"; do
|
||||
echo "==> $(basename "$f")"
|
||||
SOPS_AGE_KEY_FILE="$backup_key" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||
"sops updatekeys --yes '${f}'"
|
||||
done
|
||||
|
||||
echo "==> Verifying the new key can decrypt everything..."
|
||||
for f in "${secrets_files[@]}"; do
|
||||
if ! SOPS_AGE_KEY_FILE="$new_key_file" nix-shell "${NIX_OPTS[@]}" -p sops --run \
|
||||
"sops -d '${f}'" >/dev/null; then
|
||||
echo "ERROR: new key failed to decrypt $(basename "$f") after rotation -- investigate before committing." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " OK: $(basename "$f")"
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done. .sops.yaml's &admin key is now:
|
||||
${new_pub}
|
||||
|
||||
The old key (${old_pub}) can no longer decrypt any secrets/*.yaml
|
||||
re-encrypted above.
|
||||
|
||||
Review the diff, then commit:
|
||||
git add .sops.yaml secrets/*.yaml
|
||||
git commit -m "Rotate sops admin age key"
|
||||
EOF
|
||||
+41
-41
@@ -5,74 +5,74 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpWjNSdEdZbUUzamswa08w
|
||||
aCtzSHB0bFVZMnYxTkpuM1psdVYzWW55SzMwCkliMWVOUlBqRG5wOGZjQVg4MkFz
|
||||
NEVkMXdkTjhWRlZmVGlzZElid2pUMXMKLS0tIGtHUmRCNXNhVmloUHYzQnE5YlBS
|
||||
YnVSQjJlT3JnQ1RNMm9xV2xKOGRZUDAKc4VTl9NEI9Rv8+4J3JTeHTt2h8Dr2IJv
|
||||
tfvoNJQM/w6RAJWNTkaDmzZa9OnUW+grDlBQKlDuAnr6fZmuNTH2hQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIOWU1K2xnV2xpQVdOMmIz
|
||||
RUgyU3dvVmkxTXJabHQ4dTNtRm5ueisyeWpZCk1DeklsZG9TaEZ2KzRlSVB6Mmgy
|
||||
VVZ1aVI0U0psTTIvWkxiOG9jRU9DM0kKLS0tIHBYUHdRa2pTU0xnNWdWL0J1K0J0
|
||||
N0dLZTl0VjZIRWp6cGp0SnI0Y1RJMGMKC5x1mvsmadVnp6M0K+nU4vBOnHVU7+Yn
|
||||
ef6WlzBooYlKei5pdh5pkJHksyy+DymZywLX1EKA+LwLYGKt58zF8w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOR25UN1c0aE5SYWphbU0y
|
||||
SHRTZ1B0WC9NU3Z5VHpzTXpLSUxHTDI1ZFE0CnFpYUN1eGZQejJMblZPd1ROeUth
|
||||
dklZYVVNa1ZNZ1d4dW9vMCsvQWp1RkEKLS0tIERxakx5L0JrQitib1EyNDRMbDQ5
|
||||
Z3hDWUFEazdxczVhaHJYK3VZeEJSSDgKkw9T4ZuT+VHIF4WopqRHt8vW30kOysJ3
|
||||
vOq6EZ3Fqkgmoxm69Zp2gFnuE9GZIBy3VPQVLU2k6dZGJ3IvmLYeBA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1OUdjSHgvOC81d1kxbnIv
|
||||
SlNMbzVPY0VGeFBzN2Z0VzZPQjhZVkN2RVZFCitxTHBtUTJpTURWaWUrSGJRb1lh
|
||||
Zi91WktPZkdtVERMdkNpSytSTHlhYWsKLS0tIHJWTHR6TFQrVXZ1U2dlUmI2Z0NX
|
||||
WXRVRVVIV3hrQUR3cEFpOTRhWWVVYVUKGD+dBn5pEFekiwn0EAHTPydwKS6kRkz3
|
||||
RgakGqUKbkib5iEc1aNoyxFT2WM4S9WFx9zgSuGwFplRi87/19KNvA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0THJzMFBTTCtDMWRmZ25M
|
||||
dU55OVhBb0trWWRUNlArTnEzRjhiYngvRENNCjF4d0M5NlYyQW50TTdMRXpuUjRr
|
||||
M1NwV05JOHV6T2cxT2FheVpuZ0w2T0kKLS0tIFBxdlVpVEoxOUpSWjk1ejRsK1NM
|
||||
V1UwTU1scG91L2FIemtwSW5JbFlmeG8K/1WIlaIidy3x3ptoRpS/DG88064LQ6Mq
|
||||
GbfB0jfq5PILDQMMuZu5oIBY31SxwnhZ02Ns7gA67kgNIRSCmk9WyQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuN09aWHpKRTgwRVVranRa
|
||||
Z1dKMjF1UUxVR0RGNkhQMEpPekk5eCtQT2dzCi9CQ0s5bERaR1FDQmcyU2tzS1cz
|
||||
aENSS3MzZ0VVdndKc1g1UTVOM1Vnam8KLS0tIDdZL2p5eU8rMmRHUUhSdDJtaDEz
|
||||
eXFkZUZtVEJFdFQzYk44ZFBCalZTL2sKMb2HbPuPNw12h6HldaIBI9ZGv6X7plrn
|
||||
lyu7xcFoD5d3REu6TsEIQd1PgqQGr63/pbQ+6fteypPQrdSeBP5sqg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmRURQVGw1a1E1MkIzV2pa
|
||||
RG5HSkM0c0huUGVWcnFZOGlacGlGOHFMTVFrCm5OS09HRFc3TGVUYmtzTStxL0Q5
|
||||
N2hRMEcxUE9MTmZXL0wvME5EZXF1Z00KLS0tIHdHMVVHcTZzMmdXU0s4QlVqSS9Y
|
||||
ZUVmcWhPaURIUFJGR0V4bUZwKzM1bm8KlvGMNEClbLlfvJqNQHhd0dI4ihShLChF
|
||||
GI/fydgrBruw3Otv6KLZu3CBC7iNcKlvZxz+YGD2qbicmyQ5hAhDSQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxY3ROYmJmUDlnVVN1bStZ
|
||||
dkloZDc3NVpnRUQwRzFYbFhIYTMxZVppZFdNCnVQVDJJSWJRT0pSeXJkdjVYWEtM
|
||||
VFJSaXJEeUVMVGJidDlmTUhLZW1pTUEKLS0tIEVYb2gyMThBTDNUUFlNdVZyYzVm
|
||||
VlAyOXdScWJjVFJMVDlrcTFIMEwybzgK6v/+a62q1F1CLH2llwtfk31ZutTIUUA2
|
||||
yw1lO0m7gKabPHtySuz+zOQgNRx9EsnGcSXAmWZ2MXW1+m+6OpNuBQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5bWpFenBlQldna3RhSFpr
|
||||
bEJBaHJDMzM3OHlqTmozcWU0VDM1bTFUWFZzCnZtNHZjZ1U1RzNkUlBHZFozWXdt
|
||||
VVkxQjMvTDJtbFZnclpkUEd4TEVmNTAKLS0tIFlhV2ZSSzJLRVNoMmFyVktDOElR
|
||||
YUxqZUFoY1ZWeGlldGplMjVQa1A5aUUKWelY6yO7Mr6dRvj4MVMbq/Z9JgrAnahz
|
||||
BDhHqObzJrOCtfDCTWiYuP+0yvIFWItMWhGSMw9MwwivvwnrEa+ZuQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1SzBlOGVqL0h0Q2N0eDAw
|
||||
QTNROHRVeVNOMDdLZTN2ZXNyNERMOGxEbUNFCjU2ZXJwb3ZlSEtoV2dvbVZjMWI4
|
||||
c0NLbm9TaGtlNFZpeEFuQVVabmsvbGsKLS0tIEVRMUpZbmlTZHU0M09Cd2lpaGpl
|
||||
akJzWGg1c0RHblNueTJMaFd3SytjeFkKUN00FC0MYc7MC8UMEOFtTHWMOh/Ratyd
|
||||
e8AvxBrGnEEKjm7PtCKOnFF7l5srBIsnuhjcyGCq8uTlE/6XS8mvBQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiK2hKb2VaVnoyaEVYUHZZ
|
||||
OWVUSGtONGs5dEljTjJQdlhEcjNjdjViT2pVCkpaOHVZMlpXOFRveVlMeXZqWmoz
|
||||
ZHJRQTR2dmJQSEozeTRGMEdUdFlmZ2MKLS0tIERWS1RVdW1jQytBZzlkb3puNjhH
|
||||
ZjdlZmtzNXVOQ25DeCthUzhRRm1MT2cKaxc7zGm57iJFSeYc2IPqF4Eaxa44nR37
|
||||
pWZw+erG4F9AAZ2F047q+oLKe0B8FLSF54IbcXdQhitgGNR7B2HVeA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2MWFIOHc4RDdyWEVIdDJy
|
||||
UmtZZC9CS3l4WUlEejhkODlPb3VCc003eEdrCmlWcHJMRkpaQjI2WUpVNUozMkFE
|
||||
MEZNakE0Y2RZOU5XZXVNY3hEaDVQRTgKLS0tIGJaWGJscTZENnRQd0NFZUpsRjhs
|
||||
d3YrR3dkS2VsV3J4THJwTng2VVNMeDgKIG+vd3mnwalAKnJ3neucl7sQ0sgsNcAt
|
||||
WeUuF5C6QQXNlzUQClY5dmCvJmRjUIfS/m7s1WJAJbzUfaSOokxehg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1ekhUTG5VOFErL3pFeWZM
|
||||
RG9NVnN6NFl3bzlFeTQzdHFtZmhwem04alQ0ClpJRStObERMZ0w2V0NhR1FSeW96
|
||||
M1d2V2NjUkUrLzN2ZVNSbGY4bll5WmsKLS0tIC94dVFQcXJ6d3pLU0VHNEFGR0ls
|
||||
a1Q2UmNuSjVMNG5XZGZKV1VmNHFPQXcKQJrZGw/9fPnXeFZ4omrkEgrzwplhwvRW
|
||||
i0FXuepoU353sR7enyL34qPoOdm05ivowuPKNzkq8D4i5AF6vGv+YA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6OHRNd0FZSGZ6MFEwSnU3
|
||||
Tm1pUnoxK1Y4dGJCTk1EbldzaWlscUdzTWlJClpHcHhmVXE5WUZRaUt5TXM0TVJI
|
||||
UkxnUXhISGtmcUhndGNQcWNVWGVlOEEKLS0tIGJMSVR3OU1oSHkzQWpMMWVYTDV2
|
||||
enAxMzQ2NWk1Qzl6YnpVYTdydk5pSFEK/1s8Ej3QLoWPDmd3Yj0nLV1reQ5QCPZX
|
||||
YhGlujb3rTlgxJaQCpqynf6w2Tf4dT/Wq5edE8tHnXyr/kHacpnsFg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoZ2lHM2RBQ3lUK216dkcr
|
||||
Y3RrcXR3QTdFVjJRWlhpQWVFQ1cyZWNabkRFClAwRi9PSHF5ZWFSUzJuRXF3bU1R
|
||||
b0U5TFRZaFdmR1NMS3RRT3E3M2hUdE0KLS0tIEtrUy8ydkNyVHBiOVR6WEdjV2VN
|
||||
NVJHUVgwRkhxcmlwcFkrRlFwTEF6YVkKzXyJk0UnmUsvb+NzNVcf/gf7OEEt3P/K
|
||||
OIGxDrGfs/zNQgeKXNbQlQ4p4jOaybG8aCmX+A4qTk6/I8yY8LTJWg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrNklUU2UrN3ZTeGdYNTRw
|
||||
cXZZVXdrUEtxWjNKaWp2cDZrQW5tVkJ0MEdjCnN4NmgxYlJNNFNXODcvUFF6aEZS
|
||||
OUpEZkpXcU1CRDVEUlBGYVYzVXdYTEkKLS0tIGtyUDc4SjJYUFUzT3BtNnVCeTdZ
|
||||
azJ4bk9sTWZYYWRoV1VPV0VBUzFVekEKsrD8hFi8F8ME0VO4CHAYa5N0swt5R2Bz
|
||||
Ll6AgyEysPR3Q8dqGJnBV/b6hs2aVDyKgGjaAXlI55OxP3RgXJQ1vQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
|
||||
lastmodified: "2026-07-19T02:30:40Z"
|
||||
|
||||
+16
-16
@@ -4,29 +4,29 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbHd1Y1dpa2ZiQ2E1bXRP
|
||||
SnkrWHI0V2U1eGFKY0RZamZQQjluS0hGeENZCmduUFQyd1Q5RkZGMGFoenJhNmZh
|
||||
VmZ2OStNaFpJejFxQ2JUZXFpNThaMncKLS0tIEhJdGdQeXEzb25Nbk5YZktCWVUz
|
||||
NUJDUWRoTkd5R2pFblZjS0NSbVR5dkkKDYeW+zRpha04/CasFM91K6v1PpkNGHRu
|
||||
qAoKs9KSg9VxS7ya8RuLmylKRdpPkupm/8SXIJvQuCXp5LWmNJ4zkA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmOHlPcGZvN3o5aEZCcFdz
|
||||
WlYydFdoQ0ZHbElMdTRkZWljMmoxTHZqNjJZCnE3cmgzKzZya3FROEczbHVveDND
|
||||
VnEzRW12cnRKdzhkZm5uTXpkTEtrNUEKLS0tIHFDbkc4Mk4wVlM2R29zZXYwR2Ri
|
||||
ODBML1p4eUZiZldYUERQTUhTU1llV0UKxjvH6zbW6wKghzR1o34CyKPEa2FqZmo0
|
||||
PxgqyuXkIwas9soXVAkScx7ElaV09Fjaj+mDrKwi4a+DwdoSP7czyA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4UktiUUdmd092bnZMdFMz
|
||||
Q05xVlhnNzhPZUY4TjF5bjRCbklacTdJd1RBCnJ4bEVMaWRLZExIUFVEUDBXTnlV
|
||||
MDU4WUk5VlJjdlhVUVpzZXlnWHhqTjQKLS0tIE9iclhGVElPcU9OLzF0bEVjMjVp
|
||||
YzlQTGFYZEhLdTk3N3ZramVjVGRXREUKjOjqO/jX5iEKN43WiJ8qS/1mDfxdj8uO
|
||||
K7Yyc/Yj0qMvGwLNzphjmbT29P7dXk4Ht4TedqCd+8DfhpOgLVIaSA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0VDBXYUZNbnpEZHlqV05C
|
||||
WWxQZjVYcWlZeXlGc0RiaUpERzR0T21CWWtBCkM0dW5kYlFNK1RGSzRudFZ2Z0t3
|
||||
aVBreElGM1BIT0RzQkxTVTA3S2NhQncKLS0tIERyTU8zWWRTRm95SmRZQ1BhalVV
|
||||
SVdBajN1V1BuM2s4K216S3c0VHczNlkKM5jvsSEfCBA5uZRjBJNbM91lLRQkj+jK
|
||||
rM5uSfGLTvSjPgXIMIq03OXxH1CE7GoKxAPwFrJdAFMMQcutIethhw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjaXpvQU9yRUc3ektPMmU5
|
||||
Q0xLWU9mYXVOeTVBNGNsZzJPa1lkVlRUZHlvClhoSVV6YjB1Q3pTTzVvNFlNYkU4
|
||||
Y2RrbFk5SUFKSDVjeFU0Wlc2aGFUMFUKLS0tIDlVeXM4WWRUakg4Zk5mcjdJL3VC
|
||||
S1k5eFBpVnREQlZwYllpeTNhWW5GS1EKi2sMwyJJ0D8acjCZmxlcwdU1sglBuxR7
|
||||
2SEMsctdGC+5E3ilPXvPpZ5RONZHbXxn6kQRBlBv6AJERpGDzsfgfA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvbk9tRE9jYkpxNThQeElH
|
||||
dWpKYThDb1ZkUWtMWlRxc2tYbjhBbEFEcWtVCkNiK0NBRko2TkJQdUVtdGtHNGcv
|
||||
andSaUlKSFA4THRyZXNTYmI0Yk5WbmsKLS0tIHlQTUtpWDdPeTVZL0M1RElRdFNk
|
||||
QWlGdFo5NkZWSmY0YXdpNzNUQnlsK3MKtzC0bM7Ek+K73nMranOA1Mc98RUnYnq1
|
||||
hAt0QEFKWK4QVKubaN/rG3AzE0U7qPKWHTzoxgnAiL3WyV9teLW+iA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
|
||||
lastmodified: "2026-07-19T23:30:21Z"
|
||||
|
||||
+11
-11
@@ -3,20 +3,20 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaFZURjAxMGRJZEJ5MW1x
|
||||
ZjVwWEQrQlkwNmRibVNiL2RpTTFLeUVDQ3hRCkVIamVnZkM1MnlueloxMHVFQnBF
|
||||
RjV2bnUrZUo4WGZJTmR4Y0xITkxRUkUKLS0tIGlJdVQ5MFBubVhxRUVMWW0wSGpP
|
||||
UGdKNUNPYW9nek1UZ0tWbXd3QVNUNDgKIHOiKelITQdH5R4Nc3WF7mzz15D1f9on
|
||||
VaTdr5qkf8LNNvPI0fxsXA9is5cqeg+KbDRHtUumEhNp6Zrf8zWBkw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURmMzN3hrSlNrUkkvVWNl
|
||||
L3M1dEhWeW14N0RFNVRPci9QK1YyTFdqRVVJCk5WaWswT2NicldkYzZjbVhYU2xu
|
||||
MGFsNmUzeTN2TS9wOEdvRURpVUVYZXMKLS0tIDZ6MEdPTVhCaTQ2UXFWTUFtc0pm
|
||||
MFlJb0c2WXJtMGRLZEZYY0pZWWpFWm8K/mlYZIe8UC0QU+1mq3NtrtTF5b2m5hCK
|
||||
+K0QiZLTKmmDcr4bRhZ32VE7R7GRwtMNnOP/mElZvPAyWyHHhRiOHg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N0hvV01naytDSWVwK1B1
|
||||
MXM1ZkdpaVc2Q3FPV2VBcC83WmcvSjdGUGgwClJGSXZ6YW5JeFlValNJbjVhK29u
|
||||
bUFqN2dRQTI4ZkwyeXNWYk5JeWVJRXcKLS0tIDlMMkNBUnNUSTJwVVFmc2dlcEZS
|
||||
VGQ1VHR2dXB0M3RsalppVWxiUUROM0UKZM/4QDTam3LDTzjnDs41Ije50R7Q7GC4
|
||||
IZbUZjs72rBzY8IkJDbN9JidadEc4NAtMOJwXiJbpZGiCBNfc8+SXw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURHlUL0RNMUtNallEcG5p
|
||||
eG4xby8yVzdQUTBaNkl4ano0YjBMcDd0Wm5NCkZvaXNPZm9wemJkMmNSdGdOaTI4
|
||||
Z1RwUnhiRUpCMWZaeWtlSVBmNW5KOXMKLS0tIDk0R0k3ZHczTFNCWUZxSWF0M0FJ
|
||||
MGlZMmtuSFYrcG1meDNMWDNqSjFxcE0KDu2dAc0gqmmPkpbpBe4YohM7rYmUwEkI
|
||||
V2FUQwjlvh50svtjCVdYbx2xuq4sQLnKelk/q1onLw60FwsVfzD8sQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
|
||||
lastmodified: "2026-07-19T02:30:40Z"
|
||||
|
||||
Reference in new issue
Block a user