Archived
Compare commits
41
Commits
+26
@@ -22,6 +22,8 @@ keys:
|
||||
- &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||
- &proxmox-server age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||
- &proxmox-ha-server-1 age1vwk8r03u46rpvmw5fy20t8ud9qdhm0nx5se5ayze98y55e7zg5yqc8862n
|
||||
- &proxmox-ha-server-2 age152vrhznh92hspusx9rqxt9c70u8w3uvlys4ymc7zuv3qnxjm5c7qmp72xp
|
||||
|
||||
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
|
||||
|
||||
@@ -6,12 +6,14 @@ This repository contains flake-based NixOS configurations for Wayne's LAN
|
||||
servers and workstation.
|
||||
|
||||
The flake exposes NixOS configurations named `<platform>-<buildtype>`
|
||||
(platforms: `linode`, `proxmox`, `lxc`; build types: `minimal`, `nix-cache`,
|
||||
`server`, `docker`, `gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`), generated from `modules/platforms/*`
|
||||
and `modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not
|
||||
every combination is built — `pxe-boot` has no `linode` variant. See
|
||||
`README.md` for the full current target list; treat `flake.nix` as the
|
||||
source of truth since this list can drift.
|
||||
(platforms: `linode`, `proxmox`, `lxc`, `baremetal`; build types: `minimal`,
|
||||
`nix-cache`, `server`, `docker`, `gui`, `pxe-boot`, `tailscale-router`,
|
||||
`tor-relay`, `ha-server`), generated from `modules/platforms/*` and
|
||||
`modules/build-types/*` by the `mkTarget` function in `flake.nix`. Not every
|
||||
combination is built — `pxe-boot` has no `linode` variant, `ha-server` only
|
||||
exists on `proxmox`, and `tor-relay` only exists on `lxc`. See `README.md`
|
||||
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
|
||||
install commands from this repository unless explicitly asked.
|
||||
|
||||
@@ -10,13 +10,13 @@ pieces composed in `flake.nix`:
|
||||
|
||||
- **Platforms** (what it runs on): `linode`, `proxmox`, `lxc`, `baremetal`
|
||||
- **Build types** (what it's for): `minimal`, `nix-cache`, `server`, `docker`,
|
||||
`gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
|
||||
`gui`, `pxe-boot`, `tailscale-router`, `tor-relay`, `ha-server`
|
||||
|
||||
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,
|
||||
`tor-relay` currently only exists as `lxc-tor-relay`, and `baremetal`
|
||||
currently only exists as `baremetal-gui` (the real gui-host hardware). The
|
||||
full list:
|
||||
`tor-relay` and `ha-server` currently only exist on `lxc`/`proxmox`, and
|
||||
`baremetal` currently only exists as `baremetal-gui` (the real gui-host
|
||||
hardware). The full list:
|
||||
|
||||
| Target | Purpose |
|
||||
| --- | --- |
|
||||
@@ -29,8 +29,9 @@ full list:
|
||||
| `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 |
|
||||
| `proxmox-pxe-boot` / `lxc-pxe-boot` | HTTP/iPXE boot asset host — previously the flat `pxe-boot` target |
|
||||
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node |
|
||||
| `linode-tailscale-router` / `proxmox-tailscale-router` / `lxc-tailscale-router` | Tailscale subnet router + MagicDNS forwarder for the LAN |
|
||||
| `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
|
||||
anywhere in this repo — that's live infrastructure state, not something a
|
||||
@@ -47,8 +48,7 @@ section for which is which.
|
||||
|
||||
Each buildtype's `hosts/<name>/host.nix` carries the per-machine identity
|
||||
(hostname, hostId, per-machine secrets, `system.stateVersion`) that must stay
|
||||
fixed regardless of which platform it's built for — see
|
||||
`flake-target-refactor-spec.md` for the full rationale. Every deployed host
|
||||
fixed regardless of which platform it's built for. Every deployed host
|
||||
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
|
||||
right one even after a platform migration changes the flake attribute name.
|
||||
@@ -167,7 +167,6 @@ per-boot host key for sops-nix to derive from on ephemeral media) — see
|
||||
(`vars/per-machine/<target>/openssh/`, committed and sops-encrypted) for
|
||||
their SSH host keys.
|
||||
|
||||
This repository's git *history* still contains secrets committed before this
|
||||
migration (see `remove-sensetive-info-refactor.md`) — those are being
|
||||
scrubbed and rotated separately; don't treat the repo as safe to make public
|
||||
until that's finished.
|
||||
This repository's git *history* still contains secrets committed before the
|
||||
sops-nix migration — those are being scrubbed and rotated separately; don't
|
||||
treat the repo as safe to make public until that's finished.
|
||||
|
||||
@@ -22,7 +22,7 @@ see "LXC hosts" immediately below for why those are different.**
|
||||
## LXC hosts
|
||||
|
||||
`lxc-*` targets (`lxc-minimal`, `lxc-nix-cache`, `lxc-server`, `lxc-docker`,
|
||||
`lxc-gui`, `lxc-pxe-boot`, `lxc-tailscale-exit-node`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the
|
||||
`lxc-gui`, `lxc-pxe-boot`, `lxc-tailscale-router`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the
|
||||
interactive menu deliberately excludes them. Don't try to select one there;
|
||||
`nixos-install` would bind-mount `/` onto `/mnt` (LXC containers have no raw
|
||||
disk to partition) and then refuse to touch the filesystem it's currently
|
||||
@@ -133,13 +133,15 @@ Flake outputs:
|
||||
```nix
|
||||
nixosConfigurations.installer # ISO/netboot installer image
|
||||
|
||||
packages.x86_64-linux.iso # installer ISO/netboot image
|
||||
packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
|
||||
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-minimal # vanilla NixOS minimal netboot bundle (no installer wiring)
|
||||
```
|
||||
|
||||
```sh
|
||||
nix build .#iso
|
||||
nix build .#pxe
|
||||
nix build .#pxe-minimal
|
||||
```
|
||||
|
||||
There's no `nixosConfigurations.proxmox-lxc` (installer-boots-as-an-LXC-
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
};
|
||||
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
|
||||
# be bumped on an already-installed machine.
|
||||
system.stateVersion = "25.05";
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
};
|
||||
|
||||
# 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";
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
};
|
||||
|
||||
# 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";
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
};
|
||||
|
||||
services.beszel.agent.environment = {
|
||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
};
|
||||
|
||||
services.beszel.agent.environment = {
|
||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||
EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
LOG_LEVEL = "debug";
|
||||
|
||||
@@ -81,4 +81,70 @@
|
||||
programs.firefox.enable = 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,27 +14,32 @@
|
||||
# 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
|
||||
# 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 = [
|
||||
../ha/pacemaker-stack.nix
|
||||
../ha/iscsi-target.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 = {
|
||||
enable = true;
|
||||
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}
|
||||
'';
|
||||
exports = mkNfsExports vars.haStorageRoot;
|
||||
};
|
||||
|
||||
# Pacemaker controls nfs-server — prevent systemd from starting it at boot
|
||||
|
||||
@@ -13,6 +13,15 @@ let
|
||||
lib.concatMap (share: ancestors share.subpath)
|
||||
(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
|
||||
{
|
||||
imports = [
|
||||
@@ -95,16 +104,7 @@ in
|
||||
|
||||
services.nfs.server = {
|
||||
enable = true;
|
||||
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}
|
||||
'';
|
||||
exports = mkNfsExports vars.storageRoot;
|
||||
};
|
||||
|
||||
# mountd (20048) is needed for showmount/NFSv3 mount protocol — without it
|
||||
|
||||
@@ -25,13 +25,10 @@ let
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports =
|
||||
[
|
||||
# Include the results of the hardware scan.
|
||||
# ./hardware-configuration.nix
|
||||
./set-locale.nix
|
||||
../ipa/client.nix
|
||||
];
|
||||
imports = [
|
||||
./set-locale.nix
|
||||
../ipa/client.nix
|
||||
];
|
||||
|
||||
# System-wide shell config so all users (including IPA accounts) get the
|
||||
# same management aliases as the local nixos user's Home Manager provides.
|
||||
@@ -42,11 +39,7 @@ in
|
||||
};
|
||||
interactiveShellInit = buildImageFn;
|
||||
};
|
||||
# Use the GRUB 2 boot loader.
|
||||
# boot.loader.grub.enable = true;
|
||||
#boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
|
||||
|
||||
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
# Recommended over the true default (bypasses ZFS's own import safeguards)
|
||||
# per the option's own docs; matches hosts/docker/host.nix and
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# 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" ]; };
|
||||
};
|
||||
}
|
||||
@@ -1,23 +1,45 @@
|
||||
{ pkgs, vars, ... }:
|
||||
{ lib, pkgs, vars, ... }:
|
||||
|
||||
let
|
||||
gid = toString vars.dockerAccessGid;
|
||||
in
|
||||
{
|
||||
# virtualisation.docker.enable = true;
|
||||
virtualisation.docker = {
|
||||
enable = true;
|
||||
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; [
|
||||
docker-compose
|
||||
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
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,14 +11,38 @@
|
||||
# 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 -" ];
|
||||
|
||||
# Prevent drbd.service from auto-starting at boot / nixos-rebuild switch.
|
||||
# Pacemaker's OCF drbd agent calls drbdadm up/down directly when managing
|
||||
# the resource. If drbd.service also runs drbdadm up all while DRBD is
|
||||
# already Primary under Pacemaker, apply-al fails with "device busy" (exit 20).
|
||||
systemd.services.drbd.wantedBy = lib.mkForce [];
|
||||
|
||||
services.drbd = {
|
||||
enable = true;
|
||||
config = ''
|
||||
@@ -37,6 +61,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
+116
-94
@@ -38,47 +38,122 @@ lib.mkIf enabled {
|
||||
networking.domain = lib.mkDefault vars.homeDomain;
|
||||
networking.nameservers = lib.mkDefault [ vars.domainControllerIp ];
|
||||
|
||||
security.ipa = {
|
||||
enable = true;
|
||||
domain = vars.homeDomain;
|
||||
inherit realm;
|
||||
server = vars.ipaServer;
|
||||
certificate = caCertPkg;
|
||||
inherit basedn;
|
||||
ipaHostname = fqdn;
|
||||
offlinePasswords = true;
|
||||
cacheCredentials = true;
|
||||
security = {
|
||||
ipa = {
|
||||
enable = true;
|
||||
domain = vars.homeDomain;
|
||||
inherit realm;
|
||||
server = vars.ipaServer;
|
||||
certificate = caCertPkg;
|
||||
inherit basedn;
|
||||
ipaHostname = fqdn;
|
||||
offlinePasswords = 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" ]; }];
|
||||
}];
|
||||
};
|
||||
|
||||
# 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
|
||||
# host. sss_ssh_authorizedkeys queries SSSD (which queries IPA LDAP).
|
||||
#
|
||||
# /nix/store is 1775 (group-writable by nixbld). OpenSSH 10.0+ rejects
|
||||
# AuthorizedKeysCommand binaries whose path contains any group-writable
|
||||
# component, silently skipping the command. Copy to /usr/local/bin (all
|
||||
# components root-owned, 755) so the path passes sshd's safety check.
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /usr/local 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"
|
||||
# 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} - -"
|
||||
];
|
||||
systemd = {
|
||||
# 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
|
||||
# host. sss_ssh_authorizedkeys queries SSSD (which queries IPA LDAP).
|
||||
#
|
||||
# /nix/store is 1775 (group-writable by nixbld). OpenSSH 10.0+ rejects
|
||||
# AuthorizedKeysCommand binaries whose path contains any group-writable
|
||||
# component, silently skipping the command. Copy to /usr/local/bin (all
|
||||
# components root-owned, 755) so the path passes sshd's safety check.
|
||||
tmpfiles.rules = [
|
||||
"d /usr/local 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"
|
||||
# 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 = ''
|
||||
AuthorizedKeysCommand /usr/local/bin/sss_ssh_authorizedkeys %u
|
||||
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.
|
||||
# Placed at /etc/krb5.keytab before SSSD starts so the host authenticates
|
||||
# to IPA without running ipa-client-install.
|
||||
@@ -92,44 +167,6 @@ lib.mkIf enabled {
|
||||
restartUnits = [ "sssd.service" ];
|
||||
};
|
||||
|
||||
# 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.
|
||||
systemd.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 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.
|
||||
# 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
|
||||
@@ -137,35 +174,20 @@ lib.mkIf enabled {
|
||||
# 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).
|
||||
# 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.
|
||||
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-<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.
|
||||
systemd.services."home-manager-${vars.ipaUser}".unitConfig.ConditionPathExists =
|
||||
"/home/${vars.ipaUser}";
|
||||
|
||||
security.sudo.extraRules = [{
|
||||
users = [ vars.ipaUser ];
|
||||
commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
|
||||
}];
|
||||
|
||||
# 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)
|
||||
@@ -180,7 +202,7 @@ lib.mkIf enabled {
|
||||
homeDirectory = lib.mkForce "/home/${vars.ipaUser}";
|
||||
stateVersion = "26.05";
|
||||
packages = with pkgs; [ tmux sshfs ];
|
||||
sessionVariables.EDITOR = "nano";
|
||||
sessionVariables.EDITOR = lib.mkDefault "nano";
|
||||
};
|
||||
programs.home-manager.enable = true;
|
||||
programs.bash.enable = true;
|
||||
|
||||
@@ -52,6 +52,7 @@ in
|
||||
# LXC container does).
|
||||
imports = [
|
||||
(modulesPath + "/virtualisation/proxmox-lxc.nix")
|
||||
../common/preserve-ssh-host-key.nix
|
||||
];
|
||||
|
||||
proxmoxLXC = {
|
||||
@@ -105,49 +106,6 @@ 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
|
||||
# store DB via a systemd service (register-nix-paths) -- it never runs
|
||||
# an activation script at all. Confirmed live this means neither
|
||||
|
||||
@@ -34,6 +34,7 @@ in
|
||||
../hardware-configuration/vm/proxmox.nix
|
||||
../boot/efi.nix
|
||||
../disko/proxmox.nix
|
||||
../common/preserve-ssh-host-key.nix
|
||||
];
|
||||
|
||||
environment.etc = lib.mkIf hasKeyForThisTarget {
|
||||
@@ -46,36 +47,4 @@ in
|
||||
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" ]; };
|
||||
};
|
||||
}
|
||||
|
||||
Regular → Executable
+87
-60
@@ -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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
Regular → Executable
Regular → Executable
+149
-85
@@ -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,15 +99,20 @@ 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..."
|
||||
log "Restarting corosync and pacemaker on both nodes..."
|
||||
systemctl restart corosync
|
||||
ssh "root@${NODE2_IP}" "systemctl restart corosync"
|
||||
n2_ssh "systemctl restart corosync"
|
||||
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 ────────────────────────────────────────────────────
|
||||
log "Waiting for corosync quorum..."
|
||||
for i in $(seq 1 30); do
|
||||
@@ -99,21 +135,27 @@ for i in $(seq 1 30); do
|
||||
done
|
||||
|
||||
# ── 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..."
|
||||
if ! drbdadm dstate ha-data 2>/dev/null | grep -q "UpToDate\|Inconsistent\|Diskless"; then
|
||||
drbdadm create-md ha-data --force
|
||||
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 +179,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 +195,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 <<EOF
|
||||
/backstores/fileio create name=ha-lun0 file_or_dev=${ISCSI_LUN_FILE} size=0 write_back=false
|
||||
/iscsi create ${ISCSI_IQN}
|
||||
/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 demo_mode_write_protect=0
|
||||
saveconfig /etc/target/saveconfig.json
|
||||
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..."
|
||||
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 | grep -q "^Primary" && 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 "
|
||||
<resources>
|
||||
<clone id=\"ms-drbd0\" globally-unique=\"false\">
|
||||
<meta_attributes id=\"ms-drbd0-meta\">
|
||||
<nvpair id=\"ms-drbd0-promotable\" name=\"promotable\" value=\"true\"/>
|
||||
<nvpair id=\"ms-drbd0-master-max\" name=\"master-max\" value=\"1\"/>
|
||||
<nvpair id=\"ms-drbd0-master-node-max\" name=\"master-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\"/>
|
||||
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 <meta_attributes>, not a direct <clone> 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 '<resources>
|
||||
<clone id="ms-drbd0">
|
||||
<meta_attributes id="ms-drbd0-meta">
|
||||
<nvpair id="ms-drbd0-globally-unique" name="globally-unique" value="false"/>
|
||||
<nvpair id="ms-drbd0-promotable" name="promotable" value="true"/>
|
||||
<nvpair id="ms-drbd0-promoted-max" name="promoted-max" value="1"/>
|
||||
<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>
|
||||
<primitive id=\"drbd0\" class=\"ocf\" type=\"drbd\" provider=\"linbit\">
|
||||
<instance_attributes id=\"drbd0-attrs\">
|
||||
<nvpair id=\"drbd0-resource\" name=\"drbd_resource\" value=\"ha-data\"/>
|
||||
<primitive id="drbd0" class="ocf" type="drbd" provider="linbit">
|
||||
<instance_attributes id="drbd0-attrs">
|
||||
<nvpair id="drbd0-resource" name="drbd_resource" value="ha-data"/>
|
||||
</instance_attributes>
|
||||
<operations>
|
||||
<op id=\"drbd0-start\" name=\"start\" interval=\"0\" timeout=\"240s\"/>
|
||||
<op id=\"drbd0-stop\" name=\"stop\" interval=\"0\" timeout=\"120s\"/>
|
||||
<op id=\"drbd0-promote\" name=\"promote\" interval=\"0\" timeout=\"90s\"/>
|
||||
<op id=\"drbd0-demote\" name=\"demote\" interval=\"0\" timeout=\"90s\"/>
|
||||
<op id=\"drbd0-monitor-master\" name=\"monitor\" interval=\"20s\" timeout=\"20s\" role=\"Promoted\"/>
|
||||
<op id=\"drbd0-monitor-slave\" name=\"monitor\" interval=\"30s\" timeout=\"20s\" role=\"Unpromoted\"/>
|
||||
<op id="drbd0-start" name="start" interval="0" timeout="240s"/>
|
||||
<op id="drbd0-stop" name="stop" interval="0" timeout="120s"/>
|
||||
<op id="drbd0-promote" name="promote" interval="0" timeout="240s"/>
|
||||
<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-unpromoted" name="monitor" interval="30s" timeout="20s" role="Unpromoted"/>
|
||||
</operations>
|
||||
</primitive>
|
||||
</clone>
|
||||
<group id=\"ha-group\">
|
||||
<primitive id=\"xfs-data\" class=\"ocf\" type=\"Filesystem\" provider=\"heartbeat\">
|
||||
<instance_attributes id=\"xfs-data-attrs\">
|
||||
<nvpair id=\"xfs-data-device\" name=\"device\" value=\"${DRBD_DEVICE}\"/>
|
||||
<nvpair id=\"xfs-data-directory\" name=\"directory\" value=\"${XFS_MOUNT}\"/>
|
||||
<nvpair id=\"xfs-data-fstype\" name=\"fstype\" value=\"xfs\"/>
|
||||
<nvpair id=\"xfs-data-options\" name=\"options\" value=\"defaults\"/>
|
||||
<nvpair id=\"xfs-data-force_unmount\" name=\"force_unmount\" value=\"false\"/>
|
||||
<group id="ha-group">
|
||||
<primitive id="xfs-data" class="ocf" type="Filesystem" provider="heartbeat">
|
||||
<instance_attributes id="xfs-data-attrs">
|
||||
<nvpair id="xfs-data-device" name="device" value="/dev/drbd0"/>
|
||||
<nvpair id="xfs-data-directory" name="directory" value="/srv/ha-data"/>
|
||||
<nvpair id="xfs-data-fstype" name="fstype" value="xfs"/>
|
||||
<nvpair id="xfs-data-options" name="options" value="defaults"/>
|
||||
<nvpair id="xfs-data-force_unmount" name="force_unmount" value="true"/>
|
||||
</instance_attributes>
|
||||
<operations>
|
||||
<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-monitor\" name=\"monitor\" interval=\"20s\" timeout=\"40s\"/>
|
||||
<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-monitor" name="monitor" interval="20s" timeout="40s"/>
|
||||
</operations>
|
||||
</primitive>
|
||||
<primitive id=\"iscsi-target\" class=\"systemd\" type=\"targetctl\">
|
||||
<primitive id="iscsi-target" class="systemd" type="targetctl">
|
||||
<operations>
|
||||
<op id=\"iscsi-start\" name=\"start\" 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-start" name="start" interval="0" timeout="60s"/>
|
||||
<op id="iscsi-stop" name="stop" interval="0" timeout="60s"/>
|
||||
<op id="iscsi-monitor" name="monitor" interval="20s" timeout="40s"/>
|
||||
</operations>
|
||||
</primitive>
|
||||
<primitive id=\"nfs-server\" class=\"systemd\" type=\"nfs-server\">
|
||||
<primitive id="nfs-server" class="systemd" type="nfs-server">
|
||||
<operations>
|
||||
<op id=\"nfs-start\" name=\"start\" 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-start" name="start" interval="0" timeout="60s"/>
|
||||
<op id="nfs-stop" name="stop" interval="0" timeout="60s"/>
|
||||
<op id="nfs-monitor" name="monitor" interval="30s" timeout="40s"/>
|
||||
</operations>
|
||||
</primitive>
|
||||
<primitive id=\"vip\" class=\"ocf\" type=\"IPaddr2\" provider=\"heartbeat\">
|
||||
<instance_attributes id=\"vip-attrs\">
|
||||
<nvpair id=\"vip-ip\" name=\"ip\" value=\"${VIP}\"/>
|
||||
<nvpair id=\"vip-cidr\" name=\"cidr_netmask\" value=\"24\"/>
|
||||
<primitive id="vip" class="ocf" type="IPaddr2" provider="heartbeat">
|
||||
<instance_attributes id="vip-attrs">
|
||||
<nvpair id="vip-ip" name="ip" value="192.168.2.229"/>
|
||||
<nvpair id="vip-cidr" name="cidr_netmask" value="24"/>
|
||||
</instance_attributes>
|
||||
<operations>
|
||||
<op id=\"vip-start\" name=\"start\" 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-start" name="start" interval="0" timeout="20s"/>
|
||||
<op id="vip-stop" name="stop" interval="0" timeout="20s"/>
|
||||
<op id="vip-monitor" name="monitor" interval="10s" timeout="20s"/>
|
||||
</operations>
|
||||
</primitive>
|
||||
</group>
|
||||
</resources>
|
||||
"
|
||||
</resources>'
|
||||
|
||||
log "Adding ordering and colocation constraints..."
|
||||
cibadmin --create --scope constraints --xml-text "
|
||||
<constraints>
|
||||
<rsc_order id=\"order-drbd-group\" first=\"ms-drbd0\" first-action=\"promote\" then=\"ha-group\" then-action=\"start\"/>
|
||||
<rsc_colocation id=\"coloc-group-with-drbd\" rsc=\"ha-group\" with-rsc=\"ms-drbd0\" with-rsc-role=\"Master\" score=\"INFINITY\"/>
|
||||
</constraints>
|
||||
"
|
||||
log "Adding Pacemaker ordering and colocation constraints..."
|
||||
cibadmin --replace --scope constraints --xml-text '<constraints>
|
||||
<rsc_order id="order-drbd-group" first="ms-drbd0" first-action="promote" then="ha-group" then-action="start" kind="Mandatory"/>
|
||||
<rsc_colocation id="coloc-group-with-drbd" score="INFINITY" rsc="ha-group" with-rsc="ms-drbd0" with-rsc-role="Promoted"/>
|
||||
</constraints>'
|
||||
|
||||
log "Waiting for resources to start..."
|
||||
for i in $(seq 1 60); do
|
||||
|
||||
Executable
+410
@@ -0,0 +1,410 @@
|
||||
#!/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."
|
||||
Regular → Executable
+133
-115
@@ -5,211 +5,229 @@ sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrMGd3ZVNlNXdmOUZMUzdQ
|
||||
TW1acEs0NFA2Q01rM2dkc1h0NHkzQmhiWFZZCjMyK202VWdlaGhsZW04MnVwUVdO
|
||||
alA0Q2FETThsYkhSS0hKdHBaS3VaY28KLS0tIG1Gdk8yalREOUtIZTUyY2p1UHlJ
|
||||
eG5iQnJsaTJBY3Y1dkw1c0VEaDQwdDQKfV04fLy32Lp2ZQ2VnvQ0h/Vsf+qdaJiv
|
||||
DnLXGZ9hE5yzpKWkQIRgqYGBkF8PkH0YC4OIaVkA53wrtjqS4ZHR9Q==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrTU5DL0k1M1U0U3NMRkZR
|
||||
YXkwQlYzS3Z1MHNTRW15akd6NjNzRWhPWGh3Cm5CMmJaMmZMS0UyZW93MGxKUzhu
|
||||
b0s0MVQ5d0VZOUNEaWpoTU9RaUFtR1EKLS0tIDZMbC9kUWM4ZWIraHQ5N3JZeGFV
|
||||
dzNyTW9wL1RwWDFzNlY5dExvSmJaYjgKFdbpy/iTuTa6rt5FzY44UuOI8Ew7mhom
|
||||
I0EP1r+j9cTzyeIMTEs/5jZZlWpdfYBGgtpSdyvMkmiLmqWSYYq6LQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWFNRY2FiK3VkSm1RdHBn
|
||||
cWl1ZStLcTRFZWY5VVI5N0FhODZvR1ltM0FvCnBHZUtTUm9QeHNlbVBoZEx1V3Fa
|
||||
Nk9iMmJKVnhocEpERi9leE1ySUtNMFkKLS0tIDRRYkxnbU90S2RyMHdJNzRJNXBi
|
||||
QjRmZFhVakVic2tYODZHcWtJRmNQTDQK7G8eSJInt11P0DiL9uzNQ/ZHHLVNIYPe
|
||||
bvlhuGkEuQ/+j5sVSKOfSI2Y7CvM7TpE3APyKBcLG3ajYg6F/Ev3SA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLajJ5UlRGbXRoUWlPRFpy
|
||||
cGQxazF0VTQyOW9EZXpWRkUzS2ZEQ3Vmc21RCks5T050ZlJTWHFwV3BhcmQrQVR1
|
||||
MHJkUW9qc3NzbDBYTE8vT09jdUpuY1kKLS0tIDJ3elBDZzN4bDNPSVI0SmZwckZ0
|
||||
Z2J5M09PVFloZ1BIM0tSQUxocWlORDAK92ZjF+N3Om1hoAulxYk+PLS9AAf1QCNJ
|
||||
Q8scKKrLKoFce35djjdLxUgit9jkFKklFRw/E1nIAcLnIVvLzOv1pA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1S3dOcFdwV2NoMW1oMnY1
|
||||
VGdxeXJVR1lsbzNVZHlGb0NGOVo0SndiakVjCmg0QlNnZDV5RlFja2hCbVRXV1VF
|
||||
S1ZtbC9KU0U1ZW9zeVoyR3hxNW1XTFUKLS0tIFUzWTJhTzM4QnpWV3h1OFU0N3BK
|
||||
RnF1N2k0S0lIVitoNDJLUmZqdHRzZVkKUfNg24p8zxb3749v/A1BOKCNw75AUKpf
|
||||
RUmFCw5DDWF2aNM0mZqcjjVmJ/FRKV2HXwwUGsHPKSOTnKfOUlPNKA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVaVpUWTZKN3UrNkN1Tno3
|
||||
NDdhS2VEb0NBYnEvVDZLUVBXbmRWd2RBYTF3ClJKRHhGNUFBclJmWGQ1ZFRSSkln
|
||||
d0ErNGxab0JIcGoxbkt3NTN3V3VIZGcKLS0tIHUvNkM4QS9xT3NwZzhrV3pGMUR2
|
||||
bTJvNmh4SHR2Z3VGY2tJYkFMVWVVd2cKQSwAv+1WFEugf4E1XGFDBUECKONY5+04
|
||||
NKdm1L72XHCj5BDgn4+/vIyqkvbHdokTk5P9grZ1w9/VvVF4FegZSA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzazl5MUpUNEVNMTdLWXBp
|
||||
c1hkaVhjcTZXTUNsWS96QVVWb0RVQlZ4VlNvCnE3TFRySU5jTFk2WjBONUQyQUhl
|
||||
U1lNTDFTRmZhMUFyZmpVY2xpaUVxRW8KLS0tIE9MQ1M2U2ZXQmRCVll2UGRWL1RG
|
||||
K2tnU0NOKzU5dkpiekF0Vk1VM0ZZZDgKK13aFypGAqrKWPOr3UwtXI1EoXf1+UzS
|
||||
rBqcwnX6WPxSKUwWoins4Aojek4QhbhY4R5ei6rRS0KEQeryGxy8bg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxL3k2enk5YnF4eDErREs0
|
||||
alZLMzdMZ2NLdk9oSkFCUk1OVXB4UmhkV1NJCjhwdzJQckJpQ3kvRGszaFU4dWxM
|
||||
QVREeFdWeWNjOGVPWXZEV0ZySGtuR0EKLS0tIE5PV3pnTGNzUWd6M1NETFZkT0Ry
|
||||
VUVQQzJGUXVEejc0T09LdXlKNG10UVEKuCZVs99ugmPtFx3WFyC5nOXMfbeUblKv
|
||||
D6TBWGfx5THp8YZKzshKMCUnU+auomfTYxWphTvjYHlO13O5HJtGTw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZUpVT1p4TGZyNC9qMm5G
|
||||
UzNHV08rWURjY2lqS3FTWFdoc0FYYTFjb0NBCk1mZ2JzaXk5RmExNE9xWGZ5K0pv
|
||||
UEtqMkltV0dIWll5eVBUVVRNOUNDWUUKLS0tIGpKNVJudUM1UGNvaGl1UDBOeFA1
|
||||
RjUyRlZ6a0Y4SXNsL21zSURVRk9KTFEKU1L6BQ6ZlYQQtqx3uF/uM5CQ1ercmvRT
|
||||
TL3r2/Y07gE7CjRn3pR9z0co8KndGzxV6YR+ubyWptwBS8KQh5stkw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxSTBVOGtSeGd5UUxKbjBp
|
||||
SEhhMk5EaUpZTXF3RzJWc20wQzlOaWRaWGtnCjVFOTVpTGlvcUY0NlEzVUlod2Jv
|
||||
UXJzVUpHeUcxMThLcE9TWTRNUUZNRDQKLS0tIG04MklGcjJ0RlVmTzIzN3JhVGlh
|
||||
eVdDdHc2d1U2WmkrVzJJY0lxeXI2M1UKOBh1bUTieM+8cQ+SHVHXqkeOChkA5flu
|
||||
WmzhTmtv5vYR4D2KYVbf/E+F2SP+DNCeditO6rcoA3LU9lgA3INh1A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsaWxxWS9xQXViY3VnUEx2
|
||||
V09FbTI0WGtNbW0yclhOSGZDbG5NUTNaTkFFCitkcjJ3OE9BSnN4bjFWcE9nYVBk
|
||||
ZzMyVHlJQ2wwdU5JOXdCQm9oNkhNd2MKLS0tIE9tRzFYS05vSkUwWFRkaTdtc0k0
|
||||
blVoMWV0QklBVkluT0Z4NHYyS1F0blUKO+Uc0of/V77ZUZOsxTzeH8/LmmAOQt+J
|
||||
x/COHxnLCnZ4eWI6q1a0Qn5Br15OJYTxUI2QTV4goTnXBNUDo9wdpQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0Y0h5Y0tnTVg0WmhKMFBi
|
||||
ZkxFOTNwMFFKdkFzYWp1eWhtNEtLd1VRQjBBCk43WEdhY0VxanZoR2wxeUxoUm1I
|
||||
Z0tPb0hlNGhIbG5jVFR6Wkx1Z0h4WVEKLS0tIDZGNUN3Z3AzL3Z1S0dKOEtmTUdn
|
||||
d0dheWdPOFE4SEZvNGRJWTJxMXNtZFUKb3dxkO9fhJBlj8WWAjBB/lnIn3s+4p0D
|
||||
ONEHjNb0PSHOx8VgRvPugZGDZWAnwSgJaWkcSynnsJMjELg3ShREsQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsLzFnZkdVdGQ2dHRwcnRt
|
||||
Ui8rc1NoQzNKSHVzTk0vYU1vMlRxcjBKZ2hNCmhLYXVGSis4RU9HNGVxZkpvUkd5
|
||||
L0xvalhDYTQ4N21OcHRheTlkaUxvTkEKLS0tIFVkRGxtLzhQT0paV2U3ZnNScVdn
|
||||
alZnaVppeGI3OUVscGpONkk3YTRXd3MK61na8x5qX7+dyMHasDz2dj7yeaUlX8me
|
||||
N4/SIk1JDBhv9G7mdKLbKhSF1UJrSY7TJqJqx8/dqEc0uG3vptA1ew==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrdElscmwrSzZCTEJENmpk
|
||||
Vk4zUXI4Rkp1RU9MSWZGRUtoZXk2NmJsNkJBCmg5ZHFNTitGbW9ubmthWmNFT2Ri
|
||||
anNFUnRDSi9KeGdNdXphVXV6c2UvV0kKLS0tIEEveHJSTFlxN0dPMTRqOWhnNzhB
|
||||
d1RqVlRuMmdoaDFlL0pHcTRocHNuQUEKGdFFrKRrgfTCGMUpfbNvhnMLQ2bvV8Y/
|
||||
NzKji6MW9m4AvQLfp9Iz1N6wNpHHDeW0Kf0dL/z+9fyKhbr9FIXomg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZEtvOUhMU1FRWWpJQjF4
|
||||
NWphNXp5M0dLZXhkZndhT1Y3L09maytHazJVCm1MeEtMWXg1Zjg3bFVnZEorci9J
|
||||
bkNZQU9Ta1dDTFFHaGFWQVBpK3pYRDQKLS0tIEhPVGliRDR3ZTF2aEl3ZnJEYWtR
|
||||
anh0SEpnVW8xdXNkZEZQSjcxU1BHMFEKVRJUA71fi1QawB2TnuTWMYhzQR18u4M2
|
||||
s1V4j4TwYyyKZFoNvt8kOUayjC499c5OBUufYs6G2ciC6gK2A9E0EQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjMDJYUDFBbTY0QlhwVmcy
|
||||
ciswM2k3ZWVHVU4rN2l3a1B3cldhOXhaVVhrCmJRZE5sUTJHeGpLUmR3TU1tb1Qx
|
||||
OW5QVGZudVdYNkxCaENFWGpwMU9aNncKLS0tIFRZVkRTdjArTStscm8xS0N2VzhK
|
||||
VUtlSjY5V1J6NzhPSVBTNTlrS3paNmcK7nYxAhtKrI1o+DUgNZgr+9iXdwcxeipp
|
||||
VSqcdzwD6VyM/3d0yOS6G+TxQLsSJsyCOfE/rkqtSYfilA2TUCPw5A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTV2IzSjBEMjUwZjB0dCtj
|
||||
aTdZVlEvYkVRbTNjUC9ZRVdNZVhPQTJEd1ZZClc3NDJiR1BVYkZkdVVxMVZGc0VN
|
||||
dWdSSXBFR2xxR0xrV2thRmUwSS96S0UKLS0tIHRRTXVlUi9UYnFRRlhsU21HZVY4
|
||||
SDFYd0NwVEtVZXNsWUI1a1ZZU2xNRGMKuQUhOq2FRD+PGn5OkdODZItbxCzRKjne
|
||||
E60UOYtHjanuGjJ1svuR9cYsLZz7lLOwItklecYaQYpMRZEwzzBGCQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYOTMyWFJqRm1mQXowZHVD
|
||||
emlTYWVsMUpRYkNwWHREb081bXpZalFyd3hVCklONWcxVkJLNTcrdERvUzRlY1hX
|
||||
OFpCMGtWQTJ0UndFZzZMdFRoRmJPSGcKLS0tIGFNRjhYLzlhOHdZUXcvOFI1ZlFV
|
||||
NTl3V2NzYkZ5SkhITDlrQWx0QTU2YWMKBORbbtYWLsxu8Gq+cHwaZjlZpzsT5rao
|
||||
BqMHZJnBJJMtzqbk9d59y2xrsEt1o/RS273IhGvNTAlfV75ET+s+/w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSjJoUVUyd1JqRm1ZQzZx
|
||||
SFJ2cWNUY0E4b3FndVA5Y0hIOEZnZUlVWlhBCm1vY1luOXZBelRUTmF5Y1NMeDBn
|
||||
cE1BTDErc041UjJCWTBQbnk0Wk80dkEKLS0tIDVSZzd1UktvZGdyanFUMkVORUtl
|
||||
eVB5TnJkMlp6dUpXSTlxRlplZ2NxUlEK0AYOxIbswjM0SUASDfmZ7PqcEU844fgI
|
||||
ycFWVSEPodwUZ6UFoYXhHlJzHFcgpLvwUd1PMktLHe1qrZ7GOQJIMA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzVUxZUWhoN1ZCeHB0bW4z
|
||||
MEs0SFc0elhHcEsxSFgwMEFMTEN5VDl2OGtzCm9GYmJQaWFDR2hVSWRDd3hGN0tj
|
||||
OHpUTXAvb1dQMXFYRjhHbjl4QW1EQWcKLS0tIFNTSHJleGMwM0RPY1pjc05nejAw
|
||||
aWMzUzhvSGNxV2xyay8zQ0RsNno5OGcK+ERfAhPpghG5fWuuTgFp8bQSewQVVz3n
|
||||
nArfD+RMxwUUJKBKX3pfxfxi0K6rXYom1wYnsVUNQmLQ6IikTo9HQQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPakhQcE92ZU0zYmk2QS9D
|
||||
TG0xc1JPZXZCZ0tZOXA3MGNLVnBlZGVtRFRzCmIvMHhQKzFVWCtpMTQrQUhGVGJp
|
||||
RU1jbldYckw3TXI2SlNpZVBIZHRsWWcKLS0tIGtJTUtJejFxem5jajFQUDFTQWU1
|
||||
VnlxYmVlNG04ay9ETi9FRmVYQXVoRkUK9oFNolI7jRjo9RUs1g4ghrx7aYV4U/ce
|
||||
ZTc2tFh57+7aKgrDi+2W3jwhfkjvBsThk//p5mLlqEEgw2lwlnhvPA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNNDl4SC9WbmVFRGMvN09G
|
||||
OXRuLy9tbnErZ21DbFkydDRPQTZRMWlqRldvCnMrVEJqVUZRdkVQY1puY0ZZRjlJ
|
||||
eHNFRldVUHAzSDBOV3RDemprYmwvNWcKLS0tIGZoKzJ6VFBLbThKNURmdG9uNUFS
|
||||
bCtoYjY0aFVmTHRMaEhSUmFhS3dDVU0KxVmYLzH1rMj7+ZTq12x69YvJ9xU3jGqC
|
||||
uW+jyDIKuhCGnR6mMzmDeshap1YM7QQCSMT2uJJ3H13RoKWwRG569A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoVnh6dFAwTkY4cHJpaEs4
|
||||
QnZPeXZHK0tXYWZPNytmYXVsdGRWQVI3RlhRClFVb2I5OVZzZFNrRXFaa0JTUkRJ
|
||||
OC9GQ1V5K0JhWlhkUjU1WStCa1lPV1kKLS0tIEhzdnBBZkRnK0NtV1FuTkVsNlgv
|
||||
QzVEcEVkQm5NL0Z5dUU1U0ZFaTJITnMKaWE9vlrOpQstr6FGP5ObdilsCYk4kYAj
|
||||
/phboR+Ym7QDTyUF9LZXJCU54YJp6vEWkRnlJFqC75UW/v/lgBhBMQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmQk1GWVVkN0Fmc05sR0dB
|
||||
OVRsTm9Lc3g2THlXRzVIWGpITjVCbTV2QjFjCkgxampRRUdJRzYwZTVLL29WMnhh
|
||||
SkwyaG5Na1N4V0N6Vml3SjQyWUlyV2sKLS0tIEh2S3cxbG1Ca2lYLzRlQlBFWlg3
|
||||
T2NDVEJhbU1paFRBZVFZM1IxdWRZRTAKSkB2TP2SbjD1MEV9m3BuYLc75AoqsgOy
|
||||
kRxoAVyA3FwcCJDRl3oeoPFXYFvNrEgLXn/ZIo21GRl8XZ6/QIRDrg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkUHAzWk1KblJxVVZZV3FM
|
||||
WndzRGRtbkw2azRVWnBuZmhKWElqRFk0RENzCk5DMHlMVWpwbXEwVUhkaFZUbkp2
|
||||
QXNlZFV4SjBEdmR6UEw0N1JOUnhNKzAKLS0tIHo5RkNDUk1ESWRHQmV6bzkvSTlP
|
||||
dk1GQ0Y3V0dTRlByb2xUOERVOTVwbVEKY4sAHyAhvGSYJzPuufWUIQD2xZcSt/nX
|
||||
t2ZFXu891/QdEzyUXCIzdwAV+Y/LjvroIlCp5Hkbrk0s7N+ghqsB1A==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPbmFwUE1KK0tqcFhvendN
|
||||
NFUvTmMrSEtNWHZZSStHQ3loU1J5aWh1VFFnCi92TTkvTG8vU2RZRmxFK1pLY2R0
|
||||
VnZMbS8vR3FQUTJta1JlTnYxRlcvcmsKLS0tIFdlV2dabmdZajZnK1plOVFFSVVX
|
||||
MDNxc25sWVFyUkovenFMZ2JrT1RWelUK/93Jr3zI0AtvwBBj3fPl7wykqmxjrAUS
|
||||
1v8A38F8K8MUtisN5TfvS8HnrU3iru16jGRwuikpiVaV6JdDyQK/Yg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBYVkwYXpYSjdmM1FpbDl3
|
||||
QWpycXoyL1AyOEpZUmtpbjl3MFAwTkJoOWpVCmVDd0FBUWxaQmZCU2VmNkZGMk9o
|
||||
TUdLNGtac2N4REg2eVF1eVh0WnNaTE0KLS0tIDJvcFErSjRiWmhPMmpadjROOHdt
|
||||
NXp6Y1JpdHFlSlRoa3JTaEt3emdnalUKjoFfZAiKMPF3noX+K0+vc3+p/XUHnhic
|
||||
k888KdUwcZYl2/dAIc8UDSggbMnncJAJgoezoCHLkj97GNNAD7E+gQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBicVRQNEVKaTQ5OWZyamVY
|
||||
cHk2QThjQWlnZWZ5ZFJUSWtoeG1PVXNTUzFnClU3emFXRGU3d0NSQ1pGbm5ObHVv
|
||||
NDI4dVRrdER1WVVnd0diZGswRHk5RHMKLS0tIE80VWI4L3pHeEorSVBhRGlNWnFq
|
||||
Q1o2Q0ZmZnd6SUl3VTNzbzlVUG1YOHMKxdxzI6VrpfhsW1Sn8vSpXMEWLfnGR+1c
|
||||
/T2cy7OtRWzNCCcE6odX0ImBYpS+JntWm6wgYdhRDcoyNWrcPYtwWQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqYnQyYzF5cktZNG5PcThy
|
||||
SEszVEwrUkQ1VVRsM3pSTlRQaHVLN0VuSHhrCmN1Z3pwNlFsbDN2UGI0KzYyallM
|
||||
SHJ5eklQeEIxSlhiYW5PUlpJcG5KNjQKLS0tIEk0QkpMdlBlRjVYMmJaMzJUbDNm
|
||||
K25pZldwd3JoZi9vdURoa3Myb2RQNG8K6N6bO2YKooPfpKihgsYqilfz/yAYCLZD
|
||||
XJ/THgT4URX2VNvSspvBtN8luOiJUVcchp5WtL2m9jARL5txEcDorA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBscFo3NDhGSW8wRkl1Zk1W
|
||||
V0NUTTNqc0dFb3I5RXRKclBWODJUeUFJR0RVCjYvbzhOUVMvRjZDUSsvVTR5ZmZG
|
||||
bDBtN0lTWG1SWUEzU3BMSFVFb01Sb2sKLS0tIG5OVUFneWhjOEhGTzA3MUJnWUgy
|
||||
Mnc4UWVVVi9Tc3ZISVZqVHVYNnNXZ0UKGkMiNP3AaKruNDbcsYgPVORbg41qvDa3
|
||||
WAaEL9Euih3doTJrDM6196T7oDSWV3EuKB49+ZPp6Ph98UegU3DHtg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvY0VpeW1nazVvRWJFaGNU
|
||||
RGVzVjRmWWV4dnJzRHdsaW9ENVZYck5lWGtVClNtd3ppelowRjZpRFFSMC9EK09n
|
||||
b2hqNnkzejdrTnhYNGNKblpteDNLRWcKLS0tIE02bWVjazRWNEVKbURITGlQODlR
|
||||
cTJJVnBVdGIrdzBoSXExelNrVk1XcEUKc77o2EX7PCm/HjUo5GsUiQdm488WB2mg
|
||||
wHd/qDbQhF1W75RrVTuIKgtEtrRjZqpmr8toe+aHJizPofcrToUfzw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3Qjl2Zk1BNGVLcS82MndM
|
||||
aTYyZGsyazlENDZhdVdXd1pERWgwVVRGbkQ4CkwxZFBPSE1QQkkvQlJHODUvMnk0
|
||||
RktMYzYxU0NzOVUrS0t0cHptelY1T3cKLS0tIG1Wbll6L2d0WitqWGx6T29nR2tT
|
||||
NkZUSUR4MURQaUFTNHpWaHVua3BYSGcKQwFM36H7WUvRUCtqA/bE6JgMvcUqEptd
|
||||
uNHV+zrlMPKPKtgbxIqOsrrW2cLYd8MHoHgswhShC9ewJruqckngTQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSMmZnd1pXRWh5NDVSN2xq
|
||||
MVhTN3N1OFpkcUh0a0tjYzJnWUlINGNVd3k0CmZkTnBac0l1dllxazdLY2l0Rzli
|
||||
bE9sNTBVSkJNaWF1T3c0WktoOHl0NU0KLS0tIHEyeWZTUjdQeUN6U2t0d3JwNTBX
|
||||
ZE1Za0tXb0gwc1FSakVYdU9OTHkyd28KkwmlzSYP8XofB0VGag+S18+S2TyQjLrM
|
||||
qaXtbBtLzJGNDhe9FhAKTPFcjTLWbohlG69vxcImyCyCns+QQ+gvug==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuZExYTHhUdGVjMFoxQSsw
|
||||
NFVSSWhZeXlQVE96emFCT241Ykh4TGF3c1VZClQ5NVdML1N4RWVVOE10dVF3bU9I
|
||||
ZXRlRFN3bDBCcERwN0ZRalMvTytYMk0KLS0tIDVvVzQya2kwRWoraWVMbFRHQURy
|
||||
cUZrczVueVRjOHM3MlNNOXZsYkltQjQKMApTF14qLyNABOQ4t/dPtw9ZPEYEgTma
|
||||
UIjKqCqAu9v0w2VdhCOYtMRzYuxngV+VdnIHg/lNXNgZsCGmrMavBQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2Q2RMWmZVOEwrOC9VcUxp
|
||||
VVE4R2NqYlZWZlhKSTBHRks4bDNoaUliMWlJCm5FbWdZS05GY0VLc0sxY2x1U21V
|
||||
eExwK29GVVBqYlRPZ0l5RWVXRFhRNlEKLS0tIDl6dVZJQndwVStFVEJnRHRyMW1W
|
||||
NnFqc1F0SGJqT0xmREpaN21EdnlJK3MKRPE5rfFpVnH5wAOkuB5pNMlMd3omcpku
|
||||
do2hFZwyI7t80jxF4+g3J7EolOx8AGjpc9Ba7Gj6IMDjye728q5N+g==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPU2ozS3I2WjUrNnkrWEtV
|
||||
cVBhcHQyVmQ1NTg0dWhzNmNaUmVxdTM3NzFjCld1TGROck0yYTZhOGVmWUdtQURm
|
||||
WXdHRXZBbGtwSXdwMDZaSTNOajJFKzQKLS0tIEpadWVkLzRiOURBY3cvU3Q5Y3lM
|
||||
L2l1blBPd1MxV3JBdE1vQTJVOTNkaUEKYdXzJ0tDMUhoqtxNN7U4hYnO+Fl7LXHC
|
||||
kKVsWb9T9bG1UHfhQAJldneuPUQUxsBqiGj1bxKBB4s21jrYGeSH0Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhSG94VE96TVlTNkEyclFE
|
||||
SmdCSkJpeWlVWDFIMDhwUEkxL1RUTDJ5UG5FCjhKRDB6VGtwTVozdUVzbUxGL3BW
|
||||
SnR3cmpSN2RxNnl4QmNvT2lkYmtoVFkKLS0tIHd2V2h2Wk5xOXlISzhjVzBsVkhz
|
||||
VVpRenVnSVpHUWJqV0JHNXNWWXJOdW8Kv7PJSTDbwFOAcl7pynALaJiTXU/87bSF
|
||||
F3HQllYOwOoibGzBCe18H2N+VxyNxoQL9OWe0TvOIR6bgHFIIF0/Dg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJYXRyZGYxaXNuUTR5c0dU
|
||||
OWlEcHdLTmtuL0tkdERUNDJTM2FNTWxYQlMwCjdrWVlOZXZMUzFlL1g4clNHeVBr
|
||||
d2szT0twRWdLTGVyelVPamNoTnJNeFkKLS0tIFZiYmRpQTFRaW5sWDkrTnZDdXNJ
|
||||
ZVY0V3BuTGU3clgrSDl4Q25iVFNpNjQKFo5BYhxd/jEytclFPkPy8D1eNZVJxrsK
|
||||
EY0DeWDAdc2BGtu4zTW9kLemIifHC8qmsFrCq/8ej04ZJEw/lEDaNQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsMEZPUExRVHFFb3pSVHNO
|
||||
cGJFN1ZzTDFVNEdneVpMZ253ekFJNjVtYkNnCnRGQjU2Q3dsRGRFV25LQ3pCbTJE
|
||||
OTROaFBiT01xb200S1pUK0NYaTQ3R2sKLS0tIEdiQlZTbi9Vcm0zc2t6bHplZktF
|
||||
ekRySENXcjBuR2psdHZSSUJrR0xUdjgKvBsmnC+cbq5TUDFjXCyImIoPKvh8wsjE
|
||||
7Shk7Act8Jayrhx0lXBDRmfpHRrB4L16rDSmqO0DTE48VhT3TiFyug==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzakVuc1IwUlZWbTBKREJM
|
||||
WjJZbXVleDFWM1hJeWs1SzRpZGxLd2dmSERVCmVrb1B5NjcvZkNyazVNcnB5czhh
|
||||
SlA4dFpGU0FQMFhqb1pkVmtiNE1kU0kKLS0tIEI0cXFoWU95cmlYdDhjdFBlOVZG
|
||||
MEF0SnpDVFFqUzhDekVNZGtnUXFveUUKI0YKTPWPi9o6FJvgVlU+BJ4qXleS2USF
|
||||
mVCFNvSn1NgvpKrXID8H7B3++Y4jNOxDqDp79u7vwK+Lf3FqBujgLg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxNDlya3RmYzNhU2p6RkVw
|
||||
cHJVWWY0Yi93cW1uZi9FWkVONmtpRWh5SzFzCmtWYTRIY3BkTWU5R2Jsa0ZJK3kz
|
||||
SnZvZ3YwaGtoMVZ3V2laTlBBK3UyTmMKLS0tIFdpZWtqeGlacjlLbmFySHlSUUlj
|
||||
RmRqQWVHK0FUT3VDbFhLbXQ5WDhLeEEKcDkgV34lUFJRIHRoLB8F2IOvGAM93sM+
|
||||
AkmaM4+WRcGeYWQKMG2x6cYCUKFaT1lDXuWZ9kI8Fd7b9gTSnQMs6w==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkRFFmME8yckhzNU9kRnl3
|
||||
VGdabWgyalpNbzJmMW5HV0NHM2orc1l2dXhzCkwxYjNHMHVyb3F6Mk8vRkd0N2dJ
|
||||
TW5seFpORmFVazBZL3lycjZGUlNyR1UKLS0tIFVEck5TZ3g0eVJWaHF4NThkQlVq
|
||||
TnRvMTFhY1RVVjFscktSd3FvM0Q5c3cKV82WfobJzuBk3XhfB9pqobf2kV1Z5wBc
|
||||
XCf7kK3cxVzty8uPCYItl/q6i8xUMnOH7vQoUD43sxTdtwxbKUxm8w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjeFM4ajhHeVd4bzlqZmJ1
|
||||
MUxYMUpZVjdjR0NGTzVteVB6WFBQeFI0ZkZrCk55TEt0Zjdwbk51RnhYclNzam1H
|
||||
cExKZXQxWFVLa1pDNFpkcGZzcnl6a0kKLS0tIFJ4ZEdJc3JVaEc5RU1aZk1uYm1l
|
||||
d3RHS3hHSkRKRXFnN21FQmh0TlNtNmcKdc2G/1dhTJen6iT9kUWZM5OzCmDVprgx
|
||||
WN1Bl3JzYhLsNKn794887bVAICVqbXqkdpEZztNIS5n/Rw6geKsNvQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWOGhTMFJYNzlyRHYyb3pX
|
||||
emtCRm5BQ0tHckhRVGJUWVRsdXdRUHRqdUVvClB0T2pyS1pyM3RYZHN6UkFTeHBh
|
||||
UXV4RVp6VGtOZldVemttWjc3L0RaaWsKLS0tIHJWVVA3ODgvb0xjVmhFajlSWWJk
|
||||
MERJSGZITTJLeTJrNGxTMnAzbEVxOVUKvHUDTWMPU9cwHupPDW9IkrXGEZYw3531
|
||||
GvQvsb9Sjwr81PsCyuZK1koMBfjYO70uU4INgt+4ncF0nH1k12+fDw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYa0gxNnBwNjNNTTgxKzgv
|
||||
YkQwTjZxb25tQ053Ny9tSW4wNGVYYnlIaFZNCkVvOFNPOFkwQzhYZGxjb0FOZzJ5
|
||||
ZjNXam1ZTWZrS1M4cGhhcHZaT1NjblkKLS0tIEdvL1dDTHpWcWF0S3ZqNkxrQW52
|
||||
VlYwa29sZVloOS9qajJWQWFzY2FKRmsKy074SLdttogXsWycaFX8xso4ek7Cbjph
|
||||
MMEhZd/svmnSiYM81nmeaze7qXEUcsZXuSmZCYATTBEGtx/Srll8aA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBK1ZIaEJUNGxYcGdqQU5j
|
||||
ZndQWG10NU9wZXgyeDcwbitIcmxHYlZXemxZCncxR05MZ1owMXNkRHFKZXRaQXRK
|
||||
cUNKemc4R3pKVkozQXppeUdXaGdIMHMKLS0tIEViSUVoQUx6QmZsUk4rUk9BaGJ3
|
||||
L2dTTGo2OFhWYTNyS29MeTJ4RmEvbVEKkBusuOJo+vME4ZNVzgF8j1f0hUhFIE4P
|
||||
TNfZV1na59QukVUetSGGPx4YGlwgYVoUZ/QMlJgkh6cGkZnOYONZFA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
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"
|
||||
mac: ENC[AES256_GCM,data:qFhnPra6IE3wyKQ4WKweON0S0YtD5I0adGZVfA0m6BVilN6bX5oC/1j5NK2oHrsz920hSl0SOF8LrpqOrUyGjSRkPsN4kq8qr9bJcrX4URiktP0oRden5LLt6hf+ZRP7WmRXFqixPkPHJnZIoAvkNnTFce7cDq5NEAHkKUEKG7k=,iv:nyblUDGeu3TUfFivYylOn3C/HITj99qiPI2+mh8AGh4=,tag:FrtRzSCylC4wlIoqZdfx7w==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
|
||||
@@ -1 +1,23 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:VpxnzrdX5FY2gfDcZpqg1f2wqL6mAawcrdE5A9A3fQOKr4BWXmCT/hjQEtt8qG+yVOTApdG+vF8SKDj1AKZ25KsZ253WS7W5C4j8oiT/xHo0CtvVani4fZqN60lB/9381h7aUZ+lcBMPaQcNn35zoQqYthuCGrC9PH1qQgD2VVHr0v8J6kceXKp0KTJldtDsQxky6ROC9Zyc44wwkrFPgPss4/yGaRWWqiojDvK5fK8238hmR8HyfD6Tf5KhgbqCsSGS2g6yt1muUhmegaZwtpi/KpoqBmf1XFaObuHGRPKvblq4Fa0x/sSjpPogBZyNuvRKIwRhla0FkKOH1bX4gC9bzOlgiUUfVcRJT6Nr9jocYuhIv9wvIT4TujGbgHyorYaDSQLFPJoJEUa3fgpGq+AC6p1DJJE3b/wqUSngNbdOtAPE/imaEuprOfnbS7cRx+Ti/Yu+ZYZuAlDtLkSVdXi1hQ4fLQ==,iv:rq/BzePXa/w/Gqewz8JfM/NdU+x4ShiH1OuQT7wRyCQ=,tag:IcO3ddoj4M57CkndHEmNkQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5NnlxQXQ3azBkRk1NRXZx\nNklxVmZ4aU1zOVhKajVVbUJ1ZS9NUlJ6WXprCmYxWkc4eTJjTUxJOTR5OVVrdHlF\nOS85L214L3hqSlBKMXpDWTMvalI4R0kKLS0tIDVjNnM5NGhxVW5LcXIxd1F4dmRa\nd2grNFhjZ291My80bUNJdEU5SVZLTzQKaNAyPJOmJ9sC640NTo5aC7A7WNud6YAu\nVsVq1jB22HFbZG0hxdt+R2vCT71f2iSUVg+P2OVphXehfmJDHwnhaw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2WklMMmcwYm1BaUQ5R0N3\nM1R4cVZhaUtuclIwSGtlcys1NDg5TmNkNVIwClA3dTIySlU3L1M0TngvQllDaFY0\nakV0eGkxRDNuWDMyUUUwSU9JRis0UEUKLS0tIGNYajYvUWdRcGJrWWtXTFZDTHVK\nZGl3bkRJWUliVFMyUlNDYWtqbGFLZWsKgsLUjMbLkAxcmiQ4A+BeUv05vR555TbP\nvEF3M69btLFdJEBVLsLRaBm6scqwrsZup8P1Zd/uS182VvUslVNo9w==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1vwk8r03u46rpvmw5fy20t8ud9qdhm0nx5se5ayze98y55e7zg5yqc8862n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T11:53:06Z",
|
||||
"mac": "ENC[AES256_GCM,data:PMsPSffQkxRmqT6HwFezIoT4WKJ9WDlAcflWynsFLtzbqVTREN0YGOJgMwLd0yBR2sbGBWogG5xAuKjO16aiWUVpCFPRvP97um351679ZoXIlrOL+GlFn4NKwoLH9/eCp8BteB9mv5huhEEN631lqCr+jtYO6yS0ULk6QS4+7/c=,iv:oYWV4J1JUuGryvqntVXkj/HOzRDRr9E2C1RKNvr2bT4=,tag:e4p+I40y1LrgdlK9tLRicA==,type:str]",
|
||||
"version": "3.13.3"
|
||||
}
|
||||
}
|
||||
@@ -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+IFgyNTUxOSA3dFdQZUlBTmM2SlB4Zm8r
|
||||
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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:Ea5AGwloFMyRgmQhJaDNTW19mpDvx0+dSc3ibENEXEniEIBV4Wv135mAwE6jQPbN8wF6f5ki3B5mDXHOMuhDQEqUXg6jcs4uEm2nwz7wAKYXEX4T8p0gUj7FPISiSQrw8O3jwzt4qF7AVcPsNhMtZLgrx9ssMtVmGct5do+E61Y7dFe0XbpJLynfzeiMKYvaBngTbHwQjv1mn9AMpX98apsp3tyzg8avLDR+WXRcMusds2mdov0jCoKuWPLG/srhMwjKOp1Z1Tu+aZCr9lHTT76mytpGIWGSbrP5l3UTp3oPLmy87Kwi/FJStZYdYikMnvmSo5qovePiYsLKVD74dS+d3oFGARyW4TILd9OQdXEjzdDRak5HcPv2/HhR638kvukVMdEdH0hpeJcqcV0h+BoLyIHe1e037iF1ZkCjuXE+A8LtlzRR0wqP202uzBIyg21ca4X5bRiHkkAOX4P3riaEcMgKfw==,iv:IXq4sHHsjnK6maVd3RRVZzhnxx1hOnxfsvYX8IKLn+4=,tag:jRZinAVLH8nGVf3gE4aSKA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtR1hXcExXemc1WHFBU2Ir\nYkk0RnRvU1BBMHozeGh4SjFzTVdnSCtvb2pVCm94emZBdU40d25lUGtRbFZlaHg5\naHRPWUtwOFhkbXJBZmNGaVB3RStvMjgKLS0tIHE0SmpqU3NpQXlORDdYQ3hhallB\nWitDNzdKVFhXZ1BNRFg0SGJUK0prL2MKbo+KyHmTPlkrfeqifxORCspAfV6hi/CN\nQ04jkydGT7YbQugxqIDcQb2e4db+uyEzD6GUaPPZ6nTcLzDJZYw59g==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJTVdndzRJTFRMdlRPMUNk\nRklJMG5LeWRWT0o5R2kzLzFSbmpERGZYd25VCkJpT1VlcWRiaFZsTWlPaEN4NllZ\nQzBITWFnUmJJeDB6bnZIK0hEV2J1MUUKLS0tIExEM0lsQlcvQXpvYnRub29RNVFs\nc2g4L3kwYmI4enBpZHJ5bE9TaGU4WTgKqoY9KjWnjeIH2CLvTU2G3Uc76GzWi4Ad\neDexAU0CbiFejbjDbCjwzEyLPUqgsc2kcW6EVg0/7vDufht0TbFxDQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age152vrhznh92hspusx9rqxt9c70u8w3uvlys4ymc7zuv3qnxjm5c7qmp72xp"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T11:53:32Z",
|
||||
"mac": "ENC[AES256_GCM,data:EhWEEIamG62xHI6SqO4tzdK6gcEXxUU/UGcF25m+X11kUVXzHd6yLcfiM2DvqcwGLmwmOOf3SpFLABlQUzTka4QRzTMFIAJi7x1rzQV4J0YIGITHk9rgvy0V50TI0loONBb2du+vDt8IlTBNvF9WMbNCki+fHPAjIemyoRBdubo=,iv:wfsx0H4fD3L7WeGzz57SzZtajBr/xTRDljSpz22DD7Y=,tag:naDyGH0WTKkqgd954Ya8jA==,type:str]",
|
||||
"version": "3.13.3"
|
||||
}
|
||||
}
|
||||
@@ -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+IFgyNTUxOSA4UlllaTNtUElsOTFxaTl6
|
||||
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
|
||||
|
||||
+7
-1
@@ -16,7 +16,7 @@
|
||||
serverIp = "192.168.2.226"; # server (NFS/ZFS) 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)
|
||||
domainControllerIp = "192.168.2.253"; # FreeIPA domain controller / primary DNS (not NixOS-managed)
|
||||
domainControllerIp = "192.168.2.253"; # FreeIPA domain controller — authoritative DNS for sweet.home (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)
|
||||
|
||||
# Cross-host references (LAN hostnames/users other hosts reach over the network)
|
||||
@@ -85,6 +85,12 @@
|
||||
# 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
|
||||
# LAN IPs (vmbr0 / ens18) — client-facing: iSCSI initiators, NFS, management.
|
||||
# Storage IPs (vmbr1 / ens19) — isolated internal bridge, used for DRBD
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICvIM/0C5wFd6RNgdcWA7uLkhu4gNsi/98UBDZ4Mg45H proxmox-ha-server-1
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE4SX5hJyjWgOisxfNChg2o2aXx9Q4GewsJGqieGUYsX proxmox-ha-server-2
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user