Compare commits

..
Author SHA1 Message Date
beatzaplenty 75d09d57e3 Fix --allow-duplicate-host creating real duplicates in create-proxmox-resource.sh
Check NixOS configurations / eval-hosts (pull_request) Failing after 11m24s
--allow-duplicate-host previously just skipped the existing-resource
check entirely, so re-running e.g. --type lxc --host docker while an
lxc-docker container already existed created a second container
sharing the same hostname/identity instead of replacing it -- both
then fight over DNS/DHCP for that hostname, and it's easy to end up
testing the stale one without realizing.

Now splits matches into "exact" (same --type as the one being
created, e.g. another lxc-docker) and "cross-type" (a different
platform sharing this host identity, e.g. a proxmox-docker VM
alongside an lxc-docker container -- a deliberate, valid coexistence
this script has never managed and still won't). Only an exact match is
destroyed and replaced, after typing the hostname back to confirm; a
cross-type match is always left untouched. Without
--allow-duplicate-host, both cases still refuse to run exactly as
before.

Verified live against pve.sweet.home: correctly split VMID 103 (a
stopped proxmox-docker VM, cross-type -- left untouched) from VMID 105
(the running lxc-docker container, exact-type -- flagged for
destroy+replace), and confirmed the destroy prompt safely aborts on a
non-matching confirmation, leaving both resources untouched.
2026-07-20 13:22:08 +00:00
beatzaplenty 6847a7a6f4 Fix NFS shares never mounting on lxc-docker
Two compounding bugs, confirmed live on the running lxc-docker
container (vmid 102 on pve.sweet.home):

1. x-systemd.automount never works inside any Linux container --
   systemd logs "Starting of <unit>.automount unsupported" for every
   share and never mounts them. modules/docker/mount-data.nix and
   modules/raspi/mount-data.nix now key off config.boot.isContainer
   (set true by nixpkgs' proxmox-lxc.nix) to mount eagerly with
   `nofail` there instead, while VM-based docker targets keep automount
   unchanged.

2. The container's Proxmox `features` never included `mount=nfs`, so
   AppArmor blanket-denies the nfs/rpc_pipefs mount syscalls NFS
   needs ("permission denied"). scripts/env.sh's
   PROXMOX_DEFAULT_LXC_FEATURES now includes mount=nfs;nfs4 for future
   lxc-* containers -- the semicolon required quoting the --features
   value in create-proxmox-resource.sh's remote pct-create command,
   since it's sent as a raw string for the remote shell to parse and an
   unquoted `;` would be read as a command separator.

The already-running container needs a matching `pct set --features`
plus a restart to pick this up -- that's an operator step outside this
repo.
2026-07-20 12:56:42 +00:00
beatzaplenty 17dd00bee1 updated sops keys
Check NixOS configurations / eval-hosts (push) Failing after 11m21s
2026-07-20 12:27:24 +00:00
beatzaplenty b3c81453e4 updated sops keys
Check NixOS configurations / eval-hosts (push) Failing after 11m17s
2026-07-20 12:03:30 +00:00
beatzaplenty 656dd975f0 Merge pull request 'Add scripts/backup-admin-key.sh to back up the local sops admin key' (#13) from add-backup-admin-key-script into main
Check NixOS configurations / eval-hosts (push) Failing after 11m27s
2026-07-20 11:47:22 +00:00
rootandClaude Sonnet 5 2661f6d271 Add scripts/backup-admin-key.sh to back up the local sops admin key
Check NixOS configurations / eval-hosts (pull_request) Failing after 12m7s
Companion to rotate-admin-key.sh: copies whatever age identity sops/age
itself would resolve (or an explicit --key-file) to a given destination
path with 0600 perms, validating it's a real identity and round-tripping
the derived public key before/after the write so a corrupted copy is
caught immediately rather than discovered later during a restore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:42:02 +00:00
beatzaplenty 0532c3a282 Merge pull request 'Deduplicate reusable shell code in scripts/ into scripts/lib/' (#12) from worktree-scripts-dedup into main
Check NixOS configurations / eval-hosts (push) Failing after 12m16s
Reviewed-on: #12
2026-07-20 11:37:05 +00:00
beatzaplenty ac8c9a20e3 Merge pull request 'Fix tailscale exit-node module: enable routing, compose on enable-service' (#11) from worktree-tailscale-exit-node into main
Check NixOS configurations / eval-hosts (push) Failing after 12m10s
Reviewed-on: #11
2026-07-20 11:36:18 +00:00
root 0e66cdabc9 rotated sops age key
Check NixOS configurations / eval-hosts (push) Failing after 20m21s
2026-07-20 11:31:25 +00:00
beatzaplenty 9c892ce1c2 Merge pull request 'Add scripts/rotate-admin-key.sh to automate sops admin key rotation' (#10) from worktree-rotate-admin-key-script into main
Check NixOS configurations / eval-hosts (push) Failing after 47m28s
Reviewed-on: #10
2026-07-20 11:28:28 +00:00
beatzaplenty bfeea90597 Merge branch 'main' into worktree-rotate-admin-key-script
Check NixOS configurations / eval-hosts (pull_request) Failing after 1h6m12s
2026-07-20 11:27:45 +00:00
rootandClaude Sonnet 5 cafeb8853b Add scripts/rotate-admin-key.sh to automate sops admin key rotation
Check NixOS configurations / eval-hosts (pull_request) Failing after 11m15s
Automates the manual steps sync-host-keys.sh/create-proxmox-resource.sh
print when they bootstrap a fresh, not-yet-trusted age key: verifies a
backed-up key matches the current &admin entry, swaps in a new key, and
re-encrypts every secrets/*.yaml. Explicitly cds into repo_root before any
sops call, since sops resolves .sops.yaml by walking up from cwd rather
than from the target file's path -- confirmed via a scratch-repo test that
running from elsewhere would otherwise silently rotate against the wrong
config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:23:33 +00:00
beatzaplenty 2c2d464503 Merge pull request 'Fix stale documentation: outdated counts, missing build type, spec status' (#9) from fix-stale-docs into main
Check NixOS configurations / eval-hosts (push) Failing after 11m12s
2026-07-20 11:07:43 +00:00
rootandClaude Sonnet 5 5ec7033439 Fix stale documentation: outdated counts, missing build type, spec status
Check NixOS configurations / eval-hosts (pull_request) Failing after 11m26s
Same class of problem as the deployedTargets/README fixes: hand-maintained
prose that drifted from reality and nobody was obligated to update.

- CLAUDE.md: "18 hosts" was a stale hardcoded count (actually 20); reworded
  to not need updating as hosts are added. Also added the missing
  tailscale-exit-node build type to a list that had it everywhere else in
  the file except one bullet.
- AGENTS.md: same missing tailscale-exit-node build type.
- docs/auto-installer.md: the hand-enumerated lxc-* list was missing
  lxc-tailscale-exit-node.
- flake-target-refactor-spec.md: added a "Status: implemented" note so this
  completed historical spec (referenced elsewhere purely for rationale)
  can't be mistaken for an open plan with unresolved Open Questions.
- remove-sensetive-info-refactor.md: the "Definition of done" checklist was
  entirely unchecked despite most of the work being done. Checked off what's
  actually done (sops-nix migration, history scrub just performed, the
  pre-commit gitleaks hook), and left rotation of the GitHub PAT found in
  history explicitly flagged as the one still-open item -- an operator
  action against GitHub, not something this repo can attest to itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:06:05 +00:00
beatzaplenty 9133afd444 Merge pull request 'Fix duplicate-host check reporting false SSH failures' (#8) from worktree-fix-duplicate-host-check-exitcode into main
Check NixOS configurations / eval-hosts (push) Failing after 11m36s
2026-07-20 11:00:18 +00:00
rootandClaude Sonnet 5 7e51168d1b Deduplicate reusable shell code in scripts/ into scripts/lib/
Check NixOS configurations / eval-hosts (pull_request) Failing after 11m29s
Three chunks of copy-pasted logic were drifting across scripts/*.sh:
- codex-setup.sh and codex-maintenance.sh each carried an identical
  NIX_CONFIG bootstrap + ensure_nix_profile() -> scripts/lib/nix-bootstrap.sh
- sync-host-keys.sh and prepare-host-key.sh each ran the same
  ssh-keygen/ssh-to-age nix-shell invocations -> scripts/lib/ssh-host-keys.sh
  (prepare-host-key.sh now also calls env.sh's nix_extra_opts before using
  them, closing a gap where it alone skipped the nix-cache reachability
  check env.sh exists for)
- the "list nixosConfigurations attrNames" / "get one target's hostName"
  nix eval pattern was repeated across codex-setup.sh, codex-maintenance.sh,
  sync-host-keys.sh and create-proxmox-resource.sh (the latter twice, in
  its own --list and --host lookup) -> scripts/lib/nix-eval.sh, which also
  centralizes the --no-use-registries --no-accept-flake-config flag pair
  used on every such call

Verified against the real flake/node config (nix is available here):
create-proxmox-resource.sh --list for both --type lxc/vm, a full
--dry-run create, and prepare-host-key.sh generating and cleaning up a
real key/age-pubkey pair.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:35:49 +00:00
beatzaplentyandClaude Sonnet 5 c939454983 Fix tailscale exit-node module: enable routing, compose on enable-service
Check NixOS configurations / eval-hosts (pull_request) Failing after 11m52s
exit-node.nix advertised --advertise-exit-node/--advertise-routes but
never enabled IP forwarding, so it wouldn't actually route traffic.
services.tailscale.useRoutingFeatures = "server" is the built-in way to
get the sysctls right without duplicating modules/networking/enable-ip-forwarding.nix.
Also opens the firewall for direct (non-DERP-relayed) peer connections,
and now imports enable-service.nix instead of duplicating `enable = true`,
so "plain tailscale" and "tailscale exit node" are two composable modules.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 12:24:18 +10:00
24 changed files with 782 additions and 181 deletions
Submodule .claude/worktrees/scripts-dedup added at e578443914
+4 -2
View File
@@ -1,12 +1,13 @@
keys: keys:
- &admin age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667 - &admin age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e - &docker age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
- &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf - &server age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
- &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu - &nix-cache age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- &lxc-minimal age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8 - &lxc-minimal age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
- &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu - &nix-minimal age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
- &lxc-nix-cache age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
- &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg - &proxmox-minimal age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
- &lxc-nix-cache age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
- &lxc-docker age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
creation_rules: creation_rules:
# Shared across every currently-deployed host: root/nixos password hash, # Shared across every currently-deployed host: root/nixos password hash,
@@ -24,6 +25,7 @@ creation_rules:
- *nix-minimal - *nix-minimal
- *lxc-nix-cache - *lxc-nix-cache
- *proxmox-minimal - *proxmox-minimal
- *lxc-docker
- path_regex: secrets/nix-cache\.yaml$ - path_regex: secrets/nix-cache\.yaml$
key_groups: key_groups:
+1 -1
View File
@@ -7,7 +7,7 @@ servers and workstation.
The flake exposes NixOS configurations named `<platform>-<buildtype>` The flake exposes NixOS configurations named `<platform>-<buildtype>`
(platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`, (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 and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
every combination is built — `pxe-boot` has no `linode` variant. See every combination is built — `pxe-boot` has no `linode` variant. See
`README.md` for the full current target list; treat `flake.nix` as the `README.md` for the full current target list; treat `flake.nix` as the
+35 -9
View File
@@ -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 sweeps: after editing one or two hosts/modules, evaluate just the
`nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball` `nixosConfigurations.<host>` you touched (plus any `config.system.build.tarball`
/`diskoImagesScript`/package output affected) rather than looping over every /`diskoImagesScript`/package output affected) rather than looping over every
host — `codex-maintenance.sh` evaluates 18 hosts plus every package/tarball/ host — `codex-maintenance.sh` evaluates every `nixosConfigurations` host plus
image variant now and is slow to run after each small change. Reserve a full 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 `codex-maintenance.sh` run for changes that plausibly affect every host
(`modules/common/*`, `flake.nix`, `variables.nix`) or as a final check before (`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 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` reference `variables.nix` (confirmed empirically — `nix flake metadata`
errors on it), so this is the closest equivalent to a single source of errors on it), so this is the closest equivalent to a single source of
truth for the tracked release. 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 `sync-host-keys.sh`, `create-proxmox-resource.sh`, and
state when run for real (not `--dry-run`): real `secrets/*.yaml` `rotate-admin-key.sh` genuinely mutate real state when run for real (not
recipients, real Proxmox VMs/containers. They require the operator's own `--dry-run`): real `secrets/*.yaml` recipients, real Proxmox VMs/
SSH/sops access, which an agent session doesn't have — but don't suggest containers, real revocation of decrypt access. They require the
running either non-dry-run without the operator's explicit go-ahead even operator's own SSH/sops access, which an agent session doesn't have — but
if it becomes technically reachable. 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 ## Architecture
@@ -164,7 +190,7 @@ removing a host.
`vzdump` backup-archive metadata this doesn't have), no install step — `vzdump` backup-archive metadata this doesn't have), no install step —
see `docs/auto-installer.md`. see `docs/auto-installer.md`.
- `modules/build-types/*.nix` — what a system is for: - `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 - `modules/common/configuration.nix` — base NixOS config imported by every
host: locale, users, nix settings, git. host: locale, users, nix settings, git.
- `modules/common/home.nix` / `hosts/nixos/home.nix` — Home Manager config for - `modules/common/home.nix` / `hosts/nixos/home.nix` — Home Manager config for
+1 -1
View File
@@ -19,7 +19,7 @@ see "LXC hosts" immediately below for why those are different.**
## LXC hosts ## LXC hosts
`lxc-*` targets (`lxc-minimal`, `lxc-nix-cache`, `lxc-server`, `lxc-docker`, `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; interactive menu deliberately excludes them. Don't try to select one there;
`nixos-install` would bind-mount `/` onto `/mnt` (LXC containers have no raw `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 disk to partition) and then refuse to touch the filesystem it's currently
+8
View File
@@ -1,5 +1,13 @@
# Spec: Refactor Flake Targets into Platform × Build-Type Matrix # 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 ## Context
The flake at `~/nixos` currently defines these output targets (flat, ad-hoc naming): The flake at `~/nixos` currently defines these output targets (flat, ad-hoc naming):
+15 -10
View File
@@ -1,5 +1,15 @@
{ config, lib, pkgs, vars, ... }: { config, lib, pkgs, vars, ... }:
let
# `x-systemd.automount` never works inside a Linux container (LXC
# included, regardless of privilege) -- confirmed live on lxc-docker:
# systemd logs "Starting of <unit>.automount unsupported" for every
# share and never mounts them. Mount eagerly there instead, with
# `nofail` so a boot with the NFS server unreachable doesn't hang
# (the VM platforms rely on automount itself to get that same
# non-blocking behavior, so they don't need `nofail` too).
automountOpts = if config.boot.isContainer then [ "nofail" ] else [ "x-systemd.automount" ];
in
{ {
fileSystems = { fileSystems = {
${vars.nfsShares.dockerConfig.mountpoint} = { ${vars.nfsShares.dockerConfig.mountpoint} = {
@@ -9,9 +19,8 @@
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
]; ] ++ automountOpts;
}; };
${vars.nfsShares.dockerDatabases.mountpoint} = { ${vars.nfsShares.dockerDatabases.mountpoint} = {
@@ -21,9 +30,8 @@
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
]; ] ++ automountOpts;
}; };
${vars.nfsShares.dockerVolumes.mountpoint} = { ${vars.nfsShares.dockerVolumes.mountpoint} = {
@@ -33,9 +41,8 @@
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
]; ] ++ automountOpts;
}; };
${vars.nfsShares.nextcloudData.mountpoint} = { ${vars.nfsShares.nextcloudData.mountpoint} = {
@@ -45,9 +52,8 @@
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
]; ] ++ automountOpts;
}; };
${vars.nfsShares.raspiVolumes.mountpoint} = { ${vars.nfsShares.raspiVolumes.mountpoint} = {
@@ -57,9 +63,8 @@
options = [ options = [
"nfsvers=4.2" "nfsvers=4.2"
"_netdev" "_netdev"
"x-systemd.automount"
"noatime" "noatime"
]; ] ++ automountOpts;
}; };
}; };
} }
+10 -4
View File
@@ -1,4 +1,4 @@
{ vars, ... }: { config, lib, vars, ... }:
{ {
fileSystems.${vars.raspiMountpoint} = { fileSystems.${vars.raspiMountpoint} = {
@@ -9,6 +9,15 @@
"_netdev" "_netdev"
"noatime" "noatime"
# Explicitly use NFSv4.2 if supported
"nfsvers=4.2"
] ++ lib.optionals (!config.boot.isContainer) [
# `x-systemd.automount` never works inside a Linux container (LXC
# included) -- confirmed live on lxc-docker: systemd logs "Starting
# of <unit>.automount unsupported" and never mounts it. `nofail`
# above already keeps boot non-blocking there, so plain eager
# mounting is fine.
# Don't mount until first access # Don't mount until first access
"x-systemd.automount" "x-systemd.automount"
@@ -17,9 +26,6 @@
# Give the Pi/Tailscale a little time to appear # Give the Pi/Tailscale a little time to appear
"x-systemd.device-timeout=10s" "x-systemd.device-timeout=10s"
# Explicitly use NFSv4.2 if supported
"nfsvers=4.2"
]; ];
}; };
+9 -1
View File
@@ -1,8 +1,16 @@
_: _:
{ {
imports = [ ./enable-service.nix ];
services.tailscale = { 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 # extraSetFlags (tailscale set, via the always-on tailscaled-set
# service), not extraUpFlags -- extraUpFlags is only ever applied by # service), not extraUpFlags -- extraUpFlags is only ever applied by
+22 -10
View File
@@ -122,13 +122,25 @@ Add a pre-commit hook (or a `nix flake check` step) running `gitleaks protect --
## Definition of done ## Definition of done
- [ ] Milestone 1 inventory complete and reviewed **Status as of 2026-07-20:** Milestones 13 are done — sops-nix is fully
- [ ] All hosts have per-host age keys; admin key backed up outside the repo wired (`.sops.yaml`, `secrets/*.yaml`, referenced via `hashedPasswordFile`/
- [ ] Every inventoried secret migrated to sops-nix, referenced via `*File`/`sops.secrets.*.path`, nothing plaintext in the working tree `*File`/`sops.secrets.*.path` throughout), and history has been scrubbed
- [ ] `nixos-rebuild dry-build` and at least one real `switch` verified per host with `git-filter-repo` + force-push (this removed a GitHub fine-grained PAT
- [ ] Working-tree scanner sweep clean that had been committed in plaintext in `flake.nix`/`common/home.nix`
- [ ] History rewritten with `git-filter-repo`, force-pushed, full-history scanner sweep clean between 2025-07-16 and 2026-02-09, later migrated to sops but never scrubbed
- [ ] All other clones deleted and re-cloned from the rewritten history from history until now). **Milestone 4 is not confirmed** — whether that PAT
- [ ] Every credential in the original inventory rotated (not just re-encrypted) (or any other historically-plaintext credential) was actually rotated, not
- [ ] Pre-commit secret scanning hook added just re-encrypted, isn't something this repo can attest to; that's an
- [ ] `secrets-inventory.md` deleted from the working directory (never committed) 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)
+148
View File
@@ -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
+17 -27
View File
@@ -1,22 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
export NIX_CONFIG="${NIX_CONFIG:-} script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
experimental-features = nix-command flakes # shellcheck source=lib/nix-bootstrap.sh
accept-flake-config = false source "${script_dir}/lib/nix-bootstrap.sh"
warn-dirty = false # shellcheck source=lib/nix-eval.sh
" source "${script_dir}/lib/nix-eval.sh"
MODE="${1:-validate}" MODE="${1:-validate}"
ensure_nix_profile() {
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
fi
}
ensure_nix_profile ensure_nix_profile
if ! command -v nix >/dev/null 2>&1; then if ! command -v nix >/dev/null 2>&1; then
@@ -24,8 +16,7 @@ if ! command -v nix >/dev/null 2>&1; then
exit 127 exit 127
fi fi
hosts_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames)" hosts="$(list_flake_targets .)"
hosts="$(echo "$hosts_json" | jq -r '.[]')"
echo "Hosts:" echo "Hosts:"
echo "$hosts" echo "$hosts"
@@ -44,17 +35,17 @@ fi
echo echo
echo "Checking Nix formatting with nixpkgs-fmt..." echo "Checking Nix formatting with nixpkgs-fmt..."
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check . nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
echo echo
echo "Running statix lint..." echo "Running statix lint..."
nix run --no-use-registries --no-accept-flake-config github:NixOS/nixpkgs/nixos-25.11#statix -- check . nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check .
echo echo
echo "Evaluating host toplevel derivations..." echo "Evaluating host toplevel derivations..."
for host in $hosts; do for host in $hosts; do
echo "==> $host" echo "==> $host"
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath" nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of # lxc-* hosts deploy via a directly pct-restore-able tarball instead of
# nixos-install (see docs/auto-installer.md); proxmox-* hosts can # nixos-install (see docs/auto-installer.md); proxmox-* hosts can
@@ -64,22 +55,21 @@ for host in $hosts; do
case "$host" in case "$host" in
lxc-*) lxc-*)
echo "==> $host (tarball)" echo "==> $host (tarball)"
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath" nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball.drvPath"
;; ;;
proxmox-*) proxmox-*)
echo "==> $host (diskoImagesScript)" echo "==> $host (diskoImagesScript)"
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath" nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath"
;; ;;
esac esac
done done
echo echo
echo "Evaluating buildable packages..." echo "Evaluating buildable packages..."
packages_json="$(nix eval --json --no-use-registries --no-accept-flake-config .#packages.x86_64-linux --apply builtins.attrNames)" packages="$(nix eval --json "${NIX_EVAL_FLAGS[@]}" .#packages.x86_64-linux --apply builtins.attrNames | jq -r '.[]')"
packages="$(echo "$packages_json" | jq -r '.[]')"
for pkg in $packages; do for pkg in $packages; do
echo "==> packages.x86_64-linux.${pkg}" echo "==> packages.x86_64-linux.${pkg}"
nix eval --raw --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}" nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
done done
if [[ "$MODE" == "dry-run" ]]; then if [[ "$MODE" == "dry-run" ]]; then
@@ -87,16 +77,16 @@ if [[ "$MODE" == "dry-run" ]]; then
echo "Running dry-run builds for all hosts. This will not create result symlinks." echo "Running dry-run builds for all hosts. This will not create result symlinks."
for host in $hosts; do for host in $hosts; do
echo "==> Dry-run build: $host" echo "==> Dry-run build: $host"
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel" nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel"
case "$host" in case "$host" in
lxc-*) lxc-*)
echo "==> Dry-run build: $host (tarball)" echo "==> Dry-run build: $host (tarball)"
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.tarball" nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.tarball"
;; ;;
proxmox-*) proxmox-*)
echo "==> Dry-run build: $host (diskoImagesScript)" echo "==> Dry-run build: $host (diskoImagesScript)"
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript" nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.diskoImagesScript"
;; ;;
esac esac
done done
@@ -105,7 +95,7 @@ if [[ "$MODE" == "dry-run" ]]; then
echo "Running dry-run builds for all packages." echo "Running dry-run builds for all packages."
for pkg in $packages; do for pkg in $packages; do
echo "==> Dry-run build: packages.x86_64-linux.${pkg}" echo "==> Dry-run build: packages.x86_64-linux.${pkg}"
nix build --dry-run --no-link --no-use-registries --no-accept-flake-config ".#packages.x86_64-linux.${pkg}" nix build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}" ".#packages.x86_64-linux.${pkg}"
done done
fi fi
+7 -15
View File
@@ -1,19 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
export NIX_CONFIG="${NIX_CONFIG:-} script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
experimental-features = nix-command flakes # shellcheck source=lib/nix-bootstrap.sh
accept-flake-config = false source "${script_dir}/lib/nix-bootstrap.sh"
warn-dirty = false # shellcheck source=lib/nix-eval.sh
" source "${script_dir}/lib/nix-eval.sh"
ensure_nix_profile() {
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
fi
}
install_nix_if_missing() { install_nix_if_missing() {
if command -v nix >/dev/null 2>&1; then if command -v nix >/dev/null 2>&1; then
@@ -79,13 +71,13 @@ if ! command -v jq >/dev/null 2>&1; then
fi fi
echo "Available NixOS hosts:" echo "Available NixOS hosts:"
hosts="$(nix eval --json --no-use-registries --no-accept-flake-config .#nixosConfigurations --apply builtins.attrNames | jq -r '.[]')" hosts="$(list_flake_targets .)"
echo "$hosts" echo "$hosts"
echo "Evaluating all host toplevel derivations..." echo "Evaluating all host toplevel derivations..."
for host in $hosts; do for host in $hosts; do
echo "==> Evaluating $host" echo "==> Evaluating $host"
nix eval --raw --no-use-registries --no-accept-flake-config ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath" nix eval --raw "${NIX_EVAL_FLAGS[@]}" ".#nixosConfigurations.${host}.config.system.build.toplevel.drvPath"
done done
echo "Codex setup complete." echo "Codex setup complete."
+127 -24
View File
@@ -13,12 +13,21 @@
# refuses to run if the target VMID already exists on the node, or if # refuses to run if the target VMID already exists on the node, or if
# a VM/CT identified as --host already exists under any other VMID # a VM/CT identified as --host already exists under any other VMID
# (checked live against the node; --allow-duplicate-host overrides). # (checked live against the node; --allow-duplicate-host overrides).
# - --allow-duplicate-host distinguishes an exact match (same --type
# *and* --host, e.g. re-running --type lxc --host docker while an
# lxc-docker container already exists -- almost always a redeploy of
# the same target to pick up a rebuilt image) from a cross-type match
# (a different platform sharing the same host identity, e.g. a
# proxmox-docker VM coexisting with lxc-docker). Only the exact match
# is destroyed and replaced, after typing the hostname back to
# confirm (outside --dry-run) -- a cross-type match is always left
# untouched, matching-or-not.
# - --modify only ever touches a resource you name explicitly via # - --modify only ever touches a resource you name explicitly via
# --vmid, shows exactly what will change first, and (outside # --vmid, shows exactly what will change first, and (outside
# --dry-run) always requires typing that VMID back to confirm before # --dry-run) always requires typing that VMID back to confirm before
# anything is sent to the node. There is no bulk/implicit modify. # anything is sent to the node. There is no bulk/implicit modify.
# - Neither mode can start/stop/delete a resource. Not implemented on # - Outside of --allow-duplicate-host's exact-match replace above,
# purpose -- ask before adding it. # neither mode can start/stop/delete a resource.
# #
# See --help for the full option list. # See --help for the full option list.
set -euo pipefail set -euo pipefail
@@ -26,6 +35,8 @@ set -euo pipefail
repo_root="$(cd "$(dirname "$0")/.." && pwd)" repo_root="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=env.sh # shellcheck source=env.sh
source "${repo_root}/scripts/env.sh" source "${repo_root}/scripts/env.sh"
# shellcheck source=lib/nix-eval.sh
source "${repo_root}/scripts/lib/nix-eval.sh"
sync_keys="${repo_root}/scripts/sync-host-keys.sh" sync_keys="${repo_root}/scripts/sync-host-keys.sh"
@@ -62,7 +73,13 @@ Create mode (default):
already exists on the node (checked live via already exists on the node (checked live via
qm/pct, not any file in this repo) -- qm/pct, not any file in this repo) --
otherwise refused, since it'd share that otherwise refused, since it'd share that
host's hostName/hostId. host's hostName/hostId. An existing resource
of this *same* --type (e.g. re-running --type
lxc --host docker over an existing lxc-docker)
is destroyed and replaced, after confirming --
a different --type sharing the same --host
(e.g. a proxmox-docker VM) is always left
untouched.
Modify mode (reconfigure an EXISTING resource -- requires --modify): Modify mode (reconfigure an EXISTING resource -- requires --modify):
--modify Switch to modify mode. --modify Switch to modify mode.
@@ -240,15 +257,21 @@ platform_prefix="lxc"
[[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB" [[ -z "$memory" ]] && memory="$PROXMOX_DEFAULT_MEMORY_MB"
# --- discover / resolve the flake target from --host -------------------- # --- discover / resolve the flake target from --host --------------------
# Emits "<target>\t<hostName>" pairs for every ${platform_prefix}-* flake
# target -- the one source both --list and the --host lookup below read
# from, so they can never see a different set of targets from each other.
targets_for_platform() {
local target
for target in $(list_flake_targets "$repo_root" 2>/dev/null | grep -- "^${platform_prefix}-"); do
printf '%s\t%s\n' "$target" "$(flake_target_hostname "$repo_root" "$target")"
done
}
list_hosts() { list_hosts() {
local target hostname local target hostname
for target in $(nix eval --json --no-use-registries --no-accept-flake-config \ while IFS=$'\t' read -r target hostname; do
"${repo_root}#nixosConfigurations" --apply builtins.attrNames 2>/dev/null \
| jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do
hostname="$(nix eval --raw --no-use-registries --no-accept-flake-config \
"${repo_root}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null)"
printf ' %-12s -> %s\n' "$hostname" "$target" printf ' %-12s -> %s\n' "$hostname" "$target"
done done < <(targets_for_platform)
} }
if [[ "$do_list" -eq 1 ]]; then if [[ "$do_list" -eq 1 ]]; then
@@ -263,16 +286,12 @@ if [[ -z "$host" ]]; then
fi fi
flake_target="" flake_target=""
for target in $(nix eval --json --no-use-registries --no-accept-flake-config \ while IFS=$'\t' read -r target hostname; do
"${repo_root}#nixosConfigurations" --apply builtins.attrNames \ if [[ "$hostname" == "$host" ]]; then
| jq -r --arg p "${platform_prefix}-" '.[] | select(startswith($p))'); do
hn="$(nix eval --raw --no-use-registries --no-accept-flake-config \
"${repo_root}#nixosConfigurations.${target}.config.networking.hostName")"
if [[ "$hn" == "$host" ]]; then
flake_target="$target" flake_target="$target"
break break
fi fi
done done < <(targets_for_platform)
if [[ -z "$flake_target" ]]; then if [[ -z "$flake_target" ]]; then
echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2 echo "ERROR: no ${platform_prefix}-* target has hostName '${host}'." >&2
@@ -300,12 +319,13 @@ fi
# this script itself always uses unless --name is overridden) -- a guest # this script itself always uses unless --name is overridden) -- a guest
# manually renamed on the node afterwards wouldn't match, but nothing here # manually renamed on the node afterwards wouldn't match, but nothing here
# creates guests that way. # creates guests that way.
if [[ "$allow_duplicate_host" -eq 1 ]]; then if [[ "$dry_run" -eq 1 ]]; then
echo
echo "--allow-duplicate-host: skipping the check for an existing '${host}' on ${node}."
elif [[ "$dry_run" -eq 1 ]]; then
echo echo
echo "[dry-run] would check ${node} for an existing VM/CT identified as '${host}'" echo "[dry-run] would check ${node} for an existing VM/CT identified as '${host}'"
if [[ "$allow_duplicate_host" -eq 1 ]]; then
echo "[dry-run] --allow-duplicate-host: an existing ${type} named '${host}' would be" \
"destroyed and replaced; a different-type match would be left untouched"
fi
else else
echo echo
echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..." echo "==> Checking ${node} for an existing VM/CT identified as '${host}'..."
@@ -330,17 +350,90 @@ REMOTE_SCRIPT
echo "check entirely)." >&2 echo "check entirely)." >&2
exit 1 exit 1
fi fi
# Split into "exact" (same resource kind as --type -- i.e. literally this
# same host+platform combo already exists, almost always a redeploy of
# the same target to test a rebuilt image) vs "cross-type" (a different
# platform sharing this host identity, e.g. a stopped proxmox-docker VM
# coexisting with an lxc-docker container -- a deliberate, valid setup
# this script has never managed and still won't). Read via a herestring
# (not a pipe) so the appends below survive outside the loop.
this_kind="$type"
exact_matches=""
cross_matches=""
if [[ -n "$existing" ]]; then if [[ -n "$existing" ]]; then
echo "ERROR: '${host}' already exists on ${node}:" >&2 while read -r kind id n; do
echo "$existing" | while read -r kind id n; do [[ -z "$kind" ]] && continue
if [[ "$kind" == "$this_kind" ]]; then
exact_matches+="${kind} ${id} ${n}"$'\n'
else
cross_matches+="${kind} ${id} ${n}"$'\n'
fi
done <<<"$existing"
fi
if [[ -n "$exact_matches" && "$allow_duplicate_host" -ne 1 ]]; then
echo "ERROR: '${host}' already exists on ${node} as this same resource type:" >&2
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})" >&2
done
echo "Refusing to create a second ${this_kind} sharing this identity. Pass" >&2
echo "--allow-duplicate-host to destroy it and create a fresh one in its place" >&2
echo "(after confirming), or use --modify to reconfigure the existing one instead." >&2
exit 1
fi
if [[ -n "$cross_matches" && "$allow_duplicate_host" -ne 1 ]]; then
echo "ERROR: '${host}' already exists on ${node} as a different resource type:" >&2
echo "$cross_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})" >&2 echo " - ${kind} VMID ${id} (${n})" >&2
done done
echo "Refusing to create a second resource sharing this identity. Pass" >&2 echo "Refusing to create a second resource sharing this identity. Pass" >&2
echo "--allow-duplicate-host to create one anyway (it gets its own distinct" >&2 echo "--allow-duplicate-host to create one anyway (it gets its own distinct" >&2
echo "sops key and VMID -- the existing resource(s) above are left untouched)," >&2 echo "sops key and VMID -- the existing resource above is left untouched)," >&2
echo "or use --modify to reconfigure the existing one instead." >&2 echo "or use --modify to reconfigure the existing one instead." >&2
exit 1 exit 1
fi fi
if [[ -n "$cross_matches" ]]; then
echo "--allow-duplicate-host: '${host}' also exists on ${node} as a different resource" \
"type -- leaving it untouched:"
echo "$cross_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})"
done
fi
if [[ -n "$exact_matches" ]]; then
echo "--allow-duplicate-host: '${host}' already exists on ${node} as this same resource" \
"type -- it will be destroyed and replaced:"
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo " - ${kind} VMID ${id} (${n})"
done
echo
read -rp "Type the hostname (${host}) to confirm destroying the above and replacing it: " confirm
if [[ "$confirm" != "$host" ]]; then
echo "Cancelled -- input didn't match ${host}." >&2
exit 1
fi
echo "$exact_matches" | while read -r kind id n; do
[[ -z "$kind" ]] && continue
echo "==> Destroying ${kind} VMID ${id} (${n})..."
if [[ "$kind" == "vm" ]]; then
# qm destroy has no --force to stop-then-destroy in one call (pct's
# does) -- stop explicitly first if it's running.
if ssh "$ssh_target" "qm status ${id}" 2>/dev/null | grep -q running; then
ssh "$ssh_target" "qm stop ${id}"
fi
ssh "$ssh_target" "qm destroy ${id} --purge 1"
else
ssh "$ssh_target" "pct destroy ${id} --force 1 --purge 1"
fi
done
fi
fi fi
echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'" echo "Target: ${flake_target} (host=${host}, type=${type}) -> Proxmox resource '${name}'"
@@ -517,7 +610,17 @@ if [[ "$type" == "lxc" ]]; then
# systemd routinely uses (even plain getty units), and every getty # systemd routinely uses (even plain getty units), and every getty
# crash-loops on a denied mount every ~3s (visible as garbage on the # crash-loops on a denied mount every ~3s (visible as garbage on the
# console) while core services like nsncd fail the same way. # console) while core services like nsncd fail the same way.
create_cmd="pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged 1 --features ${PROXMOX_DEFAULT_LXC_FEATURES} --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp" #
# ...,mount=nfs;nfs4: without it AppArmor blanket-denies the `nfs`/
# `rpc_pipefs` mount syscalls any NFS client share needs -- confirmed
# live on lxc-docker: `mount: /var/lib/nfs/rpc_pipefs: permission
# denied`. The value's `;` (Proxmox's own multi-fstype separator for
# this one feature, per PVE::LXC's use of PVE::ParseUtils::split_list)
# must stay single-quoted here: create_cmd is sent to `remote()`, which
# hands the whole string to `ssh` as a single command for the *remote*
# shell to parse -- unquoted, that `;` would be read as a remote
# command separator and silently truncate this into two commands.
create_cmd="pct create ${vmid} ${iso_storage}:vztmpl/${remote_filename} --unprivileged 1 --features '${PROXMOX_DEFAULT_LXC_FEATURES}' --rootfs ${storage}:${local_disk_size} --hostname ${name} --cores ${cores} --memory ${memory} --swap ${local_swap} --net0 name=eth0,bridge=${bridge},ip=dhcp"
remote "$create_cmd" remote "$create_cmd"
remote "pct start ${vmid}" remote "pct start ${vmid}"
else else
+9 -1
View File
@@ -45,7 +45,15 @@
# crash-loops on a denied `/run/credentials/*` mount every ~3s (visible # crash-loops on a denied `/run/credentials/*` mount every ~3s (visible
# as garbage on the console) and core services like nsncd fail the same # as garbage on the console) and core services like nsncd fail the same
# way on userns_create; system.build.tarball never finishes activating. # way on userns_create; system.build.tarball never finishes activating.
: "${PROXMOX_DEFAULT_LXC_FEATURES:=nesting=1,keyctl=1}" #
# mount=nfs;nfs4: without it, AppArmor blanket-denies the `nfs`/
# `rpc_pipefs` mount syscalls any NFS client share needs -- confirmed
# live on lxc-docker (which mounts several, see modules/docker/mount-data.nix
# and modules/raspi/mount-data.nix): `mount: /var/lib/nfs/rpc_pipefs:
# permission denied`. Harmless to grant on lxc targets that don't mount
# NFS at all -- it only widens what the container is *allowed* to mount,
# nothing here forces a mount to happen.
: "${PROXMOX_DEFAULT_LXC_FEATURES:=nesting=1,keyctl=1,mount=nfs;nfs4}"
export PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE PROXMOX_ISO_STORAGE \ export PROXMOX_HOST PROXMOX_SSH_USER PROXMOX_STORAGE PROXMOX_ISO_STORAGE \
PROXMOX_BRIDGE PROXMOX_DEFAULT_CORES PROXMOX_DEFAULT_MEMORY_MB \ PROXMOX_BRIDGE PROXMOX_DEFAULT_CORES PROXMOX_DEFAULT_MEMORY_MB \
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Shared Nix bootstrap for scripts/codex-setup.sh and
# scripts/codex-maintenance.sh: the nix.conf settings both need in effect
# before a single `nix` command runs (flakes enabled, never honor a flake
# input's own nixConfig, no "dirty tree" warning spam), plus a helper to
# pull an already-installed Nix's daemon/profile script onto PATH if it
# isn't there yet. Source this instead of copying it -- see CLAUDE.md.
export NIX_CONFIG="${NIX_CONFIG:-}
experimental-features = nix-command flakes
accept-flake-config = false
warn-dirty = false
"
ensure_nix_profile() {
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
elif [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
fi
}
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Shared flake-introspection helpers for scripts/*.sh. Source alongside
# env.sh:
# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/nix-eval.sh"
#
# NIX_EVAL_FLAGS: --no-use-registries so a call here never resolves through
# the user's global flake registry (every call targets this repo's own
# flake, or an explicit github: ref, not a registry alias); --no-accept-flake-config
# so a flake input's own nixConfig (e.g. a dependency's substituters) is
# never honored -- matches accept-flake-config = false already set repo-wide
# (see lib/nix-bootstrap.sh / CLAUDE.md). Reuse this array rather than
# retyping the two flags at each call site.
declare -a NIX_EVAL_FLAGS=(--no-use-registries --no-accept-flake-config)
# list_flake_targets <flake_ref>
# Prints the attribute names under <flake_ref>#nixosConfigurations, one per
# line, e.g.:
# list_flake_targets . # from inside the repo
# list_flake_targets "$repo_root" # from anywhere
list_flake_targets() {
local flake_ref="$1"
nix eval --json "${NIX_EVAL_FLAGS[@]}" \
"${flake_ref}#nixosConfigurations" --apply builtins.attrNames \
| jq -r '.[]'
}
# flake_target_hostname <flake_ref> <target>
# Prints one nixosConfigurations target's config.networking.hostName.
# Empty (not an error under set -e) if the target doesn't exist or the
# eval otherwise fails -- callers that need to distinguish "empty" from
# "eval failed" should check $? themselves instead of relying on this.
flake_target_hostname() {
local flake_ref="$1" target="$2"
nix eval --raw "${NIX_EVAL_FLAGS[@]}" \
"${flake_ref}#nixosConfigurations.${target}.config.networking.hostName" 2>/dev/null
}
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Shared SSH-host-key / age-conversion helpers for scripts/sync-host-keys.sh
# and scripts/prepare-host-key.sh -- both generate the same kind of key
# (ed25519, no passphrase, the sops-nix age-derivation input) and convert it
# to an age recipient the same way; kept in one place so the two can't
# drift apart.
#
# Uses NIX_OPTS (an array of extra `nix-shell` options -- see env.sh's
# nix_extra_opts) if the caller has already set it, so a decision to avoid
# an unreachable nix-cache is reused here instead of probed again. Falls
# back to no extra options if the caller never sourced env.sh.
if ! declare -p NIX_OPTS >/dev/null 2>&1; then
declare -a NIX_OPTS=()
fi
# generate_host_ed25519_key <hostname> <keyfile>
# Writes <keyfile> and <keyfile>.pub. Caller is responsible for refusing to
# overwrite an existing keyfile -- this always runs ssh-keygen fresh.
generate_host_ed25519_key() {
local hostname="$1" keyfile="$2"
nix-shell "${NIX_OPTS[@]}" -p openssh --run \
"ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null
}
# ssh_pubkey_to_age <pubkeyfile>
# Prints the age public key derived from an ed25519 SSH public key file.
ssh_pubkey_to_age() {
local pubkeyfile="$1"
nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${pubkeyfile}'"
}
+7 -2
View File
@@ -23,6 +23,10 @@
set -euo pipefail set -euo pipefail
repo_root="$(cd "$(dirname "$0")/.." && pwd)" repo_root="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=env.sh
source "${repo_root}/scripts/env.sh"
# shellcheck source=lib/ssh-host-keys.sh
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
hostname="${1:?usage: scripts/prepare-host-key.sh <hostname>}" hostname="${1:?usage: scripts/prepare-host-key.sh <hostname>}"
sops_yaml="${repo_root}/.sops.yaml" sops_yaml="${repo_root}/.sops.yaml"
@@ -41,9 +45,10 @@ if [[ -f "$keyfile" ]]; then
exit 1 exit 1
fi fi
nix-shell -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${hostname}' -f '${keyfile}'" >/dev/null nix_extra_opts
generate_host_ed25519_key "$hostname" "$keyfile"
age_pub="$(nix-shell -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")" age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
cat <<EOF cat <<EOF
+190
View File
@@ -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
+7 -5
View File
@@ -31,6 +31,10 @@ editor="${repo_root}/scripts/lib/sync-host-keys-edit-sops.py"
# shellcheck source=env.sh # shellcheck source=env.sh
source "${repo_root}/scripts/env.sh" source "${repo_root}/scripts/env.sh"
# shellcheck source=lib/nix-eval.sh
source "${repo_root}/scripts/lib/nix-eval.sh"
# shellcheck source=lib/ssh-host-keys.sh
source "${repo_root}/scripts/lib/ssh-host-keys.sh"
mkdir -p "$keydir" mkdir -p "$keydir"
@@ -118,12 +122,10 @@ EOF
} }
discover_targets() { discover_targets() {
nix eval --json --no-use-registries --no-accept-flake-config \
"${repo_root}#nixosConfigurations" --apply builtins.attrNames \
| jq -r '.[] | select(. != "installer")'
# installer is the one nixosConfigurations target that doesn't import # installer is the one nixosConfigurations target that doesn't import
# sops-nix at all (see CLAUDE.md's "Security Notes" -- hardcoded login # sops-nix at all (see CLAUDE.md's "Security Notes" -- hardcoded login
# password instead) -- config.sops.secrets doesn't exist for it. # password instead) -- config.sops.secrets doesn't exist for it.
list_flake_targets "$repo_root" | grep -v '^installer$'
} }
locally_managed_hosts() { locally_managed_hosts() {
@@ -159,7 +161,7 @@ queue_host_sync() {
echo "[dry-run] ${host}: would generate host key" echo "[dry-run] ${host}: would generate host key"
else else
echo "==> ${host}: generating host key" echo "==> ${host}: generating host key"
nix-shell "${NIX_OPTS[@]}" -p openssh --run "ssh-keygen -t ed25519 -N '' -C '${host}' -f '${keyfile}'" >/dev/null generate_host_ed25519_key "$host" "$keyfile"
fi fi
else else
echo "==> ${host}: host key already present" echo "==> ${host}: host key already present"
@@ -170,7 +172,7 @@ queue_host_sync() {
if [[ "$dry_run" -eq 1 ]]; then if [[ "$dry_run" -eq 1 ]]; then
age_pub="dry-run-placeholder-not-a-real-key" age_pub="dry-run-placeholder-not-a-real-key"
else else
age_pub="$(nix-shell "${NIX_OPTS[@]}" -p ssh-to-age --run "ssh-to-age -i '${keyfile}.pub'")" age_pub="$(ssh_pubkey_to_age "${keyfile}.pub")"
fi fi
add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \ add_keys_json="$(jq --arg host "$host" --arg key "$age_pub" \
'. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")" '. + [{host: $host, age_key: $key}]' <<<"$add_keys_json")"
+51 -42
View File
@@ -5,76 +5,85 @@ sops:
age: age:
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpWjNSdEdZbUUzamswa08w YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaMUpxYmUzaVY4d1pRY3g3
aCtzSHB0bFVZMnYxTkpuM1psdVYzWW55SzMwCkliMWVOUlBqRG5wOGZjQVg4MkFz empaVUJSN0JaSlNQQlJtZENwc0VMYVhJSkFvCmJQMkpSd3RoYzlKQjBSZWErQzR2
NEVkMXdkTjhWRlZmVGlzZElid2pUMXMKLS0tIGtHUmRCNXNhVmloUHYzQnE5YlBS aG5SVndOQWpSRTBDSWJVQkg0c0hiNFUKLS0tIHhiaEdpY3gwZkpCcHl2TW5CNThn
YnVSQjJlT3JnQ1RNMm9xV2xKOGRZUDAKc4VTl9NEI9Rv8+4J3JTeHTt2h8Dr2IJv aVVUQy9Qd0trb0RNdUpVTXdrdGlrTmcK1uphQAyDV+Gk5+K1YOqw1Z8ynGP5sAPF
tfvoNJQM/w6RAJWNTkaDmzZa9OnUW+grDlBQKlDuAnr6fZmuNTH2hQ== q5icujja/SGexX18hPYXbkyUtOrBYjW62gCuGJinSBPROoFUJbiP7g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667 recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOR25UN1c0aE5SYWphbU0y YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKOWZxcFlhRHhKelhDS29M
SHRTZ1B0WC9NU3Z5VHpzTXpLSUxHTDI1ZFE0CnFpYUN1eGZQejJMblZPd1ROeUth NGRlTEhJUGUzcFV6QVFCdTNkdEN3MlljWnl3CnI3LzVxSHBwSi9TYlRUcFdyN2py
dklZYVVNa1ZNZ1d4dW9vMCsvQWp1RkEKLS0tIERxakx5L0JrQitib1EyNDRMbDQ5 cVZBV0Q3Z3FhRXA0T1NFQzd2R00yeEUKLS0tIHkxL2dHV3RkVFYxZTJNTVZvVFIr
Z3hDWUFEazdxczVhaHJYK3VZeEJSSDgKkw9T4ZuT+VHIF4WopqRHt8vW30kOysJ3 TXpJZzdnYlpJaXBmcjdWWUtxNkc4dWMKVsImJiavzUzSFn78pciNJPHaS3KWqJer
vOq6EZ3Fqkgmoxm69Zp2gFnuE9GZIBy3VPQVLU2k6dZGJ3IvmLYeBA== VkxF6kF3tl2HmW46eVXtsQowdu+zVR9HS35i/HvQ1r1TyP6qT5ofcQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e recipient: age19gfn2yedg76dmztm4hncr7vf3r3c9j0qpt4rap7y7gersjk4m3ks2lhd0e
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0THJzMFBTTCtDMWRmZ25M YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyWjdSanRRaEI4VHFZVkdw
dU55OVhBb0trWWRUNlArTnEzRjhiYngvRENNCjF4d0M5NlYyQW50TTdMRXpuUjRr NkxMODRuakFJTFNoTHZIRHptZFNxaGZ3K3lnCjJGR1FJalRsYmtLcHFxZW8yNU5W
M1NwV05JOHV6T2cxT2FheVpuZ0w2T0kKLS0tIFBxdlVpVEoxOUpSWjk1ejRsK1NM MzBjWmltbzI3MTByUjB6djlEdllHNXMKLS0tIEhEOVhLVjZkRi9vUEtDWUhxT0NX
V1UwTU1scG91L2FIemtwSW5JbFlmeG8K/1WIlaIidy3x3ptoRpS/DG88064LQ6Mq eUZ3bGdEaHdGbXZIYkowYUZuTWJ3d2MKMwgxOqlMH7GfLlseD4J277Dcg0KCD3d5
GbfB0jfq5PILDQMMuZu5oIBY31SxwnhZ02Ns7gA67kgNIRSCmk9WyQ== jwJDb82kRWoBMicTJZXoq/5oe5blJNa7dWYoqgkYxMA2O+0igT+I6g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmRURQVGw1a1E1MkIzV2pa YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsZzdRMm9zdlJHckFoREVs
RG5HSkM0c0huUGVWcnFZOGlacGlGOHFMTVFrCm5OS09HRFc3TGVUYmtzTStxL0Q5 UkVXL3dCeWRuN2NqcnVsOWVNT1FLUHY3andrCmNFY0ZCby9SdnUyYlV6R05YNDB0
N2hRMEcxUE9MTmZXL0wvME5EZXF1Z00KLS0tIHdHMVVHcTZzMmdXU0s4QlVqSS9Y em43SjRCNVhYbmpRZUVWT0cxQlhGdmsKLS0tIDZHazNyck5VNHBuNVM5bmRZUlpR
ZUVmcWhPaURIUFJGR0V4bUZwKzM1bm8KlvGMNEClbLlfvJqNQHhd0dI4ihShLChF QzBSNy84VDdLVkZZbnNlUFYydXlreEEKYZaR2b7tyRAhPdP+ytpP0veUTi7pY9Nw
GI/fydgrBruw3Otv6KLZu3CBC7iNcKlvZxz+YGD2qbicmyQ5hAhDSQ== pK0h4hcegLNYJL1AfOYwFQoW7vb256GdmwdcuuBl1YBGXWGraaBnZg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5bWpFenBlQldna3RhSFpr YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFSnZiOGpsL1BESWVEdUxx
bEJBaHJDMzM3OHlqTmozcWU0VDM1bTFUWFZzCnZtNHZjZ1U1RzNkUlBHZFozWXdt dXc5LzBHTlNodGJ1S3hOeHd5Wmgxb3BPSHl3CnBEWDNTc3NDdzN3RHQxUlNLOEs5
VVkxQjMvTDJtbFZnclpkUEd4TEVmNTAKLS0tIFlhV2ZSSzJLRVNoMmFyVktDOElR RU1SS2tVT29XbDJCWVliWnVkekN4L1kKLS0tIGFqY2pNK2h2S2ZWMndrRVN6eUdN
YUxqZUFoY1ZWeGlldGplMjVQa1A5aUUKWelY6yO7Mr6dRvj4MVMbq/Z9JgrAnahz M0NLY2EvTTVIYlZvdk9XY0NwNE14ZlEKcOwgCK3g56kId/4tEt+2iCcylisn+Fca
BDhHqObzJrOCtfDCTWiYuP+0yvIFWItMWhGSMw9MwwivvwnrEa+ZuQ== 5VWamsXdbwxbxmCEEUbgN3aHrdVz3mV4+8FZqA34yXz8pc45/PUcFg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8 recipient: age1qz9d4ka4xgexujyd247s7lp737sulp5fhxl5d65fj2ykvc4j4edqrsdks8
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiK2hKb2VaVnoyaEVYUHZZ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJU1cydFFuRnpCUU5mbjFu
OWVUSGtONGs5dEljTjJQdlhEcjNjdjViT2pVCkpaOHVZMlpXOFRveVlMeXZqWmoz TTIxREkxZ1hMalVtNjNjcHVaUXpPNWdib2tzCkRScWRTWHhxN3lhckI4UFpMSFRa
ZHJRQTR2dmJQSEozeTRGMEdUdFlmZ2MKLS0tIERWS1RVdW1jQytBZzlkb3puNjhH RGxWcldlWnd5SVJFNWxQWjB3R0pMb3MKLS0tIGdjLzlhTHgyT2hjTmRTK1RiZmxS
ZjdlZmtzNXVOQ25DeCthUzhRRm1MT2cKaxc7zGm57iJFSeYc2IPqF4Eaxa44nR37 OWRWYklBWkoyUG8raFNzd3JlRUtEUEUKw+NdAp1Mz0dEOUGPbqCV8y7029I3Kye0
pWZw+erG4F9AAZ2F047q+oLKe0B8FLSF54IbcXdQhitgGNR7B2HVeA== keU2T29JGCN1D6x//1NcMUYSaFZKgv0ZVSjVUCl1EnmlJ0nBdbaDXA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu recipient: age120whqj96g26lsgy4udvgsn8dc9lumh8jeu3a564fx79rjr5lxffqmrljuu
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1ekhUTG5VOFErL3pFeWZM YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnNFlzYVBqaGFDVE9xbmt5
RG9NVnN6NFl3bzlFeTQzdHFtZmhwem04alQ0ClpJRStObERMZ0w2V0NhR1FSeW96 OVd4ZlZRdEVJd0JFdkRIYVA5ZUEwYjJjWVJjCnZzTExNbDlYbGN6c1dYU2ZlNEVs
M1d2V2NjUkUrLzN2ZVNSbGY4bll5WmsKLS0tIC94dVFQcXJ6d3pLU0VHNEFGR0ls ZUo4MXdpc0tzbDI0MHl5eXc3d09VRDQKLS0tIGs4SGMya1RZQ0hpa2NtQk9Wa29C
a1Q2UmNuSjVMNG5XZGZKV1VmNHFPQXcKQJrZGw/9fPnXeFZ4omrkEgrzwplhwvRW bmxIYWk1VThsaFhxRDlOOGhGQ2tYamsKs+PcOiaeNFujCWwZBr+nq1MzrGohl+ch
i0FXuepoU353sR7enyL34qPoOdm05ivowuPKNzkq8D4i5AF6vGv+YA== TASI7eNsiHmSVRzSMHv4mX+8yXDHIRgHbFuUty2gdgGFLRjYXe4gDA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w recipient: age1xjst4frdh0th6q8m7p7u9g5af7ty5jqeum0p6z8a52a9q7st7ewqw8yl9j
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoZ2lHM2RBQ3lUK216dkcr YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkcmVQYXV1NzRrQ2IxTElt
Y3RrcXR3QTdFVjJRWlhpQWVFQ1cyZWNabkRFClAwRi9PSHF5ZWFSUzJuRXF3bU1R QUJRMkhiUUJqRDdwQW03K0lCTEtkSTU5UDJrCjh6SldpQkF1NzIybTJTdnBlOHdQ
b0U5TFRZaFdmR1NMS3RRT3E3M2hUdE0KLS0tIEtrUy8ydkNyVHBiOVR6WEdjV2VN VlREMy9hM0ZaL21Pa0VPUzcvb0pQQjQKLS0tIFcrK0I3Z3Nhem45eHpZYmdWeit6
NVJHUVgwRkhxcmlwcFkrRlFwTEF6YVkKzXyJk0UnmUsvb+NzNVcf/gf7OEEt3P/K eXFtRHVxODhaNTVDdVR1a2tOb1N1U1EKZBdLHq8PRMKX2ndFr3AxFVAZRyvhFa1u
OIGxDrGfs/zNQgeKXNbQlQ4p4jOaybG8aCmX+A4qTk6/I8yY8LTJWg== 72R5tordo4IR3HSxG3Z06rokOITd+KKhaQ8NEWEC8qioAAMxEC9QWw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg recipient: age10at8862478urh0eeuwh8hzln6ck78jgwtztgxatwqlzwagg77y5snm4xzg
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0WjJPbXhXNTNyQUZzMmxS
Rkd3Yk5Ga3pPRXFtMFpXcmpBZ3RJYnJlaHhjClRDbFBHRVZ0SDdrNFdVWmUwalpT
WUt5a2RUTnQ4VFBGMlowR3JycDM3aXMKLS0tIERSeFd4cGdSbzRpSmpSQnB1Yng5
OHBma3lYeWEwQ2RvelpzZHRkN1JPWFEKLXkJfEkH3lo5Z4mj8PZXTfAfZn6B46To
U0G54tUh0U1EeMnI5ZPRGwDxi5K0eD68HjeE9MBvFBysOChP/ANDPQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1ezk9x53zt8kcnscdm80jcyf0xq97vndv7jsn3rl8cc0cwm2jmpmq372dzs
lastmodified: "2026-07-19T02:30:40Z" lastmodified: "2026-07-19T02:30:40Z"
mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str] mac: ENC[AES256_GCM,data:UiL3VMDF6rq4Nr87KspcDx434q3tfNXeb5pwH2O+4ssNQ6xzcYDdzXBnhAY3zLBsqPMKrvHBd4Ot/gEMcq3FMIVe7Q6p9yWKpep66KZ/yWEhAlwIVhD79Oj8VS+1CHKjf25zpRdhZorp04oeFQQd9VfjJB4EE/Q1aVbwTGlpIic=,iv:i/0conaFgFia+wzNTdUL6tlSTw35HTK3Ap1Sr5RGHf8=,tag:ULbz5FllShA/JjlSRdxA0g==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
+16 -16
View File
@@ -4,29 +4,29 @@ sops:
age: age:
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbHd1Y1dpa2ZiQ2E1bXRP YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmOHlPcGZvN3o5aEZCcFdz
SnkrWHI0V2U1eGFKY0RZamZQQjluS0hGeENZCmduUFQyd1Q5RkZGMGFoenJhNmZh WlYydFdoQ0ZHbElMdTRkZWljMmoxTHZqNjJZCnE3cmgzKzZya3FROEczbHVveDND
VmZ2OStNaFpJejFxQ2JUZXFpNThaMncKLS0tIEhJdGdQeXEzb25Nbk5YZktCWVUz VnEzRW12cnRKdzhkZm5uTXpkTEtrNUEKLS0tIHFDbkc4Mk4wVlM2R29zZXYwR2Ri
NUJDUWRoTkd5R2pFblZjS0NSbVR5dkkKDYeW+zRpha04/CasFM91K6v1PpkNGHRu ODBML1p4eUZiZldYUERQTUhTU1llV0UKxjvH6zbW6wKghzR1o34CyKPEa2FqZmo0
qAoKs9KSg9VxS7ya8RuLmylKRdpPkupm/8SXIJvQuCXp5LWmNJ4zkA== PxgqyuXkIwas9soXVAkScx7ElaV09Fjaj+mDrKwi4a+DwdoSP7czyA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667 recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4UktiUUdmd092bnZMdFMz YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0VDBXYUZNbnpEZHlqV05C
Q05xVlhnNzhPZUY4TjF5bjRCbklacTdJd1RBCnJ4bEVMaWRLZExIUFVEUDBXTnlV WWxQZjVYcWlZeXlGc0RiaUpERzR0T21CWWtBCkM0dW5kYlFNK1RGSzRudFZ2Z0t3
MDU4WUk5VlJjdlhVUVpzZXlnWHhqTjQKLS0tIE9iclhGVElPcU9OLzF0bEVjMjVp aVBreElGM1BIT0RzQkxTVTA3S2NhQncKLS0tIERyTU8zWWRTRm95SmRZQ1BhalVV
YzlQTGFYZEhLdTk3N3ZramVjVGRXREUKjOjqO/jX5iEKN43WiJ8qS/1mDfxdj8uO SVdBajN1V1BuM2s4K216S3c0VHczNlkKM5jvsSEfCBA5uZRjBJNbM91lLRQkj+jK
K7Yyc/Yj0qMvGwLNzphjmbT29P7dXk4Ht4TedqCd+8DfhpOgLVIaSA== rM5uSfGLTvSjPgXIMIq03OXxH1CE7GoKxAPwFrJdAFMMQcutIethhw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu recipient: age120le4a5l8dh3lyfgvmj3d9ksmej6ajs5mer5y7r0vfg3x9fn69dqf8xgzu
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjaXpvQU9yRUc3ektPMmU5 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvbk9tRE9jYkpxNThQeElH
Q0xLWU9mYXVOeTVBNGNsZzJPa1lkVlRUZHlvClhoSVV6YjB1Q3pTTzVvNFlNYkU4 dWpKYThDb1ZkUWtMWlRxc2tYbjhBbEFEcWtVCkNiK0NBRko2TkJQdUVtdGtHNGcv
Y2RrbFk5SUFKSDVjeFU0Wlc2aGFUMFUKLS0tIDlVeXM4WWRUakg4Zk5mcjdJL3VC andSaUlKSFA4THRyZXNTYmI0Yk5WbmsKLS0tIHlQTUtpWDdPeTVZL0M1RElRdFNk
S1k5eFBpVnREQlZwYllpeTNhWW5GS1EKi2sMwyJJ0D8acjCZmxlcwdU1sglBuxR7 QWlGdFo5NkZWSmY0YXdpNzNUQnlsK3MKtzC0bM7Ek+K73nMranOA1Mc98RUnYnq1
2SEMsctdGC+5E3ilPXvPpZ5RONZHbXxn6kQRBlBv6AJERpGDzsfgfA== hAt0QEFKWK4QVKubaN/rG3AzE0U7qPKWHTzoxgnAiL3WyV9teLW+iA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w recipient: age164px2a8e48ptsf9ngtan38aa6jls4jdl26mzrgzf6sn3vcvt49hqjrgr8w
lastmodified: "2026-07-19T23:30:21Z" lastmodified: "2026-07-19T23:30:21Z"
+11 -11
View File
@@ -3,20 +3,20 @@ sops:
age: age:
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaFZURjAxMGRJZEJ5MW1x YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURmMzN3hrSlNrUkkvVWNl
ZjVwWEQrQlkwNmRibVNiL2RpTTFLeUVDQ3hRCkVIamVnZkM1MnlueloxMHVFQnBF L3M1dEhWeW14N0RFNVRPci9QK1YyTFdqRVVJCk5WaWswT2NicldkYzZjbVhYU2xu
RjV2bnUrZUo4WGZJTmR4Y0xITkxRUkUKLS0tIGlJdVQ5MFBubVhxRUVMWW0wSGpP MGFsNmUzeTN2TS9wOEdvRURpVUVYZXMKLS0tIDZ6MEdPTVhCaTQ2UXFWTUFtc0pm
UGdKNUNPYW9nek1UZ0tWbXd3QVNUNDgKIHOiKelITQdH5R4Nc3WF7mzz15D1f9on MFlJb0c2WXJtMGRLZEZYY0pZWWpFWm8K/mlYZIe8UC0QU+1mq3NtrtTF5b2m5hCK
VaTdr5qkf8LNNvPI0fxsXA9is5cqeg+KbDRHtUumEhNp6Zrf8zWBkw== +K0QiZLTKmmDcr4bRhZ32VE7R7GRwtMNnOP/mElZvPAyWyHHhRiOHg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age10nd382a9klsn2mrs60emdtsxe43pht3a0m9p29phfrhy0wfyt3vsq9r667 recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3N0hvV01naytDSWVwK1B1 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURHlUL0RNMUtNallEcG5p
MXM1ZkdpaVc2Q3FPV2VBcC83WmcvSjdGUGgwClJGSXZ6YW5JeFlValNJbjVhK29u eG4xby8yVzdQUTBaNkl4ano0YjBMcDd0Wm5NCkZvaXNPZm9wemJkMmNSdGdOaTI4
bUFqN2dRQTI4ZkwyeXNWYk5JeWVJRXcKLS0tIDlMMkNBUnNUSTJwVVFmc2dlcEZS Z1RwUnhiRUpCMWZaeWtlSVBmNW5KOXMKLS0tIDk0R0k3ZHczTFNCWUZxSWF0M0FJ
VGQ1VHR2dXB0M3RsalppVWxiUUROM0UKZM/4QDTam3LDTzjnDs41Ije50R7Q7GC4 MGlZMmtuSFYrcG1meDNMWDNqSjFxcE0KDu2dAc0gqmmPkpbpBe4YohM7rYmUwEkI
IZbUZjs72rBzY8IkJDbN9JidadEc4NAtMOJwXiJbpZGiCBNfc8+SXw== V2FUQwjlvh50svtjCVdYbx2xuq4sQLnKelk/q1onLw60FwsVfzD8sQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf recipient: age1ll6hj5ggruetgjwjfnplpn5xtq35uhlcdflksx3xmnjm6s3uad9sz70jkf
lastmodified: "2026-07-19T02:30:40Z" lastmodified: "2026-07-19T02:30:40Z"