diff --git a/.sops.yaml b/.sops.yaml index c62ba58..ed53050 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -22,6 +22,8 @@ keys: - &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz - &proxmox-server age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp - &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0 + - &proxmox-ha-server-1 age1nxlnrevqs2msdatze562vz6ym4pgydt2zndye83lwqauy6flggtqrevyw5 + - &proxmox-ha-server-2 age1scfc8p53q5aq2a87tcmsazmj8sfeft0s8kxg0et4nm3ucxyp3c3s6te82y creation_rules: # Shared across every currently-deployed host: root/nixos password hash, @@ -54,6 +56,8 @@ creation_rules: - *proxmox-pxe-boot - *proxmox-server - *proxmox-tailscale-router + - *proxmox-ha-server-1 + - *proxmox-ha-server-2 - path_regex: secrets/nix-cache\.yaml$ key_groups: @@ -93,12 +97,14 @@ creation_rules: key_groups: - age: - *admin + - *proxmox-ha-server-1 # proxmox-ha-server-1 added by sync-host-keys.sh - path_regex: secrets/ha-server-2\.yaml$ key_groups: - age: - *admin + - *proxmox-ha-server-2 # proxmox-ha-server-2 added by sync-host-keys.sh # Shared HA cluster corosync authkey (binary sops file). @@ -108,6 +114,8 @@ creation_rules: key_groups: - age: - *admin + - *proxmox-ha-server-1 + - *proxmox-ha-server-2 # proxmox-ha-server-1 added by sync-host-keys.sh # proxmox-ha-server-2 added by sync-host-keys.sh @@ -195,3 +203,21 @@ creation_rules: - *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 diff --git a/modules/build-types/ha-server.nix b/modules/build-types/ha-server.nix index c4490a2..163b4ec 100644 --- a/modules/build-types/ha-server.nix +++ b/modules/build-types/ha-server.nix @@ -14,7 +14,7 @@ # 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 # new agents, following the pattern in hosts/server/host.nix. -{ lib, vars, ... }: +{ lib, pkgs, vars, ... }: let # Generates /etc/exports lines for all nfsShares data entries. Shared @@ -33,6 +33,10 @@ in ../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 = { enable = true; exports = mkNfsExports vars.haStorageRoot; diff --git a/modules/ha/cluster-config.nix b/modules/ha/cluster-config.nix index 5b55776..2cce76a 100644 --- a/modules/ha/cluster-config.nix +++ b/modules/ha/cluster-config.nix @@ -11,14 +11,32 @@ # Both host keys must be registered via sync-host-keys.sh first so both nodes can decrypt it. # # DRBD fencing: -# Production setting is resource-only: DRBD waits for the STONITH fence -# agent to confirm the peer is dead before promoting to Primary. This -# requires a working fence_pve_ssh STONITH resource in Pacemaker -# (see scripts/ha/cluster-enable-stonith.sh). On a fresh cluster with -# no fence device yet, temporarily change to dont-care and run -# cluster-enable-stonith.sh once the fence key is deployed. +# resource-only with crm-fence-peer.sh: DRBD calls the Pacemaker-aware +# crm-fence-peer.sh handler before promoting. The handler checks the CIB +# to confirm the peer's DRBD resource is stopped and returns 7 (successfully +# fenced), allowing safe promotion without requiring power-fencing (STONITH). +# The unfence handler crm-unfence-peer.sh clears the outdate flag when the +# peer reconnects. This is the correct setting for Pacemaker+DRBD clusters +# 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, ... }: { + # 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 = { enable = true; config = '' @@ -37,6 +55,10 @@ disk { 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 { diff --git a/modules/ha/iscsi-target.nix b/modules/ha/iscsi-target.nix index c39ec88..2e40e25 100644 --- a/modules/ha/iscsi-target.nix +++ b/modules/ha/iscsi-target.nix @@ -25,7 +25,7 @@ let python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]); - targetctl = "${pkgs.targetcli-fb}/bin/targetctl"; + targetctl = "${python3}/bin/targetctl"; targetctlStop = pkgs.writeScript "targetctl-stop" '' #!${python3}/bin/python3 diff --git a/scripts/ha/acceptance-tests.sh b/scripts/ha/acceptance-tests.sh index ab3081c..b7d8fe9 100644 --- a/scripts/ha/acceptance-tests.sh +++ b/scripts/ha/acceptance-tests.sh @@ -7,49 +7,72 @@ set -euo pipefail # ── Configuration ───────────────────────────────────────────────────────── -NODE1="ha-server-1" -NODE2="ha-server-2" -NODE1_IP="192.168.2.200" # vars.haServer1Ip -NODE2_IP="192.168.2.201" # vars.haServer2Ip -VIP="192.168.2.202" # vars.haServerVip -XFS_MOUNT="/srv/ha-data" # vars.haStorageRoot -ISCSI_IQN="iqn.2026-01.home.sweet:ha-storage" # vars.haIscsiIqn +# All values override-able via environment variables; defaults match variables.nix. +NODE1="${NODE1:-ha-server-1}" +NODE2="${NODE2:-ha-server-2}" +NODE1_IP="${NODE1_IP:-192.168.2.228}" # vars.haServer1Ip +NODE2_IP="${NODE2_IP:-192.168.2.227}" # vars.haServer2Ip +VIP="${VIP:-192.168.2.229}" # vars.haServerVip +XFS_MOUNT="${XFS_MOUNT:-/srv/ha-data}" # vars.haStorageRoot +ISCSI_IQN="${ISCSI_IQN:-iqn.2026-01.home.sweet:ha-storage}" # vars.haIscsiIqn # ────────────────────────────────────────────────────────────────────────── PASS=0 FAIL=0 RESULTS=() -pass() { echo " PASS: $1"; ((PASS++)); RESULTS+=("PASS $1"); } -fail() { echo " FAIL: $1"; ((FAIL++)); RESULTS+=("FAIL $1"); } +# Use PASS=$((PASS+1)) instead of ((PASS++)) — the latter evaluates to 0 when +# PASS=0, which triggers set -e and kills the script after the very first PASS. +pass() { echo " PASS: $1"; PASS=$((PASS+1)); RESULTS+=("PASS $1"); } +fail() { echo " FAIL: $1"; FAIL=$((FAIL+1)); RESULTS+=("FAIL $1"); } -n1() { ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "root@${NODE1_IP}" "$@" 2>/dev/null; } -n2() { ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "root@${NODE2_IP}" "$@" 2>/dev/null; } +HA_USER="nixos" +n1() { ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE1_IP}" sudo "$@" 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 " HA Cluster Acceptance Tests — $(date '+%Y-%m-%d %H:%M:%S')" 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 ────────────────────────────────────── echo "" echo "[T1] Corosync quorum" -if n1 "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then +if na "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then pass "cluster has quorum" else fail "cluster does not have quorum — check corosync on both nodes" fi -# ── T2: DRBD Primary on node1, Secondary on node2 ──────────────────────── +# ── T2: DRBD Primary on Active node, Secondary on Standby ──────────────── echo "" echo "[T2] DRBD roles" -DRBD_ROLE=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown") +DRBD_ROLE=$(na "drbdadm role ha-data" 2>/dev/null || echo "unknown") if [[ "$DRBD_ROLE" == "Primary/Secondary" || "$DRBD_ROLE" == "Primary" ]]; then - pass "DRBD Primary on $NODE1 ($DRBD_ROLE)" + pass "DRBD Primary on $ACTIVE_NODE ($DRBD_ROLE)" else - fail "unexpected DRBD role on $NODE1: $DRBD_ROLE (expected Primary/Secondary)" + fail "unexpected DRBD role on $ACTIVE_NODE: $DRBD_ROLE (expected Primary/Secondary)" fi -DRBD_DSTATE=$(n1 "drbdadm dstate ha-data" 2>/dev/null || echo "unknown") +DRBD_DSTATE=$(na "drbdadm dstate ha-data" 2>/dev/null || echo "unknown") if echo "$DRBD_DSTATE" | grep -q "UpToDate"; then pass "DRBD disk state UpToDate ($DRBD_DSTATE)" else @@ -59,44 +82,45 @@ fi # ── T3: XFS mounted at haStorageRoot on the Active node ────────────────── echo "" echo "[T3] XFS mount" -if n1 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then - pass "XFS mounted at ${XFS_MOUNT} on $NODE1" +if na "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then + pass "XFS mounted at ${XFS_MOUNT} on $ACTIVE_NODE" else - fail "XFS not mounted at ${XFS_MOUNT} on $NODE1" + fail "XFS not mounted at ${XFS_MOUNT} on $ACTIVE_NODE" fi -if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then - fail "XFS unexpectedly mounted on $NODE2 (should only be on Active node)" +if ns "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then + fail "XFS unexpectedly mounted on $STANDBY_NODE (should only be on Active node)" else - pass "XFS not mounted on $NODE2 (correct — Secondary)" + pass "XFS not mounted on $STANDBY_NODE (correct — Standby)" fi -# ── T4: iSCSI target visible on both nodes ──────────────────────────────── +# ── T4: iSCSI target visible on Active node ─────────────────────────────── echo "" echo "[T4] iSCSI target" -IQN_COUNT=$(n1 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0") +IQN_COUNT=$(na "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0") if [[ "$IQN_COUNT" -ge 1 ]]; then - pass "iSCSI IQN active on $NODE1 ($IQN_COUNT target(s))" + pass "iSCSI IQN active on $ACTIVE_NODE ($IQN_COUNT target(s))" else - fail "no iSCSI IQN active on $NODE1" + fail "no iSCSI IQN active on $ACTIVE_NODE" fi -# iSCSI discovery from node2 via VIP -if n2 "iscsiadm -m discovery -t sendtargets -p '${VIP}' 2>/dev/null | grep -q '${ISCSI_IQN}'"; then - pass "iSCSI target discoverable from $NODE2 via VIP ${VIP}" +# iSCSI port reachable from Standby node via VIP. +# Use bash TCP probe (no iscsiadm needed — just checks port 3260 is open). +if ns "bash -c 'echo >/dev/tcp/${VIP}/3260' 2>/dev/null"; then + pass "iSCSI port 3260 reachable from $STANDBY_NODE via VIP ${VIP}" else - fail "iSCSI target not discoverable from $NODE2 via ${VIP}" + fail "iSCSI port 3260 not reachable from $STANDBY_NODE via ${VIP}" fi -# ── T5: Failover — standby node1, verify resources move to node2 ────────── +# ── T5: Failover — standby Active node, verify resources move to Standby ── echo "" -echo "[T5] Failover (standby $NODE1)" -MYNODE=$(n1 "crm_node -n" 2>/dev/null || echo "") -n1 "crm_standby -N '${MYNODE}' -v on" 2>/dev/null || true -echo " Waiting up to 30 s for resources to move to $NODE2..." +echo "[T5] Failover (standby $ACTIVE_NODE)" +ACTIVE_CRMD_NAME=$(na "crm_node -n" 2>/dev/null || echo "") +na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v on" 2>/dev/null || true +echo " Waiting up to 120 s for resources to move to $STANDBY_NODE..." MOVED=false -for i in $(seq 1 30); do - if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then +for i in $(seq 1 120); do + if ns "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then MOVED=true echo " Resources moved in ${i}s" break @@ -105,49 +129,52 @@ for i in $(seq 1 30); do done if $MOVED; then - pass "XFS mounted on $NODE2 after failover" - IQN_ON_N2=$(n2 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0") - [[ "$IQN_ON_N2" -ge 1 ]] \ - && pass "iSCSI target active on $NODE2 after failover" \ - || fail "iSCSI target NOT active on $NODE2 after failover" + pass "XFS mounted on $STANDBY_NODE after failover" + IQN_ON_STANDBY=$(ns "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0") + [[ "$IQN_ON_STANDBY" -ge 1 ]] \ + && pass "iSCSI target active on $STANDBY_NODE after failover" \ + || fail "iSCSI target NOT active on $STANDBY_NODE after failover" else - fail "XFS did not mount on $NODE2 within 30 s — failover incomplete" + fail "XFS did not mount on $STANDBY_NODE within 120 s — failover incomplete" fi -# ── T6: Data integrity — file written pre-failover readable post-failover ─ +# ── T6: Data integrity — file written post-failover readable ───────────── echo "" echo "[T6] Data integrity" -# Write a test file on node2 (now Active) and verify its content +# Write a test file on the new Active (former Standby) and verify it. +# 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_CONTENT="ha-acceptance-test-$(date +%s)" -n2 "echo '${TEST_CONTENT}' > '${TEST_FILE}'" 2>/dev/null || true -READBACK=$(n2 "cat '${TEST_FILE}' 2>/dev/null" || echo "") +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 +READBACK=$(ns cat "${TEST_FILE}" 2>/dev/null || echo "") if [[ "$READBACK" == "$TEST_CONTENT" ]]; then - pass "test file written and read back correctly on $NODE2" + pass "test file written and read back correctly on $STANDBY_NODE" else fail "data integrity check failed (wrote: '$TEST_CONTENT', read: '$READBACK')" fi -n2 "rm -f '${TEST_FILE}'" 2>/dev/null || true +ns rm -f "${TEST_FILE}" 2>/dev/null || true -# ── T7: Node rejoin — un-standby node1, verify cluster is healthy ───────── +# ── T7: Node rejoin — un-standby original Active, verify cluster is healthy ─ echo "" echo "[T7] Node rejoin" -n1 "crm_standby -N '${MYNODE}' -v off" 2>/dev/null || true -n1 "crm_resource --cleanup" 2>/dev/null || true +na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v off" 2>/dev/null || true +na "crm_resource --cleanup" 2>/dev/null || true sleep 5 -ONLINE_NODES=$(n2 "crm_mon -1 2>/dev/null | grep -c 'Online:'" || echo "0") -if n1 "corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'"; then - pass "$NODE1 rejoined — cluster has quorum" +if na "corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'"; then + pass "$ACTIVE_NODE rejoined — cluster has quorum" else - fail "$NODE1 did not rejoin with quorum" + fail "$ACTIVE_NODE did not rejoin with quorum" fi -DRBD_ROLE_AFTER=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown") +DRBD_ROLE_AFTER=$(na "drbdadm role ha-data" 2>/dev/null || echo "unknown") if echo "$DRBD_ROLE_AFTER" | grep -q "Secondary"; then - pass "$NODE1 is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)" + pass "$ACTIVE_NODE is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)" else - fail "unexpected DRBD role on $NODE1 after rejoin: $DRBD_ROLE_AFTER" + fail "unexpected DRBD role on $ACTIVE_NODE after rejoin: $DRBD_ROLE_AFTER" fi # ── Summary ─────────────────────────────────────────────────────────────── diff --git a/scripts/ha/cluster-init.sh b/scripts/ha/cluster-init.sh old mode 100644 new mode 100755 index 1aac090..917b505 --- a/scripts/ha/cluster-init.sh +++ b/scripts/ha/cluster-init.sh @@ -21,22 +21,27 @@ set -euo pipefail # ── Configuration ───────────────────────────────────────────────────────── -# These must match variables.nix haServer* values and the Proxmox VMID -# assignments. Update before running. -NODE1="ha-server-1" -NODE2="ha-server-2" -NODE1_IP="192.168.2.200" # vars.haServer1Ip -NODE2_IP="192.168.2.201" # vars.haServer2Ip -VIP="192.168.2.202" # vars.haServerVip -XFS_MOUNT="/srv/ha-data" # vars.haStorageRoot -ISCSI_IQN="iqn.2026-01.home.sweet:ha-storage" # vars.haIscsiIqn +# All values override-able via environment variables; defaults match variables.nix. +NODE1="${NODE1:-ha-server-1}" +NODE2="${NODE2:-ha-server-2}" +NODE1_IP="${NODE1_IP:-192.168.2.228}" # vars.haServer1Ip +NODE2_IP="${NODE2_IP:-192.168.2.227}" # vars.haServer2Ip +VIP="${VIP:-192.168.2.229}" # vars.haServerVip +XFS_MOUNT="${XFS_MOUNT:-/srv/ha-data}" # vars.haStorageRoot +ISCSI_IQN="${ISCSI_IQN:-iqn.2026-01.home.sweet:ha-storage}" # vars.haIscsiIqn ISCSI_LUN_FILE="${XFS_MOUNT}/iscsi-lun.img" ISCSI_LUN_SIZE="10G" DRBD_DEVICE="/dev/drbd0" -VMID_NODE1="" # FILL IN: Proxmox VMID for ha-server-1 -VMID_NODE2="" # FILL IN: Proxmox VMID for ha-server-2 -PVE_HOST="pve1.sweet.home" -PVE_USER="wayne" +VMID_NODE1="${VMID_NODE1:-}" # set by deploy.sh; needed for STONITH +VMID_NODE2="${VMID_NODE2:-}" +PVE_HOST="${PVE_HOST:-pve1.sweet.home}" +PVE_USER="${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. # Must mirror vars.nfsShares subpath values in variables.nix. @@ -59,6 +64,32 @@ warn() { echo "[cluster-init] WARNING: $*" >&2; } [[ $(id -u) -eq 0 ]] || die "must run as root" [[ "$(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 ─────────────────────────────────────────────────── AUTHKEY="/etc/corosync/authkey" mkdir -p /etc/corosync @@ -68,13 +99,13 @@ if [[ ! -f "$AUTHKEY" ]]; then chmod 0400 "$AUTHKEY" fi log "Distributing authkey to $NODE2..." -ssh "root@${NODE2_IP}" "mkdir -p /etc/corosync" -scp -q "$AUTHKEY" "root@${NODE2_IP}:${AUTHKEY}" -ssh "root@${NODE2_IP}" "chmod 0400 '${AUTHKEY}'" +n2_ssh "mkdir -p /etc/corosync" +n2_scp "$AUTHKEY" "$AUTHKEY" +n2_ssh "chmod 0400 '${AUTHKEY}'" log "Restarting corosync on both nodes..." systemctl restart corosync -ssh "root@${NODE2_IP}" "systemctl restart corosync" +n2_ssh "systemctl restart corosync" sleep 3 # ── 1. Corosync quorum ──────────────────────────────────────────────────── @@ -105,15 +136,12 @@ if ! drbdadm dstate ha-data 2>/dev/null | grep -q "UpToDate\|Inconsistent\|Diskl fi log "Initialising DRBD metadata on $NODE2..." -ssh "root@${NODE2_IP}" " - if ! drbdadm dstate ha-data 2>/dev/null | grep -q 'UpToDate\|Inconsistent\|Diskless'; then - drbdadm create-md ha-data --force - fi -" +# Use grep -E for ERE alternation inside the remote bash -c string (avoids \| quoting issues). +n2_ssh "bash -c 'drbdadm dstate ha-data 2>/dev/null | grep -qE \"UpToDate|Inconsistent|Diskless\" || drbdadm create-md ha-data --force'" log "Bringing up DRBD on both nodes..." drbdadm up ha-data 2>/dev/null || true -ssh "root@${NODE2_IP}" "drbdadm up ha-data 2>/dev/null" || true +n2_ssh "drbdadm up ha-data" 2>/dev/null || true log "Forcing $NODE1 to DRBD Primary for initial sync..." drbdadm primary ha-data --force @@ -137,7 +165,7 @@ fi log "Mounting ${DRBD_DEVICE} at ${XFS_MOUNT}..." mkdir -p "${XFS_MOUNT}" -mount "${DRBD_DEVICE}" "${XFS_MOUNT}" +mountpoint -q "${XFS_MOUNT}" || mount "${DRBD_DEVICE}" "${XFS_MOUNT}" # ── 4. NFS dataset directories ──────────────────────────────────────────── log "Creating NFS dataset directories..." @@ -153,108 +181,130 @@ fi # ── 6. LIO iSCSI target ─────────────────────────────────────────────────── 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 </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..." -scp -q /etc/target/saveconfig.json "root@${NODE2_IP}:/etc/target/saveconfig.json" +n2_scp /etc/target/saveconfig.json /etc/target/saveconfig.json + log "Unmounting ${XFS_MOUNT} — Pacemaker manages it..." -umount "${XFS_MOUNT}" +umount "${XFS_MOUNT}" || { sync; umount -l "${XFS_MOUNT}"; } log "Demoting DRBD to Secondary — Pacemaker manages primary role..." -drbdadm secondary ha-data +[[ "$(drbdadm role ha-data 2>/dev/null)" == "Primary/Secondary" ]] && drbdadm secondary ha-data || true # ── 7. Pacemaker resources ──────────────────────────────────────────────── log "Configuring Pacemaker cluster properties..." crm_attribute -t crm_config -n stonith-enabled -v false crm_attribute -t crm_config -n no-quorum-policy -v ignore -log "Creating DRBD promotable clone resource..." -cibadmin --replace --scope resources --xml-text " - - - - - - - - - - +log "Creating Pacemaker resources via cibadmin..." +# Use cibadmin --replace with pacemaker-4.0-compatible XML. +# Key schema rules for pacemaker-4.0: +# - globally-unique must be in , not a direct attribute +# - promoted-max / promoted-node-max (not master-max / master-node-max) +# - constraint with-rsc-role="Promoted" (not "Master") +cibadmin --replace --scope resources --xml-text ' + + + + + + + + + + - - - + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - + + + - + - - - + + + - + - - - + + + - - - - + + + + - - - + + + - -" +' -log "Adding ordering and colocation constraints..." -cibadmin --create --scope constraints --xml-text " - - - - -" +log "Adding Pacemaker ordering and colocation constraints..." +cibadmin --replace --scope constraints --xml-text ' + + +' log "Waiting for resources to start..." for i in $(seq 1 60); do diff --git a/scripts/ha/deploy.sh b/scripts/ha/deploy.sh new file mode 100755 index 0000000..a02f235 --- /dev/null +++ b/scripts/ha/deploy.sh @@ -0,0 +1,462 @@ +#!/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 Proxmox host to deploy on (default: pve1.sweet.home) +# --vmid1 VMID for ha-server-1 (default: 200) +# --vmid2 VMID for ha-server-2 (default: 201) +# --storage Proxmox storage pool (default: local-zfs) +# --storage-bridge
Bridge for HA storage network (default: vmbr1) +# --drbd-disk-gb DRBD data disk size in GB (default: 32) +# --memory RAM per node (default: 4096) +# --cores 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" + + # Fix targetctl.service on both nodes: the iscsi-target.nix module bakes + # pkgs.targetcli-fb for the targetctl binary, but targetctl is actually in + # rtslib-fb (a different store path). Apply a runtime dropin that corrects + # both ExecStart and ExecStop before Pacemaker ever touches the service. + # The fixed iscsi-target.nix module will make this redundant on next rebuild. + logn "Patching targetctl.service on both nodes..." + _patch_targetctl() { + local ip="$1" + ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "${HA_USER}@${ip}" sudo bash << 'PATCH' +set -euo pipefail +TC=$(find /nix/store -maxdepth 4 -path '*/python3*env/bin/targetctl' 2>/dev/null | head -1) +PY=$(find /nix/store -maxdepth 4 -path '*/python3*env/bin/python3' -name 'python3' 2>/dev/null | \ + while IFS= read -r p; do "$p" -c "import rtslib_fb" 2>/dev/null && echo "$p" && break; done | head -1) +[[ -n "$TC" && -n "$PY" ]] || { echo "targetctl or python3+rtslib_fb not found"; exit 1; } +# Write stop script that saves LIO config then tears down kernel state +"$PY" - "$TC" "$PY" << 'PYEOF' +import sys, os, stat +tc, py = sys.argv[1], sys.argv[2] +script = f"""#!{py} +import subprocess, sys, rtslib_fb +root = rtslib_fb.RTSRoot() +targets = list(root.targets) +if targets: + r = subprocess.run(["{tc}", "save", "/etc/target/saveconfig.json"], capture_output=True) + print(f"saved {{len(targets)}} target(s); rc={{r.returncode}}") +else: + print("no active LIO targets") +for t in targets: + try: + for tpg in list(t.tpgs): tpg.enable = False + t.delete() + except Exception as e: print(f"warn: {{e}}", file=sys.stderr) +for so in list(root.storage_objects): + try: so.delete() + except Exception as e: print(f"warn: {{e}}", file=sys.stderr) +print("LIO kernel target cleared") +""" +path = "/run/ha-targetctl-stop.py" +with open(path, "w") as f: f.write(script) +os.chmod(path, 0o755) +print(f"wrote {path}") +PYEOF +mkdir -p /run/systemd/system/targetctl.service.d +printf '[Service]\nExecStart=\nExecStart=%s restore /etc/target/saveconfig.json\nExecStop=\nExecStop=/run/ha-targetctl-stop.py\n' \ + "$TC" > /run/systemd/system/targetctl.service.d/fix-exec.conf +systemctl daemon-reload +echo "patched on $(hostname)" +PATCH + } + _patch_targetctl "${NODE1_IP}" + _patch_targetctl "${NODE2_IP}" + + 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." diff --git a/secrets/common.yaml b/secrets/common.yaml index baa2013..4c3bd93 100644 --- a/secrets/common.yaml +++ b/secrets/common.yaml @@ -5,211 +5,229 @@ sops: age: - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrMGd3ZVNlNXdmOUZMUzdQ - TW1acEs0NFA2Q01rM2dkc1h0NHkzQmhiWFZZCjMyK202VWdlaGhsZW04MnVwUVdO - alA0Q2FETThsYkhSS0hKdHBaS3VaY28KLS0tIG1Gdk8yalREOUtIZTUyY2p1UHlJ - eG5iQnJsaTJBY3Y1dkw1c0VEaDQwdDQKfV04fLy32Lp2ZQ2VnvQ0h/Vsf+qdaJiv - DnLXGZ9hE5yzpKWkQIRgqYGBkF8PkH0YC4OIaVkA53wrtjqS4ZHR9Q== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJR3NKa2kxZGRHd3RWdWRR + WDkreDcxZzE1RVZ5NFFUN1pLUktzVHZSUVd3CmJkMzVsRVVraGZWVEZLWkQydVJI + NGF5Ui9ranRZU1lqS2RKZzRmWGJVYmMKLS0tIGdIRlRMOFBDMnNnT3hZdjcvUVA3 + cUxXa0tjWWk3K0ZTNUx2OU9MRHpobzQKMZLQH+z8o27s1bAXyJI8HD8jHnU5JaZV + WLHwwptGYz7pYyCkWc25IkA0nR3KR4lfHT2o5eGn2BBivpI5o/QQMw== -----END AGE ENCRYPTED FILE----- recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWFNRY2FiK3VkSm1RdHBn - cWl1ZStLcTRFZWY5VVI5N0FhODZvR1ltM0FvCnBHZUtTUm9QeHNlbVBoZEx1V3Fa - Nk9iMmJKVnhocEpERi9leE1ySUtNMFkKLS0tIDRRYkxnbU90S2RyMHdJNzRJNXBi - QjRmZFhVakVic2tYODZHcWtJRmNQTDQK7G8eSJInt11P0DiL9uzNQ/ZHHLVNIYPe - bvlhuGkEuQ/+j5sVSKOfSI2Y7CvM7TpE3APyKBcLG3ajYg6F/Ev3SA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpOVVzNDZvcE1Kb1Zua2pF + cjllRWZkTDZUb0JtVW84Z01OOHF0Q3JsZW1RCmswV0lxUW02NEZzY3lWeHNXQ1Ez + K0VILzFhemZjTTFLbEpVZVdJY21HTTAKLS0tIHVqZ2E5aU1NVk10bEJ5UXkvMVFK + dXdSWEx2OXBSZG9aM0VGNVQzTjVRUkUKYgZjGG7I0ea9I+gG4Ah1VSkiONNAHdDQ + 5zG9LnKQZ5faY4tWsX7JHFWDX2Y83mXbHV6jMCutAfQG2zt/5psTMw== -----END AGE ENCRYPTED FILE----- recipient: age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1S3dOcFdwV2NoMW1oMnY1 - VGdxeXJVR1lsbzNVZHlGb0NGOVo0SndiakVjCmg0QlNnZDV5RlFja2hCbVRXV1VF - S1ZtbC9KU0U1ZW9zeVoyR3hxNW1XTFUKLS0tIFUzWTJhTzM4QnpWV3h1OFU0N3BK - RnF1N2k0S0lIVitoNDJLUmZqdHRzZVkKUfNg24p8zxb3749v/A1BOKCNw75AUKpf - RUmFCw5DDWF2aNM0mZqcjjVmJ/FRKV2HXwwUGsHPKSOTnKfOUlPNKA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxS3VuaHlaVGJObVNPb3Rh + UEdBMk9meDQvWi85NVRHR3dXSlhkcFZVa1RBClZDR1MxOUdwVjhzSjFaWVFWelVX + cUgzMWNrUzl3L1R6elc1ZTRGamxDU2MKLS0tIDFwUUJFQ0hIUWNTVmFwdGt0czZi + V3VGSndCT0tQb2Nzd2w2TWxBbEFBeUEK8kQQVqISb3h0snOtqM0w/mhWEpjIxlhk + N84QzgxhD40tvutfO+57HsfOIqjKi4Yhdmbd8+iW9GzGnrbfrDuVGw== -----END AGE ENCRYPTED FILE----- recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39 - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzazl5MUpUNEVNMTdLWXBp - c1hkaVhjcTZXTUNsWS96QVVWb0RVQlZ4VlNvCnE3TFRySU5jTFk2WjBONUQyQUhl - U1lNTDFTRmZhMUFyZmpVY2xpaUVxRW8KLS0tIE9MQ1M2U2ZXQmRCVll2UGRWL1RG - K2tnU0NOKzU5dkpiekF0Vk1VM0ZZZDgKK13aFypGAqrKWPOr3UwtXI1EoXf1+UzS - rBqcwnX6WPxSKUwWoins4Aojek4QhbhY4R5ei6rRS0KEQeryGxy8bg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoYXBpZlpjT2oxMURTcW9t + NTdNWkMyT0d0SVBCSFBqVC9vNkxYUUk5Wnd3CmlyNUhIa0FmR0lwSFEwK2QzRjlO + ajNKMitzZGlDaXpRS1pNNDhhaFdPYjgKLS0tIFVtSjNXSWdlQTVwOEUzcjdEVExu + aklINzU2QUFsZmxPaUI5aFR2Z3V0bXMK/rDuNYW2r022tHjuk4KqIEOxqvjQsBIg + D2lG4ih5h3idm++gnhCSlP/9tKSaVHoq+BpunsmFObrrWzZRhKI6jw== -----END AGE ENCRYPTED FILE----- recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZUpVT1p4TGZyNC9qMm5G - UzNHV08rWURjY2lqS3FTWFdoc0FYYTFjb0NBCk1mZ2JzaXk5RmExNE9xWGZ5K0pv - UEtqMkltV0dIWll5eVBUVVRNOUNDWUUKLS0tIGpKNVJudUM1UGNvaGl1UDBOeFA1 - RjUyRlZ6a0Y4SXNsL21zSURVRk9KTFEKU1L6BQ6ZlYQQtqx3uF/uM5CQ1ercmvRT - TL3r2/Y07gE7CjRn3pR9z0co8KndGzxV6YR+ubyWptwBS8KQh5stkw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArbkhEaEhoUk1SUHNjVm5C + Q2svcmpselJ1eU5qMVJKV2JWV1J0eUpSWlV3CnhIWDF2NXFMM0VMbDQyeHdkNDJV + aForNXJ0UFd0aXFGZzZhekNTdUd2U3cKLS0tIFljclNTa2I5RHJTUU1yMm5XakVo + amlHVmZzSDZRTndFSEpjQlVOUTVhRVEKz4Tx0PceUCxw8MsREB2HRPDyC/lIskhs + /HWmseMlnsoEMu8E1OIO0TpWY3qowHrdFFr4njpxMJUTXusGvRK8Qw== -----END AGE ENCRYPTED FILE----- recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsaWxxWS9xQXViY3VnUEx2 - V09FbTI0WGtNbW0yclhOSGZDbG5NUTNaTkFFCitkcjJ3OE9BSnN4bjFWcE9nYVBk - ZzMyVHlJQ2wwdU5JOXdCQm9oNkhNd2MKLS0tIE9tRzFYS05vSkUwWFRkaTdtc0k0 - blVoMWV0QklBVkluT0Z4NHYyS1F0blUKO+Uc0of/V77ZUZOsxTzeH8/LmmAOQt+J - x/COHxnLCnZ4eWI6q1a0Qn5Br15OJYTxUI2QTV4goTnXBNUDo9wdpQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXbm1jWW1DZDVNdnVZOE5E + bTRLTzRhckVMNjV5VkMrcllLZk9NeVRiT0NrCkh3RDRicXFTQTNqSmw4VEQ0QlNt + cGJNRlFTSjU5YWFXWmM1MjBjVGZSeVkKLS0tIHFPOERUTGVOVSs5U0VMalAwUXcx + QTYvK2RScDErVENFdUhpYUVsejBYR0UKCNBN3n4q2X1YLturxpjDv4IlnLXoPtOM + DqXMNEgGubagOBhfVFrOjpFxonP7JThSfzEJT/pmL+S7bCvgyosYXA== -----END AGE ENCRYPTED FILE----- recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsLzFnZkdVdGQ2dHRwcnRt - Ui8rc1NoQzNKSHVzTk0vYU1vMlRxcjBKZ2hNCmhLYXVGSis4RU9HNGVxZkpvUkd5 - L0xvalhDYTQ4N21OcHRheTlkaUxvTkEKLS0tIFVkRGxtLzhQT0paV2U3ZnNScVdn - alZnaVppeGI3OUVscGpONkk3YTRXd3MK61na8x5qX7+dyMHasDz2dj7yeaUlX8me - N4/SIk1JDBhv9G7mdKLbKhSF1UJrSY7TJqJqx8/dqEc0uG3vptA1ew== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXdDRCMlVyaTdPWFQ5VVhh + UWMzUTBkZVYrSDRQWUpWLytDeHVmcVh0L0g0CjVIZE1jZHA0N3RtSmZKVThrcmt5 + N3VrenFGTS9LUDdZRHBiNDhORE4wa2sKLS0tIGdieDQxQ0lWZnVMZUZzSExKWEJU + azl2Q01YdHZacDRSaElpZ0h3SG9xcGsK4s30qRC2eXbuKqPUHfRUJrE8FCMdz5EQ + 25UhdrspVBadt0G92hHV06Uwm/KKnG4Mi5crLKIMI+HAF+5Uxrh/xA== -----END AGE ENCRYPTED FILE----- recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZEtvOUhMU1FRWWpJQjF4 - NWphNXp5M0dLZXhkZndhT1Y3L09maytHazJVCm1MeEtMWXg1Zjg3bFVnZEorci9J - bkNZQU9Ta1dDTFFHaGFWQVBpK3pYRDQKLS0tIEhPVGliRDR3ZTF2aEl3ZnJEYWtR - anh0SEpnVW8xdXNkZEZQSjcxU1BHMFEKVRJUA71fi1QawB2TnuTWMYhzQR18u4M2 - s1V4j4TwYyyKZFoNvt8kOUayjC499c5OBUufYs6G2ciC6gK2A9E0EQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwL2xrRnJyOWxRT01YbHI0 + OG8vU2xQaEJySkZkQnp6ck90dmkzQlV1RER3CmtGc3duTGRxZXhlbDlHd2hGUDdH + QmxYTjBVbkVWRVlIcEhpaml2MlVzc0kKLS0tIFRWTnZGQm94VDhJK0J6SS9oa2FB + aGpJUGFydUhvQlpFaURWSm9VS1AzM28KWvU5knwB/ViUrSxbP0zPR4iUE4LXxYi3 + qraWrbv3jXZwu4Sgv0H+/k6St/Xo5RU+nIqSOCKEy/kpACwk4BhMXg== -----END AGE ENCRYPTED FILE----- recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTV2IzSjBEMjUwZjB0dCtj - aTdZVlEvYkVRbTNjUC9ZRVdNZVhPQTJEd1ZZClc3NDJiR1BVYkZkdVVxMVZGc0VN - dWdSSXBFR2xxR0xrV2thRmUwSS96S0UKLS0tIHRRTXVlUi9UYnFRRlhsU21HZVY4 - SDFYd0NwVEtVZXNsWUI1a1ZZU2xNRGMKuQUhOq2FRD+PGn5OkdODZItbxCzRKjne - E60UOYtHjanuGjJ1svuR9cYsLZz7lLOwItklecYaQYpMRZEwzzBGCQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3K2tIY25TZCtReTQ2Zkc5 + d2w2aGZqYXNBSmZ2VVc5WWtZNDU4YlJpNm5VCkpGUkNOdGVSeXU1WG1YV2NQYjhu + c2NKd0czSm5FUWxmNTdHVGVVWGlUMWcKLS0tIDJmRy9vbGJDNGxMVDM3b25TRVVI + blpZTkZsTzNrdmFwUGQzR2RIOVJINlkKwa06bE6TmYtXPx0daHadbwL9/u1iYuGm + n62kEwTKdyfncODl895qqjWkiA3JnbwQxzsaVDtEGfauzUph9DPY2g== -----END AGE ENCRYPTED FILE----- recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSjJoUVUyd1JqRm1ZQzZx - SFJ2cWNUY0E4b3FndVA5Y0hIOEZnZUlVWlhBCm1vY1luOXZBelRUTmF5Y1NMeDBn - cE1BTDErc041UjJCWTBQbnk0Wk80dkEKLS0tIDVSZzd1UktvZGdyanFUMkVORUtl - eVB5TnJkMlp6dUpXSTlxRlplZ2NxUlEK0AYOxIbswjM0SUASDfmZ7PqcEU844fgI - ycFWVSEPodwUZ6UFoYXhHlJzHFcgpLvwUd1PMktLHe1qrZ7GOQJIMA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1ZlVxYkZ4UFJmRVBHeExi + OWZ0akNwc0dMenlHQ2dUSDlERGxSNzZtdFJJCnRYK2cwNHJRK0xtY0NnYlh1a1pG + aTk0WURSYVdUY1ZTSUhBNVpZZUFyaUUKLS0tIHZKTFZmS2ZsNlZ6UjZUWmhNK0Q4 + RC9TREh3ZWZRTzc0WmFkQm5qVlZJcFkKUngIUXCVV1CKpUKfAlal1KjoJwk83mq5 + 1MvU9mjx8Oq/suUlW4axFfAO99mUc9yCwb8TCMUZDhQp802v5FjBcw== -----END AGE ENCRYPTED FILE----- recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPakhQcE92ZU0zYmk2QS9D - TG0xc1JPZXZCZ0tZOXA3MGNLVnBlZGVtRFRzCmIvMHhQKzFVWCtpMTQrQUhGVGJp - RU1jbldYckw3TXI2SlNpZVBIZHRsWWcKLS0tIGtJTUtJejFxem5jajFQUDFTQWU1 - VnlxYmVlNG04ay9ETi9FRmVYQXVoRkUK9oFNolI7jRjo9RUs1g4ghrx7aYV4U/ce - ZTc2tFh57+7aKgrDi+2W3jwhfkjvBsThk//p5mLlqEEgw2lwlnhvPA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRM0hrc0I3UCtBOXJWUm1H + MW5QWHAwV3lETFp2TGtPWVZHZ2s0ZHJmQ2w4CnBlQmlTVmEvRTlLMjVXMkRJeWIw + QUR3aXpPUndocCt4aWUybndxK3RUN00KLS0tIDJrNkY0d1Jqby9pa0tXWVVuTER5 + YTlVNklTakFlaGQxdjJqRks5cUZoK3MKkV199oB7lPBzNYd30nSY0J6Cd09ViBA8 + uZWlmMGKlveBQ98qOt/vDTouRDxedASz0ZGg7+jxUXkUKbujkCYYXw== -----END AGE ENCRYPTED FILE----- recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoVnh6dFAwTkY4cHJpaEs4 - QnZPeXZHK0tXYWZPNytmYXVsdGRWQVI3RlhRClFVb2I5OVZzZFNrRXFaa0JTUkRJ - OC9GQ1V5K0JhWlhkUjU1WStCa1lPV1kKLS0tIEhzdnBBZkRnK0NtV1FuTkVsNlgv - QzVEcEVkQm5NL0Z5dUU1U0ZFaTJITnMKaWE9vlrOpQstr6FGP5ObdilsCYk4kYAj - /phboR+Ym7QDTyUF9LZXJCU54YJp6vEWkRnlJFqC75UW/v/lgBhBMQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwVmlDOHh6dUFaR1YxOHJM + N09LdWxscStGSTYvMm1QaWRkZVVod1JTTlJnCkp0Z1pSa3lVU2JqUnU0ZnAzWWEv + bHc3S2ZjK3E5Z1BRL2pYWklvSFoxSnMKLS0tIGxTd29MK0dRWFFnaGVwa2NVQXM2 + TjRQdHRZNDFTTWNWR2FvZXhQMVZORncKdAcSOB50bNQsaTGtAqnvjYyFpcjdMGzO + NvwI5ZquoT/B37Xsg4NQ7++/ZkemzkuX5VYoXLVAQ9WJwQxewoMFKg== -----END AGE ENCRYPTED FILE----- recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkUHAzWk1KblJxVVZZV3FM - WndzRGRtbkw2azRVWnBuZmhKWElqRFk0RENzCk5DMHlMVWpwbXEwVUhkaFZUbkp2 - QXNlZFV4SjBEdmR6UEw0N1JOUnhNKzAKLS0tIHo5RkNDUk1ESWRHQmV6bzkvSTlP - dk1GQ0Y3V0dTRlByb2xUOERVOTVwbVEKY4sAHyAhvGSYJzPuufWUIQD2xZcSt/nX - t2ZFXu891/QdEzyUXCIzdwAV+Y/LjvroIlCp5Hkbrk0s7N+ghqsB1A== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3V0M5bnBzamdGemU4Y0g0 + TEdZdzQvUVpYSkozZlRsMWM4MStmN2lxckYwCms0ZTF3cTB5OTRzWWE0a1FXWkhG + SGptVCtiOHptV3V0aVNQUUJ4NkE0dnMKLS0tIEEvK0lwa1pnc1YwWXhkYmh3cy94 + S2E0SUk4RDdSWktQTjZoTkNUallLTVEKNqTmhQo74Q00ZhrSrD+/JB3TwxL9OQdC + XSONu6XXzPzfhllNIwe3BnmRVJM/1ru8rvllk98Cvle0k4K5+xBJ/g== -----END AGE ENCRYPTED FILE----- recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7 - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBYVkwYXpYSjdmM1FpbDl3 - QWpycXoyL1AyOEpZUmtpbjl3MFAwTkJoOWpVCmVDd0FBUWxaQmZCU2VmNkZGMk9o - TUdLNGtac2N4REg2eVF1eVh0WnNaTE0KLS0tIDJvcFErSjRiWmhPMmpadjROOHdt - NXp6Y1JpdHFlSlRoa3JTaEt3emdnalUKjoFfZAiKMPF3noX+K0+vc3+p/XUHnhic - k888KdUwcZYl2/dAIc8UDSggbMnncJAJgoezoCHLkj97GNNAD7E+gQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKYnFtM29UUzVRYWpTN0w2 + MDVmQk83THEwR3liN0hmemxBZlF3eDVZV0RRCjRFcjZKQk8yaXViWm0vTUdrSlM3 + UDNnemoyMG0wbnpzK0FRYXNiRTRnaW8KLS0tIHd1QVYvWWl2QlcxVGZzbFBLbjlu + djV4dVpnQzF6LzFiWTREaTYrMHAreUkK82l9Njt3B7HJBLtdLjNSnQ4TDTvBgG9/ + iUEn3fd9OmvfSGcbp4hhwB7q0UJqb7AP47ctwDOEg/FYeF7eSBvVgg== -----END AGE ENCRYPTED FILE----- recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqYnQyYzF5cktZNG5PcThy - SEszVEwrUkQ1VVRsM3pSTlRQaHVLN0VuSHhrCmN1Z3pwNlFsbDN2UGI0KzYyallM - SHJ5eklQeEIxSlhiYW5PUlpJcG5KNjQKLS0tIEk0QkpMdlBlRjVYMmJaMzJUbDNm - K25pZldwd3JoZi9vdURoa3Myb2RQNG8K6N6bO2YKooPfpKihgsYqilfz/yAYCLZD - XJ/THgT4URX2VNvSspvBtN8luOiJUVcchp5WtL2m9jARL5txEcDorA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoZnVZekVieVVRT2ViWUpN + bFk3ckJUU1RHRUJrbkNkTUZkbVEzRGd1VWtRCk03MFEweENBOWtOS3gvM3ZpNTF2 + WG1UWTM3OFp3RERNNC9iV05aOTZnWmMKLS0tIFZqZ0NhZVhxODMyWENhVlI3SUR0 + b2doaGp1VHBOUVFacG51Rk02d2o1MWMKnNYktA42DgSWWGuy0XTOtUN9ry49WOBd + NaGq27ke16XhQ0ZC+r2a1g7YAFaL+qWJxpaH+0ojhUjti3x9O3pWqw== -----END AGE ENCRYPTED FILE----- recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvY0VpeW1nazVvRWJFaGNU - RGVzVjRmWWV4dnJzRHdsaW9ENVZYck5lWGtVClNtd3ppelowRjZpRFFSMC9EK09n - b2hqNnkzejdrTnhYNGNKblpteDNLRWcKLS0tIE02bWVjazRWNEVKbURITGlQODlR - cTJJVnBVdGIrdzBoSXExelNrVk1XcEUKc77o2EX7PCm/HjUo5GsUiQdm488WB2mg - wHd/qDbQhF1W75RrVTuIKgtEtrRjZqpmr8toe+aHJizPofcrToUfzw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRTkQ0cm9MSTMzZjF5T0Vh + TWlOdVJzZ1JvQWdLTzZsYWZ6djJhTjk2dnpFCmxUZkhrOW5OTWtrS2FLOUdsWm9F + WVB2VzhTdGwxNzZIZ2lSRmRielAzZkUKLS0tIEd1RlIwQXhocmJkVWtvVGMvTlVB + WWtyVjUrWjYrWDFmc3VtdzJUaEFPeGMKvWONIbN6B9Ims3f0l/lfUTU116TFPXew + K6kAvyySbh9Z9JK8Uyp31WLSbqVy56eGr2Su8Hj6rAGIoJdo0v5NoQ== -----END AGE ENCRYPTED FILE----- recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2 - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSMmZnd1pXRWh5NDVSN2xq - MVhTN3N1OFpkcUh0a0tjYzJnWUlINGNVd3k0CmZkTnBac0l1dllxazdLY2l0Rzli - bE9sNTBVSkJNaWF1T3c0WktoOHl0NU0KLS0tIHEyeWZTUjdQeUN6U2t0d3JwNTBX - ZE1Za0tXb0gwc1FSakVYdU9OTHkyd28KkwmlzSYP8XofB0VGag+S18+S2TyQjLrM - qaXtbBtLzJGNDhe9FhAKTPFcjTLWbohlG69vxcImyCyCns+QQ+gvug== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlSWtxbHJsQllITit4NVZv + b2Z3YzJZTHFOTGU3VzkwWEFZUjdWTC96UjNVCi83SkYyaFdoTVN3eFhLQnA0bDVa + OExZRFl1M28yV0VTUC9hZUJJR3dYTWMKLS0tIGNuc00rZDI2dHFqWEc2R1ZVb1E5 + Vm45YUpvWlMvMXUxdDB0NHpncWpIMjgKcMIZK2ww/VPuuBXlL8klSTm8ySME2njC + vbhCzg9VIqIV3q2i8WEbWFmo7uyiyF89Z52xf/3uUc/TUEqjVVDnZQ== -----END AGE ENCRYPTED FILE----- recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2 - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2Q2RMWmZVOEwrOC9VcUxp - VVE4R2NqYlZWZlhKSTBHRks4bDNoaUliMWlJCm5FbWdZS05GY0VLc0sxY2x1U21V - eExwK29GVVBqYlRPZ0l5RWVXRFhRNlEKLS0tIDl6dVZJQndwVStFVEJnRHRyMW1W - NnFqc1F0SGJqT0xmREpaN21EdnlJK3MKRPE5rfFpVnH5wAOkuB5pNMlMd3omcpku - do2hFZwyI7t80jxF4+g3J7EolOx8AGjpc9Ba7Gj6IMDjye728q5N+g== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZRkRvN2dBZ1ErZmNhSktm + LzNLdzJLR3RhR2VqS0xCdlVSVUJtRnpLTkVNCjFmZlp3U05Vb2FwbDBBdmhjVUJt + Uk01dDVzS1lWeEZueDlmcUw4MlFFTHMKLS0tIGJ1bENFSFZPa0tNNFZsblFOM05x + THAyY3draFB5eHpydk94Um1qU1BZb0UKUVszMlSqUPsEj3vVseGksI+SEEjEblNo + zK5fwkYH0aDubsotkVcJjoXc2ZArHyygIMImLNpRBJYIScDGzzTBRA== -----END AGE ENCRYPTED FILE----- recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhSG94VE96TVlTNkEyclFE - SmdCSkJpeWlVWDFIMDhwUEkxL1RUTDJ5UG5FCjhKRDB6VGtwTVozdUVzbUxGL3BW - SnR3cmpSN2RxNnl4QmNvT2lkYmtoVFkKLS0tIHd2V2h2Wk5xOXlISzhjVzBsVkhz - VVpRenVnSVpHUWJqV0JHNXNWWXJOdW8Kv7PJSTDbwFOAcl7pynALaJiTXU/87bSF - F3HQllYOwOoibGzBCe18H2N+VxyNxoQL9OWe0TvOIR6bgHFIIF0/Dg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBic1JIWi9xKzVkVUtqdHBx + OVU1djJDbDcxYTFCSEQ0MTVRaS8zYm5QWFhRCjVNNFE0RUtPTlE5U3pRTlh2eVNy + OUU0cnpLNlluemdkVVNjWGxhV1NTRUEKLS0tIDNkWmhoSTNkWExWek4zdjduaFBB + UEJjYnZwYVN3L3p5Vk9sRGVQM3dld0kKsFjQKYOClBjWBaU3kCEP5yYGBphUfgOO + E1epPU6UI3asa2AC/svfFBZI3u/7EpxDH9jaSSpPec4QTwkheiVVrw== -----END AGE ENCRYPTED FILE----- recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsMEZPUExRVHFFb3pSVHNO - cGJFN1ZzTDFVNEdneVpMZ253ekFJNjVtYkNnCnRGQjU2Q3dsRGRFV25LQ3pCbTJE - OTROaFBiT01xb200S1pUK0NYaTQ3R2sKLS0tIEdiQlZTbi9Vcm0zc2t6bHplZktF - ekRySENXcjBuR2psdHZSSUJrR0xUdjgKvBsmnC+cbq5TUDFjXCyImIoPKvh8wsjE - 7Shk7Act8Jayrhx0lXBDRmfpHRrB4L16rDSmqO0DTE48VhT3TiFyug== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEb0VlcGQrWnZHWlM4YjhG + NlN6c2I3UnpSdkhYTXg4NlM5aG82K3YzUGwwCkdsM0JLUm03RTk0cm5SSitzR1ls + bVBLZ3kzaElDaTRHR1Q3bjdXYm9BMk0KLS0tIEpHNE9MTDJuMDEzV002YTVpT2J5 + Rm9nem5FSnA3M1NzNi91NnBtRi9XMmMKaGB7uE6HCE2cYHfI1VscO8meeq6O5JPi + 7bTansMUKCPKhDTBYlSnxxjlDE3DRB8ZdxW7LiG+8iVI7HUbsjTejQ== -----END AGE ENCRYPTED FILE----- recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68 - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxNDlya3RmYzNhU2p6RkVw - cHJVWWY0Yi93cW1uZi9FWkVONmtpRWh5SzFzCmtWYTRIY3BkTWU5R2Jsa0ZJK3kz - SnZvZ3YwaGtoMVZ3V2laTlBBK3UyTmMKLS0tIFdpZWtqeGlacjlLbmFySHlSUUlj - RmRqQWVHK0FUT3VDbFhLbXQ5WDhLeEEKcDkgV34lUFJRIHRoLB8F2IOvGAM93sM+ - AkmaM4+WRcGeYWQKMG2x6cYCUKFaT1lDXuWZ9kI8Fd7b9gTSnQMs6w== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlSnYrVVFObDRmUE5UN0xY + K0ZIMzNtUWM5RE8xMmlhTm5pcHNEemVwYVZNCnVLWGRObkxPcWdaOGVMcExqRlRw + VngrNGJFWXBHZGh3YXVBQmtSZXBsTzgKLS0tIFovVG43V0FBbmRJYW1qRGJCRUZp + RENmSmFuMmhFV0xHUHB2N2xWbHVvSWcKJXaXCfz3Zh4SRgTJkMuPabOq3laRorIo + F4R4bJDkao5L5QWDH1BWSi97kTTLz3QgmsEGPHm/SpODM7Llx7EPMQ== -----END AGE ENCRYPTED FILE----- recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjeFM4ajhHeVd4bzlqZmJ1 - MUxYMUpZVjdjR0NGTzVteVB6WFBQeFI0ZkZrCk55TEt0Zjdwbk51RnhYclNzam1H - cExKZXQxWFVLa1pDNFpkcGZzcnl6a0kKLS0tIFJ4ZEdJc3JVaEc5RU1aZk1uYm1l - d3RHS3hHSkRKRXFnN21FQmh0TlNtNmcKdc2G/1dhTJen6iT9kUWZM5OzCmDVprgx - WN1Bl3JzYhLsNKn794887bVAICVqbXqkdpEZztNIS5n/Rw6geKsNvQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyR3VNVisxbnoyQXZGaTJV + Y0lMemtYK1F1QjAyWFVqSm9VRzhhbHRCRFRJCnZINVpiS0ZCRXUrVEJhcVlSZk9H + TnhaeDU4b0loQjdOazJVbkpEbDZ4K3cKLS0tIGFKRjd0Rkh3RTk2WG9uQUVpWU1K + c3IxVmU0Tnc5dnBBWUZhYUgxV0NaaE0K+00bh1AiHdTL3gsA98fvFI2/IDsnWiiK + 3tOBK4UB5OE5PRupqVXlpEK3ZqKXju/MQbu0/KdaVwHxlL+WXdjAGA== -----END AGE ENCRYPTED FILE----- recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYa0gxNnBwNjNNTTgxKzgv - YkQwTjZxb25tQ053Ny9tSW4wNGVYYnlIaFZNCkVvOFNPOFkwQzhYZGxjb0FOZzJ5 - ZjNXam1ZTWZrS1M4cGhhcHZaT1NjblkKLS0tIEdvL1dDTHpWcWF0S3ZqNkxrQW52 - VlYwa29sZVloOS9qajJWQWFzY2FKRmsKy074SLdttogXsWycaFX8xso4ek7Cbjph - MMEhZd/svmnSiYM81nmeaze7qXEUcsZXuSmZCYATTBEGtx/Srll8aA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjSHFHbmU0WGg2a1poMzRF + Ry85ZWZCYVdVWkVoNnRWWTZXWWNqQURtendJCndHcmtIaFNPRGMrblJndElMKy9Q + NHlaeDVOS2k1OSt1bWs0RlU3a0dJTFEKLS0tIFRrMGJWYThKbVQ5SzdjLzlQbk9a + b0xlZGFvTVJGRjZHQk5XM3VObFRnaUkKMavcISNlQh+5yHpA1M5JIkQEF2qasnXH + Pd/JWKhnvk3Lyd45ZBJEqFV2wOknJF7v4Z0jdbo4WiUaLh5shqOp2A== -----END AGE ENCRYPTED FILE----- recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3ckNVU2lUQS94RkRGeTFo + aWNJRTdpcWxUOWZEOHF5ODIrRkZ0MkpnRndBClREbDNNUW9RZWFSRHdON3pOT3VZ + bUlFZnFGTnJpSzhFVjFLMVhKZTNDWUkKLS0tIG44Ui9YL2RPYzJLck1UVVBiRTRJ + MWxURWJlVEFBQ3RSZzZFYWJvaW1FVW8KX8/o/4LP+Wp/qOfF4wt7cTt0O+kAZbWR + Xu+dthboRmZBV21zzrEvzKGRFBj2T3EMyGDqqPcfX/vuhbVEJJgZyg== + -----END AGE ENCRYPTED FILE----- + recipient: age1nxlnrevqs2msdatze562vz6ym4pgydt2zndye83lwqauy6flggtqrevyw5 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYck0zNDJaMmhLZUxxVUxR + OGJqdTBHMFpzb2tQV3NsUVhPc1JTeGxCWWkwCmtWeWhqRXJFNVRoVVBTcjJtWlM2 + UmQ1OXh3SnJVQWQ2NnloVjRCdUZnaDQKLS0tIGZITFdwcjJFMU5iRHhvRllKaTA0 + dW1xN2ZhUmZ3VlNzWFprYWNOVG90aVUKH8Gd6gkbDIZydnb1mL0tpujmucLhbPbm + sUm8KZpNUfWpP+t7SK5M91HFaAZiqGJOARb31oszwqLGcNDliV40MQ== + -----END AGE ENCRYPTED FILE----- + recipient: age1scfc8p53q5aq2a87tcmsazmj8sfeft0s8kxg0et4nm3ucxyp3c3s6te82y lastmodified: "2026-07-23T21:15:41Z" mac: ENC[AES256_GCM,data:qFhnPra6IE3wyKQ4WKweON0S0YtD5I0adGZVfA0m6BVilN6bX5oC/1j5NK2oHrsz920hSl0SOF8LrpqOrUyGjSRkPsN4kq8qr9bJcrX4URiktP0oRden5LLt6hf+ZRP7WmRXFqixPkPHJnZIoAvkNnTFce7cDq5NEAHkKUEKG7k=,iv:nyblUDGeu3TUfFivYylOn3C/HITj99qiPI2+mh8AGh4=,tag:FrtRzSCylC4wlIoqZdfx7w==,type:str] unencrypted_suffix: _unencrypted diff --git a/secrets/ha-corosync-authkey b/secrets/ha-corosync-authkey index 74db83c..2a05dd0 100644 --- a/secrets/ha-corosync-authkey +++ b/secrets/ha-corosync-authkey @@ -1 +1,22 @@ -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:olqZm/3yxtVnlDeYzAWnXjUP9pgkruFEpeS6qONHSfdrj0lJOch44zn1pbG6Jg5QYcksny0rHki9PJEZSbRIyrZ2mSKfTJing3n9M5ODm9xPMbNYn8cpVB4AzZmCSAH7mN4p/98+5hJ1qzopGwCgKTcOOJ9DMUl9AStXORY=,iv:R2K3OehIfyKZXRFH3Y9KYnvEp6CjHPT4Ki2P1YrC6+I=,tag:1FM/E0n9teARAZ1HZM4b3A==,type:str]", + "sops": { + "age": [ + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3RFRoRW1rRklxbXYybUtu\nL1lEOXJBMDNCN1paU1k5N0hQRTJYQ1dQcTBVCk9OYnloOFZad2toOHo0UXFYSzNR\nZDUwV00vKzVnckMwSE9MMnE4RUozZTgKLS0tIFByL3Z3SzJFeEVNZjhuRDNTMzFo\nNU5tZ2tQU1ZORE5qNm1JUEJrTGdzUHMKypYeJz6BeUUY4aPKazB1nxncOA3DGkal\nfemLr9uwCJw+D3xfXzrIKrI0w3OH7bu2LmWqrNDSz3Bwa5VDuHoLqQ==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqTzhtOHM1Q1lWRXdadHB6\nZ1VrMXFsQWZEbDBMMnQvYnZSN090VnpyQnd3CkdJaHBsbzQ4SERmU0dBS2RZMUpi\nZVYrZmtSNXhiaDNTZkNMVEZud3ZzT3cKLS0tIFhETER5eWJRK05ValFhenRnSGg1\nc1prdWVTVmMyWWt2UkNXWVRLcWRHcm8KjVn1faGmsWiFzcNg4PnxZQfeQONFKz/i\nyJGJc7w2KSZ3La+44jfitMziJQ3AFRUlAhGDX25kWZJ7PtjmFvYwuQ==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1nxlnrevqs2msdatze562vz6ym4pgydt2zndye83lwqauy6flggtqrevyw5" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkVXFQazlvUEx2NzVUNlF1\nN3FQNTdFL1FtSDhCdmNQclRMdG1vRVhvcEhzClF4Ykh1ZS9JekFzOHkvOVphejVy\nd2s0a0IrZ1d4V3diOGNvMlBHUVlwTmMKLS0tIEFhTE13Q2F1di9ESXdJVDhJUVl4\na1kzNDBLcHhlRGg2Y3hSSmdEc2k5MlEKpU2sIv5Pq0LaKkvqA/fRkiB9PcvsSjvq\no4iEFzGNDi63QzXqftWnzengEy/6nWGNA31fzScQftESesGlKfYYlA==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1scfc8p53q5aq2a87tcmsazmj8sfeft0s8kxg0et4nm3ucxyp3c3s6te82y" + } + ], + "lastmodified": "2026-07-28T06:46:00Z", + "mac": "ENC[AES256_GCM,data:xJHyh4JuY+qnKljS3mtfN4Cy0pGPUszhU4ud2L8JxTFGUHTXhZzwQ3s1vM88efLTwM7yDK7oBMBUH2xligAzglDnqkE1T0l7R9k+hyldm4nYqOK6Y8GyPt/aald6oLYGCx3J7Ap0Oqq/crU+s4NiY9enf+R+08jliFyrPBHuRSY=,iv:FGtURIfSrf1tRGqpPfn40GqybhNYFBcvR7n1YkcAMGY=,tag:ir0TU7YuxpPuew+nyC9/eg==,type:str]", + "version": "3.13.3" + } +} diff --git a/secrets/ha-server-1.keytab b/secrets/ha-server-1.keytab new file mode 100644 index 0000000..c1193cd --- /dev/null +++ b/secrets/ha-server-1.keytab @@ -0,0 +1,18 @@ +{ + "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+IFgyNTUxOSBIaDJyeGJKeDhVNU9xYzhk\nb2Z5K0VTbnM2OExVWWZPaVBoYkNrOVVwbjJBCnBmQlRsdjdPZHZydU14UDl2Nm5G\nUkJWd1N0SXljTW9Kb2VMVXpETGZlS1kKLS0tIEZaWkdrWkVqMENQUmpQOERSY0hQ\ncHU5anBjeUQ0U2ZFYmFaa3ozSmpGWXcKU8QpWAk7Zkc3N/ZmWn5xLsOSTAtMxU5W\nIQOvdDNzIn7Ecoj4zQ91T9puAhaO6PZ1wWjo+rAN0TyWjM+TAwTuFw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMbGg2WW1nOUw0RzhaNXNa\nbk1iWmRFWng5SlgwY1V1ZXZIcEp1SWlKYnlBCmJ1Q3Z1KzJoY2ZZWi9UckFOTWZC\nUGxZeUlJZDdRci9ZbzhsdTM4cXk0SjgKLS0tIEhWUy91bTZkTyt2ZFRIb2VKL1Rn\nMm0wN2RjUUJ3dHAxQU1nd0VRTGlQbUEKsb6AFoMZw+FxZW1A1rf4YUlNaODUgbS3\npZfGZIocS2y9uMTaXWQ56Y078zDD/F40wLYsuknm0ECVfNysSVacVw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1nxlnrevqs2msdatze562vz6ym4pgydt2zndye83lwqauy6flggtqrevyw5" + } + ], + "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]", + "version": "3.13.3" + } +} diff --git a/secrets/ha-server-1.yaml b/secrets/ha-server-1.yaml index f33120f..f947de9 100644 --- a/secrets/ha-server-1.yaml +++ b/secrets/ha-server-1.yaml @@ -1,6 +1,25 @@ -# STUB — not yet encrypted with sops. -# Bootstrap: -# bash scripts/secrets/sync-host-keys.sh proxmox-ha-server-1 -# sops updatekeys secrets/common.yaml (allows ha-server-1 to decrypt shared secrets) -# sops secrets/ha-server-1.yaml (create with: beszel-token) -beszel-token: REPLACE +beszel-token: ENC[AES256_GCM,data:tT2a1I6AhVgXWfc=,iv:1iXoOMAakHXpI2lYiipxcVSzXODUdpqYmfrpNKZwljg=,tag:Vuxxz7pyIsZYbkg2x6GvgQ==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzeEVTTVZ1ZHdVNWNNUW53 + amN2d1NhQ2lydVNJUXdyaG52ditNTHNJZmdZCnljakFpaW92a0xEbjNZSEhXdDcr + ZUN1MmN2dlA0T2lYTVNibjB6UExqU1UKLS0tIGliYmVicTBrWmE3dFZWVzhwUm9r + T2dUUUdUQUVzTWRvbzlWSExKRUorTGMKktkw3uuydGeChy+9pxtysHLvssZ5LKoK + 8i+CNr76/nyKCRns6tHj5lHYefY/B8IpdmlQ4fXQKSrna/dDjkdWTA== + -----END AGE ENCRYPTED FILE----- + recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0dmd5ZkdDcEM1akJna3Yv + RnZFYVJNZ1IvY2R0QnZ6WC9PQnFmVkdWNHpFClVJWnNaRFV1UVBQTWlXb1NEZEVp + Z0ZiUC8yTXVyMFdmRnB5OWdwQVVTSUEKLS0tIFFxLzdyNlZ2WlFPdEJFTTl0Mnhh + TXVMdVIvKzRTeGRIRXBqTUNxM0Z1cmsKMLHPN6n2i18LHkgjinq033qZsrt7BdFG + NuQA4EbCqg5uiHPCSGU6/EVk2XmfROrJz5eyMWIwrwCbt53jsUNTFw== + -----END AGE ENCRYPTED FILE----- + recipient: age1nxlnrevqs2msdatze562vz6ym4pgydt2zndye83lwqauy6flggtqrevyw5 + 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 diff --git a/secrets/ha-server-2.keytab b/secrets/ha-server-2.keytab new file mode 100644 index 0000000..6327b6e --- /dev/null +++ b/secrets/ha-server-2.keytab @@ -0,0 +1,18 @@ +{ + "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+IFgyNTUxOSA1QVJsNHBYaUt6WWFDRjFX\nYVkrMHRtd1pTVFJsano1TG9hVyt4NGZyUEY0Ck1RU2U3MTQ1YTVkaXVhNlM5clg5\nWDZKZ1duTHZrM3FxWldnVXB1alJBeGsKLS0tIC9wUmZWcG1yLzBIL05qQ3lVV0xp\nS2lhWWVaTU9NWjd0K2hEaTcwNWE5YjgKmy3SHvYur/2sn6PjVLZFYzHnVMTFe66r\nm7MsWeUXU6zs4o4RWPPNBrpsVXIGZJx5Gbl+lp9Zubn61Wt/KVqyfw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRNG5SQ0FvWWVvaGhOTmI1\naTE2anlXbTJDcHVBdWlxamlBam9jQlVyL2kwCmVJMkx2NnU4dno2T2dQOGUrRFRp\nQnl3TkJ2R0ZOS3Q2K2g3VDZQZ2VWS2cKLS0tIHgvdjJ3WURmcHVSQytHUGlwSnpm\nc1k2UHZORWt5TlJvaVp0YlpzWDdob00KKJ4bSM2CQD675b0ceEv9tz+wBtCIx/wz\n7Nr2CJoY68cB8KjM1eMzioZI1Rlt49tTQuD4XSs2/9Ej/dDDbP8p6A==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1scfc8p53q5aq2a87tcmsazmj8sfeft0s8kxg0et4nm3ucxyp3c3s6te82y" + } + ], + "lastmodified": "2026-07-28T06:45:59Z", + "mac": "ENC[AES256_GCM,data:13BJaaM1Zrcg8/SZToXdlEM8J4GtFZnkoVt6vBWoeLU58wsGi58jdEAqMzWpnT3Vf2Sb8OjNUB1cbBVv9uMitbffFR27y06diKkAICnVc4shS0czoO/6u9O0k9gXicU04jUr/FKLL+sSiNi2ypCFSZT1C3WY44sTB15RNYQR7XQ=,iv:/4owaf5HYndcojBy5rZv6LZp/ipe3UuXy34dq3XI4Qc=,tag:i3Qk8jdl5hUmLScLCirm9w==,type:str]", + "version": "3.13.3" + } +} diff --git a/secrets/ha-server-2.yaml b/secrets/ha-server-2.yaml index 19bbbe9..c508d19 100644 --- a/secrets/ha-server-2.yaml +++ b/secrets/ha-server-2.yaml @@ -1,6 +1,25 @@ -# STUB — not yet encrypted with sops. -# Bootstrap: -# bash scripts/secrets/sync-host-keys.sh proxmox-ha-server-2 -# sops updatekeys secrets/common.yaml (allows ha-server-2 to decrypt shared secrets) -# sops secrets/ha-server-2.yaml (create with: beszel-token) -beszel-token: REPLACE +beszel-token: ENC[AES256_GCM,data:0wooMaPiytDTGug=,iv:/WATBCEakrIvujt4oAeprw18W1n/WoK2zjpQdlVJl4Y=,tag:lhJa9hJgFqNFSB1Ogj5CtQ==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhdmNmSFlmei9KbDRWU3lj + TVJoSW9FRE9sWUdBRkNIMTB3aVNERUp0U0c4CnZUUzIvYm1xL1JNMGFKcWlBK2xT + SCt4TTdlU0ZralArcUVFZEh1YTRQaWcKLS0tIEtKbnBZVlkrU2U3VzlycS9odXVi + VjhXeWtZdDhpTG12cUZwVDhKeWZPMmsKOtXLVB8Z0faO05BPCVQXBn8gbMOZeHxC + sHmt3vMWO0jrxM0WLql59VWAFsczNwb1fw89NdZMfd/XoVq2EUV8dA== + -----END AGE ENCRYPTED FILE----- + recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBucCtBR3N3Qm9JbTlLdTZT + eTlxWEsxUWZsUnMrWi9zSDdRVWZWejBUWFNVCnpyVEVLbTllVHg1a2RXRkVRZ05z + ZWlsSTBDWFF3bzkzaTFMVDQrVW1yczgKLS0tIFhKMll2OVdzWTcyeWdoaU85Vmla + WWEwRFgxNDF5czA3Q01LZ0NjY2toaTQKKawEiHUWss0NPuJng6UimekEOlxipiyC + ielJbLxma8WPLeLwBukFOBOTyNFxgsVBvwmBJ2k1iMIg1lCjhLD7RA== + -----END AGE ENCRYPTED FILE----- + recipient: age1scfc8p53q5aq2a87tcmsazmj8sfeft0s8kxg0et4nm3ucxyp3c3s6te82y + 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 diff --git a/vars/per-machine/proxmox-ha-server-1/openssh/ssh_host_ed25519_key.pub/value b/vars/per-machine/proxmox-ha-server-1/openssh/ssh_host_ed25519_key.pub/value new file mode 100644 index 0000000..2c4fd87 --- /dev/null +++ b/vars/per-machine/proxmox-ha-server-1/openssh/ssh_host_ed25519_key.pub/value @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILdvVsj5cw/y6s2q5cPqKnULgnkEnsIQahrhm9DyCdd/ proxmox-ha-server-1 diff --git a/vars/per-machine/proxmox-ha-server-1/openssh/ssh_host_ed25519_key/secret b/vars/per-machine/proxmox-ha-server-1/openssh/ssh_host_ed25519_key/secret new file mode 100644 index 0000000..bd70e99 --- /dev/null +++ b/vars/per-machine/proxmox-ha-server-1/openssh/ssh_host_ed25519_key/secret @@ -0,0 +1,14 @@ +{ + "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" + } +} diff --git a/vars/per-machine/proxmox-ha-server-2/openssh/ssh_host_ed25519_key.pub/value b/vars/per-machine/proxmox-ha-server-2/openssh/ssh_host_ed25519_key.pub/value new file mode 100644 index 0000000..e413a19 --- /dev/null +++ b/vars/per-machine/proxmox-ha-server-2/openssh/ssh_host_ed25519_key.pub/value @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjgkgUFZfpTLkFQeEbqXlvktE641EexWLv2I4iVKyQi proxmox-ha-server-2 diff --git a/vars/per-machine/proxmox-ha-server-2/openssh/ssh_host_ed25519_key/secret b/vars/per-machine/proxmox-ha-server-2/openssh/ssh_host_ed25519_key/secret new file mode 100644 index 0000000..6e21f61 --- /dev/null +++ b/vars/per-machine/proxmox-ha-server-2/openssh/ssh_host_ed25519_key/secret @@ -0,0 +1,14 @@ +{ + "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" + } +}