Compare commits

..
Author SHA1 Message Date
beatzaplentyandClaude Sonnet 4.6 f4831c9370 feat(pxe-boot): enroll in FreeIPA domain
- Add IPA client module with pre-provisioned keytab
- Set networking.domain for correct Kerberos FQDN
- Encrypted keytab covers lxc-pxe-boot and proxmox-pxe-boot age keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 10:31:36 +10:00
46 changed files with 557 additions and 1566 deletions

No files matched your search

+22 -89
View File
@@ -22,8 +22,6 @@ keys:
- &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz - &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
- &proxmox-server age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp - &proxmox-server age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0 - &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
- &proxmox-ha-server-1 age1vwk8r03u46rpvmw5fy20t8ud9qdhm0nx5se5ayze98y55e7zg5yqc8862n
- &proxmox-ha-server-2 age152vrhznh92hspusx9rqxt9c70u8w3uvlys4ymc7zuv3qnxjm5c7qmp72xp
creation_rules: creation_rules:
# Shared across every currently-deployed host: root/nixos password hash, # Shared across every currently-deployed host: root/nixos password hash,
@@ -56,8 +54,6 @@ creation_rules:
- *proxmox-pxe-boot - *proxmox-pxe-boot
- *proxmox-server - *proxmox-server
- *proxmox-tailscale-router - *proxmox-tailscale-router
- *proxmox-ha-server-1
- *proxmox-ha-server-2
- path_regex: secrets/nix-cache\.yaml$ - path_regex: secrets/nix-cache\.yaml$
key_groups: key_groups:
@@ -67,6 +63,16 @@ creation_rules:
- *lxc-nix-cache - *lxc-nix-cache
- *proxmox-nix-cache - *proxmox-nix-cache
# Host keytab for nix-cache FreeIPA enrollment (binary sops file).
# Generate with: sops -e --input-type binary /tmp/nix-cache.keytab > secrets/nix-cache.keytab
- path_regex: secrets/nix-cache\.keytab$
key_groups:
- age:
- *admin
- *linode-nix-cache
- *lxc-nix-cache
- *proxmox-nix-cache
- path_regex: secrets/server\.yaml$ - path_regex: secrets/server\.yaml$
key_groups: key_groups:
- age: - age:
@@ -97,14 +103,12 @@ creation_rules:
key_groups: key_groups:
- age: - age:
- *admin - *admin
- *proxmox-ha-server-1
# proxmox-ha-server-1 added by sync-host-keys.sh # proxmox-ha-server-1 added by sync-host-keys.sh
- path_regex: secrets/ha-server-2\.yaml$ - path_regex: secrets/ha-server-2\.yaml$
key_groups: key_groups:
- age: - age:
- *admin - *admin
- *proxmox-ha-server-2
# proxmox-ha-server-2 added by sync-host-keys.sh # proxmox-ha-server-2 added by sync-host-keys.sh
# Shared HA cluster corosync authkey (binary sops file). # Shared HA cluster corosync authkey (binary sops file).
@@ -114,8 +118,6 @@ creation_rules:
key_groups: key_groups:
- age: - age:
- *admin - *admin
- *proxmox-ha-server-1
- *proxmox-ha-server-2
# proxmox-ha-server-1 added by sync-host-keys.sh # proxmox-ha-server-1 added by sync-host-keys.sh
# proxmox-ha-server-2 added by sync-host-keys.sh # proxmox-ha-server-2 added by sync-host-keys.sh
@@ -125,6 +127,16 @@ creation_rules:
# scripts/secrets/sync-host-keys.sh yet, so whichever variant is actually # scripts/secrets/sync-host-keys.sh yet, so whichever variant is actually
# deployed next needs its recipient added here (and `sops updatekeys` rerun) # deployed next needs its recipient added here (and `sops updatekeys` rerun)
# before it can decrypt this. # before it can decrypt this.
# Host keytab for tailscale-router FreeIPA enrollment (binary sops file).
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
- path_regex: secrets/tailscale-router\.keytab$
key_groups:
- age:
- *admin
- *lxc-tailscale-router
- *proxmox-tailscale-router
- *linode-tailscale-router
- path_regex: secrets/gui\.yaml$ - path_regex: secrets/gui\.yaml$
key_groups: key_groups:
- age: - age:
@@ -134,90 +146,11 @@ creation_rules:
- *linode-gui - *linode-gui
- *proxmox-gui - *proxmox-gui
# IPA host keytabs (binary sops files). # Host keytab for pxe-boot FreeIPA enrollment (binary sops file).
# Each keytab is encrypted for all platform variants of that host so any # Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
# deployed variant can decrypt it at boot. Run
# scripts/ipa/create-nixos-ipa-host-account.sh <hostname> to enroll a new
# host and produce the keytab; this section is updated by that script.
- path_regex: secrets/nix-cache\.keytab$
key_groups:
- age:
- *admin
- *linode-nix-cache
- *lxc-nix-cache
- *proxmox-nix-cache
- path_regex: secrets/tailscale-router\.keytab$
key_groups:
- age:
- *admin
- *linode-tailscale-router
- *lxc-tailscale-router
- *proxmox-tailscale-router
- path_regex: secrets/pxe-boot\.keytab$ - path_regex: secrets/pxe-boot\.keytab$
key_groups: key_groups:
- age: - age:
- *admin - *admin
- *lxc-pxe-boot - *lxc-pxe-boot
- *proxmox-pxe-boot - *proxmox-pxe-boot
# nixos = the workstation (hosts/nixos/host.nix). All gui platform variants
# share the hostname "nixos" and must be able to decrypt at boot.
- path_regex: secrets/nixos\.keytab$
key_groups:
- age:
- *admin
- *baremetal-gui
- *lxc-gui
- *proxmox-gui
- *linode-gui
- path_regex: secrets/server\.keytab$
key_groups:
- age:
- *admin
- *linode-server
- *lxc-server
- *proxmox-server
- path_regex: secrets/docker\.keytab$
key_groups:
- age:
- *admin
- *linode-docker
- *lxc-docker
- *proxmox-docker
- path_regex: secrets/tor-relay\.keytab$
key_groups:
- age:
- *admin
- *lxc-tor-relay
- path_regex: secrets/nix-minimal\.keytab$
key_groups:
- age:
- *admin
- *lxc-minimal
- *proxmox-minimal
- *linode-minimal
# Host keytab for ha-server-1 FreeIPA enrollment (binary sops file).
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
- path_regex: secrets/ha-server-1\.keytab$
key_groups:
- age:
- *admin
- *proxmox-ha-server-1
# proxmox-ha-server-1 added by sync-host-keys.sh
# Host keytab for ha-server-2 FreeIPA enrollment (binary sops file).
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
- path_regex: secrets/ha-server-2\.keytab$
key_groups:
- age:
- *admin
- *proxmox-ha-server-2
# proxmox-ha-server-2 added by sync-host-keys.sh
+6 -8
View File
@@ -6,14 +6,12 @@ This repository contains flake-based NixOS configurations for Wayne's LAN
servers and workstation. servers and workstation.
The flake exposes NixOS configurations named `<platform>-<buildtype>` The flake exposes NixOS configurations named `<platform>-<buildtype>`
(platforms: `linode`, `proxmox`, `lxc`, `baremetal`; build types: `minimal`, (platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`,
`nix-cache`, `server`, `docker`, `gui`, `pxe-boot`, `tailscale-router`, `server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`), generated from `modules/platforms/*`
`tor-relay`, `ha-server`), generated from `modules/platforms/*` and and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
`modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not every every combination is built — `pxe-boot` has no `linode` variant. See
combination is built — `pxe-boot` has no `linode` variant, `ha-server` only `README.md` for the full current target list; treat `flake.nix` as the
exists on `proxmox`, and `tor-relay` only exists on `lxc`. See `README.md` source of truth since this list can drift.
for the full current target list; treat `flake.nix` as the source of truth
since this list can drift.
Do not deploy, switch, reboot, repartition, format disks, or run destructive Do not deploy, switch, reboot, repartition, format disks, or run destructive
install commands from this repository unless explicitly asked. install commands from this repository unless explicitly asked.
+11 -10
View File
@@ -10,13 +10,13 @@ pieces composed in `flake.nix`:
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`, `baremetal` - **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`, `baremetal`
- **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`, - **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`,
`gui`, `pxe-boot`, `tailscale-router`, `tor-relay`, `ha-server` `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
Not every combination exists — `pxe-boot` has no `linode` variant, since Not every combination exists — `pxe-boot` has no `linode` variant, since
PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have, PXE/DHCP/TFTP need LAN L2 adjacency that a Linode VPS doesn't have,
`tor-relay` and `ha-server` currently only exist on `lxc`/`proxmox`, and `tor-relay` currently only exists as `lxc-tor-relay`, and `baremetal`
`baremetal` currently only exists as `baremetal-gui` (the real gui-host currently only exists as `baremetal-gui` (the real gui-host hardware). The
hardware). The full list: full list:
| Target | Purpose | | Target | Purpose |
| --- | --- | | --- | --- |
@@ -29,9 +29,8 @@ hardware). The full list:
| `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation — previously the flat `nixos` target | | `linode-gui` / `proxmox-gui` / `lxc-gui` | Cinnamon desktop workstation — previously the flat `nixos` target |
| `baremetal-gui` | Same Cinnamon desktop workstation, on the real gui-host hardware — ZFS RAID0 root, systemd-boot | | `baremetal-gui` | Same Cinnamon desktop workstation, on the real gui-host hardware — ZFS RAID0 root, systemd-boot |
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host — previously the flat `pxe-boot` target | | `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host — previously the flat `pxe-boot` target |
| `linode-tailscale-router` / `proxmox-tailscale-router` / `lxc-tailscale-router` | Tailscale subnet router + MagicDNS forwarder for the LAN | | `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node |
| `lxc-tor-relay` | Tor middle relay | | `lxc-tor-relay` | Tor middle relay |
| `proxmox-ha-server-1` / `proxmox-ha-server-2` | HA file-server cluster nodes — DRBD + XFS + iSCSI + NFS, managed by Corosync + Pacemaker |
Which variant of a given buildtype is actually deployed isn't tracked Which variant of a given buildtype is actually deployed isn't tracked
anywhere in this repo — that's live infrastructure state, not something a anywhere in this repo — that's live infrastructure state, not something a
@@ -48,7 +47,8 @@ section for which is which.
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay (hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
fixed regardless of which platform it's built for. Every deployed host fixed regardless of which platform it's built for — see
`flake-target-refactor-spec.md` for the full rationale. Every deployed host
stamps its own active target name into `/etc/flake-target` at build time, so stamps its own active target name into `/etc/flake-target` at build time, so
`nixos-rebuild switch --flake .#$(cat /etc/flake-target)` always picks up the `nixos-rebuild switch --flake .#$(cat /etc/flake-target)` always picks up the
right one even after a platform migration changes the flake attribute name. right one even after a platform migration changes the flake attribute name.
@@ -167,6 +167,7 @@ per-boot host key for sops-nix to derive from on ephemeral media) — see
(`vars/per-machine/<target>/openssh/`, committed and sops-encrypted) for (`vars/per-machine/<target>/openssh/`, committed and sops-encrypted) for
their SSH host keys. their SSH host keys.
This repository's git *history* still contains secrets committed before the This repository's git *history* still contains secrets committed before this
sops-nix migration — those are being scrubbed and rotated separately; don't migration (see `remove-sensetive-info-refactor.md`) — those are being
treat the repo as safe to make public until that's finished. scrubbed and rotated separately; don't treat the repo as safe to make public
until that's finished.
+2 -4
View File
@@ -22,7 +22,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`, `lxc-tailscale-router`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the `lxc-gui`, `lxc-pxe-boot`, `lxc-tailscale-exit-node`, `lxc-tor-relay`) 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
@@ -134,14 +134,12 @@ Flake outputs:
nixosConfigurations.installer # ISO/netboot installer image nixosConfigurations.installer # ISO/netboot installer image
packages.x86_64-linux.iso # installer ISO/netboot image packages.x86_64-linux.iso # installer ISO/netboot image
packages.x86_64-linux.pxe # auto-installer netboot bundle (kernel + initrd + ipxe script) packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
packages.x86_64-linux.pxe-minimal # vanilla NixOS minimal netboot bundle (no installer wiring)
``` ```
```sh ```sh
nix build .#iso nix build .#iso
nix build .#pxe nix build .#pxe
nix build .#pxe-minimal
``` ```
There's no `nixosConfigurations.proxmox-lxc` (installer-boots-as-an-LXC- There's no `nixosConfigurations.proxmox-lxc` (installer-boots-as-an-LXC-
Generated
+6 -6
View File
@@ -173,11 +173,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1785119570, "lastModified": 1784350909,
"narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=", "narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d", "rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -259,11 +259,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1785104993, "lastModified": 1784432872,
"narHash": "sha256-eKbrvPoAOFutbYMdbB3r5EQVmFxKv24iKqHPPUXA0gM=", "narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "8623c4c20aa4ca2f5fb81510d2944066c3fb0d96", "rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
"type": "github" "type": "github"
}, },
"original": { "original": {
-5
View File
@@ -14,11 +14,6 @@
}; };
boot.zfs.forceImportRoot = false; boot.zfs.forceImportRoot = false;
# Only advertise the LAN interface to IPA DNS. Without this, SSSD registers
# every Docker bridge (172.x.x.x) as an A record for docker.sweet.home —
# the default dyndns.interface = "*" catches them all.
security.ipa.dyndns.interface = vars.lxcLanInterface; # eth0
# Preserved from the pre-refactor `docker` target — stateVersion must never # Preserved from the pre-refactor `docker` target — stateVersion must never
# be bumped on an already-installed machine. # be bumped on an already-installed machine.
system.stateVersion = "25.05"; system.stateVersion = "25.05";
+1 -1
View File
@@ -24,7 +24,7 @@
}; };
# Set KEY after pairing this host with the beszel hub; the token is sops-managed. # Set KEY after pairing this host with the beszel hub; the token is sops-managed.
# services.beszel.agent.environment.KEY = ""; services.beszel.agent.environment.KEY = "";
system.stateVersion = "26.05"; system.stateVersion = "26.05";
} }
+1 -1
View File
@@ -24,7 +24,7 @@
}; };
# Set KEY after pairing this host with the beszel hub; the token is sops-managed. # Set KEY after pairing this host with the beszel hub; the token is sops-managed.
# services.beszel.agent.environment.KEY = ""; services.beszel.agent.environment.KEY = "";
system.stateVersion = "26.05"; system.stateVersion = "26.05";
} }
+6
View File
@@ -6,10 +6,15 @@
name = "nix-cache"; name = "nix-cache";
sopsFile = ../../secrets/nix-cache.yaml; sopsFile = ../../secrets/nix-cache.yaml;
}) })
(import ../../modules/ipa/client.nix {
keytabSopsFile = ../../secrets/nix-cache.keytab;
caCertFile = ../../certs/ipa-ca.crt;
})
]; ];
networking = { networking = {
hostName = vars.nixCacheHost; hostName = vars.nixCacheHost;
domain = vars.homeDomain;
useDHCP = false; useDHCP = false;
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{ interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
address = vars.nixCacheIp; address = vars.nixCacheIp;
@@ -20,6 +25,7 @@
}; };
services.beszel.agent.environment = { services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG"; KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
}; };
+8
View File
@@ -1,8 +1,16 @@
{ vars, ... }: { vars, ... }:
{ {
imports = [
(import ../../modules/ipa/client.nix {
keytabSopsFile = ../../secrets/pxe-boot.keytab;
caCertFile = ../../certs/ipa-ca.crt;
})
];
networking = { networking = {
hostName = "pxe-boot"; hostName = "pxe-boot";
domain = vars.homeDomain;
useDHCP = false; useDHCP = false;
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{ interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
address = vars.pxeServerIp; address = vars.pxeServerIp;
+1
View File
@@ -21,6 +21,7 @@
}; };
services.beszel.agent.environment = { services.beszel.agent.environment = {
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG"; KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}"; EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
LOG_LEVEL = "debug"; LOG_LEVEL = "debug";
+5
View File
@@ -6,10 +6,15 @@
name = "tailscale-router"; name = "tailscale-router";
sopsFile = ../../secrets/tailscale-router.yaml; sopsFile = ../../secrets/tailscale-router.yaml;
}) })
(import ../../modules/ipa/client.nix {
keytabSopsFile = ../../secrets/tailscale-router.keytab;
caCertFile = ../../certs/ipa-ca.crt;
})
]; ];
networking = { networking = {
hostName = "tailscale-router"; hostName = "tailscale-router";
domain = vars.homeDomain;
useDHCP = false; useDHCP = false;
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{ interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
address = vars.tailscaleRouterIp; address = vars.tailscaleRouterIp;
+1 -78
View File
@@ -1,17 +1,6 @@
{ config, pkgs, lib, inputs, vars, ... }: { config, pkgs, lib, inputs, vars, ... }:
{ {
imports = [
../docker/enable-service.nix
];
nixpkgs.overlays = [
(final: prev: {
docker = prev.docker_29;
docker_cli = prev.docker_29;
})
];
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
inputs.nixos-conf-editor.packages.${pkgs.stdenv.hostPlatform.system}.nixos-conf-editor inputs.nixos-conf-editor.packages.${pkgs.stdenv.hostPlatform.system}.nixos-conf-editor
nodejs nodejs
@@ -29,7 +18,7 @@
]; ];
boot.loader.grub.useOSProber = true; boot.loader.grub.useOSProber = true;
programs.direnv.enable = true; programs.direnv.enable = true;
services = { services = {
xserver = { xserver = {
enable = true; enable = true;
@@ -81,70 +70,4 @@
programs.firefox.enable = true; programs.firefox.enable = true;
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
# GUI-specific Home Manager additions for the IPA primary user, extending
# the baseline in modules/ipa/client.nix with desktop apps and services
# that only make sense on a graphical workstation.
home-manager.users.${vars.ipaUser} = { pkgs, ... }: {
home = {
packages = with pkgs; [
git
vim
nextcloud-client
chromium
claude-code
fish
sops
];
sessionVariables = {
EDITOR = "vim";
SOPS_AGE_KEY_FILE = "/home/${vars.ipaUser}/.config/sops/age/keys.txt";
};
file = {
".local/share/applications/proxmox-chromium-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox (Chromium)
Exec=chromium --app=https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --window-size=1920,1080 --window-position=0,0
Icon=/home/${vars.ipaUser}/.local/share/icons/proxmox.png
Terminal=false
Categories=Hypervisor;
StartupWMClass=PVE
'';
".local/share/applications/pbs-chromium-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox Backup Server (Chromium)
Exec=chromium --app=https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --window-size=1920,1080 --window-position=0,0
Icon=/home/${vars.ipaUser}/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
'';
".local/share/applications/proxmox-firefox-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox (Firefox)
Exec=firefox --new-instance https://pve.${vars.homeDomain}:${toString vars.ports.pveWeb} --profile ProxmoxWebApp --window-size=1920,1080 --class ProxmoxWebApp
Icon=/home/${vars.ipaUser}/.local/share/icons/proxmox.png
Terminal=false
Categories=Hypervisor;
StartupWMClass=PVE
'';
".local/share/applications/pbs-firefox-app.desktop".text = ''
[Desktop Entry]
Type=Application
Name=Proxmox Backup Server (Firefox)
Exec=firefox --new-window https://${vars.pbsIp}:${toString vars.ports.pbsWeb} --profile PbsWebApp --window-size=1920,1080 --class PbsWebApp
Icon=/home/${vars.ipaUser}/.local/share/icons/proxmox.png
Terminal=false
Categories=backup;
StartupWMClass=PBS
'';
};
};
services.nextcloud-client = {
enable = true;
startInBackground = true;
};
};
} }
+12 -17
View File
@@ -14,32 +14,27 @@
# the Beszel hub) is not set yet — add it to hosts/ha-server-{1,2}/host.nix # the Beszel hub) is not set yet — add it to hosts/ha-server-{1,2}/host.nix
# under services.beszel.agent.environment.KEY once the hub accepts the # under services.beszel.agent.environment.KEY once the hub accepts the
# new agents, following the pattern in hosts/server/host.nix. # new agents, following the pattern in hosts/server/host.nix.
{ lib, pkgs, vars, ... }: { lib, vars, ... }:
let
# Generates /etc/exports lines for all nfsShares data entries. Shared
# pattern with modules/build-types/server.nix — both export the same
# set of shares, differing only in the storage root they serve from.
mkNfsExports = storageRoot:
lib.concatMapStrings
(share: " ${storageRoot}/${share.subpath} ${vars.lanCidr}${vars.nfsShares.options}\n")
(lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares));
in
{ {
imports = [ imports = [
../ha/pacemaker-stack.nix ../ha/pacemaker-stack.nix
../ha/iscsi-target.nix ../ha/iscsi-target.nix
../ha/cluster-config.nix ../ha/cluster-config.nix
# ../beszel/enable-agent.nix ../beszel/enable-agent.nix
]; ];
# xfsprogs: mkfs.xfs/xfs_info needed by cluster-init.sh.
# openiscsi: iscsiadm needed by acceptance-tests.sh T4 (iSCSI discovery check).
environment.systemPackages = [ pkgs.xfsprogs pkgs.openiscsi ];
services.nfs.server = { services.nfs.server = {
enable = true; enable = true;
exports = mkNfsExports vars.haStorageRoot; exports = ''
${vars.haStorageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.haStorageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.haStorageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.haStorageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.haStorageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.haStorageRoot}/${vars.nfsShares.proxmoxIsos.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.haStorageRoot}/${vars.nfsShares.proxmoxLxcImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.haStorageRoot}/${vars.nfsShares.pxebootImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
'';
}; };
# Pacemaker controls nfs-server — prevent systemd from starting it at boot # Pacemaker controls nfs-server — prevent systemd from starting it at boot
+10 -10
View File
@@ -13,15 +13,6 @@ let
lib.concatMap (share: ancestors share.subpath) lib.concatMap (share: ancestors share.subpath)
(lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares)) (lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares))
); );
# Generates /etc/exports lines for all nfsShares data entries (every
# attrset value — excludes the bare `options` string). Both server and
# ha-server export the same share set from different storage roots, so
# this helper is the single source of truth for the export line format.
mkNfsExports = storageRoot:
lib.concatMapStrings
(share: " ${storageRoot}/${share.subpath} ${vars.lanCidr}${vars.nfsShares.options}\n")
(lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares));
in in
{ {
imports = [ imports = [
@@ -104,7 +95,16 @@ in
services.nfs.server = { services.nfs.server = {
enable = true; enable = true;
exports = mkNfsExports vars.storageRoot; exports = ''
${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.storageRoot}/${vars.nfsShares.proxmoxIsos.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.storageRoot}/${vars.nfsShares.proxmoxLxcImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
${vars.storageRoot}/${vars.nfsShares.pxebootImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
'';
}; };
# mountd (20048) is needed for showmount/NFSv3 mount protocol — without it # mountd (20048) is needed for showmount/NFSv3 mount protocol — without it
+47 -4
View File
@@ -1,7 +1,50 @@
_: { config, pkgs, lib, vars, ... }:
let
# Flake attribute names are now <platform>-<buildtype> (e.g. proxmox-docker)
# and no longer match networking.hostName, since a host's hostname stays
# fixed while the platform backing it can change. Each nixosConfiguration
# stamps its own active target name into /etc/flake-target at build time.
mySwitchCmd = ''
sudo nixos-rebuild switch \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
'';
myTestCmd = ''
sudo nixos-rebuild test \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
'';
# lxc-* hosts pre-seed their SSH host key at build time (see
# modules/platforms/lxc.nix) so sops-nix's .sops.yaml recipient matches on
# first boot -- without it, secrets permanently fail to decrypt (see that
# file's comment for the confirmed failure). That requires --impure plus
# NIXOS_HOST_KEYS_DIR pointing at the repo's host-keys/ dir, same pattern
# docs/auto-installer.md uses for the installer ISO. A function, not a
# shellAlias, since the target name has to interpolate into the middle of
# the flake attribute path, not just append after it. Must be run from the
# repo root, same as every other host-keys/ command in this repo.
buildImageFn = ''
buildImage() {
if [ -z "$1" ]; then
echo "usage: buildImage <flake-target> (e.g. lxc-docker)" >&2
return 1
fi
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
".#nixosConfigurations.$1.config.system.build.tarball"
}
'';
in
{ {
# Switch-nix, Test-nix, and buildImage are defined system-wide in programs.bash = {
# modules/common/configuration.nix so all users (including IPA accounts) enable = true;
# get them. Add any Home-Manager-only per-user shell config here. shellAliases = {
"Switch-nix" = mySwitchCmd;
"Test-nix" = myTestCmd;
};
initExtra = buildImageFn;
};
} }
+8 -36
View File
@@ -1,45 +1,17 @@
{ config, lib, pkgs, vars, ... }: { config, lib, pkgs, vars, ... }:
let
switchCmd = ''
sudo nixos-rebuild switch \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
'';
testCmd = ''
sudo nixos-rebuild test \
--no-write-lock-file \
--refresh \
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
'';
buildImageFn = ''
buildImage() {
if [ -z "$1" ]; then
echo "usage: buildImage <flake-target> (e.g. lxc-docker)" >&2
return 1
fi
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
".#nixosConfigurations.$1.config.system.build.tarball"
}
'';
in
{ {
imports = [ imports =
[
# Include the results of the hardware scan.
# ./hardware-configuration.nix
./set-locale.nix ./set-locale.nix
../ipa/client.nix
]; ];
# Use the GRUB 2 boot loader.
# boot.loader.grub.enable = true;
#boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
# System-wide shell config so all users (including IPA accounts) get the networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
# same management aliases as the local nixos user's Home Manager provides.
programs.bash = {
shellAliases = {
"Switch-nix" = switchCmd;
"Test-nix" = testCmd;
};
interactiveShellInit = buildImageFn;
};
networking.networkmanager.enable = true;
# Recommended over the true default (bypasses ZFS's own import safeguards) # Recommended over the true default (bypasses ZFS's own import safeguards)
# per the option's own docs; matches hosts/docker/host.nix and # per the option's own docs; matches hosts/docker/host.nix and
-35
View File
@@ -1,35 +0,0 @@
# Shared activation-script logic to preserve the SSH host key across
# nixos-rebuild on platforms that embed the key via environment.etc (lxc and
# proxmox). When NIXOS_HOST_KEYS_DIR is not set the key is absent from
# environment.etc, and NixOS's etc activation removes any /etc file not in
# the new generation — which would destroy the live key and break sops-nix
# decryption permanently. These scripts save the key to /run before etc
# removes it, then restore it afterward.
#
# Explicit deps enforce the correct ordering: without them the topological
# sort places preserveSshHostKey after etc (confirmed live on lxc-tor-relay:
# position 7 vs etc's position 5), so the key is gone before it can be saved.
_: {
system.activationScripts = {
preserveSshHostKey = ''
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
fi
'';
restoreSshHostKey = {
deps = [ "etc" ];
text = ''
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
fi
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
'';
};
etc = { deps = [ "preserveSshHostKey" ]; };
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
};
}
+12 -34
View File
@@ -1,45 +1,23 @@
{ lib, pkgs, vars, ... }: { pkgs, ... }:
let
gid = toString vars.dockerAccessGid;
in
{ {
# virtualisation.docker.enable = true;
virtualisation.docker = { virtualisation.docker = {
enable = true; enable = true;
package = pkgs.docker; package = pkgs.docker;
# listenOptions = [
# "unix:///var/run/docker.sock"
# "tcp://0.0.0.0:2375"
#];
# daemon.settings = {
# metrics-addr = "0.0.0.0:9323";
# experimental = true;
# };
}; };
# Pin the docker group GID to match the IPA "docker-access" group so that
# IPA group membership alone grants access to the Docker socket. Any user
# whose supplementary groups (resolved by SSSD from IPA) include GID
# vars.dockerAccessGid will pass the socket group-permission check without
# any per-host users.groups.docker.members entry.
users.groups.docker.gid = lib.mkForce vars.dockerAccessGid;
users.users.${vars.primaryUser}.extraGroups = [ "docker" ];
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
docker-compose docker-compose
docker-buildx docker-buildx
]; ];
# NixOS's group activation uses plain `groupmod` without --non-unique.
# When SSSD is active it exposes the IPA "docker-access" group at
# vars.dockerAccessGid via NSS, so groupmod sees that GID as already in
# use and silently skips the change (warning: "not applying GID change").
# This script runs after the normal "groups" step and applies the change
# with --non-unique (which lets the local docker group share the GID with
# the SSSD-provided IPA group). If the GID actually changed it also
# restarts docker.socket so the socket is recreated with the new GID.
system.activationScripts.docker-group-gid = {
deps = [ "groups" ];
text = ''
current=$(grep "^docker:" /etc/group | cut -d: -f3)
if [ "$current" != "${gid}" ]; then
${pkgs.shadow}/bin/groupmod --non-unique -g ${gid} docker
if ${pkgs.systemd}/bin/systemctl is-active --quiet docker.socket; then
${pkgs.systemd}/bin/systemctl stop docker.service docker.socket
rm -f /var/run/docker.sock
${pkgs.systemd}/bin/systemctl start docker.socket docker.service
fi
fi
'';
};
} }
+6 -28
View File
@@ -11,32 +11,14 @@
# Both host keys must be registered via sync-host-keys.sh first so both nodes can decrypt it. # Both host keys must be registered via sync-host-keys.sh first so both nodes can decrypt it.
# #
# DRBD fencing: # DRBD fencing:
# resource-only with crm-fence-peer.sh: DRBD calls the Pacemaker-aware # Production setting is resource-only: DRBD waits for the STONITH fence
# crm-fence-peer.sh handler before promoting. The handler checks the CIB # agent to confirm the peer is dead before promoting to Primary. This
# to confirm the peer's DRBD resource is stopped and returns 7 (successfully # requires a working fence_pve_ssh STONITH resource in Pacemaker
# fenced), allowing safe promotion without requiring power-fencing (STONITH). # (see scripts/ha/cluster-enable-stonith.sh). On a fresh cluster with
# The unfence handler crm-unfence-peer.sh clears the outdate flag when the # no fence device yet, temporarily change to dont-care and run
# peer reconnects. This is the correct setting for Pacemaker+DRBD clusters # cluster-enable-stonith.sh once the fence key is deployed.
# with STONITH disabled; crm-fence-peer.sh replaces the need for a separate
# STONITH device during the testing phase. Switch to resource-and-stonith
# once the fence_pve_ssh STONITH resource is active (see
# scripts/ha/cluster-enable-stonith.sh).
{ lib, vars, ... }: { lib, vars, ... }:
{ {
# Root SSH access — same key set as nixos user so all admin keys can reach root.
users.users.root.openssh.authorizedKeys.keys = [
vars.adminSshKey
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGygkCljN6uKpdJbHTOQtn8ZnH+wKXDLAwrDFbLrE/65 nixos@nixos"
];
# Passwordless sudo for wheel — operator SSHes as nixos and uses sudo for
# cluster management commands (drbdadm, crm*, pcs, etc.)
security.sudo.wheelNeedsPassword = lib.mkForce false;
# DRBD lock-file directory (drbd-utils checks for it; missing → harmless but noisy warnings).
systemd.tmpfiles.rules = [ "d /var/lib/drbd 0750 root root -" ];
services.drbd = { services.drbd = {
enable = true; enable = true;
config = '' config = ''
@@ -55,10 +37,6 @@
disk { disk {
fencing resource-only; fencing resource-only;
} }
handlers {
fence-peer "/run/current-system/sw/lib/drbd/crm-fence-peer.sh";
unfence-peer "/run/current-system/sw/lib/drbd/crm-unfence-peer.sh";
}
} }
resource ha-data { resource ha-data {
+1 -1
View File
@@ -25,7 +25,7 @@
let let
python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]); python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]);
targetctl = "${python3}/bin/targetctl"; targetctl = "${pkgs.targetcli-fb}/bin/targetctl";
targetctlStop = pkgs.writeScript "targetctl-stop" '' targetctlStop = pkgs.writeScript "targetctl-stop" ''
#!${python3}/bin/python3 #!${python3}/bin/python3
+42 -149
View File
@@ -1,15 +1,34 @@
# Fully declarative FreeIPA domain membership. # Fully declarative FreeIPA domain membership.
# #
# Imported by modules/common/configuration.nix — no per-host wiring needed. # Configures security.ipa (SSSD, Kerberos, PAM, NSSwitch) and places a
# Enables itself automatically on any host that has a sops-encrypted keytab # pre-provisioned host keytab via sops-nix so no imperative ipa-client-install
# at secrets/<hostname>.keytab; is a no-op for all other hosts. # step is needed after deployment.
# #
# To enroll a new host: # Usage (in a host.nix imports list):
# 0. scripts/secrets/sync-host-keys.sh <flake-target> # (import ../../modules/ipa/client.nix {
# 1. scripts/ipa/create-nixos-ipa-host-account.sh [--ip <addr>] <hostname> # keytabSopsFile = ../../secrets/<hostname>.keytab;
# (adds .sops.yaml rule, runs ipa host-add, encrypts keytab in one step) # caCertFile = ../../certs/ipa-ca.crt; # already committed — do not re-fetch
# 2. git add secrets/<hostname>.keytab .sops.yaml && git commit # })
# 3. Deploy — no further steps required. #
# The host.nix networking block must also set:
# networking.domain = vars.homeDomain; # needed for Kerberos FQDN
# networking.nameservers = [ vars.domainControllerIp ]; # IPA DNS
#
# One-time operator setup per host (do this BEFORE deploying):
#
# 0. Generate SSH host keys and the host's age key for sops:
# scripts/secrets/sync-host-keys.sh <flake-target>
# This must run before step 1 so the host age key is in .sops.yaml
# and the keytab can be encrypted for the host to read at boot.
#
# 1. Add the IPA host account and produce the sops-encrypted keytab:
# scripts/ipa/create-nixos-ipa-host-account.sh [--ip <addr>] <hostname>
# The script handles ipa host-add, ipa-getkeytab, .sops.yaml patching,
# and sops encryption in one step. See the script header for details.
#
# 2. Wire up the host (see "Usage" above), then deploy:
# nixos-rebuild switch (or create-proxmox-resource.sh)
# No further manual enrollment steps are required after deployment.
# #
# Manual fallback (if the script isn't usable): # Manual fallback (if the script isn't usable):
# a. On the FreeIPA server: ipa host-add <fqdn> [--ip-address=<ip>] --force # a. On the FreeIPA server: ipa host-add <fqdn> [--ip-address=<ip>] --force
@@ -19,74 +38,32 @@
# sops -e --input-type binary -i secrets/<host>.keytab # sops -e --input-type binary -i secrets/<host>.keytab
# d. Commit secrets/<host>.keytab and the updated .sops.yaml, then deploy. # d. Commit secrets/<host>.keytab and the updated .sops.yaml, then deploy.
# #
# vars dependencies: homeDomain, ipaServer, domainControllerIp, ipaUser # vars dependencies: homeDomain, ipaServer, domainControllerIp
{ keytabSopsFile, caCertFile }:
{ config, lib, pkgs, vars, ... }: { config, lib, pkgs, vars, ... }:
let let
keytabPath = ../../secrets + "/${config.networking.hostName}.keytab";
enabled = builtins.pathExists keytabPath;
realm = lib.strings.toUpper vars.homeDomain; realm = lib.strings.toUpper vars.homeDomain;
fqdn = "${config.networking.hostName}.${vars.homeDomain}"; fqdn = "${config.networking.hostName}.${vars.homeDomain}";
# "sweet.home" -> "dc=sweet,dc=home" # "sweet.home" -> "dc=sweet,dc=home"
basedn = lib.strings.concatMapStringsSep "," (c: "dc=${c}") (lib.strings.splitString "." vars.homeDomain); basedn = lib.strings.concatMapStringsSep "," (c: "dc=${c}") (lib.strings.splitString "." vars.homeDomain);
# security.ipa.certificate expects a derivation (package), not a raw path. # security.ipa.certificate expects a derivation (package), not a raw path.
caCertPkg = pkgs.writeText "ipa-ca.crt" (builtins.readFile ../../certs/ipa-ca.crt); caCertPkg = pkgs.writeText "ipa-ca.crt" (builtins.readFile caCertFile);
in in
lib.mkIf enabled { {
networking.domain = lib.mkDefault vars.homeDomain; security.ipa = {
networking.nameservers = lib.mkDefault [ vars.domainControllerIp ];
security = {
ipa = {
enable = true; enable = true;
domain = vars.homeDomain; domain = vars.homeDomain;
inherit realm; realm = realm;
server = vars.ipaServer; server = vars.ipaServer;
certificate = caCertPkg; certificate = caCertPkg;
inherit basedn; basedn = basedn;
ipaHostname = fqdn; ipaHostname = fqdn;
offlinePasswords = true; offlinePasswords = true;
cacheCredentials = true; cacheCredentials = true;
}; };
# Create the home directory on first login if it doesn't exist yet.
# IPA users have no pre-created home on the host; without this sshd
# opens a session to a non-existent directory and resets the connection.
# lightdm also needs this so the GUI login path can create the home dir
# if it was not pre-seeded by the tmpfiles rule above (e.g. on first boot
# before SSSD has resolved the user).
pam.services = {
sshd.makeHomeDir = true;
lightdm.makeHomeDir = true;
# pam_unix returns PAM_AUTHINFO_UNAVAIL without prompting when the local
# stub has "!" in shadow (account locked), so PAM_AUTHTOK is never set
# and pam_sss's use_first_pass fails with "No authentication token".
# Changing to try_first_pass makes pam_sss prompt independently when no
# prior module has set the token, restoring IPA password login via
# LightDM and su.
login.rules.auth.sss.settings = lib.mkForce { try_first_pass = true; };
su.rules.auth.sss.settings = lib.mkForce { try_first_pass = true; };
};
# HM with useUserPackages = true (flake.nix) sets users.users.${ipaUser}.packages,
# which forces the stub into /etc/passwd. pam_sss.so with the "localusers" flag
# (added by NixOS when SSSD is enabled) then skips SSSD for any user it finds in
# local /etc/passwd — including this stub — falling through to pam_unix, which has
# no password for the stub → sudo auth always fails.
#
# Fix: NOPASSWD for the IPA user. The IPA user already authenticated to reach a
# shell (SSH public key from IPA or Kerberos), so re-prompting via a broken PAM
# path is security theater on a single-admin homelab.
sudo.extraRules = [{
users = [ vars.ipaUser ];
commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
}];
};
systemd = {
# Fetch SSH public keys from IPA so users can log in with the key stored # Fetch SSH public keys from IPA so users can log in with the key stored
# in their IPA profile rather than needing ~/.ssh/authorized_keys on every # in their IPA profile rather than needing ~/.ssh/authorized_keys on every
# host. sss_ssh_authorizedkeys queries SSSD (which queries IPA LDAP). # host. sss_ssh_authorizedkeys queries SSSD (which queries IPA LDAP).
@@ -95,70 +72,27 @@ lib.mkIf enabled {
# AuthorizedKeysCommand binaries whose path contains any group-writable # AuthorizedKeysCommand binaries whose path contains any group-writable
# component, silently skipping the command. Copy to /usr/local/bin (all # component, silently skipping the command. Copy to /usr/local/bin (all
# components root-owned, 755) so the path passes sshd's safety check. # components root-owned, 755) so the path passes sshd's safety check.
tmpfiles.rules = [ systemd.tmpfiles.rules = [
"d /usr/local 0755 root root - -" "d /usr/local 0755 root root - -"
"d /usr/local/bin 0755 root root - -" "d /usr/local/bin 0755 root root - -"
"C+ /usr/local/bin/sss_ssh_authorizedkeys 0555 root root - ${pkgs.sssd}/bin/sss_ssh_authorizedkeys" "C+ /usr/local/bin/sss_ssh_authorizedkeys 0555 root root - ${pkgs.sssd}/bin/sss_ssh_authorizedkeys"
# Pre-create the IPA user's home dir so Home Manager activation succeeds
# even before their first login. On a fresh system SSSD may not have
# resolved the user yet — tmpfiles warns and skips in that case (non-fatal),
# and pam_mkhomedir covers the first-login path as a fallback.
"d /home/${vars.ipaUser} 0700 ${vars.ipaUser} ${vars.ipaUser} - -"
]; ];
# security.ipa enables Kerberos (security.krb5) which causes systemd to
# start auth-rpcgss-module.service and rpc-gssd.service for Kerberos NFS
# authentication. LXC containers can't load the auth_rpcgss kernel module
# and don't have /var/lib/nfs/rpc_pipefs, so both services fail.
#
# The NixOS IPA module already adds a drop-in for auth-rpcgss-module.service
# with ConditionPathExists=/etc/krb5.keytab. We use lib.mkForce to win the
# text conflict and add ConditionVirtualization=!container alongside it so
# the service is skipped (not failed) in containers that do have a keytab.
# Same fix for rpc-gssd.service which also fails in containers.
units = lib.mkIf config.boot.isContainer {
"auth-rpcgss-module.service" = {
overrideStrategy = "asDropinIfExists";
text = lib.mkForce ''
[Unit]
ConditionPathExists=
ConditionPathExists=/etc/krb5.keytab
ConditionVirtualization=!container
'';
};
# rpc-gssd also has ConditionPathExists from the NixOS IPA module (and an
# X-Restart-Triggers store path from systemd.nix). Use mkForce to win;
# omit X-Restart-Triggers since this service is skipped in containers anyway.
"rpc-gssd.service" = {
overrideStrategy = "asDropinIfExists";
text = lib.mkForce ''
[Unit]
ConditionPathExists=
ConditionPathExists=/etc/krb5.keytab
ConditionVirtualization=!container
'';
};
};
# home-manager-<user>.service fails on first enrollment because /home/wayne
# doesn't exist until the user's first login (pam_mkhomedir creates it then).
# ConditionPathExists makes systemd skip the service (exit 0, condition not
# met) instead of failing. After first login the dir exists and subsequent
# rebuilds activate HM normally.
services."home-manager-${vars.ipaUser}".unitConfig.ConditionPathExists =
"/home/${vars.ipaUser}";
};
services.openssh.extraConfig = '' services.openssh.extraConfig = ''
AuthorizedKeysCommand /usr/local/bin/sss_ssh_authorizedkeys %u AuthorizedKeysCommand /usr/local/bin/sss_ssh_authorizedkeys %u
AuthorizedKeysCommandUser nobody AuthorizedKeysCommandUser nobody
''; '';
# Create the home directory on first login if it doesn't exist yet.
# IPA users have no pre-created home on the host; without this sshd
# opens a session to a non-existent directory and resets the connection.
security.pam.services.sshd.makeHomeDir = true;
# Host keytab: pre-provisioned on the IPA server, sops-encrypted binary. # Host keytab: pre-provisioned on the IPA server, sops-encrypted binary.
# Placed at /etc/krb5.keytab before SSSD starts so the host authenticates # Placed at /etc/krb5.keytab before SSSD starts so the host authenticates
# to IPA without running ipa-client-install. # to IPA without running ipa-client-install.
sops.secrets."ipa-host-keytab" = { sops.secrets."ipa-host-keytab" = {
sopsFile = keytabPath; sopsFile = keytabSopsFile;
format = "binary"; format = "binary";
path = "/etc/krb5.keytab"; path = "/etc/krb5.keytab";
owner = "root"; owner = "root";
@@ -166,45 +100,4 @@ lib.mkIf enabled {
mode = "0600"; mode = "0600";
restartUnits = [ "sssd.service" ]; restartUnits = [ "sssd.service" ];
}; };
# NixOS requires isNormalUser/isSystemUser + group on any entry in
# users.users. HM with useUserPackages = true (set in flake.nix) adds a stub
# entry for each HM user so it can install packages to
# /etc/profiles/per-user/<name>/. This definition satisfies those assertions.
# With security.ipa setting "passwd: sss files" in nsswitch, SSSD's IPA entry
# takes priority for NSS lookups — this local stub is only a fallback when
# SSSD is unreachable (at which point auth fails anyway).
users.users.${vars.ipaUser} = {
isNormalUser = true;
group = "users";
extraGroups = [ "wheel" ];
createHome = false;
# "!" is not a password hash — it is the standard "account locked" marker.
# It cannot authenticate anyone locally. It exists solely so NixOS generates
# a shadow entry for this stub user; without one pam_unix returns
# PAM_AUTHINFO_UNAVAIL before prompting, which means PAM_AUTHTOK is never
# set and the subsequent pam_sss use_first_pass call has nothing to work
# with — blocking LightDM and su logins even when IPA/SSSD auth succeeds.
hashedPassword = "!";
};
# Home Manager config for the IPA primary user, applied on every enrolled
# host. Manages what IPA doesn't: dotfiles, user-scoped packages, session
# variables. Switch-nix/Test-nix/buildImage are system-wide (configuration.nix)
# so they don't need to be repeated here.
#
# homeDirectory uses mkForce because HM's NixOS integration module sets it to
# "/var/empty" for users not found in config.users.users at eval time (SSSD
# users aren't visible there).
home-manager.users.${vars.ipaUser} = { pkgs, ... }: {
home = {
username = vars.ipaUser;
homeDirectory = lib.mkForce "/home/${vars.ipaUser}";
stateVersion = "26.05";
packages = with pkgs; [ tmux sshfs ];
sessionVariables.EDITOR = lib.mkDefault "nano";
};
programs.home-manager.enable = true;
programs.bash.enable = true;
};
} }
+43 -1
View File
@@ -52,7 +52,6 @@ in
# LXC container does). # LXC container does).
imports = [ imports = [
(modulesPath + "/virtualisation/proxmox-lxc.nix") (modulesPath + "/virtualisation/proxmox-lxc.nix")
../common/preserve-ssh-host-key.nix
]; ];
proxmoxLXC = { proxmoxLXC = {
@@ -106,6 +105,49 @@ in
}; };
}; };
# NixOS's etc activation removes any /etc file that was in the previous
# generation's environment.etc but is absent from the current one — even
# real (non-symlink) copies. On every routine nixos-rebuild switch/test that
# lacks NIXOS_HOST_KEYS_DIR the key is absent from environment.etc, so it
# gets removed as "obsolete". sops-nix derives its age decryption key from
# /etc/ssh/ssh_host_ed25519_key; deletion cascades into every sops secret
# failing with "Error getting data key: 0 successful groups required, got 0".
#
# Fix: activation scripts that bracket the etc step, with explicit deps
# to enforce the correct ordering. Without deps the topological sort places
# preserveSshHostKey AFTER etc (confirmed live on a deployed lxc-tor-relay:
# position 7 vs etc's position 5) -- the key is already gone by the time it
# tries to save it. The etc/setupSecrets entries ADD to existing deps
# (types.listOf concatenates across module definitions).
system.activationScripts = {
# Saves the live key to /run before etc can delete it.
preserveSshHostKey = ''
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
fi
'';
# Reinstalls the key after etc runs if it was removed as "obsolete".
# The resulting file is not registered in environment.etc for either
# generation, so subsequent rebuilds leave it alone permanently.
restoreSshHostKey = {
deps = [ "etc" ];
text = ''
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
fi
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
'';
};
# Force etc to wait until the key is saved, and sops to wait until the
# key is restored. Without these the topological sort breaks the chain.
etc = { deps = [ "preserveSshHostKey" ]; };
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
};
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix # virtualisation/proxmox-lxc.nix (imported above) registers the Nix
# store DB via a systemd service (register-nix-paths) -- it never runs # store DB via a systemd service (register-nix-paths) -- it never runs
# an activation script at all. Confirmed live this means neither # an activation script at all. Confirmed live this means neither
+32 -1
View File
@@ -34,7 +34,6 @@ in
../hardware-configuration/vm/proxmox.nix ../hardware-configuration/vm/proxmox.nix
../boot/efi.nix ../boot/efi.nix
../disko/proxmox.nix ../disko/proxmox.nix
../common/preserve-ssh-host-key.nix
]; ];
environment.etc = lib.mkIf hasKeyForThisTarget { environment.etc = lib.mkIf hasKeyForThisTarget {
@@ -47,4 +46,36 @@ in
mode = "0644"; mode = "0644";
}; };
}; };
# NixOS's etc activation removes any /etc file that was in the previous
# generation's environment.etc but is absent from the current one. Since
# the SSH key is only in environment.etc during the --impure build (when
# NIXOS_HOST_KEYS_DIR is set), normal rebuilds would remove it as
# "obsolete". These scripts mirror lxc.nix's approach: save the live key
# before etc runs, restore it after. Without the explicit deps, the
# topological sort places preserveSshHostKey after etc (confirmed live on
# lxc-tor-relay: position 7 vs etc's position 5), so the key is gone
# before it can be saved.
system.activationScripts = {
preserveSshHostKey = ''
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
fi
'';
restoreSshHostKey = {
deps = [ "etc" ];
text = ''
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
fi
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
'';
};
etc = { deps = [ "preserveSshHostKey" ]; };
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
};
} }
-8
View File
@@ -19,14 +19,6 @@
# tailnet-specific subdomain (vars.tailnetDomain) instead. # tailnet-specific subdomain (vars.tailnetDomain) instead.
services.dnsmasq = { services.dnsmasq = {
enable = true; enable = true;
# NixOS's dnsmasq module defaults resolveLocalQueries to true, which adds
# 127.0.0.1 to networking.nameservers and makes dnsmasq bind to
# listen-address=127.0.0.1. This instance is not the host's local
# resolver — it only serves IPA's conditional forwarder for tailnet names.
# The host uses domainControllerIp directly (networking.nameservers in
# host.nix). Without this, all host DNS goes through dnsmasq, which has
# no upstream for general queries (no-resolv=true), breaking resolution.
resolveLocalQueries = false;
settings = { settings = {
# Listen only on the LAN interface — not tailscale0 or loopback. # Listen only on the LAN interface — not tailscale0 or loopback.
# bind-interfaces prevents dnsmasq from binding to 0.0.0.0 and # bind-interfaces prevents dnsmasq from binding to 0.0.0.0 and
Executable → Regular
+60 -87
View File
@@ -7,72 +7,49 @@
set -euo pipefail set -euo pipefail
# ── Configuration ───────────────────────────────────────────────────────── # ── Configuration ─────────────────────────────────────────────────────────
# All values override-able via environment variables; defaults match variables.nix. NODE1="ha-server-1"
NODE1="${NODE1:-ha-server-1}" NODE2="ha-server-2"
NODE2="${NODE2:-ha-server-2}" NODE1_IP="192.168.2.200" # vars.haServer1Ip
NODE1_IP="${NODE1_IP:-192.168.2.228}" # vars.haServer1Ip NODE2_IP="192.168.2.201" # vars.haServer2Ip
NODE2_IP="${NODE2_IP:-192.168.2.227}" # vars.haServer2Ip VIP="192.168.2.202" # vars.haServerVip
VIP="${VIP:-192.168.2.229}" # vars.haServerVip XFS_MOUNT="/srv/ha-data" # vars.haStorageRoot
XFS_MOUNT="${XFS_MOUNT:-/srv/ha-data}" # vars.haStorageRoot ISCSI_IQN="iqn.2026-01.home.sweet:ha-storage" # vars.haIscsiIqn
ISCSI_IQN="${ISCSI_IQN:-iqn.2026-01.home.sweet:ha-storage}" # vars.haIscsiIqn
# ────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────
PASS=0 PASS=0
FAIL=0 FAIL=0
RESULTS=() RESULTS=()
# Use PASS=$((PASS+1)) instead of ((PASS++)) — the latter evaluates to 0 when pass() { echo " PASS: $1"; ((PASS++)); RESULTS+=("PASS $1"); }
# PASS=0, which triggers set -e and kills the script after the very first PASS. fail() { echo " FAIL: $1"; ((FAIL++)); RESULTS+=("FAIL $1"); }
pass() { echo " PASS: $1"; PASS=$((PASS+1)); RESULTS+=("PASS $1"); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL+1)); RESULTS+=("FAIL $1"); }
HA_USER="nixos" n1() { ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "root@${NODE1_IP}" "$@" 2>/dev/null; }
n1() { ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE1_IP}" sudo "$@" 2>/dev/null; } n2() { ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "root@${NODE2_IP}" "$@" 2>/dev/null; }
n2() { ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE2_IP}" sudo "$@" 2>/dev/null; }
echo "════════════════════════════════════════════════════" echo "════════════════════════════════════════════════════"
echo " HA Cluster Acceptance Tests — $(date '+%Y-%m-%d %H:%M:%S')" echo " HA Cluster Acceptance Tests — $(date '+%Y-%m-%d %H:%M:%S')"
echo "════════════════════════════════════════════════════" echo "════════════════════════════════════════════════════"
# ── Detect Active/Standby nodes ────────────────────────────────────────────
# Pacemaker can promote either node; determine which is currently Active
# (DRBD Primary / holds ha-group resources) before running tests.
echo ""
echo "Detecting Active/Standby nodes..."
if n1 "drbdadm role ha-data" 2>/dev/null | grep -q "^Primary"; then
ACTIVE_NODE="$NODE1"; ACTIVE_IP="$NODE1_IP"
STANDBY_NODE="$NODE2"; STANDBY_IP="$NODE2_IP"
na() { n1 "$@"; }
ns() { n2 "$@"; }
else
ACTIVE_NODE="$NODE2"; ACTIVE_IP="$NODE2_IP"
STANDBY_NODE="$NODE1"; STANDBY_IP="$NODE1_IP"
na() { n2 "$@"; }
ns() { n1 "$@"; }
fi
echo " Active: $ACTIVE_NODE ($ACTIVE_IP)"
echo " Standby: $STANDBY_NODE ($STANDBY_IP)"
# ── T1: Corosync quorum established ────────────────────────────────────── # ── T1: Corosync quorum established ──────────────────────────────────────
echo "" echo ""
echo "[T1] Corosync quorum" echo "[T1] Corosync quorum"
if na "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then if n1 "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then
pass "cluster has quorum" pass "cluster has quorum"
else else
fail "cluster does not have quorum — check corosync on both nodes" fail "cluster does not have quorum — check corosync on both nodes"
fi fi
# ── T2: DRBD Primary on Active node, Secondary on Standby ──────────────── # ── T2: DRBD Primary on node1, Secondary on node2 ────────────────────────
echo "" echo ""
echo "[T2] DRBD roles" echo "[T2] DRBD roles"
DRBD_ROLE=$(na "drbdadm role ha-data" 2>/dev/null || echo "unknown") DRBD_ROLE=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown")
if [[ "$DRBD_ROLE" == "Primary/Secondary" || "$DRBD_ROLE" == "Primary" ]]; then if [[ "$DRBD_ROLE" == "Primary/Secondary" || "$DRBD_ROLE" == "Primary" ]]; then
pass "DRBD Primary on $ACTIVE_NODE ($DRBD_ROLE)" pass "DRBD Primary on $NODE1 ($DRBD_ROLE)"
else else
fail "unexpected DRBD role on $ACTIVE_NODE: $DRBD_ROLE (expected Primary/Secondary)" fail "unexpected DRBD role on $NODE1: $DRBD_ROLE (expected Primary/Secondary)"
fi fi
DRBD_DSTATE=$(na "drbdadm dstate ha-data" 2>/dev/null || echo "unknown") DRBD_DSTATE=$(n1 "drbdadm dstate ha-data" 2>/dev/null || echo "unknown")
if echo "$DRBD_DSTATE" | grep -q "UpToDate"; then if echo "$DRBD_DSTATE" | grep -q "UpToDate"; then
pass "DRBD disk state UpToDate ($DRBD_DSTATE)" pass "DRBD disk state UpToDate ($DRBD_DSTATE)"
else else
@@ -82,45 +59,44 @@ fi
# ── T3: XFS mounted at haStorageRoot on the Active node ────────────────── # ── T3: XFS mounted at haStorageRoot on the Active node ──────────────────
echo "" echo ""
echo "[T3] XFS mount" echo "[T3] XFS mount"
if na "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then if n1 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
pass "XFS mounted at ${XFS_MOUNT} on $ACTIVE_NODE" pass "XFS mounted at ${XFS_MOUNT} on $NODE1"
else else
fail "XFS not mounted at ${XFS_MOUNT} on $ACTIVE_NODE" fail "XFS not mounted at ${XFS_MOUNT} on $NODE1"
fi fi
if ns "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
fail "XFS unexpectedly mounted on $STANDBY_NODE (should only be on Active node)" fail "XFS unexpectedly mounted on $NODE2 (should only be on Active node)"
else else
pass "XFS not mounted on $STANDBY_NODE (correct — Standby)" pass "XFS not mounted on $NODE2 (correct — Secondary)"
fi fi
# ── T4: iSCSI target visible on Active node ─────────────────────────────── # ── T4: iSCSI target visible on both nodes ────────────────────────────────
echo "" echo ""
echo "[T4] iSCSI target" echo "[T4] iSCSI target"
IQN_COUNT=$(na "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0") IQN_COUNT=$(n1 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0")
if [[ "$IQN_COUNT" -ge 1 ]]; then if [[ "$IQN_COUNT" -ge 1 ]]; then
pass "iSCSI IQN active on $ACTIVE_NODE ($IQN_COUNT target(s))" pass "iSCSI IQN active on $NODE1 ($IQN_COUNT target(s))"
else else
fail "no iSCSI IQN active on $ACTIVE_NODE" fail "no iSCSI IQN active on $NODE1"
fi fi
# iSCSI port reachable from Standby node via VIP. # iSCSI discovery from node2 via VIP
# Use bash TCP probe (no iscsiadm needed — just checks port 3260 is open). if n2 "iscsiadm -m discovery -t sendtargets -p '${VIP}' 2>/dev/null | grep -q '${ISCSI_IQN}'"; then
if ns "bash -c 'echo >/dev/tcp/${VIP}/3260' 2>/dev/null"; then pass "iSCSI target discoverable from $NODE2 via VIP ${VIP}"
pass "iSCSI port 3260 reachable from $STANDBY_NODE via VIP ${VIP}"
else else
fail "iSCSI port 3260 not reachable from $STANDBY_NODE via ${VIP}" fail "iSCSI target not discoverable from $NODE2 via ${VIP}"
fi fi
# ── T5: Failover — standby Active node, verify resources move to Standby ── # ── T5: Failover — standby node1, verify resources move to node2 ──────────
echo "" echo ""
echo "[T5] Failover (standby $ACTIVE_NODE)" echo "[T5] Failover (standby $NODE1)"
ACTIVE_CRMD_NAME=$(na "crm_node -n" 2>/dev/null || echo "") MYNODE=$(n1 "crm_node -n" 2>/dev/null || echo "")
na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v on" 2>/dev/null || true n1 "crm_standby -N '${MYNODE}' -v on" 2>/dev/null || true
echo " Waiting up to 120 s for resources to move to $STANDBY_NODE..." echo " Waiting up to 30 s for resources to move to $NODE2..."
MOVED=false MOVED=false
for i in $(seq 1 120); do for i in $(seq 1 30); do
if ns "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
MOVED=true MOVED=true
echo " Resources moved in ${i}s" echo " Resources moved in ${i}s"
break break
@@ -129,52 +105,49 @@ for i in $(seq 1 120); do
done done
if $MOVED; then if $MOVED; then
pass "XFS mounted on $STANDBY_NODE after failover" pass "XFS mounted on $NODE2 after failover"
IQN_ON_STANDBY=$(ns "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0") IQN_ON_N2=$(n2 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0")
[[ "$IQN_ON_STANDBY" -ge 1 ]] \ [[ "$IQN_ON_N2" -ge 1 ]] \
&& pass "iSCSI target active on $STANDBY_NODE after failover" \ && pass "iSCSI target active on $NODE2 after failover" \
|| fail "iSCSI target NOT active on $STANDBY_NODE after failover" || fail "iSCSI target NOT active on $NODE2 after failover"
else else
fail "XFS did not mount on $STANDBY_NODE within 120 s — failover incomplete" fail "XFS did not mount on $NODE2 within 30 s — failover incomplete"
fi fi
# ── T6: Data integrity — file written post-failover readable ───────────── # ── T6: Data integrity — file written pre-failover readable post-failover ─
echo "" echo ""
echo "[T6] Data integrity" echo "[T6] Data integrity"
# Write a test file on the new Active (former Standby) and verify it. # Write a test file on node2 (now Active) and verify its content
# Use `echo | sudo tee` for the write: "echo ... > file" via bash -c has the
# redirect interpreted by the remote nixos shell (not sudo), so the file open
# runs as nixos and fails with EACCES on the root-owned XFS mount. Piping
# through sudo tee lets tee (running as root) open the file instead.
TEST_FILE="${XFS_MOUNT}/.acceptance-test-$$" TEST_FILE="${XFS_MOUNT}/.acceptance-test-$$"
TEST_CONTENT="ha-acceptance-test-$(date +%s)" TEST_CONTENT="ha-acceptance-test-$(date +%s)"
echo "${TEST_CONTENT}" | ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${STANDBY_IP}" sudo tee "${TEST_FILE}" > /dev/null 2>/dev/null || true n2 "echo '${TEST_CONTENT}' > '${TEST_FILE}'" 2>/dev/null || true
READBACK=$(ns cat "${TEST_FILE}" 2>/dev/null || echo "") READBACK=$(n2 "cat '${TEST_FILE}' 2>/dev/null" || echo "")
if [[ "$READBACK" == "$TEST_CONTENT" ]]; then if [[ "$READBACK" == "$TEST_CONTENT" ]]; then
pass "test file written and read back correctly on $STANDBY_NODE" pass "test file written and read back correctly on $NODE2"
else else
fail "data integrity check failed (wrote: '$TEST_CONTENT', read: '$READBACK')" fail "data integrity check failed (wrote: '$TEST_CONTENT', read: '$READBACK')"
fi fi
ns rm -f "${TEST_FILE}" 2>/dev/null || true n2 "rm -f '${TEST_FILE}'" 2>/dev/null || true
# ── T7: Node rejoin — un-standby original Active, verify cluster is healthy ─ # ── T7: Node rejoin — un-standby node1, verify cluster is healthy ─────────
echo "" echo ""
echo "[T7] Node rejoin" echo "[T7] Node rejoin"
na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v off" 2>/dev/null || true n1 "crm_standby -N '${MYNODE}' -v off" 2>/dev/null || true
na "crm_resource --cleanup" 2>/dev/null || true n1 "crm_resource --cleanup" 2>/dev/null || true
sleep 5 sleep 5
if na "corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'"; then ONLINE_NODES=$(n2 "crm_mon -1 2>/dev/null | grep -c 'Online:'" || echo "0")
pass "$ACTIVE_NODE rejoined — cluster has quorum" if n1 "corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'"; then
pass "$NODE1 rejoined — cluster has quorum"
else else
fail "$ACTIVE_NODE did not rejoin with quorum" fail "$NODE1 did not rejoin with quorum"
fi fi
DRBD_ROLE_AFTER=$(na "drbdadm role ha-data" 2>/dev/null || echo "unknown") DRBD_ROLE_AFTER=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown")
if echo "$DRBD_ROLE_AFTER" | grep -q "Secondary"; then if echo "$DRBD_ROLE_AFTER" | grep -q "Secondary"; then
pass "$ACTIVE_NODE is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)" pass "$NODE1 is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)"
else else
fail "unexpected DRBD role on $ACTIVE_NODE after rejoin: $DRBD_ROLE_AFTER" fail "unexpected DRBD role on $NODE1 after rejoin: $DRBD_ROLE_AFTER"
fi fi
# ── Summary ─────────────────────────────────────────────────────────────── # ── Summary ───────────────────────────────────────────────────────────────
View File
File mode changed.
Executable → Regular
+85 -149
View File
@@ -21,27 +21,22 @@
set -euo pipefail set -euo pipefail
# ── Configuration ───────────────────────────────────────────────────────── # ── Configuration ─────────────────────────────────────────────────────────
# All values override-able via environment variables; defaults match variables.nix. # These must match variables.nix haServer* values and the Proxmox VMID
NODE1="${NODE1:-ha-server-1}" # assignments. Update before running.
NODE2="${NODE2:-ha-server-2}" NODE1="ha-server-1"
NODE1_IP="${NODE1_IP:-192.168.2.228}" # vars.haServer1Ip NODE2="ha-server-2"
NODE2_IP="${NODE2_IP:-192.168.2.227}" # vars.haServer2Ip NODE1_IP="192.168.2.200" # vars.haServer1Ip
VIP="${VIP:-192.168.2.229}" # vars.haServerVip NODE2_IP="192.168.2.201" # vars.haServer2Ip
XFS_MOUNT="${XFS_MOUNT:-/srv/ha-data}" # vars.haStorageRoot VIP="192.168.2.202" # vars.haServerVip
ISCSI_IQN="${ISCSI_IQN:-iqn.2026-01.home.sweet:ha-storage}" # vars.haIscsiIqn XFS_MOUNT="/srv/ha-data" # vars.haStorageRoot
ISCSI_IQN="iqn.2026-01.home.sweet:ha-storage" # vars.haIscsiIqn
ISCSI_LUN_FILE="${XFS_MOUNT}/iscsi-lun.img" ISCSI_LUN_FILE="${XFS_MOUNT}/iscsi-lun.img"
ISCSI_LUN_SIZE="10G" ISCSI_LUN_SIZE="10G"
DRBD_DEVICE="/dev/drbd0" DRBD_DEVICE="/dev/drbd0"
VMID_NODE1="${VMID_NODE1:-}" # set by deploy.sh; needed for STONITH VMID_NODE1="" # FILL IN: Proxmox VMID for ha-server-1
VMID_NODE2="${VMID_NODE2:-}" VMID_NODE2="" # FILL IN: Proxmox VMID for ha-server-2
PVE_HOST="${PVE_HOST:-pve1.sweet.home}" PVE_HOST="pve1.sweet.home"
PVE_USER="${PVE_USER:-wayne}" PVE_USER="wayne"
# Inter-node SSH: HA_USER is the user to SSH as on NODE2; HA_KEY is the private
# key to use. Default is root-to-root (no key arg). deploy.sh sets HA_USER=nixos
# and HA_KEY=/tmp/cluster-init-key so the script works even when root-to-root SSH
# is not available.
HA_USER="${HA_USER:-root}"
HA_KEY="${HA_KEY:-}"
# NFS dataset subdirectories to create under XFS_MOUNT. # NFS dataset subdirectories to create under XFS_MOUNT.
# Must mirror vars.nfsShares subpath values in variables.nix. # Must mirror vars.nfsShares subpath values in variables.nix.
@@ -64,32 +59,6 @@ warn() { echo "[cluster-init] WARNING: $*" >&2; }
[[ $(id -u) -eq 0 ]] || die "must run as root" [[ $(id -u) -eq 0 ]] || die "must run as root"
[[ "$(hostname)" == "$NODE1" ]] || die "must run on $NODE1" [[ "$(hostname)" == "$NODE1" ]] || die "must run on $NODE1"
# NixOS may not include xfsprogs in root's PATH even when it's in the store.
# If mkfs.xfs is missing, search the Nix store for it.
if ! command -v mkfs.xfs &>/dev/null; then
_xfs_bin=$(find /nix/store -maxdepth 3 -name mkfs.xfs 2>/dev/null | head -1 | xargs dirname 2>/dev/null || true)
[[ -n "$_xfs_bin" ]] && export PATH="$_xfs_bin:$PATH" \
|| die "mkfs.xfs not found — add xfsprogs to ha-server.nix environment.systemPackages and rebuild"
fi
# Inter-node SSH/SCP helpers — abstract over root-to-root vs nixos+sudo.
_SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
[[ -n "$HA_KEY" ]] && _SSH_OPTS="-i $HA_KEY $_SSH_OPTS"
if [[ "$HA_USER" == "root" ]]; then
n2_ssh() { ssh $_SSH_OPTS "root@${NODE2_IP}" "$@"; }
n2_scp() { scp $_SSH_OPTS "$1" "root@${NODE2_IP}:$2"; }
else
# Non-root user with passwordless sudo; wrap each command with sudo.
n2_ssh() { ssh $_SSH_OPTS "${HA_USER}@${NODE2_IP}" sudo "$@"; }
n2_scp() {
# SCP to a tmp path, then sudo-move to the real destination as the remote user.
local src="$1" dst="$2"
local tmp="/tmp/_cluster_init_scp_$$"
scp $_SSH_OPTS "$src" "${HA_USER}@${NODE2_IP}:${tmp}"
ssh $_SSH_OPTS "${HA_USER}@${NODE2_IP}" sudo mv "${tmp}" "${dst}"
}
fi
# ── 0. Corosync authkey ─────────────────────────────────────────────────── # ── 0. Corosync authkey ───────────────────────────────────────────────────
AUTHKEY="/etc/corosync/authkey" AUTHKEY="/etc/corosync/authkey"
mkdir -p /etc/corosync mkdir -p /etc/corosync
@@ -99,20 +68,15 @@ if [[ ! -f "$AUTHKEY" ]]; then
chmod 0400 "$AUTHKEY" chmod 0400 "$AUTHKEY"
fi fi
log "Distributing authkey to $NODE2..." log "Distributing authkey to $NODE2..."
n2_ssh "mkdir -p /etc/corosync" ssh "root@${NODE2_IP}" "mkdir -p /etc/corosync"
n2_scp "$AUTHKEY" "$AUTHKEY" scp -q "$AUTHKEY" "root@${NODE2_IP}:${AUTHKEY}"
n2_ssh "chmod 0400 '${AUTHKEY}'" ssh "root@${NODE2_IP}" "chmod 0400 '${AUTHKEY}'"
log "Restarting corosync and pacemaker on both nodes..." log "Restarting corosync on both nodes..."
systemctl restart corosync systemctl restart corosync
n2_ssh "systemctl restart corosync" ssh "root@${NODE2_IP}" "systemctl restart corosync"
sleep 3 sleep 3
log "Starting pacemaker on both nodes (may have failed at boot before authkey was placed)..."
systemctl start pacemaker 2>/dev/null || systemctl restart pacemaker 2>/dev/null || true
n2_ssh "systemctl start pacemaker 2>/dev/null || systemctl restart pacemaker 2>/dev/null || true"
sleep 2
# ── 1. Corosync quorum ──────────────────────────────────────────────────── # ── 1. Corosync quorum ────────────────────────────────────────────────────
log "Waiting for corosync quorum..." log "Waiting for corosync quorum..."
for i in $(seq 1 30); do for i in $(seq 1 30); do
@@ -135,27 +99,21 @@ for i in $(seq 1 30); do
done done
# ── 2. DRBD initialisation ──────────────────────────────────────────────── # ── 2. DRBD initialisation ────────────────────────────────────────────────
# Down DRBD first on both nodes before (re-)initialising metadata.
# This ensures /dev/sdb is not held open by the kernel module, which would
# trigger a drbdmeta TTY-confirmation prompt ("stdin not a TTY, not waiting
# for confirmation") during the write-dev-uuid step even when --force is set.
log "Detaching DRBD on $NODE1 (idempotent pre-init clean-up)..."
drbdadm down ha-data 2>/dev/null || true
log "Detaching DRBD on $NODE2 (idempotent pre-init clean-up)..."
n2_ssh "drbdadm down ha-data 2>/dev/null || true"
log "Initialising DRBD metadata on $NODE1..." log "Initialising DRBD metadata on $NODE1..."
if ! drbdadm dstate ha-data 2>/dev/null | grep -q "UpToDate\|Inconsistent\|Diskless"; then if ! drbdadm dstate ha-data 2>/dev/null | grep -q "UpToDate\|Inconsistent\|Diskless"; then
drbdadm create-md ha-data --force drbdadm create-md ha-data --force
fi fi
log "Initialising DRBD metadata on $NODE2..." log "Initialising DRBD metadata on $NODE2..."
# Use grep -E for ERE alternation inside the remote bash -c string (avoids \| quoting issues). ssh "root@${NODE2_IP}" "
n2_ssh "bash -c 'drbdadm dstate ha-data 2>/dev/null | grep -qE \"UpToDate|Inconsistent|Diskless\" || drbdadm create-md ha-data --force'" if ! drbdadm dstate ha-data 2>/dev/null | grep -q 'UpToDate\|Inconsistent\|Diskless'; then
drbdadm create-md ha-data --force
fi
"
log "Bringing up DRBD on both nodes..." log "Bringing up DRBD on both nodes..."
drbdadm up ha-data 2>/dev/null || true drbdadm up ha-data 2>/dev/null || true
n2_ssh "drbdadm up ha-data" 2>/dev/null || true ssh "root@${NODE2_IP}" "drbdadm up ha-data 2>/dev/null" || true
log "Forcing $NODE1 to DRBD Primary for initial sync..." log "Forcing $NODE1 to DRBD Primary for initial sync..."
drbdadm primary ha-data --force drbdadm primary ha-data --force
@@ -179,7 +137,7 @@ fi
log "Mounting ${DRBD_DEVICE} at ${XFS_MOUNT}..." log "Mounting ${DRBD_DEVICE} at ${XFS_MOUNT}..."
mkdir -p "${XFS_MOUNT}" mkdir -p "${XFS_MOUNT}"
mountpoint -q "${XFS_MOUNT}" || mount "${DRBD_DEVICE}" "${XFS_MOUNT}" mount "${DRBD_DEVICE}" "${XFS_MOUNT}"
# ── 4. NFS dataset directories ──────────────────────────────────────────── # ── 4. NFS dataset directories ────────────────────────────────────────────
log "Creating NFS dataset directories..." log "Creating NFS dataset directories..."
@@ -195,130 +153,108 @@ fi
# ── 6. LIO iSCSI target ─────────────────────────────────────────────────── # ── 6. LIO iSCSI target ───────────────────────────────────────────────────
log "Configuring LIO iSCSI target via targetcli..." log "Configuring LIO iSCSI target via targetcli..."
# Note: do NOT bind portal to ${VIP} here — the VIP isn't assigned yet (Pacemaker
# creates it). The default portal (all IPs, port 3260) is correct; Pacemaker's
# VIP resource will make the target reachable at the VIP address.
#
# Clear any existing LIO state first (idempotent: re-run after a partial failure).
# Use specific delete commands — clearconfig does not reliably clear kernel state.
if ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -q "${ISCSI_IQN}"; then
log "Clearing existing LIO target ${ISCSI_IQN} before reconfiguration..."
targetcli "/iscsi delete ${ISCSI_IQN}" 2>/dev/null || true
fi
if ls /sys/kernel/config/target/core/ 2>/dev/null | grep -q "fileio"; then
log "Clearing existing LIO backstore ha-lun0 before reconfiguration..."
targetcli "/backstores/fileio delete ha-lun0" 2>/dev/null || true
fi
targetcli <<EOF targetcli <<EOF
/backstores/fileio create name=ha-lun0 file_or_dev=${ISCSI_LUN_FILE} size=0 write_back=false /backstores/fileio create name=ha-lun0 file_or_dev=${ISCSI_LUN_FILE} size=0 write_back=false
/iscsi create ${ISCSI_IQN} /iscsi create ${ISCSI_IQN}
/iscsi/${ISCSI_IQN}/tpg1/luns create /backstores/fileio/ha-lun0 /iscsi/${ISCSI_IQN}/tpg1/luns create /backstores/fileio/ha-lun0
/iscsi/${ISCSI_IQN}/tpg1/portals create ${VIP}
/iscsi/${ISCSI_IQN}/tpg1 set attribute authentication=0 /iscsi/${ISCSI_IQN}/tpg1 set attribute authentication=0
/iscsi/${ISCSI_IQN}/tpg1 set attribute demo_mode_write_protect=0 /iscsi/${ISCSI_IQN}/tpg1 set attribute demo_mode_write_protect=0
saveconfig /etc/target/saveconfig.json saveconfig /etc/target/saveconfig.json
EOF EOF
log "Tearing down LIO kernel objects — Pacemaker will restore via targetctl on the Active node..."
# LIO holds the backing file open; clear kernel state now so the XFS unmount succeeds.
# Use specific delete commands (clearconfig does not reliably clear kernel configfs state).
targetcli "/iscsi delete ${ISCSI_IQN}" 2>/dev/null || warn "LIO iscsi delete failed — umount may fail"
targetcli "/backstores/fileio delete ha-lun0" 2>/dev/null || warn "LIO backstore delete failed"
log "Distributing iSCSI saveconfig to $NODE2..." log "Distributing iSCSI saveconfig to $NODE2..."
n2_scp /etc/target/saveconfig.json /etc/target/saveconfig.json scp -q /etc/target/saveconfig.json "root@${NODE2_IP}:/etc/target/saveconfig.json"
log "Unmounting ${XFS_MOUNT} — Pacemaker manages it..." log "Unmounting ${XFS_MOUNT} — Pacemaker manages it..."
umount "${XFS_MOUNT}" || { sync; umount -l "${XFS_MOUNT}"; } umount "${XFS_MOUNT}"
log "Demoting DRBD to Secondary — Pacemaker manages primary role..." log "Demoting DRBD to Secondary — Pacemaker manages primary role..."
drbdadm role ha-data 2>/dev/null | grep -q "^Primary" && drbdadm secondary ha-data || true drbdadm secondary ha-data
# ── 7. Pacemaker resources ──────────────────────────────────────────────── # ── 7. Pacemaker resources ────────────────────────────────────────────────
log "Configuring Pacemaker cluster properties..." log "Configuring Pacemaker cluster properties..."
crm_attribute -t crm_config -n stonith-enabled -v false crm_attribute -t crm_config -n stonith-enabled -v false
crm_attribute -t crm_config -n no-quorum-policy -v ignore crm_attribute -t crm_config -n no-quorum-policy -v ignore
log "Creating Pacemaker resources via cibadmin..." log "Creating DRBD promotable clone resource..."
# Use cibadmin --replace with pacemaker-4.0-compatible XML. cibadmin --replace --scope resources --xml-text "
# Key schema rules for pacemaker-4.0: <resources>
# - globally-unique must be in <meta_attributes>, not a direct <clone> attribute <clone id=\"ms-drbd0\" globally-unique=\"false\">
# - promoted-max / promoted-node-max (not master-max / master-node-max) <meta_attributes id=\"ms-drbd0-meta\">
# - constraint with-rsc-role="Promoted" (not "Master") <nvpair id=\"ms-drbd0-promotable\" name=\"promotable\" value=\"true\"/>
cibadmin --replace --scope resources --xml-text '<resources> <nvpair id=\"ms-drbd0-master-max\" name=\"master-max\" value=\"1\"/>
<clone id="ms-drbd0"> <nvpair id=\"ms-drbd0-master-node-max\" name=\"master-node-max\" value=\"1\"/>
<meta_attributes id="ms-drbd0-meta"> <nvpair id=\"ms-drbd0-clone-max\" name=\"clone-max\" value=\"2\"/>
<nvpair id="ms-drbd0-globally-unique" name="globally-unique" value="false"/> <nvpair id=\"ms-drbd0-clone-node-max\" name=\"clone-node-max\" value=\"1\"/>
<nvpair id="ms-drbd0-promotable" name="promotable" value="true"/> <nvpair id=\"ms-drbd0-notify\" name=\"notify\" value=\"true\"/>
<nvpair id="ms-drbd0-promoted-max" name="promoted-max" value="1"/> <nvpair id=\"ms-drbd0-interleave\" name=\"interleave\" value=\"true\"/>
<nvpair id="ms-drbd0-promoted-node-max" name="promoted-node-max" value="1"/>
<nvpair id="ms-drbd0-clone-max" name="clone-max" value="2"/>
<nvpair id="ms-drbd0-clone-node-max" name="clone-node-max" value="1"/>
<nvpair id="ms-drbd0-notify" name="notify" value="true"/>
<nvpair id="ms-drbd0-interleave" name="interleave" value="true"/>
</meta_attributes> </meta_attributes>
<primitive id="drbd0" class="ocf" type="drbd" provider="linbit"> <primitive id=\"drbd0\" class=\"ocf\" type=\"drbd\" provider=\"linbit\">
<instance_attributes id="drbd0-attrs"> <instance_attributes id=\"drbd0-attrs\">
<nvpair id="drbd0-resource" name="drbd_resource" value="ha-data"/> <nvpair id=\"drbd0-resource\" name=\"drbd_resource\" value=\"ha-data\"/>
</instance_attributes> </instance_attributes>
<operations> <operations>
<op id="drbd0-start" name="start" interval="0" timeout="240s"/> <op id=\"drbd0-start\" name=\"start\" interval=\"0\" timeout=\"240s\"/>
<op id="drbd0-stop" name="stop" interval="0" timeout="120s"/> <op id=\"drbd0-stop\" name=\"stop\" interval=\"0\" timeout=\"120s\"/>
<op id="drbd0-promote" name="promote" interval="0" timeout="240s"/> <op id=\"drbd0-promote\" name=\"promote\" interval=\"0\" timeout=\"90s\"/>
<op id="drbd0-demote" name="demote" interval="0" timeout="90s"/> <op id=\"drbd0-demote\" name=\"demote\" interval=\"0\" timeout=\"90s\"/>
<op id="drbd0-monitor-promoted" name="monitor" interval="20s" timeout="20s" role="Promoted"/> <op id=\"drbd0-monitor-master\" name=\"monitor\" interval=\"20s\" timeout=\"20s\" role=\"Promoted\"/>
<op id="drbd0-monitor-unpromoted" name="monitor" interval="30s" timeout="20s" role="Unpromoted"/> <op id=\"drbd0-monitor-slave\" name=\"monitor\" interval=\"30s\" timeout=\"20s\" role=\"Unpromoted\"/>
</operations> </operations>
</primitive> </primitive>
</clone> </clone>
<group id="ha-group"> <group id=\"ha-group\">
<primitive id="xfs-data" class="ocf" type="Filesystem" provider="heartbeat"> <primitive id=\"xfs-data\" class=\"ocf\" type=\"Filesystem\" provider=\"heartbeat\">
<instance_attributes id="xfs-data-attrs"> <instance_attributes id=\"xfs-data-attrs\">
<nvpair id="xfs-data-device" name="device" value="/dev/drbd0"/> <nvpair id=\"xfs-data-device\" name=\"device\" value=\"${DRBD_DEVICE}\"/>
<nvpair id="xfs-data-directory" name="directory" value="/srv/ha-data"/> <nvpair id=\"xfs-data-directory\" name=\"directory\" value=\"${XFS_MOUNT}\"/>
<nvpair id="xfs-data-fstype" name="fstype" value="xfs"/> <nvpair id=\"xfs-data-fstype\" name=\"fstype\" value=\"xfs\"/>
<nvpair id="xfs-data-options" name="options" value="defaults"/> <nvpair id=\"xfs-data-options\" name=\"options\" value=\"defaults\"/>
<nvpair id="xfs-data-force_unmount" name="force_unmount" value="true"/> <nvpair id=\"xfs-data-force_unmount\" name=\"force_unmount\" value=\"false\"/>
</instance_attributes> </instance_attributes>
<operations> <operations>
<op id="xfs-data-start" name="start" interval="0" timeout="60s"/> <op id=\"xfs-data-start\" name=\"start\" interval=\"0\" timeout=\"60s\"/>
<op id="xfs-data-stop" name="stop" interval="0" timeout="60s"/> <op id=\"xfs-data-stop\" name=\"stop\" interval=\"0\" timeout=\"60s\"/>
<op id="xfs-data-monitor" name="monitor" interval="20s" timeout="40s"/> <op id=\"xfs-data-monitor\" name=\"monitor\" interval=\"20s\" timeout=\"40s\"/>
</operations> </operations>
</primitive> </primitive>
<primitive id="iscsi-target" class="systemd" type="targetctl"> <primitive id=\"iscsi-target\" class=\"systemd\" type=\"targetctl\">
<operations> <operations>
<op id="iscsi-start" name="start" interval="0" timeout="60s"/> <op id=\"iscsi-start\" name=\"start\" interval=\"0\" timeout=\"60s\"/>
<op id="iscsi-stop" name="stop" interval="0" timeout="60s"/> <op id=\"iscsi-stop\" name=\"stop\" interval=\"0\" timeout=\"60s\"/>
<op id="iscsi-monitor" name="monitor" interval="20s" timeout="40s"/> <op id=\"iscsi-monitor\" name=\"monitor\" interval=\"20s\" timeout=\"40s\"/>
</operations> </operations>
</primitive> </primitive>
<primitive id="nfs-server" class="systemd" type="nfs-server"> <primitive id=\"nfs-server\" class=\"systemd\" type=\"nfs-server\">
<operations> <operations>
<op id="nfs-start" name="start" interval="0" timeout="60s"/> <op id=\"nfs-start\" name=\"start\" interval=\"0\" timeout=\"60s\"/>
<op id="nfs-stop" name="stop" interval="0" timeout="60s"/> <op id=\"nfs-stop\" name=\"stop\" interval=\"0\" timeout=\"60s\"/>
<op id="nfs-monitor" name="monitor" interval="30s" timeout="40s"/> <op id=\"nfs-monitor\" name=\"monitor\" interval=\"30s\" timeout=\"40s\"/>
</operations> </operations>
</primitive> </primitive>
<primitive id="vip" class="ocf" type="IPaddr2" provider="heartbeat"> <primitive id=\"vip\" class=\"ocf\" type=\"IPaddr2\" provider=\"heartbeat\">
<instance_attributes id="vip-attrs"> <instance_attributes id=\"vip-attrs\">
<nvpair id="vip-ip" name="ip" value="192.168.2.229"/> <nvpair id=\"vip-ip\" name=\"ip\" value=\"${VIP}\"/>
<nvpair id="vip-cidr" name="cidr_netmask" value="24"/> <nvpair id=\"vip-cidr\" name=\"cidr_netmask\" value=\"24\"/>
</instance_attributes> </instance_attributes>
<operations> <operations>
<op id="vip-start" name="start" interval="0" timeout="20s"/> <op id=\"vip-start\" name=\"start\" interval=\"0\" timeout=\"20s\"/>
<op id="vip-stop" name="stop" interval="0" timeout="20s"/> <op id=\"vip-stop\" name=\"stop\" interval=\"0\" timeout=\"20s\"/>
<op id="vip-monitor" name="monitor" interval="10s" timeout="20s"/> <op id=\"vip-monitor\" name=\"monitor\" interval=\"10s\" timeout=\"20s\"/>
</operations> </operations>
</primitive> </primitive>
</group> </group>
</resources>' </resources>
"
log "Adding Pacemaker ordering and colocation constraints..." log "Adding ordering and colocation constraints..."
cibadmin --replace --scope constraints --xml-text '<constraints> cibadmin --create --scope constraints --xml-text "
<rsc_order id="order-drbd-group" first="ms-drbd0" first-action="promote" then="ha-group" then-action="start" kind="Mandatory"/> <constraints>
<rsc_colocation id="coloc-group-with-drbd" score="INFINITY" rsc="ha-group" with-rsc="ms-drbd0" with-rsc-role="Promoted"/> <rsc_order id=\"order-drbd-group\" first=\"ms-drbd0\" first-action=\"promote\" then=\"ha-group\" then-action=\"start\"/>
</constraints>' <rsc_colocation id=\"coloc-group-with-drbd\" rsc=\"ha-group\" with-rsc=\"ms-drbd0\" with-rsc-role=\"Master\" score=\"INFINITY\"/>
</constraints>
"
log "Waiting for resources to start..." log "Waiting for resources to start..."
for i in $(seq 1 60); do for i in $(seq 1 60); do
-410
View File
@@ -1,410 +0,0 @@
#!/usr/bin/env bash
# deploy.sh — Full lifecycle management for the HA file-server cluster.
#
# Handles everything from zero (no VMs, no secrets) through a running,
# tested cluster, and optionally tears it back down.
#
# Usage:
# scripts/ha/deploy.sh [options]
# scripts/ha/deploy.sh --destroy [options]
#
# Phases (all run by default; skip any with --skip-*):
# 1. ensure-bridge Create storage bridge (vmbr1) on the Proxmox node if absent.
# 2. sync-keys Generate SSH host keys and register age keys for both nodes.
# 3. create-vms Build disk images and create both VMs via create-proxmox-resource.sh.
# 4. add-hardware Attach storage NIC (vmbr1) and DRBD data disk to each VM.
# 5. boot-wait Start VMs, wait for SSH on both nodes.
# 6. cluster-init Form the cluster: DRBD, corosync, Pacemaker, NFS, VIP.
# Also encrypts the generated corosync authkey into the repo.
# 7. run-tests Run acceptance tests (T1–T7).
#
# Options:
# --node <host> Proxmox host to deploy on (default: pve1.sweet.home)
# --vmid1 <n> VMID for ha-server-1 (default: 200)
# --vmid2 <n> VMID for ha-server-2 (default: 201)
# --storage <pool> Proxmox storage pool (default: local-zfs)
# --storage-bridge <br> Bridge for HA storage network (default: vmbr1)
# --drbd-disk-gb <n> DRBD data disk size in GB (default: 32)
# --memory <MB> RAM per node (default: 4096)
# --cores <n> vCPUs per node (default: 4)
# --skip-ensure-bridge Skip storage bridge creation/check
# --skip-sync-keys Skip sync-host-keys.sh (clan vars already exist)
# --skip-create-vms Skip VM creation (VMs already exist)
# --skip-add-hardware Skip net1/scsi1 attachment (already attached)
# --skip-boot-wait Skip boot/SSH wait (VMs already running)
# --skip-cluster-init Skip cluster formation (cluster already configured)
# --skip-tests Skip acceptance tests
# --force-rebuild Pass --force-rebuild to create-proxmox-resource.sh
# --destroy Stop and delete both VMs (skip all other phases)
# --dry-run Print what would run without executing
# -h|--help Show this message
#
# Prerequisites:
# - SSH access to the Proxmox node as $PROXMOX_SSH_USER (wayne).
# - sops age key in the standard location (used by sync-host-keys.sh).
# - For --skip-sync-keys: clan vars already in vars/per-machine/proxmox-ha-server-{1,2}/.
# - For full tests: secrets/common.yaml decryptable on both nodes (run
# `sops updatekeys secrets/common.yaml` after sync-keys).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=../env.sh
source "${REPO_ROOT}/scripts/env.sh"
# ── Defaults ──────────────────────────────────────────────────────────────────
NODE="${PROXMOX_HOST:-$PVE1_HOST}"
VMID1=200
VMID2=201
STORAGE="${PROXMOX_STORAGE:-local-zfs}"
STORAGE_BRIDGE="vmbr1"
DRBD_DISK_GB=32
MEMORY_MB=4096
CORES=4
SKIP_ENSURE_BRIDGE=false
SKIP_SYNC_KEYS=false
SKIP_CREATE_VMS=false
SKIP_ADD_HARDWARE=false
SKIP_BOOT_WAIT=false
SKIP_CLUSTER_INIT=false
SKIP_TESTS=false
FORCE_REBUILD=false
DESTROY=false
DRY_RUN=false
# ── Variables from repo ───────────────────────────────────────────────────────
NODE1_HOST="ha-server-1"
NODE2_HOST="ha-server-2"
NODE1_IP="192.168.2.228"
NODE2_IP="192.168.2.227"
STORAGE_IP1="192.168.4.228"
STORAGE_IP2="192.168.4.227"
STORAGE_CIDR="192.168.4.0/29"
SSH_USER="${PROXMOX_SSH_USER:-wayne}"
# ── Argument parsing ──────────────────────────────────────────────────────────
usage() {
sed -n '/^# Usage:/,/^[^#]/{ /^#/{ s/^# \?//; p } }' "$0"
exit "${1:-0}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--node) NODE="$2"; shift 2 ;;
--vmid1) VMID1="$2"; shift 2 ;;
--vmid2) VMID2="$2"; shift 2 ;;
--storage) STORAGE="$2"; shift 2 ;;
--storage-bridge) STORAGE_BRIDGE="$2"; shift 2 ;;
--drbd-disk-gb) DRBD_DISK_GB="$2"; shift 2 ;;
--memory) MEMORY_MB="$2"; shift 2 ;;
--cores) CORES="$2"; shift 2 ;;
--skip-ensure-bridge) SKIP_ENSURE_BRIDGE=true; shift ;;
--skip-sync-keys) SKIP_SYNC_KEYS=true; shift ;;
--skip-create-vms) SKIP_CREATE_VMS=true; shift ;;
--skip-add-hardware) SKIP_ADD_HARDWARE=true; shift ;;
--skip-boot-wait) SKIP_BOOT_WAIT=true; shift ;;
--skip-cluster-init) SKIP_CLUSTER_INIT=true; shift ;;
--skip-tests) SKIP_TESTS=true; shift ;;
--force-rebuild) FORCE_REBUILD=true; shift ;;
--destroy) DESTROY=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage 0 ;;
*) echo "Unknown option: $1" >&2; usage 1 ;;
esac
done
# ── Helpers ───────────────────────────────────────────────────────────────────
log() { echo "==> $*"; }
logn() { echo " $*"; }
err() { echo "ERROR: $*" >&2; exit 1; }
run() {
if $DRY_RUN; then
echo "[dry-run] $*"
else
"$@"
fi
}
pve() {
# Run a command on the Proxmox node via SSH.
if $DRY_RUN; then
echo "[dry-run] ssh ${SSH_USER}@${NODE} sudo $*"
else
ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" "sudo $*"
fi
}
pve_check() {
# Run a read-only probe on the Proxmox node — always executes even in dry-run.
ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" "sudo $*"
}
HA_USER="nixos"
n1() {
# Run a command on ha-server-1 via SSH as nixos with sudo.
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE1_IP}" sudo "$@" 2>/dev/null
}
n2() {
# Run a command on ha-server-2 via SSH as nixos with sudo.
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE2_IP}" sudo "$@" 2>/dev/null
}
wait_for_ssh() {
local ip="$1" label="$2"
if $DRY_RUN; then
logn "[dry-run] Skipping SSH wait for ${label} (${ip})"
return 0
fi
local deadline=$(( $(date +%s) + 300 ))
log "Waiting for SSH on ${label} (${ip}) — up to 5 min..."
while [[ $(date +%s) -lt $deadline ]]; do
if ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=3 \
-o BatchMode=yes "${HA_USER}@${ip}" true 2>/dev/null; then
logn "${label} is up."
return 0
fi
sleep 5
done
err "Timed out waiting for SSH on ${label} (${ip})"
}
# ── Destroy mode ─────────────────────────────────────────────────────────────
if $DESTROY; then
log "Destroying HA cluster VMs (${VMID1}=${NODE1_HOST}, ${VMID2}=${NODE2_HOST}) on ${NODE}"
for vmid in "$VMID1" "$VMID2"; do
STATUS=$(pve "qm status ${vmid} 2>/dev/null" 2>/dev/null || true)
if echo "$STATUS" | grep -q "running"; then
log "Stopping VMID ${vmid}..."
pve "qm stop ${vmid} --skiplock 1"
sleep 5
fi
if $DRY_RUN || pve "qm config ${vmid} >/dev/null 2>&1"; then
log "Deleting VMID ${vmid}..."
run pve "qm destroy ${vmid} --purge 1"
else
logn "VMID ${vmid} not found — already gone."
fi
done
log "Done — cluster VMs destroyed."
exit 0
fi
# ── Phase 1: Storage bridge ───────────────────────────────────────────────────
if ! $SKIP_ENSURE_BRIDGE; then
log "Phase 1: Ensuring storage bridge ${STORAGE_BRIDGE} on ${NODE}"
if pve_check "test -d /sys/class/net/${STORAGE_BRIDGE}" &>/dev/null; then
logn "${STORAGE_BRIDGE} already exists — skipping."
else
logn "Creating isolated internal bridge ${STORAGE_BRIDGE} (no upstream port, ${STORAGE_CIDR})"
BRIDGE_CONF="auto ${STORAGE_BRIDGE}
iface ${STORAGE_BRIDGE} inet manual
bridge-ports none
bridge-stp off
bridge-fd 0"
if $DRY_RUN; then
echo "[dry-run] Would write /etc/network/interfaces.d/${STORAGE_BRIDGE}.conf and ifup it"
else
ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" \
"echo '${BRIDGE_CONF}' | sudo tee /etc/network/interfaces.d/${STORAGE_BRIDGE}.conf > /dev/null && sudo ifup ${STORAGE_BRIDGE}"
logn "${STORAGE_BRIDGE} created and brought up."
fi
fi
fi
# ── Phase 2: Sync host keys ───────────────────────────────────────────────────
if ! $SKIP_SYNC_KEYS; then
log "Phase 2: Syncing SSH host keys for both HA targets"
for target in proxmox-ha-server-1 proxmox-ha-server-2; do
CLAN_DIR="${REPO_ROOT}/vars/per-machine/${target}/openssh"
if [[ -d "$CLAN_DIR" ]]; then
logn "Clan vars for ${target} already exist — skipping."
else
logn "Generating host keys for ${target}..."
run bash "${REPO_ROOT}/scripts/secrets/sync-host-keys.sh" "$target"
fi
done
fi
# ── Phase 2.5: Prepare Proxmox node for building ─────────────────────────────
if ! $SKIP_CREATE_VMS && ! $DRY_RUN; then
CURRENT_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
# Fix /nix ownership if it exists but belongs to a different UID.
# pve1's IPA-enrolled wayne (UID 50002) can't write to a store created by
# another UID — passwordless sudo corrects it once.
# Use direct SSH (no sudo) for the writability check so we test wayne's own
# access, not root's.
local_ssh() { ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" "$*"; }
if local_ssh "test -d /nix" &>/dev/null && ! local_ssh "test -w /nix" &>/dev/null; then
logn "/nix exists but not writable by ${SSH_USER} — fixing ownership with sudo (one-time)..."
local_ssh "sudo chown -R ${SSH_USER} /nix"
logn "Done."
fi
unset -f local_ssh
# Ensure the remote clone is on the correct branch so create-proxmox-resource.sh
# builds from the same commits we're deploying.
REMOTE_REPO="/home/${SSH_USER}/nixos"
if pve_check "test -d ${REMOTE_REPO}/.git" &>/dev/null; then
REMOTE_BRANCH=$(ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" \
"cd ${REMOTE_REPO} && git rev-parse --abbrev-ref HEAD 2>/dev/null")
if [[ "$REMOTE_BRANCH" != "$CURRENT_BRANCH" ]]; then
logn "Remote clone is on '${REMOTE_BRANCH}', switching to '${CURRENT_BRANCH}'..."
ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" \
"cd ${REMOTE_REPO} && git fetch origin && git checkout '${CURRENT_BRANCH}' && git pull --ff-only"
logn "Done."
fi
fi
fi
# ── Phase 3: Create VMs ───────────────────────────────────────────────────────
if ! $SKIP_CREATE_VMS; then
log "Phase 3: Building and creating VMs on ${NODE}"
REBUILD_FLAG=""
$FORCE_REBUILD && REBUILD_FLAG="--force-rebuild"
CREATE="${REPO_ROOT}/scripts/proxmox/create-proxmox-resource.sh"
for spec in "${VMID1}:ha-server-1:proxmox-ha-server-1" "${VMID2}:ha-server-2:proxmox-ha-server-2"; do
IFS=: read -r vmid host_name flake_target <<< "$spec"
log "Creating ${flake_target} (VMID ${vmid}) on ${NODE}..."
run bash "$CREATE" \
--type vm \
--host "$host_name" \
--vmid "$vmid" \
--node "$NODE" \
--storage "$STORAGE" \
--memory "$MEMORY_MB" \
--cores "$CORES" \
${REBUILD_FLAG}
done
fi
# ── Phase 4: Add storage NIC and DRBD disk ────────────────────────────────────
if ! $SKIP_ADD_HARDWARE; then
log "Phase 4: Attaching storage NIC (${STORAGE_BRIDGE}) and DRBD disk (${DRBD_DISK_GB}G) to each VM"
for vmid in "$VMID1" "$VMID2"; do
log " VMID ${vmid}: stopping to add hardware..."
pve "qm stop ${vmid} --skiplock 1 2>/dev/null; sleep 3" || true
logn "Adding net1 (${STORAGE_BRIDGE})..."
pve "qm set ${vmid} --net1 virtio,bridge=${STORAGE_BRIDGE},firewall=0"
logn "Adding scsi1 (${STORAGE}:${DRBD_DISK_GB}G for DRBD)..."
pve "qm set ${vmid} --scsi1 ${STORAGE}:${DRBD_DISK_GB},format=raw"
logn "Starting VMID ${vmid}..."
pve "qm start ${vmid}"
done
fi
# ── Phase 5: Wait for SSH ─────────────────────────────────────────────────────
if ! $SKIP_BOOT_WAIT; then
log "Phase 5: Waiting for both nodes to come up"
wait_for_ssh "$NODE1_IP" "$NODE1_HOST"
wait_for_ssh "$NODE2_IP" "$NODE2_HOST"
logn "Both nodes are SSHable."
# Give systemd a few seconds to settle after activation
sleep 10
fi
# ── Phase 6: Cluster init ─────────────────────────────────────────────────────
if ! $SKIP_CLUSTER_INIT; then
log "Phase 6: Initialising HA cluster"
CLUSTER_INIT="${REPO_ROOT}/scripts/ha/cluster-init.sh"
[[ -x "$CLUSTER_INIT" ]] || chmod +x "$CLUSTER_INIT"
if $DRY_RUN; then
logn "[dry-run] Would generate temp key, authorise on ${NODE2_HOST}, scp cluster-init.sh to ${NODE1_HOST}, and run it as root via sudo"
else
# Generate a temp keypair so cluster-init.sh can SSH node1→node2 as ${HA_USER}.
# Root on node1 has no keys; a temp key bridging node1→node2 nixos solves this.
TEMP_KEY="${REPO_ROOT}/.tmp-cluster-init-key"
TEMP_KEY_PUB="${TEMP_KEY}.pub"
rm -f "$TEMP_KEY" "$TEMP_KEY_PUB"
ssh-keygen -t ed25519 -f "$TEMP_KEY" -N "" -C "cluster-init-temp-$(date +%s)" -q
TEMP_PUBKEY=$(cat "$TEMP_KEY_PUB")
logn "Authorising temp key on ${NODE2_HOST} for ${HA_USER}..."
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "${HA_USER}@${NODE2_IP}" \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '${TEMP_PUBKEY}' >> ~/.ssh/authorized_keys"
logn "Placing temp key on ${NODE1_HOST} for root..."
scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
"$TEMP_KEY" "${HA_USER}@${NODE1_IP}:/tmp/cluster-init-key"
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "${HA_USER}@${NODE1_IP}" \
"sudo mkdir -p /root/.ssh && sudo cp /tmp/cluster-init-key /root/.ssh/cluster-init-key && \
sudo chmod 600 /root/.ssh/cluster-init-key && rm -f /tmp/cluster-init-key"
logn "Uploading cluster-init.sh to ${NODE1_HOST}..."
scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
"$CLUSTER_INIT" "${HA_USER}@${NODE1_IP}:/tmp/cluster-init.sh"
logn "Running cluster-init.sh on ${NODE1_HOST}..."
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "${HA_USER}@${NODE1_IP}" \
"sudo env NODE1=${NODE1_HOST} NODE2=${NODE2_HOST} \
NODE1_IP=${NODE1_IP} NODE2_IP=${NODE2_IP} \
VIP=192.168.2.229 XFS_MOUNT=/srv/ha-data \
ISCSI_IQN=iqn.2026-01.home.sweet:ha-storage \
VMID_NODE1=${VMID1} VMID_NODE2=${VMID2} \
HA_USER=${HA_USER} HA_KEY=/root/.ssh/cluster-init-key \
bash /tmp/cluster-init.sh"
logn "Cleaning up temp key from both nodes..."
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "${HA_USER}@${NODE2_IP}" \
"sed -i '/cluster-init-temp/d' ~/.ssh/authorized_keys" 2>/dev/null || true
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "${HA_USER}@${NODE1_IP}" \
"sudo rm -f /root/.ssh/cluster-init-key" 2>/dev/null || true
rm -f "$TEMP_KEY" "$TEMP_KEY_PUB"
# Encrypt the corosync authkey generated by cluster-init and commit it.
log " Encrypting corosync authkey into secrets/ha-corosync-authkey..."
AUTHKEY_TMP="${REPO_ROOT}/secrets/ha-corosync-authkey.tmp"
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "${HA_USER}@${NODE1_IP}" \
"sudo cat /etc/corosync/authkey" > "$AUTHKEY_TMP"
if [[ ! -s "$AUTHKEY_TMP" ]]; then
err "corosync authkey on node1 is empty — cluster-init may have failed."
fi
mv "$AUTHKEY_TMP" "${REPO_ROOT}/secrets/ha-corosync-authkey"
(cd "${REPO_ROOT}" && nix run nixpkgs#sops -- -e --input-type binary -i secrets/ha-corosync-authkey)
logn "Authkey encrypted. Committing..."
(cd "${REPO_ROOT}" && git add secrets/ha-corosync-authkey && \
git commit -m "secrets(ha): encrypt corosync authkey generated by cluster-init")
logn "Committed."
fi
fi
# ── Phase 7: Acceptance tests ─────────────────────────────────────────────────
if ! $SKIP_TESTS; then
log "Phase 7: Running acceptance tests (T1–T7)"
if $DRY_RUN; then
logn "[dry-run] Would run acceptance-tests.sh against ${NODE1_HOST}/${NODE2_HOST}"
else
NODE1="$NODE1_HOST" NODE2="$NODE2_HOST" \
NODE1_IP="$NODE1_IP" NODE2_IP="$NODE2_IP" \
VIP="192.168.2.229" \
bash "${REPO_ROOT}/scripts/ha/acceptance-tests.sh"
fi
fi
log "Deploy complete."
Executable → Regular
View File
File mode changed.
@@ -81,14 +81,6 @@ if [[ -z "${TARGET}" ]]; then
usage 1 usage 1
fi fi
# Reject FQDNs passed by mistake — the script appends HOME_DOMAIN itself.
# "nixos.sweet.home" → FQDN would become "nixos.sweet.home.sweet.home".
if [[ "${TARGET}" == *"."* ]]; then
echo "Error: <hostname> must be the short name (e.g. 'nixos'), not a FQDN." >&2
echo " The FQDN is derived automatically as ${TARGET}.${HOME_DOMAIN}." >&2
exit 1
fi
FQDN="${TARGET}.${HOME_DOMAIN}" FQDN="${TARGET}.${HOME_DOMAIN}"
KEYTAB_SECRET="${REPO_ROOT}/secrets/${TARGET}.keytab" KEYTAB_SECRET="${REPO_ROOT}/secrets/${TARGET}.keytab"
# Temp path on the domain controller — use a name that won't collide. # Temp path on the domain controller — use a name that won't collide.
+115 -133
View File
@@ -5,229 +5,211 @@ sops:
age: age:
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrTU5DL0k1M1U0U3NMRkZR YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrMGd3ZVNlNXdmOUZMUzdQ
YXkwQlYzS3Z1MHNTRW15akd6NjNzRWhPWGh3Cm5CMmJaMmZMS0UyZW93MGxKUzhu TW1acEs0NFA2Q01rM2dkc1h0NHkzQmhiWFZZCjMyK202VWdlaGhsZW04MnVwUVdO
b0s0MVQ5d0VZOUNEaWpoTU9RaUFtR1EKLS0tIDZMbC9kUWM4ZWIraHQ5N3JZeGFV alA0Q2FETThsYkhSS0hKdHBaS3VaY28KLS0tIG1Gdk8yalREOUtIZTUyY2p1UHlJ
dzNyTW9wL1RwWDFzNlY5dExvSmJaYjgKFdbpy/iTuTa6rt5FzY44UuOI8Ew7mhom eG5iQnJsaTJBY3Y1dkw1c0VEaDQwdDQKfV04fLy32Lp2ZQ2VnvQ0h/Vsf+qdaJiv
I0EP1r+j9cTzyeIMTEs/5jZZlWpdfYBGgtpSdyvMkmiLmqWSYYq6LQ== DnLXGZ9hE5yzpKWkQIRgqYGBkF8PkH0YC4OIaVkA53wrtjqS4ZHR9Q==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLajJ5UlRGbXRoUWlPRFpy YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWFNRY2FiK3VkSm1RdHBn
cGQxazF0VTQyOW9EZXpWRkUzS2ZEQ3Vmc21RCks5T050ZlJTWHFwV3BhcmQrQVR1 cWl1ZStLcTRFZWY5VVI5N0FhODZvR1ltM0FvCnBHZUtTUm9QeHNlbVBoZEx1V3Fa
MHJkUW9qc3NzbDBYTE8vT09jdUpuY1kKLS0tIDJ3elBDZzN4bDNPSVI0SmZwckZ0 Nk9iMmJKVnhocEpERi9leE1ySUtNMFkKLS0tIDRRYkxnbU90S2RyMHdJNzRJNXBi
Z2J5M09PVFloZ1BIM0tSQUxocWlORDAK92ZjF+N3Om1hoAulxYk+PLS9AAf1QCNJ QjRmZFhVakVic2tYODZHcWtJRmNQTDQK7G8eSJInt11P0DiL9uzNQ/ZHHLVNIYPe
Q8scKKrLKoFce35djjdLxUgit9jkFKklFRw/E1nIAcLnIVvLzOv1pA== bvlhuGkEuQ/+j5sVSKOfSI2Y7CvM7TpE3APyKBcLG3ajYg6F/Ev3SA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n recipient: age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVaVpUWTZKN3UrNkN1Tno3 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1S3dOcFdwV2NoMW1oMnY1
NDdhS2VEb0NBYnEvVDZLUVBXbmRWd2RBYTF3ClJKRHhGNUFBclJmWGQ1ZFRSSkln VGdxeXJVR1lsbzNVZHlGb0NGOVo0SndiakVjCmg0QlNnZDV5RlFja2hCbVRXV1VF
d0ErNGxab0JIcGoxbkt3NTN3V3VIZGcKLS0tIHUvNkM4QS9xT3NwZzhrV3pGMUR2 S1ZtbC9KU0U1ZW9zeVoyR3hxNW1XTFUKLS0tIFUzWTJhTzM4QnpWV3h1OFU0N3BK
bTJvNmh4SHR2Z3VGY2tJYkFMVWVVd2cKQSwAv+1WFEugf4E1XGFDBUECKONY5+04 RnF1N2k0S0lIVitoNDJLUmZqdHRzZVkKUfNg24p8zxb3749v/A1BOKCNw75AUKpf
NKdm1L72XHCj5BDgn4+/vIyqkvbHdokTk5P9grZ1w9/VvVF4FegZSA== RUmFCw5DDWF2aNM0mZqcjjVmJ/FRKV2HXwwUGsHPKSOTnKfOUlPNKA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39 recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxL3k2enk5YnF4eDErREs0 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzazl5MUpUNEVNMTdLWXBp
alZLMzdMZ2NLdk9oSkFCUk1OVXB4UmhkV1NJCjhwdzJQckJpQ3kvRGszaFU4dWxM c1hkaVhjcTZXTUNsWS96QVVWb0RVQlZ4VlNvCnE3TFRySU5jTFk2WjBONUQyQUhl
QVREeFdWeWNjOGVPWXZEV0ZySGtuR0EKLS0tIE5PV3pnTGNzUWd6M1NETFZkT0Ry U1lNTDFTRmZhMUFyZmpVY2xpaUVxRW8KLS0tIE9MQ1M2U2ZXQmRCVll2UGRWL1RG
VUVQQzJGUXVEejc0T09LdXlKNG10UVEKuCZVs99ugmPtFx3WFyC5nOXMfbeUblKv K2tnU0NOKzU5dkpiekF0Vk1VM0ZZZDgKK13aFypGAqrKWPOr3UwtXI1EoXf1+UzS
D6TBWGfx5THp8YZKzshKMCUnU+auomfTYxWphTvjYHlO13O5HJtGTw== rBqcwnX6WPxSKUwWoins4Aojek4QhbhY4R5ei6rRS0KEQeryGxy8bg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxSTBVOGtSeGd5UUxKbjBp YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZUpVT1p4TGZyNC9qMm5G
SEhhMk5EaUpZTXF3RzJWc20wQzlOaWRaWGtnCjVFOTVpTGlvcUY0NlEzVUlod2Jv UzNHV08rWURjY2lqS3FTWFdoc0FYYTFjb0NBCk1mZ2JzaXk5RmExNE9xWGZ5K0pv
UXJzVUpHeUcxMThLcE9TWTRNUUZNRDQKLS0tIG04MklGcjJ0RlVmTzIzN3JhVGlh UEtqMkltV0dIWll5eVBUVVRNOUNDWUUKLS0tIGpKNVJudUM1UGNvaGl1UDBOeFA1
eVdDdHc2d1U2WmkrVzJJY0lxeXI2M1UKOBh1bUTieM+8cQ+SHVHXqkeOChkA5flu RjUyRlZ6a0Y4SXNsL21zSURVRk9KTFEKU1L6BQ6ZlYQQtqx3uF/uM5CQ1ercmvRT
WmzhTmtv5vYR4D2KYVbf/E+F2SP+DNCeditO6rcoA3LU9lgA3INh1A== TL3r2/Y07gE7CjRn3pR9z0co8KndGzxV6YR+ubyWptwBS8KQh5stkw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0Y0h5Y0tnTVg0WmhKMFBi YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsaWxxWS9xQXViY3VnUEx2
ZkxFOTNwMFFKdkFzYWp1eWhtNEtLd1VRQjBBCk43WEdhY0VxanZoR2wxeUxoUm1I V09FbTI0WGtNbW0yclhOSGZDbG5NUTNaTkFFCitkcjJ3OE9BSnN4bjFWcE9nYVBk
Z0tPb0hlNGhIbG5jVFR6Wkx1Z0h4WVEKLS0tIDZGNUN3Z3AzL3Z1S0dKOEtmTUdn ZzMyVHlJQ2wwdU5JOXdCQm9oNkhNd2MKLS0tIE9tRzFYS05vSkUwWFRkaTdtc0k0
d0dheWdPOFE4SEZvNGRJWTJxMXNtZFUKb3dxkO9fhJBlj8WWAjBB/lnIn3s+4p0D blVoMWV0QklBVkluT0Z4NHYyS1F0blUKO+Uc0of/V77ZUZOsxTzeH8/LmmAOQt+J
ONEHjNb0PSHOx8VgRvPugZGDZWAnwSgJaWkcSynnsJMjELg3ShREsQ== x/COHxnLCnZ4eWI6q1a0Qn5Br15OJYTxUI2QTV4goTnXBNUDo9wdpQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrdElscmwrSzZCTEJENmpk YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsLzFnZkdVdGQ2dHRwcnRt
Vk4zUXI4Rkp1RU9MSWZGRUtoZXk2NmJsNkJBCmg5ZHFNTitGbW9ubmthWmNFT2Ri Ui8rc1NoQzNKSHVzTk0vYU1vMlRxcjBKZ2hNCmhLYXVGSis4RU9HNGVxZkpvUkd5
anNFUnRDSi9KeGdNdXphVXV6c2UvV0kKLS0tIEEveHJSTFlxN0dPMTRqOWhnNzhB L0xvalhDYTQ4N21OcHRheTlkaUxvTkEKLS0tIFVkRGxtLzhQT0paV2U3ZnNScVdn
d1RqVlRuMmdoaDFlL0pHcTRocHNuQUEKGdFFrKRrgfTCGMUpfbNvhnMLQ2bvV8Y/ alZnaVppeGI3OUVscGpONkk3YTRXd3MK61na8x5qX7+dyMHasDz2dj7yeaUlX8me
NzKji6MW9m4AvQLfp9Iz1N6wNpHHDeW0Kf0dL/z+9fyKhbr9FIXomg== N4/SIk1JDBhv9G7mdKLbKhSF1UJrSY7TJqJqx8/dqEc0uG3vptA1ew==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjMDJYUDFBbTY0QlhwVmcy YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZEtvOUhMU1FRWWpJQjF4
ciswM2k3ZWVHVU4rN2l3a1B3cldhOXhaVVhrCmJRZE5sUTJHeGpLUmR3TU1tb1Qx NWphNXp5M0dLZXhkZndhT1Y3L09maytHazJVCm1MeEtMWXg1Zjg3bFVnZEorci9J
OW5QVGZudVdYNkxCaENFWGpwMU9aNncKLS0tIFRZVkRTdjArTStscm8xS0N2VzhK bkNZQU9Ta1dDTFFHaGFWQVBpK3pYRDQKLS0tIEhPVGliRDR3ZTF2aEl3ZnJEYWtR
VUtlSjY5V1J6NzhPSVBTNTlrS3paNmcK7nYxAhtKrI1o+DUgNZgr+9iXdwcxeipp anh0SEpnVW8xdXNkZEZQSjcxU1BHMFEKVRJUA71fi1QawB2TnuTWMYhzQR18u4M2
VSqcdzwD6VyM/3d0yOS6G+TxQLsSJsyCOfE/rkqtSYfilA2TUCPw5A== s1V4j4TwYyyKZFoNvt8kOUayjC499c5OBUufYs6G2ciC6gK2A9E0EQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYOTMyWFJqRm1mQXowZHVD YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTV2IzSjBEMjUwZjB0dCtj
emlTYWVsMUpRYkNwWHREb081bXpZalFyd3hVCklONWcxVkJLNTcrdERvUzRlY1hX aTdZVlEvYkVRbTNjUC9ZRVdNZVhPQTJEd1ZZClc3NDJiR1BVYkZkdVVxMVZGc0VN
OFpCMGtWQTJ0UndFZzZMdFRoRmJPSGcKLS0tIGFNRjhYLzlhOHdZUXcvOFI1ZlFV dWdSSXBFR2xxR0xrV2thRmUwSS96S0UKLS0tIHRRTXVlUi9UYnFRRlhsU21HZVY4
NTl3V2NzYkZ5SkhITDlrQWx0QTU2YWMKBORbbtYWLsxu8Gq+cHwaZjlZpzsT5rao SDFYd0NwVEtVZXNsWUI1a1ZZU2xNRGMKuQUhOq2FRD+PGn5OkdODZItbxCzRKjne
BqMHZJnBJJMtzqbk9d59y2xrsEt1o/RS273IhGvNTAlfV75ET+s+/w== E60UOYtHjanuGjJ1svuR9cYsLZz7lLOwItklecYaQYpMRZEwzzBGCQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzVUxZUWhoN1ZCeHB0bW4z YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSjJoUVUyd1JqRm1ZQzZx
MEs0SFc0elhHcEsxSFgwMEFMTEN5VDl2OGtzCm9GYmJQaWFDR2hVSWRDd3hGN0tj SFJ2cWNUY0E4b3FndVA5Y0hIOEZnZUlVWlhBCm1vY1luOXZBelRUTmF5Y1NMeDBn
OHpUTXAvb1dQMXFYRjhHbjl4QW1EQWcKLS0tIFNTSHJleGMwM0RPY1pjc05nejAw cE1BTDErc041UjJCWTBQbnk0Wk80dkEKLS0tIDVSZzd1UktvZGdyanFUMkVORUtl
aWMzUzhvSGNxV2xyay8zQ0RsNno5OGcK+ERfAhPpghG5fWuuTgFp8bQSewQVVz3n eVB5TnJkMlp6dUpXSTlxRlplZ2NxUlEK0AYOxIbswjM0SUASDfmZ7PqcEU844fgI
nArfD+RMxwUUJKBKX3pfxfxi0K6rXYom1wYnsVUNQmLQ6IikTo9HQQ== ycFWVSEPodwUZ6UFoYXhHlJzHFcgpLvwUd1PMktLHe1qrZ7GOQJIMA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNNDl4SC9WbmVFRGMvN09G YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPakhQcE92ZU0zYmk2QS9D
OXRuLy9tbnErZ21DbFkydDRPQTZRMWlqRldvCnMrVEJqVUZRdkVQY1puY0ZZRjlJ TG0xc1JPZXZCZ0tZOXA3MGNLVnBlZGVtRFRzCmIvMHhQKzFVWCtpMTQrQUhGVGJp
eHNFRldVUHAzSDBOV3RDemprYmwvNWcKLS0tIGZoKzJ6VFBLbThKNURmdG9uNUFS RU1jbldYckw3TXI2SlNpZVBIZHRsWWcKLS0tIGtJTUtJejFxem5jajFQUDFTQWU1
bCtoYjY0aFVmTHRMaEhSUmFhS3dDVU0KxVmYLzH1rMj7+ZTq12x69YvJ9xU3jGqC VnlxYmVlNG04ay9ETi9FRmVYQXVoRkUK9oFNolI7jRjo9RUs1g4ghrx7aYV4U/ce
uW+jyDIKuhCGnR6mMzmDeshap1YM7QQCSMT2uJJ3H13RoKWwRG569A== ZTc2tFh57+7aKgrDi+2W3jwhfkjvBsThk//p5mLlqEEgw2lwlnhvPA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmQk1GWVVkN0Fmc05sR0dB YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoVnh6dFAwTkY4cHJpaEs4
OVRsTm9Lc3g2THlXRzVIWGpITjVCbTV2QjFjCkgxampRRUdJRzYwZTVLL29WMnhh QnZPeXZHK0tXYWZPNytmYXVsdGRWQVI3RlhRClFVb2I5OVZzZFNrRXFaa0JTUkRJ
SkwyaG5Na1N4V0N6Vml3SjQyWUlyV2sKLS0tIEh2S3cxbG1Ca2lYLzRlQlBFWlg3 OC9GQ1V5K0JhWlhkUjU1WStCa1lPV1kKLS0tIEhzdnBBZkRnK0NtV1FuTkVsNlgv
T2NDVEJhbU1paFRBZVFZM1IxdWRZRTAKSkB2TP2SbjD1MEV9m3BuYLc75AoqsgOy QzVEcEVkQm5NL0Z5dUU1U0ZFaTJITnMKaWE9vlrOpQstr6FGP5ObdilsCYk4kYAj
kRxoAVyA3FwcCJDRl3oeoPFXYFvNrEgLXn/ZIo21GRl8XZ6/QIRDrg== /phboR+Ym7QDTyUF9LZXJCU54YJp6vEWkRnlJFqC75UW/v/lgBhBMQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPbmFwUE1KK0tqcFhvendN YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkUHAzWk1KblJxVVZZV3FM
NFUvTmMrSEtNWHZZSStHQ3loU1J5aWh1VFFnCi92TTkvTG8vU2RZRmxFK1pLY2R0 WndzRGRtbkw2azRVWnBuZmhKWElqRFk0RENzCk5DMHlMVWpwbXEwVUhkaFZUbkp2
VnZMbS8vR3FQUTJta1JlTnYxRlcvcmsKLS0tIFdlV2dabmdZajZnK1plOVFFSVVX QXNlZFV4SjBEdmR6UEw0N1JOUnhNKzAKLS0tIHo5RkNDUk1ESWRHQmV6bzkvSTlP
MDNxc25sWVFyUkovenFMZ2JrT1RWelUK/93Jr3zI0AtvwBBj3fPl7wykqmxjrAUS dk1GQ0Y3V0dTRlByb2xUOERVOTVwbVEKY4sAHyAhvGSYJzPuufWUIQD2xZcSt/nX
1v8A38F8K8MUtisN5TfvS8HnrU3iru16jGRwuikpiVaV6JdDyQK/Yg== t2ZFXu891/QdEzyUXCIzdwAV+Y/LjvroIlCp5Hkbrk0s7N+ghqsB1A==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7 recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBicVRQNEVKaTQ5OWZyamVY YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBYVkwYXpYSjdmM1FpbDl3
cHk2QThjQWlnZWZ5ZFJUSWtoeG1PVXNTUzFnClU3emFXRGU3d0NSQ1pGbm5ObHVv QWpycXoyL1AyOEpZUmtpbjl3MFAwTkJoOWpVCmVDd0FBUWxaQmZCU2VmNkZGMk9o
NDI4dVRrdER1WVVnd0diZGswRHk5RHMKLS0tIE80VWI4L3pHeEorSVBhRGlNWnFq TUdLNGtac2N4REg2eVF1eVh0WnNaTE0KLS0tIDJvcFErSjRiWmhPMmpadjROOHdt
Q1o2Q0ZmZnd6SUl3VTNzbzlVUG1YOHMKxdxzI6VrpfhsW1Sn8vSpXMEWLfnGR+1c NXp6Y1JpdHFlSlRoa3JTaEt3emdnalUKjoFfZAiKMPF3noX+K0+vc3+p/XUHnhic
/T2cy7OtRWzNCCcE6odX0ImBYpS+JntWm6wgYdhRDcoyNWrcPYtwWQ== k888KdUwcZYl2/dAIc8UDSggbMnncJAJgoezoCHLkj97GNNAD7E+gQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBscFo3NDhGSW8wRkl1Zk1W YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqYnQyYzF5cktZNG5PcThy
V0NUTTNqc0dFb3I5RXRKclBWODJUeUFJR0RVCjYvbzhOUVMvRjZDUSsvVTR5ZmZG SEszVEwrUkQ1VVRsM3pSTlRQaHVLN0VuSHhrCmN1Z3pwNlFsbDN2UGI0KzYyallM
bDBtN0lTWG1SWUEzU3BMSFVFb01Sb2sKLS0tIG5OVUFneWhjOEhGTzA3MUJnWUgy SHJ5eklQeEIxSlhiYW5PUlpJcG5KNjQKLS0tIEk0QkpMdlBlRjVYMmJaMzJUbDNm
Mnc4UWVVVi9Tc3ZISVZqVHVYNnNXZ0UKGkMiNP3AaKruNDbcsYgPVORbg41qvDa3 K25pZldwd3JoZi9vdURoa3Myb2RQNG8K6N6bO2YKooPfpKihgsYqilfz/yAYCLZD
WAaEL9Euih3doTJrDM6196T7oDSWV3EuKB49+ZPp6Ph98UegU3DHtg== XJ/THgT4URX2VNvSspvBtN8luOiJUVcchp5WtL2m9jARL5txEcDorA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3Qjl2Zk1BNGVLcS82MndM YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvY0VpeW1nazVvRWJFaGNU
aTYyZGsyazlENDZhdVdXd1pERWgwVVRGbkQ4CkwxZFBPSE1QQkkvQlJHODUvMnk0 RGVzVjRmWWV4dnJzRHdsaW9ENVZYck5lWGtVClNtd3ppelowRjZpRFFSMC9EK09n
RktMYzYxU0NzOVUrS0t0cHptelY1T3cKLS0tIG1Wbll6L2d0WitqWGx6T29nR2tT b2hqNnkzejdrTnhYNGNKblpteDNLRWcKLS0tIE02bWVjazRWNEVKbURITGlQODlR
NkZUSUR4MURQaUFTNHpWaHVua3BYSGcKQwFM36H7WUvRUCtqA/bE6JgMvcUqEptd cTJJVnBVdGIrdzBoSXExelNrVk1XcEUKc77o2EX7PCm/HjUo5GsUiQdm488WB2mg
uNHV+zrlMPKPKtgbxIqOsrrW2cLYd8MHoHgswhShC9ewJruqckngTQ== wHd/qDbQhF1W75RrVTuIKgtEtrRjZqpmr8toe+aHJizPofcrToUfzw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2 recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuZExYTHhUdGVjMFoxQSsw YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSMmZnd1pXRWh5NDVSN2xq
NFVSSWhZeXlQVE96emFCT241Ykh4TGF3c1VZClQ5NVdML1N4RWVVOE10dVF3bU9I MVhTN3N1OFpkcUh0a0tjYzJnWUlINGNVd3k0CmZkTnBac0l1dllxazdLY2l0Rzli
ZXRlRFN3bDBCcERwN0ZRalMvTytYMk0KLS0tIDVvVzQya2kwRWoraWVMbFRHQURy bE9sNTBVSkJNaWF1T3c0WktoOHl0NU0KLS0tIHEyeWZTUjdQeUN6U2t0d3JwNTBX
cUZrczVueVRjOHM3MlNNOXZsYkltQjQKMApTF14qLyNABOQ4t/dPtw9ZPEYEgTma ZE1Za0tXb0gwc1FSakVYdU9OTHkyd28KkwmlzSYP8XofB0VGag+S18+S2TyQjLrM
UIjKqCqAu9v0w2VdhCOYtMRzYuxngV+VdnIHg/lNXNgZsCGmrMavBQ== qaXtbBtLzJGNDhe9FhAKTPFcjTLWbohlG69vxcImyCyCns+QQ+gvug==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2 recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPU2ozS3I2WjUrNnkrWEtV YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2Q2RMWmZVOEwrOC9VcUxp
cVBhcHQyVmQ1NTg0dWhzNmNaUmVxdTM3NzFjCld1TGROck0yYTZhOGVmWUdtQURm VVE4R2NqYlZWZlhKSTBHRks4bDNoaUliMWlJCm5FbWdZS05GY0VLc0sxY2x1U21V
WXdHRXZBbGtwSXdwMDZaSTNOajJFKzQKLS0tIEpadWVkLzRiOURBY3cvU3Q5Y3lM eExwK29GVVBqYlRPZ0l5RWVXRFhRNlEKLS0tIDl6dVZJQndwVStFVEJnRHRyMW1W
L2l1blBPd1MxV3JBdE1vQTJVOTNkaUEKYdXzJ0tDMUhoqtxNN7U4hYnO+Fl7LXHC NnFqc1F0SGJqT0xmREpaN21EdnlJK3MKRPE5rfFpVnH5wAOkuB5pNMlMd3omcpku
kKVsWb9T9bG1UHfhQAJldneuPUQUxsBqiGj1bxKBB4s21jrYGeSH0Q== do2hFZwyI7t80jxF4+g3J7EolOx8AGjpc9Ba7Gj6IMDjye728q5N+g==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJYXRyZGYxaXNuUTR5c0dU YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhSG94VE96TVlTNkEyclFE
OWlEcHdLTmtuL0tkdERUNDJTM2FNTWxYQlMwCjdrWVlOZXZMUzFlL1g4clNHeVBr SmdCSkJpeWlVWDFIMDhwUEkxL1RUTDJ5UG5FCjhKRDB6VGtwTVozdUVzbUxGL3BW
d2szT0twRWdLTGVyelVPamNoTnJNeFkKLS0tIFZiYmRpQTFRaW5sWDkrTnZDdXNJ SnR3cmpSN2RxNnl4QmNvT2lkYmtoVFkKLS0tIHd2V2h2Wk5xOXlISzhjVzBsVkhz
ZVY0V3BuTGU3clgrSDl4Q25iVFNpNjQKFo5BYhxd/jEytclFPkPy8D1eNZVJxrsK VVpRenVnSVpHUWJqV0JHNXNWWXJOdW8Kv7PJSTDbwFOAcl7pynALaJiTXU/87bSF
EY0DeWDAdc2BGtu4zTW9kLemIifHC8qmsFrCq/8ej04ZJEw/lEDaNQ== F3HQllYOwOoibGzBCe18H2N+VxyNxoQL9OWe0TvOIR6bgHFIIF0/Dg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzakVuc1IwUlZWbTBKREJM YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsMEZPUExRVHFFb3pSVHNO
WjJZbXVleDFWM1hJeWs1SzRpZGxLd2dmSERVCmVrb1B5NjcvZkNyazVNcnB5czhh cGJFN1ZzTDFVNEdneVpMZ253ekFJNjVtYkNnCnRGQjU2Q3dsRGRFV25LQ3pCbTJE
SlA4dFpGU0FQMFhqb1pkVmtiNE1kU0kKLS0tIEI0cXFoWU95cmlYdDhjdFBlOVZG OTROaFBiT01xb200S1pUK0NYaTQ3R2sKLS0tIEdiQlZTbi9Vcm0zc2t6bHplZktF
MEF0SnpDVFFqUzhDekVNZGtnUXFveUUKI0YKTPWPi9o6FJvgVlU+BJ4qXleS2USF ekRySENXcjBuR2psdHZSSUJrR0xUdjgKvBsmnC+cbq5TUDFjXCyImIoPKvh8wsjE
mVCFNvSn1NgvpKrXID8H7B3++Y4jNOxDqDp79u7vwK+Lf3FqBujgLg== 7Shk7Act8Jayrhx0lXBDRmfpHRrB4L16rDSmqO0DTE48VhT3TiFyug==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68 recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkRFFmME8yckhzNU9kRnl3 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxNDlya3RmYzNhU2p6RkVw
VGdabWgyalpNbzJmMW5HV0NHM2orc1l2dXhzCkwxYjNHMHVyb3F6Mk8vRkd0N2dJ cHJVWWY0Yi93cW1uZi9FWkVONmtpRWh5SzFzCmtWYTRIY3BkTWU5R2Jsa0ZJK3kz
TW5seFpORmFVazBZL3lycjZGUlNyR1UKLS0tIFVEck5TZ3g0eVJWaHF4NThkQlVq SnZvZ3YwaGtoMVZ3V2laTlBBK3UyTmMKLS0tIFdpZWtqeGlacjlLbmFySHlSUUlj
TnRvMTFhY1RVVjFscktSd3FvM0Q5c3cKV82WfobJzuBk3XhfB9pqobf2kV1Z5wBc RmRqQWVHK0FUT3VDbFhLbXQ5WDhLeEEKcDkgV34lUFJRIHRoLB8F2IOvGAM93sM+
XCf7kK3cxVzty8uPCYItl/q6i8xUMnOH7vQoUD43sxTdtwxbKUxm8w== AkmaM4+WRcGeYWQKMG2x6cYCUKFaT1lDXuWZ9kI8Fd7b9gTSnQMs6w==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWOGhTMFJYNzlyRHYyb3pX YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjeFM4ajhHeVd4bzlqZmJ1
emtCRm5BQ0tHckhRVGJUWVRsdXdRUHRqdUVvClB0T2pyS1pyM3RYZHN6UkFTeHBh MUxYMUpZVjdjR0NGTzVteVB6WFBQeFI0ZkZrCk55TEt0Zjdwbk51RnhYclNzam1H
UXV4RVp6VGtOZldVemttWjc3L0RaaWsKLS0tIHJWVVA3ODgvb0xjVmhFajlSWWJk cExKZXQxWFVLa1pDNFpkcGZzcnl6a0kKLS0tIFJ4ZEdJc3JVaEc5RU1aZk1uYm1l
MERJSGZITTJLeTJrNGxTMnAzbEVxOVUKvHUDTWMPU9cwHupPDW9IkrXGEZYw3531 d3RHS3hHSkRKRXFnN21FQmh0TlNtNmcKdc2G/1dhTJen6iT9kUWZM5OzCmDVprgx
GvQvsb9Sjwr81PsCyuZK1koMBfjYO70uU4INgt+4ncF0nH1k12+fDw== WN1Bl3JzYhLsNKn794887bVAICVqbXqkdpEZztNIS5n/Rw6geKsNvQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
- enc: | - enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBK1ZIaEJUNGxYcGdqQU5j YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYa0gxNnBwNjNNTTgxKzgv
ZndQWG10NU9wZXgyeDcwbitIcmxHYlZXemxZCncxR05MZ1owMXNkRHFKZXRaQXRK YkQwTjZxb25tQ053Ny9tSW4wNGVYYnlIaFZNCkVvOFNPOFkwQzhYZGxjb0FOZzJ5
cUNKemc4R3pKVkozQXppeUdXaGdIMHMKLS0tIEViSUVoQUx6QmZsUk4rUk9BaGJ3 ZjNXam1ZTWZrS1M4cGhhcHZaT1NjblkKLS0tIEdvL1dDTHpWcWF0S3ZqNkxrQW52
L2dTTGo2OFhWYTNyS29MeTJ4RmEvbVEKkBusuOJo+vME4ZNVzgF8j1f0hUhFIE4P VlYwa29sZVloOS9qajJWQWFzY2FKRmsKy074SLdttogXsWycaFX8xso4ek7Cbjph
TNfZV1na59QukVUetSGGPx4YGlwgYVoUZ/QMlJgkh6cGkZnOYONZFA== MMEhZd/svmnSiYM81nmeaze7qXEUcsZXuSmZCYATTBEGtx/Srll8aA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0 recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1ZW5BUFpFMU1mRjJqN1JW
M2YxYzBmWnoxeXJWeElqYTNveG55N283MzF3CnNsNTY4NnJmVE5SUDExS0M1SS9D
am1KbHA2QVFkZU1tTkIwUG1OZGNod2cKLS0tIGVUeGhOVG1aYVhqS0pSdFBEMUIv
RTRXdjdjQS85UDE4Yi85dS96ZGVpdTAKMggs/SpFcZFRp/M4XnT9Twhk+T5ZsgXK
J4qqhDPOlPOHpuntmRMBOtAROlcrAukE2m3dPzbBtLqLAARDUOi/Iw==
-----END AGE ENCRYPTED FILE-----
recipient: age1vwk8r03u46rpvmw5fy20t8ud9qdhm0nx5se5ayze98y55e7zg5yqc8862n
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvNVZnZXVYYm4xaUhaMnE0
MGhTNk10TTNmV3k4bEgwdE4vWFFtaUNFZTM0Cm8veVh3Qyt5RXAzOGR5aHdvQmpV
YkQycjVkRFRoc2NlY2JhajFUaC9HTGMKLS0tIGJJRVdmNC9FNG1DZDJJWGUxK0pP
dWtzWkJNQlN2U0VoR3FqakhkZVlTTVEKjTXT9pyoljo0Z1K+ufFdm7UEnCf/8V6q
RX2BqzHGvPNnpGjolg71dIsRf9553bS9XuaynQ6GkNmjH9ZXUWAVfg==
-----END AGE ENCRYPTED FILE-----
recipient: age152vrhznh92hspusx9rqxt9c70u8w3uvlys4ymc7zuv3qnxjm5c7qmp72xp
lastmodified: "2026-07-23T21:15:41Z" lastmodified: "2026-07-23T21:15:41Z"
mac: ENC[AES256_GCM,data:qFhnPra6IE3wyKQ4WKweON0S0YtD5I0adGZVfA0m6BVilN6bX5oC/1j5NK2oHrsz920hSl0SOF8LrpqOrUyGjSRkPsN4kq8qr9bJcrX4URiktP0oRden5LLt6hf+ZRP7WmRXFqixPkPHJnZIoAvkNnTFce7cDq5NEAHkKUEKG7k=,iv:nyblUDGeu3TUfFivYylOn3C/HITj99qiPI2+mh8AGh4=,tag:FrtRzSCylC4wlIoqZdfx7w==,type:str] mac: ENC[AES256_GCM,data:qFhnPra6IE3wyKQ4WKweON0S0YtD5I0adGZVfA0m6BVilN6bX5oC/1j5NK2oHrsz920hSl0SOF8LrpqOrUyGjSRkPsN4kq8qr9bJcrX4URiktP0oRden5LLt6hf+ZRP7WmRXFqixPkPHJnZIoAvkNnTFce7cDq5NEAHkKUEKG7k=,iv:nyblUDGeu3TUfFivYylOn3C/HITj99qiPI2+mh8AGh4=,tag:FrtRzSCylC4wlIoqZdfx7w==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
-26
View File
@@ -1,26 +0,0 @@
{
"data": "ENC[AES256_GCM,data://9IEHIVCfHjyMNao5sCu2zNlZ/CaW+JyxVpGpD/vab2qvURunCUY7eMfSOyvOx/2WPXnWWlkoVJPCR1ec/yUg09EaSMfxrvqlu4UJI3Sxvu9xNuDszMwMMD/sCulJDiMWFNLp8qaYt7UGzexp4+GlGiqWDxk3ZEu/iLmSApzBrpciTF0lfehT4qblDovo9QXG2KDWFhCt2SwEKmHJ61Yl3pVAQnPLyTWaNhwWD/mYL76mIiDVKq7DJlvi9MBxzi3aYh/ttuHgRCvnCL0C9oUvOyT2cljwra0LXuOrum7FhPIXXboA7WTEbTHJm1LB5yLfxDrnWCrGxQzFQAwNixVHIcjuvjjvA/LifTvbj5FYM8x7an+DwXPfPhruFrDew1paAWsEGNNYXSmAlju2QLI/OwLHFFLuDyLnBKQ6RLtZbCEAOUKf2h4iZj7nH86eb/aGU=,iv:rj76+MJBCpiYyx2Ogut5UxJj3Gn+bygvu2mtuY5hFLA=,tag:yQmTvWVu8ZLug/7fo6RnwA==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqcURBS2tIMGl1aWxLMHFQ\nS3ZGVTE0cnYzTmpnQ0daL1F3QnlpemRCMmtjClJiZzIrc2syN29sRXVoaXlPRUNF\nakw4RndmbEduTFg5ZVRKUTVwRWpGRjgKLS0tIHZ5ZFlyODlGWTJoNGpXR3RhY0ZH\nOGpPdDZQVmt6UWZXbHkxQTBoeW1JencKbsfH1V1lUj8mmHyLNj36VaRDgaBojcDU\ndoQWmSEXxjticJqdadbVKb3UABpvzAZxASCy81sa3wH0gT+7zLaNyQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrbFdaMmRjcHgyV3Y0eWZL\neG1uMEtpQVUxQmNTU0Z3aC95Zi9uQlgvMEhRCmJiVlRTR2NocGlnbVU1UmlLVVA2\neERiMXpiQlVPNVhRU09XVGxrY0Ixa2sKLS0tIFRuOHdoelJxOXNRRHBJNnlhSE9j\nUUZHZmQzOVFwRmhFV3pvdnpRMTMraGcKRHBuSUpbHaEzH2tuSBE5MsLJDCuH3vUx\nO0jnDldCWkCw7Wvr/tQAkaDI8axZcYVDUkCEk+xqAdxDozLCPhEO6g==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZMW5taXh2QkJkT0JjaVpq\nQlBjVFpFYUhXTHhOT3prbFcvUFB0YnRGUzMwCjhHT01lY3dPWDE2dDZWZnJza1hN\nS1AvZWIvdjZoYmRjWlliK1hiOEdrT00KLS0tIDljWWY0NlQveS9VR2hOSUNyTUtH\nK2gvbEVibmZSdktPemdEQ2p5U0Y4d2MKKitoTi2vbxJ41IoWMlj4vO91Ahpj0hHP\nrKCPyx7ws/IUMmKGyvDLpZ3pYqHD9jl5pLfB05Hh4Emhv4lA1qtwwQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3K2dVV1BHbDk2Z3FhNmdV\nZEN2Um94K0pma1k4b1V5ZUJxQ1hWU1grZjJNCi9iRVVqOVpEMmtBUi80NThlYldY\nTUtZOVErT2VSWk43NndpVzBlL1lQaTgKLS0tIEpCM3Q4NjM3N1lpUE56N04rTXN0\nZkZ6TkV1TU9OV3dpc25RNWRpVnprM00KItUzKBdShakOfX8Sr+k906nsvYPl8QLb\nge//1GA+ukGsaS9rcChOY89vFdm61JDmj1jXSJ0CN4wLMW9/eblZRw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c"
}
],
"lastmodified": "2026-07-28T01:44:28Z",
"mac": "ENC[AES256_GCM,data:efFXIqbauOURR7lrVpK7kqRIPgYjgWfenBYoX7mUrQ/thFc+ApcAx58Fi1zg4biwIs7NarJAgDqAxZi+yKb2Sll8H/wlsEjWDU4iQlLJdIQw7wey9eDW8pgBLd+6E7FXrgn1Vh49dS5GXlC6clAYk1lCiB4NvfzPDy5Ktpl+Chs=,iv:+zCqj5I1MLJfRRiIrqgocYB49PZnlV45PUTH4gYlfrQ=,tag:WxY1sF4kFOTEzbSFcfJFbQ==,type:str]",
"version": "3.13.2"
}
}
+1 -23
View File
@@ -1,23 +1 @@
{ STUB: run cluster-init.sh to generate, then: sops -e --input-type binary /etc/corosync/authkey > secrets/ha-corosync-authkey
"data": "ENC[AES256_GCM,data:hGnZibXsfTgXvl115clWkWduiQQRpA5KhUs+kxO6U03LhE+Yeq0hLRmtICnnMC502YOfprGUC8wSZ6YOJ/7ZIp86Ob+LxahpsN1k/+rJc3/l6dQEZhIr2yyIPOXkSS89OWh4wVdvPOWLwNtg1A7RwIGFBMQZ47Gh15XtADvotI8hAj0VBuN+41y2DwhDXzP+iRZExDIeuOLaezlv/vxENVgweEII9QAoW7LntZjeAv0t2cduYx3X3ASbcFmPIuVilFHZS1nG9djmmZs129hlnYCzDYqhq+Hl8JsvYDV8XWJnWhYEo9OrssBD6Wq8Lx+F7j6NeBHpxg7UjepWkDSzdQ==,iv:3c9qJNIay6x17qrUW39sqLQW3eHn/S3X51CwikNV4rs=,tag:KIL0e55SJVoPHd2B1DklxQ==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4NHpHVEVzWWEvdEpRcUFu\nN1RxWDFmRCtnZzByZXE1SzVGeTl0TTRDdmswCkVKakFVUmp1VWFpU0lmVEVWTThW\nZVBIaTF0Tjg2VXJLbXZJL082Y2lRU1kKLS0tIG9pOWppVU1IK0RBS1d5N2RON1Fz\nb2JRZGhtOXBtb0pWNWZjc2xZbXY5Q2cKwod8V72Fyc8glDtxJ/7sDsChm8Nfu/2t\n/DpkajHkF+VfIGPYTDNcR4UfA0oE6WJGF7GIJrvokgT4/smomPiyZQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBieXRmSDM1U2tQMEQ3OXUy\nQ016OGRRWGptekZ0SW85bVpYYmpyN2JkejNFCjBka0NwREQ3Yy9IRDJZUmczRkVy\nSjMxck1MZzVHUUpQcUYzNkFPRk1jL0EKLS0tICszSlBEbFFzRXNYM2p1bzhDUkVN\nNWptWU5VYWh4OGdObHpWQ3J4K1k2Zk0Kmnb3y8ZnjTiN+wt79DrxCRYpVjyLttnt\nShqKnhTeoieVs0lEwtprjOv4ZYj8kYfAIh+FLrualJaFeiha3I5nsQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1vwk8r03u46rpvmw5fy20t8ud9qdhm0nx5se5ayze98y55e7zg5yqc8862n"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlMm9lY2lkRy9wNFRVSXhw\nMkF5RzZZS1FhWC9kS2RUT2dqUGRxRHFJSERvCjBBSGxBRVpwa3J6ZUlmRDFqOGM0\nYWNBNlFqc2JWZ0JPWmpncHdHNnR1NkkKLS0tIGkrYVlDcUZvNmdsQUc2VndzdlEw\nSzJPaXpNQk1HN2hNbkZEbzcxQ050TUkKSn/llmRaJkdf/5mQfYkycnrPhNKZCgon\n12K9UM87mAnsf8vFR1MzHSRIYuY+CVLHjzu4DfiILNQa9c/JGlVwfg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age152vrhznh92hspusx9rqxt9c70u8w3uvlys4ymc7zuv3qnxjm5c7qmp72xp"
}
],
"lastmodified": "2026-07-28T11:19:22Z",
"mac": "ENC[AES256_GCM,data:2grZR+vFcdTQnVn4PASqgxqi2kHues26IjeCYvVt3ykFPOnAuc2lGnzCzm6AzcrSPeNBTNBGmsSw5FqHO2JWC9KIE6Y9PjzOX87PeSvQqVWfbwbz0c4PZ7myoUkqTF5cv7wvum1JwDrL2p0Sw21cGa0km3y//ovz7OJOsK6CFo8=,iv:BIgrCaJEZCkcFB3xmNVF71FdqfDDP9txLPHgFi5SIy4=,tag:m5dkyNagf47cLq0XEcyS8w==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.13.3"
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"data": "ENC[AES256_GCM,data:4Plrw4SFb7Qx7ofyHwXkHHiCaY+54QT+XLnqoMullnxoTEwf4aZGmjZKx+J5eNnXO/+aPcheg50pe6vXggIgA02a736l3xkIU8mmRdqDSXwROwy7DWBfpcsT5bcL5umGpJ3tnJUAoA8TWVvxoj14J2k=,iv:IwoLgVeWJ+1E1yjeVLjMGEcEDNK0MD1caaP0hcbYJhs=,tag:KPT4mFN2MitDcWAggBV7Mg==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2bHd2Sm05Y28zb0llbko4\naHdmZ0hkN1E0OWxSNktndEdGeExlZURMNzFVCmhXdHA1U1RKbS90TXFlRkZLaXFs\nRDArZnYrY0JyYmFVVVBxQTNqK0FDTW8KLS0tIGt5Z2JFV1B1S0VqOFp2OGFkakpN\naU9nN2REcUdHYm1YdU9Bb2lRMmNtT0kK65IAwgg/kezOeWRmPn/35+O/nA5NJ/ua\nj3p5WqtJKb7mRemqu4j8QP1KnVqaIeIZga7Zr1MBoBgzqEMC7LwwEg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGM0ZaQUh4VDVpME8xcnE0\nMWZtME9CdkdFelJvRWxDNFpLdHpsU0pBbEZnCnA2ZzlHbFlXTjk1UkhOSnlna1Fv\nUzRmdDFJVjdBUzNCdno0RDZueGdzTTAKLS0tIG9EN1k2Z1lFeUpZWmh0N1UyOTE1\nS0NZRGlFTm0raHlETHpwL053M2pUN3MKG0ELDHporuL32Ly4GVg9xS66R26sdwq6\nCLRB+kCIF44R+9xZqa43RDDIj8GHVaE7Ded1Bdhi2wgXCDZOulvKsw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1vwk8r03u46rpvmw5fy20t8ud9qdhm0nx5se5ayze98y55e7zg5yqc8862n"
}
],
"lastmodified": "2026-07-28T06:44:56Z",
"mac": "ENC[AES256_GCM,data:N95kOsHFH2nKVhr79EB/zILMpJ0rgh+QZ1weYbQJgvxRYjLMl+AK/ALjtx7Mrj9ufOnIWnndrUpD7X2cod2oPrlAPT3c8P0IY8LaJTNDx6TUljKHFe9Nvm/JcnI9i5skioY0X8sRC1oosL0loVTmVb9yaf5xsf+wursIsj7pI3A=,iv:kQLx4FLeiKZIevOOOPS3NAu2NUUhnRw/jpws3FZ6zcM=,tag:rCp0h+wujci4N5VFuzCuLA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.13.3"
}
}
+6 -25
View File
@@ -1,25 +1,6 @@
beszel-token: ENC[AES256_GCM,data:tT2a1I6AhVgXWfc=,iv:1iXoOMAakHXpI2lYiipxcVSzXODUdpqYmfrpNKZwljg=,tag:Vuxxz7pyIsZYbkg2x6GvgQ==,type:str] # STUB — not yet encrypted with sops.
sops: # Bootstrap:
age: # bash scripts/secrets/sync-host-keys.sh proxmox-ha-server-1
- enc: | # sops updatekeys secrets/common.yaml (allows ha-server-1 to decrypt shared secrets)
-----BEGIN AGE ENCRYPTED FILE----- # sops secrets/ha-server-1.yaml (create with: beszel-token)
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3dFdQZUlBTmM2SlB4Zm8r beszel-token: REPLACE
VUd2ZXlScFJPK1gxK1pIbytIZ2FneTAzRzJzCkU0TGd3RlJ3TElEalUxRFdIR2V1
b2ZYNWR3QXo1RnN5UHh0cGtmNDlEZ3MKLS0tIGV4aVRpeGVsQ0JIejk1NGZYcXBD
cmtnL2hjOWZLaE1YR0pkcTVQdUExa1EKuoNgg+LTS3f2bK6K03WKHFx3L6WqaCaU
jBA5qUPbJD3/SnH7PXkJKgUDLUuTIwtUDq2O23+iPT2uenHywK3qDA==
-----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFWStZeE1KRmZ3UGZDNzY2
elZyZnlOekVYek14MkprN0t1d0hlSnhSVG4wCkNHd1h0eDI3U3VLc2xxdFpZbWc3
bzdBZ1pxODJTcnYrZlRQWkQ5RmVRZ0EKLS0tIDlpYWxJWkxBQ21VTThxVVpYYnlo
NjFYUkxTZXkvNWFIRXUwTGxZeEMrM1EKWkd/mRAHaYxls+0wUN5oao41taJQLxjr
YQGCMN8otYZNS99tbxQnMxLjqAm4dLVvJ6FCpqLjYL5kzXpNmNjb4g==
-----END AGE ENCRYPTED FILE-----
recipient: age1vwk8r03u46rpvmw5fy20t8ud9qdhm0nx5se5ayze98y55e7zg5yqc8862n
lastmodified: "2026-07-28T06:44:32Z"
mac: ENC[AES256_GCM,data:rtDzbONqaNpbEKo1btGiHArAJVCVsStHhgvM8MTgkRMUNGU8p9W4tiuQLmobDpFh81kj/mDnkx4cCpScuiMhQojz9H74l7P9o9b1Wxy0fSiCtGoQhaq6JoIVJoKtEZoHfMStdIio5EBW9DBB5abvjPLTaC0afgHo5pNn3A9qr6g=,iv:SqtJhel/5qW0V7IV6V8dCRWqPnsK6QCzbC15/BKeq1k=,tag:vF7RJJccm0q7BSyNGYnplQ==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3
-19
View File
@@ -1,19 +0,0 @@
{
"data": "ENC[AES256_GCM,data:Gi6HP0EuWQcTESaFOEq7wgAvuvlyevuFIKAnShDXJIUBg7xNNlGJ3PmBfx+YyFvChCqOqThAS8TxStogMVQqyWxm0vtCoVshkHG+9Q9xHV72AiuTcmJdsZ0DiSf9jwJYLZ80x1VI9a++Mqg0yLs5few=,iv:H9YYUQ4nOhQTBaonA/H7F7KroMRyU7kGul/bL/AnQ2Y=,tag:7ESjzSEH+XTqvvtxpinKJg==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBcnFYTnpZeG9YeXhHZFdR\nQUROMmw3YkIzY0VTQkdoM2dvYW1iVjF0ekRRCllwWTQya0NIUHhDbmtRMVVEcTZL\ndXlGTkg2UzJFSGErQVBpSFBkRXQvWm8KLS0tIFpHSktETDB5Y0wvM0RSKzhCcmQr\naVJTZ2g3S2xXU1cyMld2Z25tSVg1WjAKIPuzFoSR3L66NMWw7WFW3f6lJNtTtyLB\nm/cWyp9p0swmIovd5OAsoKFF1ZJq+edmLnADqHGvGNgkUGnv1oaXHQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPZC92ZitOeXJ3R2crRmI1\nb2cvT0MyZDc0NWl4MVpnb0U1R3k3MDcvYmlNCkZJUjJUWXFoWm1USDlycGh4ci9t\nV0hhZk5LQkRQbUQyU2tCU1d6TldXUUUKLS0tIFRldUl5eFhYK1YzbVJ6WlJNb2V6\nbzBOSVk2MlE1RkpJazkzUGlKV0U5a2cK5QETF7KW8mrtMDXiLcrT4enjcAd3QXaO\nRPaAcUd5U3ZbNmDGdxvmTABj0KQb/qZI1z8hiGrsR//m6lMEge9h8g==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age152vrhznh92hspusx9rqxt9c70u8w3uvlys4ymc7zuv3qnxjm5c7qmp72xp"
}
],
"lastmodified": "2026-07-28T06:45:59Z",
"mac": "ENC[AES256_GCM,data:13BJaaM1Zrcg8/SZToXdlEM8J4GtFZnkoVt6vBWoeLU58wsGi58jdEAqMzWpnT3Vf2Sb8OjNUB1cbBVv9uMitbffFR27y06diKkAICnVc4shS0czoO/6u9O0k9gXicU04jUr/FKLL+sSiNi2ypCFSZT1C3WY44sTB15RNYQR7XQ=,iv:/4owaf5HYndcojBy5rZv6LZp/ipe3UuXy34dq3XI4Qc=,tag:i3Qk8jdl5hUmLScLCirm9w==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.13.3"
}
}
+6 -25
View File
@@ -1,25 +1,6 @@
beszel-token: ENC[AES256_GCM,data:0wooMaPiytDTGug=,iv:/WATBCEakrIvujt4oAeprw18W1n/WoK2zjpQdlVJl4Y=,tag:lhJa9hJgFqNFSB1Ogj5CtQ==,type:str] # STUB — not yet encrypted with sops.
sops: # Bootstrap:
age: # bash scripts/secrets/sync-host-keys.sh proxmox-ha-server-2
- enc: | # sops updatekeys secrets/common.yaml (allows ha-server-2 to decrypt shared secrets)
-----BEGIN AGE ENCRYPTED FILE----- # sops secrets/ha-server-2.yaml (create with: beszel-token)
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4UlllaTNtUElsOTFxaTl6 beszel-token: REPLACE
c21tN1A3ZGYxRHoyT2pmRFk0MUhKK2FYUVZjCmJGV3JEbHFqZFk4QnZxZktaWmNn
R0JPbkNGZGRsYSt2UTNMM3cvY3hocGcKLS0tIE9JSVc0ZUxaQ2MyQ3pIaGF2YlhN
Q3ZSTEpHTG9rZWtEYk00TWxJRlViYUEKKvvVG6Je3mbDSX1ibelZAZwh2tAhyfMW
tAwXP8c1WLm3ZzO0wGWwH1pYrjU44EkXo45AiqqqhRNtQsGqmW7Eag==
-----END AGE ENCRYPTED FILE-----
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPR05EZ1lkTnlXMGIyOGdk
UVBmQTZhRmljVUE5OXFMOFFvS0J1UVU4RTJZClJWaEZBRFJqQm5iR2hrQlloaUVV
RGprN0d0OWdMVXpTUld1aTdOSFgzRm8KLS0tIDFqZUZPUHFPaEowOFJPc0dtZnU2
cmFGSlVqRitKOEM5eFIrSVlQZE9oQjQKyLwPsrvyi/Ipyb95EE6fTLyn1aTjcU7X
jtAecZUjl6xE/8kREZH7IPfFqNpiHmIDUWiB3wsD5WMTZHYbgatlLA==
-----END AGE ENCRYPTED FILE-----
recipient: age152vrhznh92hspusx9rqxt9c70u8w3uvlys4ymc7zuv3qnxjm5c7qmp72xp
lastmodified: "2026-07-28T06:45:50Z"
mac: ENC[AES256_GCM,data:f50JYTzMJiEjB9+fNIs734CXrkN1IRgJKcoJ0GHoI5p75cfLfPxuzUifty9kQ/4DbM7LbWa5w+9occoV/yNOa86BKpPtU0LmlzXf904/SgOCUKhyHqhyO5mp8w71GoyLxx3ld8CbqfBu8t5l4gwbQamftsmQRxnUdU/Oa/CzdZQ=,iv:EewXBjeihw89h8ypH5u/G4YfoKZzkHqRWgSSIXU3/a0=,tag:lEWMvAkebMexDGGpE+wqkg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3
-30
View File
@@ -1,30 +0,0 @@
{
"data": "ENC[AES256_GCM,data:apiijrtrqd77CTizITg0R35BfCi8PBnufpxIyC+hLYqwoBzP//3z/yjFyHPLG98m/c/qywoi3Kn+zsaTT7MjP++9OMhhX94YKlSHV1/cHB76OkwsNc+ClqWxl6vpaFX29Qvh3gFX9c/NR3xvYQutYwrIrQ9NR+t/M52IMC8hvtR1LQy0ak3VIuXJlSnG2r4kF2Ym1iP7phjuq39Gd245Axzw8OB7yGvOjNxSdTPxW/qL0fMlzNcMrjr9hw15WlqnZfWPOsB1+gZjHXpGfPD5BCbAAMoTRJd75vhKKXP/ERhIffewuuH2x/QHfSFvXVB3QyhBQMxd2b8QEEE5cjvcExOST3tkj6QARkzoUpRT7AE3jhl3XZ0uA2qu9SwyrSvbr0tBRKxCdK0g2E2/hqwcK/Tck5GB1eKb4aN+UkqxOblNDH+B1RfDoyNAuN+KEg==,iv:0p+ScrKpP4kQvO52gBAlwAis6oAzZ0EHFnU74hYPrn4=,tag:ON7qOjztF52xsJWAou7ogg==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlYXB3cVAzb2xEZ2pGa1RJ\nbS9ZVTc4Ums5eUZJUjEvd1g1aGVyNUNramowCnptZXFOZVB3MFRFcUtzSXBEZk1B\neEtKcDdLS0h0b1h3VjRjRXRvV3V5V3MKLS0tIHBDemkyUnV6ZXhTeE5VOVVOMlky\nWWMzVGVzZlAxMjZYUGpQUCs5QmxiYkkKcuBshCgWX4TwfVlQ5lHikzvwWdLEXWD1\n/uSiy0J6yMSiu8u6cg2SxeFrlKJ3j47dDlT6WHCxS0PfeEA0bJb3LA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGa2VTc2NWdkFRNUJxelVR\nWFk4RWxoelYzNHo1UFVhU2ZkLzEySlRWN2xNCmNmcmJod2crL3NMRlVsSmpmVkU2\nMjlXMktjc3piUVNhUXlTdnVGTWJkUTQKLS0tIGQrMUxrNDlNTkRCSUtFWkxRdXgw\nRlV4ZmtYSGhPQU84eWtiQXVqTmxUK3cKk5fn72UZPH68t5ZappfAhZJwzpLkfKmT\ny9TbUPIr4Pbrexau6YiH43QIbDQFdwYPfkBjGkd57zCg8AVo1+MBRw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArNWkwQXFWT1RpaDNvbzYy\nQWc1aHhmNHFEUXVsQjZqb0EzM0wrV0dwN1hnCjFsUFJiT3REK05uSGRWTEw2SFE4\nY1FleE1XVjhBbndiMmZxTWNTYmhYeVEKLS0tIEJiZzJvS3BsYzB3cHIxa2k5N1Ro\nenFFZDVaODNnVGdBZTBOYWJwRjQzc1kKlXJgee8wTSN4Beq4P0t9cYbk0BWHCseQ\nyaWpiPT9aZBEGLFmuEd3zKABc8lrilX/ySTmOG49vRg6CPmr7cT0Wg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEa0JYenFMNmNzWnVmcXdz\ndG90ZUZ0WWlIU0FCZG9OaWpBM3ZDWnFhZFhNCjhuV1FTOTJ2WVJGa2RuNVV2MjR0\naVNXa3diaWxWUlJtdkNOQXZ2R2NsQkUKLS0tIDcyYXh3N3B2QmNiK3dzemFFMGV1\nNTZpTk5yNGV5YVo3cGswK0NLWFQxQlEKIe0N5OxooWXzt1cUViBmjihmGEe3G6/f\nkz2/IscnG78ZvNgYKjdoG1jlsyje/3zI4C8aWXLq2DnIyxUyAhPgsQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2b0NVVm9QNm9waUUzcXBi\nWnZWU0JETjZRZmprZVRUL1h6ZHB4cXkrdm5rCm9GZ0VnTXB3S1BYSmlGWFJVcDhJ\naUl3RjR0ak9BRmQvVk1GRnQxNmtYM00KLS0tIFlTU1p2OHhWUGlOWngwbE56NEhF\nRW5QSkVVUWZpdDZXWEIxZ1BkbzVwclEK2P25nBgf8255vaKW/+T97aNTecRgNjLu\nedIUiPdXbFATCe3v/YRo6sqzFwIsvM6Bl9yHh/SXo6Ftc7eWZZd8zQ==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs"
}
],
"lastmodified": "2026-07-28T01:44:24Z",
"mac": "ENC[AES256_GCM,data:J0D8bEs5mHLraLS6TvYuCgfiNU1xKM2Yfb5Y0f/q/4wM4LzXufNzv3+SWDHumTe328U8UnNXLqjNHEKL0bZi0coxpU5hVM+BvPcmqD72vscETzbQ2hnU05sfW+XjfZhcN8/ke0bpLt7nP0crD5hsZv3esV1E2UWvzjEiYtWzFHY=,iv:lcmXYG2H469UKBYDndWKMO+GP0mSGLztenm+kBaUdYI=,tag:ujiXbLM9CUsuoFwQQWI84Q==,type:str]",
"version": "3.13.2"
}
}
-26
View File
@@ -1,26 +0,0 @@
{
"data": "ENC[AES256_GCM,data:aqlMnoVkGtH9z3fJRweeC0OYf7LGqJU2sWA9Q25dKK6NuNyJd4BvjPtpfeg/WhVtJsaOtcbwVm4WAhVK9FARE8g8j+vmq0f6BAU4s6mx0ZIhl+mP+/hIpt//LOdd+9YezelJxdpzUyZbdAngU99rsTluLRe2XmZ7Fquxd8yH/OHenSDY6dizp9+5jfEi8EU+EmuXvuWMPY59xnlnqYNPfSFxs43/pS402LzJoJ5H+cBPprddkUBVzy4cBQvMnrRFUSjnqp74ovZkfIWFqDWQ5YgSU2PjatBg18oulZ7wNRhQ6OLqj6gsu+xrMjNFwnp7rMlA3X//hIidxTkVcYITycXd8KzuMIaofUpnwoyT34fy6+H35/39iiEyG4LRTrOOKRDzXkY2rhJUxFSZ8GlhNhMd0RlkmLngVYrtjsswJ9meIwoAFLPYt7BC61PJf0TXdtk=,iv:mCA819J9LpAOj8QxFAkrHI9wFJIy8qVxv31D6IwWFnk=,tag:qqMtM0eyHbQEyl6ND/wf7g==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOZVpRYmNMQ1E4ZlFXMDVq\nbW1FcnYzWTBHQmdhSjlsZUtZK3dzNUZjY1FvCkV4VXU4MTcwNVZoZUpVVS9JeHE5\nbGk3UzE3YmpZQ1JISHV0RHJqSG9ZNXcKLS0tIHhBdnIvMjJkeWlVRE9Fb0FhWjNX\nUjBVWDltK2w0akkwOTJaTGNSWWNrRXMKqZRNnHiXvn1QBoSGdABp7vOqNlsEN6Xr\nDp3NByXow6PuRuWvQXHzd+WC+ADkwNaaiT6TUrbZcd/Pl8Ges9kcZg==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2VFBxc2ZMUFp0YkJhVFhW\nc0ZNbnJUcGtkT1VLR2YrVGhRRFVTZFlyZXlJCmVnblNBNGxIdHh4Q0IweWR1b0dq\nRWFyOURuWkdkRE1RTnJRMEpXdk9HaUUKLS0tIFhKV3FoWW1zRk1pMlhuWWlhV2E5\nQzJTRHAyc0JtSjd2NHlJODZVbndaVDgKFA4565X/4FqNq/fZDZTg81/55hZi4c7b\nTti2AnyE3OcY/kurXJFHRinVMqURQf1fx9MxqUYRitiCz4qe5zFF+A==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwRWlKTFNocnBRN01pZ0hm\na3pyRUkxMWM3dE1iTkZZcGRYUFcyb1RDb2h3Ckt4blpGZHBjcnZ4SVE5UnF4cmY4\nWW0rZG5wUkZINVk5a2lmSzN2L3JJY1kKLS0tIExpWGlBRFZJVGpGbFhxRE5ZYjBo\nQVliTlFIZ1U5dE9xbDhHMUtxenpBSm8KY6sIFEfK8p+70IXsC4Jwb9Lm/pd9+V6K\n4JAzGrpA6mAuIwwSNnbdcA5j8FmBhCpK6nLBWmFhGm9Y+MRTaM7Jrw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoV0pIUXlyOFFTUHVTdllu\neG10VENOYWl5Wk0ycnpLUytTbnlrQ25pMkFRCnhyRU5xWW9vQXJJOHdFMGZqMkR1\nSktqT3lOdVVPRUN4YTlveGp0NXpqd0kKLS0tIGRaTktra0ZtSVpzSHBrY3VSSUph\nRVRZOSsxTTNmMmltMlJnVy9oT2VEWU0Krxf49B1BsrWn05fqg+cZ0k0PtfJJNfn0\nUL44RUWXWbK2igQHaIct9DfYe7DEonBJeROuxDYm8g7yNOv15S+P4Q==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp"
}
],
"lastmodified": "2026-07-28T01:44:26Z",
"mac": "ENC[AES256_GCM,data:eyOSn03dzHSgkshPzLVwc95382eEFaDQarHs9l83dtcsb1Ui9CjkKipl2DVSUb6bdMUH1qKYmXqJhwFnAZbFZFjT4VTKtutNtM+OkhVXfT+fJs+1+u7k+ZUYFL9HuxKA6AWpwX3eJ8vmJDJZaAayJbm4PRzOyJywvKeneQqdUS0=,iv:QRKltR4qzVofo/Elt2Us/lrHlD7BenX605X31x+Ng78=,tag:SZ97CcfgMJeu3yqNk8Y/cA==,type:str]",
"version": "3.13.2"
}
}
-18
View File
@@ -1,18 +0,0 @@
{
"data": "ENC[AES256_GCM,data:QvViw/s1N7eIN3CoD27llEjriuSrohOou4Cv310nigcW8xMnA2SDN2id3H3AoKii1JlJ+qWpKn+gUmt5HOM0UlbeNe3os2RVwiX38O7eN9xFH9F5kA3TFs6Umqq3EoI586PwIVmB2LyxDnTeEEXVd7v5PFkBcfu7u8YIcNF7lpcj+6rOyHMB8uxPhrGep3yiKawFd9c9wWD0hlSSatV5tMHA1qmdK8VmDbCU/iuGwIoMzN1eZwGAXzG6LkCA63bUfdxU6yGuTboD+kN2Wbo+GZB0EACmiZoofl2wqlXuiw6qwTvlXkyauc9O5EG//PUkIECzDwiXcX+qSOM9DIBlZNth4ebhtic/PskyF09etL/gICz5YvV4ph5lyrWHq41KxljSU7QXOkGhzagruuMrYzhZb35wFH4Tie1ee2DXbGhreJr8V3Zse/zTMaD+iM57V8bvNcarTzOFXyKfp7Y=,iv:XBPhj2wT0k/yRCRHU4d+BQA/k00ZHWSKOucnZ5+PGys=,tag:Nh5MOxZIUXAYVZY5SZh/JA==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0aUtETXlBMmRlTC9vK3NF\nRW4zS1FrSmo1dDN0OGN4SmV5ODd5MTRVejFJCnpPQkM2MVJ5WFlYRW1NVjduV1hv\nUWUzY0hHV05LV1BROVZVUlM2NldsTlEKLS0tIHFsTzI2SVZzYUtJWTM0MmFiUlVQ\neHEvUXgzc1pxSU1OZFo0cXhSZDdGUVEKpLVfzQEnntluUGsblnkHZJ9Jezu8tFte\nxEoV96GVHxUca6TFWpTLMqdR7NtuQGCkx295W3i1tkp58DP1OzRTUA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
},
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZW5ybVBvTkxibWJMWGRL\nZ1lnWEkxZ3l6d0FWbmxMY2VsTVlzOHJJM3owClNYd3BlQi9pL1lIdzJmekJUVEVi\ncWhSK3ZkZEMyeTNoZlpTT0NMUXZFWVEKLS0tIHVkVlhmQXdRQktTK3J5dXZ4aTNt\ncUw3WCt5dXJhTkdUbVpmeWoxWkoxNnMKj4XtdwmgFVOiVsIJs2Du7QJ09A9tv/Lf\nkFOq8y4tlZe0nCwRjq43sVz7hdCTdQ0rsaWjBGY90LLkJbOA+f+Wrw==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2"
}
],
"lastmodified": "2026-07-28T01:44:30Z",
"mac": "ENC[AES256_GCM,data:zR2WLWX7NaHA15gi4kX0jDvzUIe9jtz5bMCAggbPW+IXOEedPrddAHZ8OfPErVMfx8O1pJKkAKSzoPTAbEle54FisSLMHXp8fI0297MByJrF9pOsMFpVcDy/L4Q+pBzmB7aS9r7+u7KRVVTZT3QwG1rFWZaDs5dFTP80RhtCbWQ=,iv:ZzPoO+h7ebS+jsSH7tWMx6QK8umpa2/HFQmx9dnJN+Y=,tag:vG9+ifxw4HaBE6YsmOwXcg==,type:str]",
"version": "3.13.2"
}
}
+1 -12
View File
@@ -16,7 +16,7 @@
serverIp = "192.168.2.226"; # server (NFS/ZFS) Proxmox VM LAN IP serverIp = "192.168.2.226"; # server (NFS/ZFS) Proxmox VM LAN IP
dockerIp = "192.168.2.225"; # docker Proxmox VM LAN IP dockerIp = "192.168.2.225"; # docker Proxmox VM LAN IP
pbsIp = "192.168.2.244"; # Proxmox Backup Server LAN IP (not NixOS-managed) pbsIp = "192.168.2.244"; # Proxmox Backup Server LAN IP (not NixOS-managed)
domainControllerIp = "192.168.2.253"; # FreeIPA domain controller — authoritative DNS for sweet.home (not NixOS-managed) domainControllerIp = "192.168.2.253"; # FreeIPA domain controller / primary DNS (not NixOS-managed)
ipaServer = "domain-controller.sweet.home"; # FreeIPA server hostname (used by security.ipa and Kerberos; must be a resolvable FQDN, not an IP) ipaServer = "domain-controller.sweet.home"; # FreeIPA server hostname (used by security.ipa and Kerberos; must be a resolvable FQDN, not an IP)
# Cross-host references (LAN hostnames/users other hosts reach over the network) # Cross-host references (LAN hostnames/users other hosts reach over the network)
@@ -80,17 +80,6 @@
# one-line change. # one-line change.
primaryUser = "nixos"; primaryUser = "nixos";
# Primary IPA/domain user. Home Manager is configured for this user on every
# IPA-enrolled host (see modules/ipa/client.nix) to manage the environment
# that IPA itself doesn't cover: dotfiles, user packages, session variables.
ipaUser = "wayne";
# GID of the IPA "docker-access" group (GID 50010 on the IPA server).
# The local "docker" group is pinned to this GID on every host that runs
# Docker so that IPA group membership alone grants docker socket access -
# no per-host users.groups.docker.members entry for the IPA user needed.
dockerAccessGid = 50010;
# HA file server cluster # HA file server cluster
# LAN IPs (vmbr0 / ens18) — client-facing: iSCSI initiators, NFS, management. # LAN IPs (vmbr0 / ens18) — client-facing: iSCSI initiators, NFS, management.
# Storage IPs (vmbr1 / ens19) — isolated internal bridge, used for DRBD # Storage IPs (vmbr1 / ens19) — isolated internal bridge, used for DRBD
@@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICvIM/0C5wFd6RNgdcWA7uLkhu4gNsi/98UBDZ4Mg45H proxmox-ha-server-1
@@ -1,14 +0,0 @@
{
"data": "ENC[AES256_GCM,data:EhYMNPzlMDyby6g3hfvBKisKJnU+YAxZjf326tPdflQ2PcmPTzZ0GjHqJOPrOWyLU6IyvnsLMBV0JF5/UUi2c/H3p2wvoBgsxDMaaMW0YUK2IZFo+PZpWoYJSMrx40sH90W0qL0VBw17JLwPU6CjeUZrn5+sCN9EhQvncD8NoJ6QwZpWKhWLn9/7mxd/PF1uxjEQXzHYFeuUuVpQdKSUfV/BSJIT3pMM/VdH2203jTMtwUbK1/7UtGKtDWdfFbkEvJ6qsV7hnN3EYU16WtdyAiHEZvvZ3gqg6YK2DemojWG7z7VQkQGbGm40yA6/mYGW2dVK5ULzjxALl05G6lo1vzxODbMHhzsfJZVw6ocGx3r8xwaasiX8S3loq8WNUWJ9QgjbxuEX9uGZja6A3nPhcwVGGZExbUoU+syXUk2m+WfIQ0epplaPDyAxrYO5ZPWLdMYoPRxbDx94rx5fMxA3jst++IEL2kQMTieGQYJy3R8veQIe3Jb074zY+Y79EmRxfo8ruR6ZMqORtbZzJOBcd0OObjUVLUqB4rnC,iv:XkCfOKtmZNz+UTZdElLm8L0PpymkDzCM24VkAa9Y/Qo=,tag:hmr9ik0V7mxEKR0DcDB3/w==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqQ3J3VDVwUVBvUjVXSWNz\nU3YrRXZlT2NLRThCNzd3YWNHVTlHeS9xMHpzCi9HVW5kMXZrdjE4bXdML3NhRnI1\nL0NQQkNQUmg2M0NBYzh2cG1vdHA0clkKLS0tIDJZYUwzK0RGNzJWckJJekI0SFg1\nUXE3N05xWThtdjdCZkdJTFJ2YUx6cm8KVANudVL54WBNc9DK9s9h4WQRLMewUqgN\ntu4LdMOmi5oV3LX06lbxhBq79dmsV5uos/qszhJkVGhEZwD1RGBoZA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
}
],
"lastmodified": "2026-07-28T06:42:29Z",
"mac": "ENC[AES256_GCM,data:Xn3/CWhQJHWtd3QHoqpdwLuJdZTp+oxx43k38j4jspQSBPdVDSG6Ifi7Sb2r1g9YTxpjUto8mv7FH+BqI0wSYUkLfjaxI51xs3dq6e6kl5NWOsvWwIOGmBuhfIwusHGgsfBguxk0J6Ev8Irw674CRFc37jozXTVdpOOzFhPTvFs=,iv:/7OFE9NYkw2kYEetzeZRXah0KXuD5OXmfjQMIeqI75Q=,tag:VamuInZqYGYRwlyU8TtL9g==,type:str]",
"version": "3.13.3"
}
}
@@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE4SX5hJyjWgOisxfNChg2o2aXx9Q4GewsJGqieGUYsX proxmox-ha-server-2
@@ -1,14 +0,0 @@
{
"data": "ENC[AES256_GCM,data:GEebcQrPPrQxLC6mWXLLtytGkIy6Q2FbcQ/Z+ImGa3gahI/nigdNyR/avIz1DAUsOibpoMVlrk0GQUBMuxiEXulIwUEzeTh7sMI3XTZiugujOFHPyJoFeynlgeRNBbl2EVVjGmEmOsR/cZJGyKaet9aNTHTPDJUhodY5d+D/sE3SgpHoch1DI02/DRnqlnlOYiBoujk++BW71EDHLVyz3n9NsurTDYDK4L0Ch5cMgdlvwIuxGuPKtkhU9z6PtGxPbNlYlNP8U+3jG4XWEEY5hZHBGTVHn+bd5FViGY+sMiigO6yaOmddbUqzJoblKeI6F/rfrL3kXETLgVP7uVm3gw9KPEl5JaTdVqkG4hQ9G9/LvRlyEh619mCp9IRXxabm5SQ4NWFzah1ZwqIKp6GoAtqeWRgZNVqijoF+bnb0YmNFmjwpSrSMZvKsrZb5geyGmqah+NJii0xqVebnufB5p8lW9eytBSNgC/XLjj0olwHMw7CWGmnY6Q7lxF1tNYAD/K3dkbxQYFM3kBnmAdweC8WYc+hDjtk0HJul,iv:H4BonLmf3VoW2S9+IUE+SzPb0qiWHuNuGpVIUgUYScs=,tag:5+bq7vC81DMmOk4jn3u8lg==,type:str]",
"sops": {
"age": [
{
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrQitsaDlqTmlYM2RvR1Ew\nb3NzZTJzR3F2bjM5NHhsa3dCUzZ4TW1pa3pBCkxIeEJtbWdaUFMwMkFyRHpCZ1Bl\nbmM5dGV3b3BDcDEwVlJ4UWV5RFNvRkUKLS0tIFRDd0U3dHBHb0Y3WDhNV3hkd2Ew\nclRDVEZHYjZUSzNVZkFjWGt3SGVQTlUKOULuXiYD9k2uVUmhuC15Kgezrd69rc9P\n7SocPa8kBliffP9IuxxW2S/hPbK8rqEx/sh/Km85ZIi4pG0AUQJ3fA==\n-----END AGE ENCRYPTED FILE-----\n",
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
}
],
"lastmodified": "2026-07-28T06:45:10Z",
"mac": "ENC[AES256_GCM,data:ZnsqhgELqEdNzlTKJx2+xzcZRMNGTme/0a+PVvpbUv5IApimtif0zkTEC1LJsyLPut3MmO3QGprVlb8ihoRdm/IVvLaQonc36f6vN5cxrpQadru/RWEjLbnge1E8VkN/PVvHqRfLyIOPSq6wtGjAPaC/iHW1scBi50pWr0XO628=,iv:FlqAL1It6JuvoBhZotHWdSz17nckBf2rXnwovl6ZJBA=,tag:tNwcaL1sJakXtgejsB/Sbg==,type:str]",
"version": "3.13.3"
}
}