Archived
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4831c9370 |
@@ -23,5 +23,3 @@ host-keys/
|
||||
# Temporary Milestone 1 audit checklist (remove-sensetive-info-refactor.md)
|
||||
# - working notes only, never committed, deleted once every row is rotated.
|
||||
secrets-inventory.md
|
||||
.claude/worktrees/
|
||||
.claude/settings.local.json
|
||||
+36
-81
@@ -7,20 +7,21 @@ keys:
|
||||
- &linode-gui age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||
- &linode-minimal age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
||||
- &linode-nix-cache age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||
- &linode-server age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||
- &linode-tailscale-router age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||
- &lxc-docker age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
||||
- &lxc-minimal age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
||||
- &lxc-nix-cache age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||
- &lxc-pxe-boot age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
||||
- &lxc-server age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||
- &lxc-tailscale-router age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||
- &lxc-tor-relay age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||
- &proxmox-docker age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
||||
- &proxmox-gui age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||
- &proxmox-nix-cache age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||
- &proxmox-pxe-boot age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||
- &proxmox-server age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||
- &proxmox-ha-server-1 age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6
|
||||
- &proxmox-ha-server-2 age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r
|
||||
|
||||
creation_rules:
|
||||
# Shared across every currently-deployed host: root/nixos password hash,
|
||||
@@ -38,20 +39,21 @@ creation_rules:
|
||||
- *linode-gui
|
||||
- *linode-minimal
|
||||
- *linode-nix-cache
|
||||
- *linode-server
|
||||
- *linode-tailscale-router
|
||||
- *lxc-docker
|
||||
- *lxc-minimal
|
||||
- *lxc-nix-cache
|
||||
- *lxc-pxe-boot
|
||||
- *lxc-server
|
||||
- *lxc-tailscale-router
|
||||
- *lxc-tor-relay
|
||||
- *proxmox-docker
|
||||
- *proxmox-gui
|
||||
- *proxmox-nix-cache
|
||||
- *proxmox-pxe-boot
|
||||
- *proxmox-server
|
||||
- *proxmox-tailscale-router
|
||||
- *proxmox-ha-server-1
|
||||
- *proxmox-ha-server-2
|
||||
|
||||
- path_regex: secrets/nix-cache\.yaml$
|
||||
key_groups:
|
||||
@@ -61,6 +63,24 @@ creation_rules:
|
||||
- *lxc-nix-cache
|
||||
- *proxmox-nix-cache
|
||||
|
||||
# Host keytab for nix-cache FreeIPA enrollment (binary sops file).
|
||||
# Generate with: sops -e --input-type binary /tmp/nix-cache.keytab > secrets/nix-cache.keytab
|
||||
- path_regex: secrets/nix-cache\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *linode-nix-cache
|
||||
- *lxc-nix-cache
|
||||
- *proxmox-nix-cache
|
||||
|
||||
- path_regex: secrets/server\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *linode-server
|
||||
- *lxc-server
|
||||
- *proxmox-server
|
||||
|
||||
- path_regex: secrets/tor-relay\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
@@ -83,14 +103,12 @@ 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).
|
||||
@@ -100,8 +118,6 @@ 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
|
||||
|
||||
@@ -111,6 +127,16 @@ creation_rules:
|
||||
# scripts/secrets/sync-host-keys.sh yet, so whichever variant is actually
|
||||
# deployed next needs its recipient added here (and `sops updatekeys` rerun)
|
||||
# before it can decrypt this.
|
||||
# Host keytab for tailscale-router FreeIPA enrollment (binary sops file).
|
||||
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
|
||||
- path_regex: secrets/tailscale-router\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *lxc-tailscale-router
|
||||
- *proxmox-tailscale-router
|
||||
- *linode-tailscale-router
|
||||
|
||||
- path_regex: secrets/gui\.yaml$
|
||||
key_groups:
|
||||
- age:
|
||||
@@ -120,82 +146,11 @@ creation_rules:
|
||||
- *linode-gui
|
||||
- *proxmox-gui
|
||||
|
||||
# IPA host keytabs (binary sops files).
|
||||
# Each keytab is encrypted for all platform variants of that host so any
|
||||
# deployed variant can decrypt it at boot. Run
|
||||
# scripts/ipa/create-nixos-ipa-host-account.sh <hostname> to enroll a new
|
||||
# host and produce the keytab; this section is updated by that script.
|
||||
|
||||
- path_regex: secrets/nix-cache\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *linode-nix-cache
|
||||
- *lxc-nix-cache
|
||||
- *proxmox-nix-cache
|
||||
|
||||
- path_regex: secrets/tailscale-router\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *linode-tailscale-router
|
||||
- *lxc-tailscale-router
|
||||
- *proxmox-tailscale-router
|
||||
|
||||
# Host keytab for pxe-boot FreeIPA enrollment (binary sops file).
|
||||
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
|
||||
- path_regex: secrets/pxe-boot\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *lxc-pxe-boot
|
||||
- *proxmox-pxe-boot
|
||||
|
||||
# nixos = the workstation (hosts/nixos/host.nix). All gui platform variants
|
||||
# share the hostname "nixos" and must be able to decrypt at boot.
|
||||
- path_regex: secrets/nixos\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *baremetal-gui
|
||||
- *lxc-gui
|
||||
- *proxmox-gui
|
||||
- *linode-gui
|
||||
|
||||
- path_regex: secrets/docker\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *linode-docker
|
||||
- *lxc-docker
|
||||
- *proxmox-docker
|
||||
|
||||
- path_regex: secrets/tor-relay\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *lxc-tor-relay
|
||||
|
||||
- path_regex: secrets/nix-minimal\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *lxc-minimal
|
||||
- *proxmox-minimal
|
||||
- *linode-minimal
|
||||
|
||||
# Host keytab for ha-server-1 FreeIPA enrollment (binary sops file).
|
||||
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
|
||||
- path_regex: secrets/ha-server-1\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *proxmox-ha-server-1
|
||||
# proxmox-ha-server-1 added by sync-host-keys.sh
|
||||
|
||||
# Host keytab for ha-server-2 FreeIPA enrollment (binary sops file).
|
||||
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
|
||||
- path_regex: secrets/ha-server-2\.keytab$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin
|
||||
- *proxmox-ha-server-2
|
||||
# proxmox-ha-server-2 added by sync-host-keys.sh
|
||||
|
||||
@@ -6,14 +6,12 @@ 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`, `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.
|
||||
(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.
|
||||
|
||||
Do not deploy, switch, reboot, repartition, format disks, or run destructive
|
||||
install commands from this repository unless explicitly asked.
|
||||
|
||||
@@ -21,7 +21,9 @@ machines when deployed.
|
||||
`modules/installer/common.nix` (the auto-installer's own root/nixos login —
|
||||
a deliberate, documented choice, see `docs/auto-installer.md`, not
|
||||
accidental tech debt) and **SSH public keys** in `variables.nix`
|
||||
(`vars.adminSshKey`, `vars.remoteBuilderAuthorizedKeys`, `vars.beszelHubKey`). Don't use the installer's hardcoded hash as a
|
||||
(`vars.adminSshKey`, `vars.remoteBuilderAuthorizedKeys`) plus a couple of
|
||||
per-host `KEY` values for beszel-agent auth (`hosts/server/host.nix`,
|
||||
`hosts/nix-cache/host.nix`). Don't use the installer's hardcoded hash as a
|
||||
template for a *real* host — every other host uses sops-nix
|
||||
(`hashedPasswordFile`, see "Security Notes" in `README.md`). Flag any *new*
|
||||
secret-like string you encounter instead of committing it.
|
||||
@@ -373,8 +375,9 @@ removing a host.
|
||||
|
||||
- `hosts/<name>/host.nix` — per-machine identity **only**: hostname, hostId,
|
||||
per-machine secrets, `system.stateVersion`. These files carry no `imports`
|
||||
of their own — all shared behavior comes from the platform/build-type modules
|
||||
composed in `flake.nix`, not from the host file.
|
||||
of their own beyond narrow parameterized helpers (see
|
||||
`modules/beszel/host-token.nix` below) — all shared behavior comes from the
|
||||
platform/build-type modules composed in `flake.nix`, not from the host file.
|
||||
- `modules/platforms/{linode,proxmox,lxc,baremetal}.nix` — platform-specific
|
||||
config: boot method, guest tooling, and the hardware config, imported
|
||||
directly by the platform module itself — **not** wired in from
|
||||
@@ -431,10 +434,10 @@ removing a host.
|
||||
substituter + SSH remote-builder wiring; see `docs/nix-cache.md` for the
|
||||
full design (per-host local stores, no shared `/nix/store`, and how the
|
||||
`nixremote` signing/SSH keys fit together).
|
||||
- `modules/beszel/enable-agent.nix` — enables beszel-agent, sets `HUB_URL`,
|
||||
fixes the upstream `StateDirectory` bug, and wires the universal
|
||||
`beszel-token` sops secret (from `secrets/common.yaml`) into the agent's
|
||||
`environmentFile`; see `docs/beszel.md` for the full setup guide.
|
||||
- `modules/beszel/host-token.nix` — parameterized helper module
|
||||
(`{ name, sopsFile }`) that wires a host's beszel-agent sops secret/template
|
||||
and `environmentFile`; used by `hosts/server/host.nix` and
|
||||
`hosts/nix-cache/host.nix` to avoid duplicating that boilerplate.
|
||||
- `modules/tailscale/`, `modules/docker/`, `modules/networking/`,
|
||||
`modules/traefik/`, `modules/tor/`, `modules/services/*` — single-purpose,
|
||||
single-host
|
||||
|
||||
@@ -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-router`, `tor-relay`, `ha-server`
|
||||
`gui`, `pxe-boot`, `tailscale-exit-node`, `tor-relay`
|
||||
|
||||
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` 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:
|
||||
`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:
|
||||
|
||||
| Target | Purpose |
|
||||
| --- | --- |
|
||||
@@ -29,9 +29,8 @@ hardware). The 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-router` / `proxmox-tailscale-router` / `lxc-tailscale-router` | Tailscale subnet router + MagicDNS forwarder for the LAN |
|
||||
| `linode-tailscale-exit-node` / `proxmox-tailscale-exit-node` / `lxc-tailscale-exit-node` | Tailscale exit node |
|
||||
| `lxc-tor-relay` | Tor middle relay |
|
||||
| `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
|
||||
@@ -48,7 +47,8 @@ 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. Every deployed host
|
||||
fixed regardless of which platform it's built for — see
|
||||
`flake-target-refactor-spec.md` for the full rationale. Every deployed host
|
||||
stamps its own active target name into `/etc/flake-target` at build time, so
|
||||
`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,6 +167,7 @@ per-boot host key for sops-nix to derive from on ephemeral media) — see
|
||||
(`vars/per-machine/<target>/openssh/`, committed and sops-encrypted) for
|
||||
their SSH host keys.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -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-router`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the
|
||||
`lxc-gui`, `lxc-pxe-boot`, `lxc-tailscale-exit-node`, `lxc-tor-relay`) are **not** installed via `auto-install.sh` — the
|
||||
interactive menu deliberately excludes them. Don't try to select one there;
|
||||
`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,15 +133,13 @@ Flake outputs:
|
||||
```nix
|
||||
nixosConfigurations.installer # ISO/netboot installer image
|
||||
|
||||
packages.x86_64-linux.iso # installer ISO/netboot image
|
||||
packages.x86_64-linux.pxe # auto-installer netboot bundle (kernel + initrd + ipxe script)
|
||||
packages.x86_64-linux.pxe-minimal # vanilla NixOS minimal netboot bundle (no installer wiring)
|
||||
packages.x86_64-linux.iso # installer ISO/netboot image
|
||||
packages.x86_64-linux.pxe # netboot-ipxe + netboot-initrd + netboot-kernel, bundled
|
||||
```
|
||||
|
||||
```sh
|
||||
nix build .#iso
|
||||
nix build .#pxe
|
||||
nix build .#pxe-minimal
|
||||
```
|
||||
|
||||
There's no `nixosConfigurations.proxmox-lxc` (installer-boots-as-an-LXC-
|
||||
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
# Beszel agent
|
||||
|
||||
[Beszel](https://github.com/henrygd/beszel) is the monitoring dashboard used
|
||||
in this LAN. The hub runs as a Docker container on `docker.sweet.home` (port
|
||||
`vars.ports.beszelHub`, 8090). Each monitored NixOS host runs a
|
||||
`beszel-agent` that connects back to the hub.
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
Everything is handled by a single module:
|
||||
|
||||
**`modules/beszel/enable-agent.nix`** — imported by a build type. It:
|
||||
- Enables `beszel-agent`
|
||||
- Sets `HUB_URL` to `docker.sweet.home:8090`
|
||||
- Sets `KEY` from `vars.beszelHubKey` (`variables.nix`) — the hub's SSH
|
||||
public key, shared by every agent. Update `beszelHubKey` if the docker
|
||||
host is ever rebuilt and the hub generates a new keypair.
|
||||
- Reads the universal `beszel-token` from `secrets/common.yaml` via sops
|
||||
and passes it to the agent as `TOKEN` in an env file
|
||||
- Fixes an upstream bug where the agent couldn't persist its hub-pairing
|
||||
fingerprint across restarts (adds a real `StateDirectory`)
|
||||
|
||||
A host file needs no beszel configuration at all — just import the module
|
||||
in the build type and add the system in the hub UI.
|
||||
|
||||
---
|
||||
|
||||
## Adding beszel to a new build type
|
||||
|
||||
Add `../beszel/enable-agent.nix` to the `imports` list in
|
||||
`modules/build-types/<type>.nix`:
|
||||
|
||||
```nix
|
||||
imports = [
|
||||
../beszel/enable-agent.nix
|
||||
# ... other imports
|
||||
];
|
||||
```
|
||||
|
||||
That's the only change required. The host file needs nothing.
|
||||
|
||||
---
|
||||
|
||||
## Adding a new system to the hub
|
||||
|
||||
1. Rebuild and deploy the host with its build type importing `enable-agent.nix`.
|
||||
2. Open the beszel hub (`http://docker.sweet.home:8090`).
|
||||
3. Go to **Systems → Add system**, enter the host's IP and the default port
|
||||
(45876). The agent will connect and the system will appear as active.
|
||||
|
||||
---
|
||||
|
||||
## One-time setup: add the token to `secrets/common.yaml`
|
||||
|
||||
The universal token is stored once in the common secrets file, shared by all
|
||||
agents. Only needed once, not per-host:
|
||||
|
||||
```sh
|
||||
sops secrets/common.yaml
|
||||
```
|
||||
|
||||
Add:
|
||||
```yaml
|
||||
beszel-token: <token from the beszel hub Settings → Keys>
|
||||
```
|
||||
|
||||
`secrets/common.yaml` is already a sops recipient for every host via their
|
||||
SSH host keys, so no additional sops recipient setup is needed.
|
||||
|
||||
---
|
||||
|
||||
## Optional: monitoring extra filesystems
|
||||
|
||||
To report disk usage for a mount beyond the root filesystem, add
|
||||
`EXTRA_FILESYSTEMS` in the host file:
|
||||
|
||||
```nix
|
||||
services.beszel.agent.environment = {
|
||||
EXTRA_FILESYSTEMS = "/mnt/data"; # colon-separated for multiple paths
|
||||
};
|
||||
```
|
||||
|
||||
The `server` host uses this to expose its ZFS data pool:
|
||||
|
||||
```nix
|
||||
services.beszel.agent.environment = {
|
||||
EXTRA_FILESYSTEMS = "${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
LOG_LEVEL = "debug";
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optional: monitoring Docker containers
|
||||
|
||||
`enable-agent.nix` has a commented-out line for Docker monitoring:
|
||||
|
||||
```nix
|
||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||
```
|
||||
|
||||
Uncomment it if the host runs docker-socket-proxy and you want per-container
|
||||
stats. Hosts without Docker should leave it commented out.
|
||||
|
||||
---
|
||||
|
||||
## If the hub key changes
|
||||
|
||||
If the docker host is ever rebuilt and beszel generates a new SSH keypair,
|
||||
update `beszelHubKey` in `variables.nix` and rebuild all beszel-enabled hosts.
|
||||
The new key is visible in the beszel hub under **Settings → Keys**.
|
||||
@@ -1,261 +0,0 @@
|
||||
# Storage/Cluster Network Segmentation Audit — pve1.sweet.home
|
||||
|
||||
**Date:** 2026-07-29
|
||||
**Scope:** Read-only discovery of pve1.sweet.home host networking, HA cluster VMs (200/201), and Docker CT (105). No changes made.
|
||||
|
||||
> **Implementation status — 2026-07-29:** All recommendations from this audit have been
|
||||
> implemented in the same session. See `docs/ip-addressing.md` for the current state.
|
||||
> Key decisions that diverged from the original recommendations:
|
||||
> - VLAN IDs renumbered: cluster → VLAN 10 (192.168.10.x), storage-client → VLAN 20 (192.168.20.x)
|
||||
> - Two Pacemaker VIPs: `vip-lan` (192.168.2.229, NFS for LAN) and `vip-storage` (192.168.20.229, NFS + iSCSI for VLAN 20)
|
||||
> - NFS served on **both** VIPs (each firewalled to its own subnet); iSCSI available on VLAN 20 but NFS is preferred for docker to support future Docker Swarm multi-host access
|
||||
> - `corosync.conf` ring1 added using LAN IPs (RF-1 resolved)
|
||||
> - `vmbr2` created and NICs added to HA VMs and docker CT (RF-6/RF-7 resolved)
|
||||
> - iSCSI portal remains on `[::0]`; firewall enforces VLAN 20 restriction (RF-4 mitigated)
|
||||
> - STONITH still disabled (RF-3 deferred — accepted risk during development phase)
|
||||
> - iSCSI ACLs not configured (RF-5 deferred — iSCSI not in active use)
|
||||
|
||||
---
|
||||
|
||||
## 1. Current State Summary
|
||||
|
||||
### pve1.sweet.home Host — Physical NICs
|
||||
|
||||
| Interface | Speed/Duplex | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| `nic0` | 2500 Mb/s / Full (2.5GbE) | Only active physical NIC; sole bridge port for vmbr0 |
|
||||
| `nic1` | (not connected / no data) | Present in config, not UP |
|
||||
| `wlp4s0` | DOWN | WiFi, unused |
|
||||
|
||||
No bonding configured. Every guest's traffic ultimately funnels through the single 2.5GbE `nic0`.
|
||||
|
||||
### Proxmox Bridges
|
||||
|
||||
| Bridge | Physical NIC | Host IP | Subnet | VLAN-aware | Purpose (current) |
|
||||
|--------|-------------|---------|--------|------------|-------------------|
|
||||
| `vmbr0` | `nic0` (2.5GbE) | 192.168.2.245/24 | 192.168.2.0/24 | No | General LAN, management, **iSCSI/NFS VIP** |
|
||||
| `vmbr1` | **none** (internal-only) | — | 192.168.4.224/29 | No | Corosync heartbeat + DRBD replication |
|
||||
|
||||
`vmbr1` has `bridge-ports none` in `/etc/network/interfaces.d/vmbr1.conf` — it is a purely software bridge with zero physical uplink. All traffic on it stays inside the hypervisor's memory.
|
||||
|
||||
### Guest NIC Assignments
|
||||
|
||||
| Guest | VMID | Role | NIC | Bridge | IP | Traffic type |
|
||||
|-------|------|------|-----|--------|----|--------------|
|
||||
| nix-cache | CT 102 | Build cache | eth0 | vmbr0 | DHCP/LAN | LAN |
|
||||
| pxe-boot | CT 103 | PXE/TFTP | eth0 | vmbr0 | DHCP/LAN | LAN |
|
||||
| tor-relay | CT 104 | Tor | eth0 | vmbr0 | DHCP/LAN | LAN |
|
||||
| **docker** | **CT 105** | **Docker host** | **eth0** | **vmbr0** | **192.168.2.225/24** | **LAN only** |
|
||||
| pdm | CT 106 | Proxmox mgmt | eth0 | vmbr0 | 192.168.2.220/24 | LAN |
|
||||
| server | VM 101 | General server | net0 | vmbr0 | DHCP/LAN | LAN |
|
||||
| tailscale-router | VM 107 | Tailscale exit | net0 | vmbr0 | DHCP/LAN | LAN |
|
||||
| domain-controller | VM 108 | FreeIPA | net0 | vmbr0 | 192.168.2.253/24 | LAN |
|
||||
| **ha-server-1** | **VM 200** | **HA primary** | **net0** | **vmbr0** | **192.168.2.228/24 + VIP 192.168.2.229/24** | **LAN + VIP** |
|
||||
| **ha-server-1** | **VM 200** | **HA primary** | **net1** | **vmbr1** | **192.168.4.228/29** | **Corosync + DRBD** |
|
||||
| **ha-server-2** | **VM 201** | **HA secondary** | **net0** | **vmbr0** | **192.168.2.227/24** | **LAN** |
|
||||
| **ha-server-2** | **VM 201** | **HA secondary** | **net1** | **vmbr1** | **192.168.4.227/29** | **Corosync + DRBD** |
|
||||
|
||||
### Corosync/Pacemaker State
|
||||
|
||||
- **Transport:** knet/UDP
|
||||
- **Rings:** 1 only — ring0 on `192.168.4.228` / `192.168.4.227` (vmbr1/ens19)
|
||||
- **Cluster status:** Both nodes online, DC = ha-server-1, quorum achieved
|
||||
- **STONITH:** `stonith-enabled: false`
|
||||
- **no-quorum-policy:** `ignore`
|
||||
- **Resources (all active on ha-server-1):**
|
||||
- `ms-drbd0` — promotable DRBD clone (Primary: ha-server-1, Secondary: ha-server-2)
|
||||
- `xfs-data` — XFS on `/dev/drbd0` → `/srv/ha-data`
|
||||
- `iscsi-target` — targetctl service
|
||||
- `nfs-server` — nfs-server service
|
||||
- `vip` — IPaddr2 at **192.168.2.229/24** (no `nic=` parameter specified; floats to ens18/vmbr0 automatically based on subnet match)
|
||||
- **Resource ordering:** ha-group starts only after DRBD is promoted; collocated with Promoted DRBD clone.
|
||||
|
||||
### DRBD State
|
||||
|
||||
- **Resource:** `ha-data` (DRBD 8.4.11 kernel module, config in `/etc/drbd.conf`)
|
||||
- **Protocol:** C (synchronous)
|
||||
- **Replication endpoints:**
|
||||
- ha-server-1: `192.168.4.228:7789` (ens19 / vmbr1)
|
||||
- ha-server-2: `192.168.4.227:7789` (ens19 / vmbr1)
|
||||
- **State at audit time:** Initial sync in progress — ~20% complete, ~40 MB/s, ~34 min remaining (100 GB disk)
|
||||
- **Fencing config:** `fencing resource-only` + fence-peer/unfence-peer handlers
|
||||
|
||||
### iSCSI Target
|
||||
|
||||
- **IQN:** `iqn.2026-01.home.sweet:ha-storage`
|
||||
- **Portal:** `[::0]:3260` — confirmed listening on all interfaces (`ss -tnlp` shows `*:3260 *:*`)
|
||||
- **LUN 0:** fileio backstore — `/srv/ha-data/iscsi-lun.img` (10 GiB, write-thru)
|
||||
- **ACLs:** **None** (`no-gen-acls`, `no-auth`)
|
||||
- **VIP (intended portal):** 192.168.2.229 — on vmbr0/LAN, no storage-NIC-specific binding
|
||||
|
||||
### NFS Exports
|
||||
|
||||
Served from the same `ha-group` as iSCSI (starts/stops together):
|
||||
|
||||
| Export path | Client subnet |
|
||||
|------------|---------------|
|
||||
| `/srv/ha-data/docker/{config,databases,volumes,nextcloud-data}` | 192.168.2.0/24 |
|
||||
| `/srv/ha-data/proxmox/{iso,lxc}` | 192.168.2.0/24 |
|
||||
| `/srv/ha-data/pxe-boot/images` | 192.168.2.0/24 |
|
||||
| `/srv/ha-data/raspi/volumes` | 192.168.2.0/24 |
|
||||
|
||||
All NFS exports restrict to 192.168.2.0/24 and are served via the VIP at 192.168.2.229. `rw,sync,no_subtree_check,no_root_squash`.
|
||||
|
||||
### Docker Host Current State
|
||||
|
||||
- **Single NIC:** eth0 on vmbr0, 192.168.2.225/24, gateway 192.168.2.254
|
||||
- **Route to storage network (192.168.4.x):** none — no NIC and no route
|
||||
- **iSCSI sessions:** none
|
||||
- **iSCSI nodes discovered:** none
|
||||
- **Docker networks:** several active compose-project networks (core_traefik, core_nextcloud, core_passbolt, core_gramps, core_docker-socket-proxy, plus CI isolation networks)
|
||||
|
||||
---
|
||||
|
||||
## 2. Risk Flags
|
||||
|
||||
### RF-1: Corosync has only one ring (no heartbeat path redundancy)
|
||||
|
||||
`corosync.conf` defines only `ring0_addr` for each node, using 192.168.4.x on vmbr1. No `ring1_addr` / second knet link is configured. On a single Proxmox host, vmbr1 is a software bridge (no physical NIC), so physical link failure is not the concern — but a kernel network stack hiccup, a `pveproxy` restart dropping bridge state, or vmbr1 getting disrupted during heavy DRBD sync all leave corosync with zero fallback path. Missed heartbeats on a two-node cluster with `no-quorum-policy: ignore` do not cause a clean shutdown; they cause a false failover or split-brain.
|
||||
|
||||
Adding the LAN addresses (192.168.2.228 / 192.168.2.227 via ens18/vmbr0) as a second knet link would provide a backup path with no infrastructure changes needed.
|
||||
|
||||
### RF-2: Corosync heartbeat and DRBD replication share vmbr1 — no isolation between them
|
||||
|
||||
Both corosync (knet/UDP, ~1 kB heartbeat packets every ~100 ms) and DRBD replication (protocol C, synchronous, syncing at ~40 MB/s on a 100 GB initial fill at audit time) traverse the same `vmbr1` virtual bridge and terminate on the same ens19 NIC pair inside each HA VM. Under heavy DRBD write load, the guest-kernel scheduler's NIC transmit queue processes both flows together. While corosync's heartbeat is tiny, the absence of QoS/priority marking on vmbr1 means a DRBD burst can delay a heartbeat enough to trigger a ring fault warning. This is a latent risk that grows under high-write workloads.
|
||||
|
||||
### RF-3: STONITH disabled — split-brain protection relies solely on DRBD's resource-only fencing
|
||||
|
||||
`stonith-enabled: false` in the CIB. With `no-quorum-policy: ignore`, both nodes will continue running if corosync loses communication. DRBD's `fencing resource-only` does call `fence-peer` before allowing a Primary promotion, which provides some protection, but there is no hard external power fence to guarantee the other node actually stops. In a real split-brain (both nodes believe they are Primary), data corruption on the shared XFS filesystem is possible. **This is the highest-severity risk in the current setup.**
|
||||
|
||||
Getting STONITH to work on Proxmox-hosted VMs requires either a `fence_pve` agent (Proxmox API fencing) or `fence_virtd` (QEMU guest agent fencing). Neither is configured.
|
||||
|
||||
### RF-4: iSCSI portal bound to `[::0]:3260` — listens on every interface, not just the VIP
|
||||
|
||||
The targetcli portal is `[::0]:3260` (confirmed: `*:3260 *:*` in ss). This means the target is reachable on:
|
||||
- 192.168.2.229 (VIP — correct, failover-safe)
|
||||
- 192.168.2.228 (ha-server-1 LAN IP — does **not** move during failover; an initiator session connecting here would break on failover)
|
||||
- 192.168.4.228 (storage NIC — not reachable by the Docker host today, but unintentionally exposed)
|
||||
|
||||
Binding the portal explicitly to the VIP IP instead of wildcard eliminates the non-VIP reachability risks.
|
||||
|
||||
### RF-5: iSCSI has zero ACLs and no authentication
|
||||
|
||||
`targetcli ls` shows `acls: 0`, `no-gen-acls`, `no-auth`. Any host that can reach port 3260 on any of the above IPs can log into the LUN with no credentials. The Docker host is not yet configured as an initiator — but neither is it blocked.
|
||||
|
||||
### RF-6: Docker host has no path to the storage network — iSCSI would traverse vmbr0/nic0
|
||||
|
||||
CT 105 (docker, 192.168.2.225) has one NIC, on vmbr0. To reach the VIP at 192.168.2.229, iSCSI traffic would travel:
|
||||
|
||||
```
|
||||
docker (eth0/vmbr0) → nic0 (2.5GbE) → vmbr0 → tap200i0 (VM 200 net0/ens18)
|
||||
```
|
||||
|
||||
All of the following share this same path over vmbr0 → nic0:
|
||||
- Docker container traffic (outbound and inter-container)
|
||||
- CI/CD runner traffic (Gitea Actions jobs visible in `docker network ls`)
|
||||
- NFS mounts from pxe-boot, proxmox host itself, and other LAN clients
|
||||
- iSCSI block traffic (protocol-sensitive to latency and retransmit)
|
||||
|
||||
A Nextcloud upload or a CI `nix build` job can saturate nic0 and starve the iSCSI session, causing command timeouts and filesystem errors on the Docker host.
|
||||
|
||||
### RF-7: VIP is on the LAN interface with no storage-specific binding
|
||||
|
||||
The Pacemaker `vip` resource specifies `ip=192.168.2.229, cidr_netmask=24` with no `nic=` parameter. Pacemaker's IPaddr2 agent selects the interface by longest-prefix match, landing it on ens18 (vmbr0/LAN). There is no way to keep this VIP from competing with general LAN traffic on nic0 without moving the VIP to a separate subnet on a different virtual bridge.
|
||||
|
||||
---
|
||||
|
||||
## 3. Recommended Target Layout
|
||||
|
||||
### Design constraints
|
||||
|
||||
- Single Proxmox host: all traffic ultimately shares nic0's bandwidth. The goal is QoS partitioning via separate bridges and subnets, not true physical isolation.
|
||||
- Future physical split: bridge/VLAN IDs chosen here should map cleanly to physical uplink VLAN tags when the HA nodes move to separate hardware.
|
||||
|
||||
### Proposed bridge layout
|
||||
|
||||
| Bridge | Physical port | VLAN tag (future) | Subnet | Purpose |
|
||||
|--------|-------------|-------------------|--------|---------|
|
||||
| `vmbr0` | nic0 | untagged / VLAN 1 | 192.168.2.0/24 | **LAN/management only** — no storage traffic |
|
||||
| `vmbr1` | (none / VLAN 10 on future trunk) | VLAN 10 | 192.168.4.224/29 | **Corosync heartbeat + DRBD replication** (current, keep) |
|
||||
| `vmbr2` *(new)* | (none / VLAN 20 on future trunk) | VLAN 20 | 192.168.5.0/24 | **Storage: iSCSI + NFS client access** |
|
||||
|
||||
This is the minimum-disruption path: vmbr1 stays as-is (no DRBD reconfiguration needed), and the new vmbr2 gives the Docker host a direct path to the storage VIP without crossing vmbr0.
|
||||
|
||||
If stricter isolation is later desired, DRBD can be migrated from vmbr1 to vmbr2 in a separate maintenance window (see §5), leaving vmbr1 as corosync-only.
|
||||
|
||||
### Per-guest NIC assignments in target layout
|
||||
|
||||
| Guest | VMID | NIC | Bridge | Proposed IP | Purpose |
|
||||
|-------|------|-----|--------|-------------|---------|
|
||||
| ha-server-1 | VM 200 | net0 | vmbr0 | 192.168.2.228/24 | LAN/management (keep) |
|
||||
| ha-server-1 | VM 200 | net1 | vmbr1 | 192.168.4.228/29 | Corosync + DRBD (keep) |
|
||||
| ha-server-1 | VM 200 | **net2 (new)** | **vmbr2** | **192.168.5.1/24** | iSCSI + NFS storage client |
|
||||
| ha-server-2 | VM 201 | net0 | vmbr0 | 192.168.2.227/24 | LAN/management (keep) |
|
||||
| ha-server-2 | VM 201 | net1 | vmbr1 | 192.168.4.227/29 | Corosync + DRBD (keep) |
|
||||
| ha-server-2 | VM 201 | **net2 (new)** | **vmbr2** | **192.168.5.2/24** | iSCSI + NFS storage client |
|
||||
| docker | CT 105 | net0 | vmbr0 | 192.168.2.225/24 | LAN/management (keep) |
|
||||
| docker | CT 105 | **net1 (new)** | **vmbr2** | **192.168.5.10/24** | iSCSI + NFS |
|
||||
|
||||
**VIP target:** `192.168.5.100/24` on vmbr2. The Pacemaker `vip` resource changes from `ip=192.168.2.229` to `ip=192.168.5.100, nic=<ens20>` (whichever name the new NIC gets inside the HA VMs). The existing `192.168.2.229` LAN VIP can optionally be retained as a separate static alias on ens18 for management-plane access, but should not be the iSCSI portal target.
|
||||
|
||||
**iSCSI portal:** Bind to `192.168.5.100:3260` instead of `[::0]:3260`. In targetcli: remove the wildcard portal, add `portals/ create 192.168.5.100`.
|
||||
|
||||
**NFS exports:** NFS is a file-level protocol and is fine being accessed over a routed path. After the VIP moves, non-docker LAN clients (proxmox host, pxe-boot, raspi) can reach NFS either via a static route to 192.168.5.0/24 or by keeping a secondary static alias at 192.168.2.229 on ens18 dedicated to NFS. Either approach works — NFS handles reconnect gracefully in ways iSCSI block I/O cannot.
|
||||
|
||||
**Corosync second ring (independent, low-disruption improvement):**
|
||||
|
||||
Add a second knet link using the LAN addresses as a backup heartbeat path. Edit `corosync.conf` on both nodes:
|
||||
|
||||
```
|
||||
node { ring0_addr: 192.168.4.228; ring1_addr: 192.168.2.228; name: ha-server-1; nodeid: 1; }
|
||||
node { ring0_addr: 192.168.4.227; ring1_addr: 192.168.2.227; name: ha-server-2; nodeid: 2; }
|
||||
```
|
||||
|
||||
Requires a corosync service restart (brief cluster pause, ~5 seconds), no interface or bridge changes.
|
||||
|
||||
**Future physical-host split:**
|
||||
When ha-server-1 and ha-server-2 move to separate physical machines, vmbr1 and vmbr2 become VLAN-tagged sub-interfaces on a physical trunk (e.g. VLAN 10 → cluster, VLAN 20 → storage). The bridge/subnet/IP layout above is designed so the tag numbers can be layered onto the existing addresses without renumbering.
|
||||
|
||||
---
|
||||
|
||||
## 4. Gap List
|
||||
|
||||
| Gap | Action needed |
|
||||
|-----|--------------|
|
||||
| `vmbr2` does not exist on pve1 | Create internal bridge: `/etc/network/interfaces.d/vmbr2.conf` with `bridge-ports none`, `inet manual` |
|
||||
| VM 200 and VM 201 have no net2 | `qm set 200 --net2 virtio,bridge=vmbr2` / `qm set 201 --net2 virtio,bridge=vmbr2` (hot-plug, no reboot needed) |
|
||||
| CT 105 has no net1 | `pct set 105 --net1 name=eth1,bridge=vmbr2,ip=192.168.5.10/24` |
|
||||
| HA VMs have no OS config for the new NIC | NixOS `networking.interfaces.<ens20>` with `ipv4.addresses = [{address="192.168.5.1"; prefixLength=24;}]` per host (name may differ — check `ip link` after hotplug) |
|
||||
| VIP needs to move to 192.168.5.100 on vmbr2 | `pcs resource update vip ip=192.168.5.100 cidr_netmask=24 nic=<ens20>` |
|
||||
| iSCSI portal bound to `[::0]` | `targetcli /iscsi/iqn.2026-01.home.sweet:ha-storage/tpg1/portals delete ::0 3260` then `create 192.168.5.100`; save and restart via `pcs resource restart iscsi-target` |
|
||||
| iSCSI ACLs empty | Get Docker initiator IQN via `iscsiadm -m iface` on CT 105, then add via targetcli `acls/ create <iqn>` |
|
||||
| Docker host has no iSCSI initiator config | `iscsiadm -m discoverydb -t sendtargets -p 192.168.5.100 -D` then `iscsiadm -m node -l` once ACLs are set |
|
||||
| Corosync single ring | Add `ring1_addr` entries in `corosync.conf` using LAN IPs; restart corosync cluster-wide (one node at a time) |
|
||||
| STONITH not configured | Evaluate `fence_pve` (Proxmox API agent); document accepted risk if deferred |
|
||||
| DRBD still on vmbr1 (optional, separate window) | Stop ms-drbd0 via pcs, edit `/etc/drbd.conf` on both nodes (change `192.168.4.x` → `192.168.5.x`), restart DRBD, re-enable via pcs |
|
||||
|
||||
---
|
||||
|
||||
## 5. Migration Notes
|
||||
|
||||
### Non-disruptive (no service impact)
|
||||
|
||||
- **Create vmbr2 on pve1:** Bridge definition edit only; no effect on existing bridges or guests.
|
||||
- **Hot-add net2 to VMs 200/201:** Proxmox allows adding a NIC without reboot (`qm set 200 --net2 ...`). The NIC appears inside the VM immediately via QEMU hotplug but will be unconfigured (down) inside NixOS until the NixOS config is deployed — no impact on running services.
|
||||
- **Add net1 to CT 105:** LXC NIC hotplug works similarly; CT does not need to restart.
|
||||
- **Add corosync ring1:** Requires `systemctl restart corosync` on both nodes (one at a time). Pacemaker briefly sees corosync go offline and recover; with two nodes and `wait_for_all: 0`, this typically completes in under 5 seconds and resources stay running.
|
||||
|
||||
### Disruptive — requires maintenance window
|
||||
|
||||
- **Move VIP from 192.168.2.229 to 192.168.5.100:** `pcs resource update vip ip=192.168.5.100` causes Pacemaker to immediately stop the old VIP and start the new one. Any NFS mounts referencing 192.168.2.229 will stall until remounted at the new address (or a static alias is added at 192.168.2.229 on ens18). No iSCSI sessions exist yet, so no iSCSI disruption.
|
||||
|
||||
- **Change iSCSI portal from `[::0]` to VIP-specific:** Requires `pcs resource restart iscsi-target` after the targetcli portal change — brief target unavailability. Any initiator sessions (once configured) will need to re-login.
|
||||
|
||||
- **Migrate DRBD replication from 192.168.4.x to 192.168.5.x** (optional — only needed to give corosync sole ownership of vmbr1):
|
||||
1. `pcs resource disable ms-drbd0` — demotes DRBD Primary, stops ha-group (unmounts XFS, stops iSCSI + NFS + VIP)
|
||||
2. `drbdadm down ha-data` on both nodes
|
||||
3. Edit `/etc/drbd.conf` on both nodes (change `address` lines)
|
||||
4. `drbdadm up ha-data` on both nodes
|
||||
5. `pcs resource enable ms-drbd0` — Pacemaker re-promotes, mounts, starts services
|
||||
|
||||
DRBD does **not** require a full resync when only the address changes — the disk data and metadata are unchanged; only the TCP connection endpoint changes. However, the initial sync was in progress at audit time (~20% at ~40 MB/s). Recommend waiting for that sync to complete before scheduling this migration.
|
||||
+18
-115
@@ -2,88 +2,16 @@
|
||||
|
||||
## Subnets
|
||||
|
||||
| Subnet | VLAN | CIDR | Purpose | Routed? |
|
||||
|---|---|---|---|---|
|
||||
| LAN | 2 (native/untagged) | `192.168.2.0/24` | General LAN — clients and infrastructure | Yes (gateway .254) |
|
||||
| Cluster | 10 | `192.168.10.224/29` | HA file server DRBD replication + Corosync heartbeat | No — internal `vmbr1` only, no uplink |
|
||||
| Storage client | 20 | `192.168.20.0/24` | HA file server NFS (and iSCSI if needed) — docker and swarm nodes mount from VIP here | No — internal `vmbr2` only, no uplink |
|
||||
| Subnet | CIDR | Purpose | Routed? |
|
||||
|---|---|---|---|
|
||||
| LAN | `192.168.2.0/24` | General LAN — clients and infrastructure | Yes (gateway .254) |
|
||||
| Storage | `192.168.4.0/29` | HA file server DRBD replication | No — internal `vmbr1` only, no uplink |
|
||||
|
||||
The cluster and storage-client subnets never leave pve1. `vmbr1` and `vmbr2` are Proxmox Linux
|
||||
bridges with no physical port attached; traffic between guests on each bridge stays in-kernel.
|
||||
The storage subnet never leaves pve1. `vmbr1` is a Proxmox Linux bridge with no physical port
|
||||
attached; traffic between the two HA file server VMs stays in-kernel.
|
||||
|
||||
VLAN IDs match the third octet of each subnet (VLAN 2 → 192.168.**2**.x, VLAN 10 → 192.168.**10**.x,
|
||||
VLAN 20 → 192.168.**20**.x). The host octet is consistent across all subnets — e.g. ha-node1
|
||||
is always `.228`: `192.168.2.228` (LAN), `192.168.10.228` (cluster), `192.168.20.228` (storage client).
|
||||
|
||||
**Protocol separation** (enforced by firewall on HA nodes):
|
||||
- NFS (ports 111, 2049, 20048): both subnets, each restricted to its own CIDR
|
||||
- VLAN 2 only → `vip-lan` (192.168.2.229) — pxe-boot and other LAN clients
|
||||
- VLAN 20 only → `vip-storage` (192.168.20.229) — docker, future swarm nodes
|
||||
- iSCSI (port 3260): VLAN 20 only — available but not in active use; NFS is preferred
|
||||
for multi-host access (shared volumes across a Docker Swarm require a shared filesystem,
|
||||
not per-host block devices)
|
||||
|
||||
---
|
||||
|
||||
## DNS Zones
|
||||
|
||||
FreeIPA (domain-controller.sweet.home) is authoritative for all zones. Three
|
||||
zones correspond to the three subnets — one per VLAN. All zones are internal
|
||||
only; no external delegation.
|
||||
|
||||
### sweet.home — VLAN 2 (192.168.2.x)
|
||||
|
||||
General LAN zone. All infrastructure hostnames live here.
|
||||
|
||||
| Hostname | A record | Notes |
|
||||
|---|---|---|
|
||||
| `domain-controller.sweet.home` | `192.168.2.253` | FreeIPA / KDC / DNS |
|
||||
| `ha-vip-lan.sweet.home` | `192.168.2.229` | Pacemaker `vip-lan` — NFS for LAN clients |
|
||||
| `ha-server-1.sweet.home` | `192.168.2.228` | HA node 1 management NIC |
|
||||
| `ha-server-2.sweet.home` | `192.168.2.227` | HA node 2 management NIC |
|
||||
| `server.sweet.home` | `192.168.2.226` | Current ZFS/NFS server (retiring) |
|
||||
| `docker.sweet.home` | `192.168.2.225` | Docker/Traefik host |
|
||||
| `nix-cache.sweet.home` | `192.168.2.224` | Nix binary cache + remote builder |
|
||||
| `pxe-boot.sweet.home` | `192.168.2.223` | PXE / TFTP / HTTP netboot |
|
||||
| `tailscale-router.sweet.home` | `192.168.2.222` | Tailscale exit node |
|
||||
| `tor-relay.sweet.home` | `192.168.2.221` | Tor relay |
|
||||
| `pdm.sweet.home` | `192.168.2.220` | Proxmox Deploy Manager |
|
||||
| `nixos.sweet.home` | `192.168.2.39` | Bare-metal workstation (DHCP) |
|
||||
| `pve1.sweet.home` | `192.168.2.245` | Proxmox VE hypervisor |
|
||||
| `pbs.sweet.home` | `192.168.2.244` | Proxmox Backup Server |
|
||||
|
||||
PTR records exist for all static hosts. The workstation (`nixos.sweet.home`) is
|
||||
DHCP-assigned; its PTR is omitted.
|
||||
|
||||
### cluster.home — VLAN 10 (192.168.10.x)
|
||||
|
||||
Internal only — Corosync ring0 heartbeat and DRBD replication between HA nodes.
|
||||
No VIP exists on this subnet (DRBD/Corosync endpoints are static per-node IPs).
|
||||
|
||||
| Hostname | A record | Notes |
|
||||
|---|---|---|
|
||||
| `ha-server-1.cluster.home` | `192.168.10.228` | HA node 1 cluster NIC (ens19 / vmbr1) |
|
||||
| `ha-server-2.cluster.home` | `192.168.10.227` | HA node 2 cluster NIC (ens19 / vmbr1) |
|
||||
|
||||
PTR records exist for both. DNS here is for debugging convenience — DRBD and
|
||||
Corosync use the IPs from the NixOS config directly, not DNS.
|
||||
|
||||
### storage.home — VLAN 20 (192.168.20.x)
|
||||
|
||||
Internal only — NFS (and iSCSI) client access to the HA storage VIP. NFS clients
|
||||
mount from **`nfs.storage.home`** (the Pacemaker floating VIP) so mounts survive
|
||||
failover transparently without reconfiguration.
|
||||
|
||||
| Hostname | A record | Notes |
|
||||
|---|---|---|
|
||||
| `nfs.storage.home` | `192.168.20.229` | Pacemaker `vip-storage` — NFS + iSCSI VIP |
|
||||
| `ha-server-1.storage.home` | `192.168.20.228` | HA node 1 storage-client NIC (ens20 / vmbr2) |
|
||||
| `ha-server-2.storage.home` | `192.168.20.227` | HA node 2 storage-client NIC (ens20 / vmbr2) |
|
||||
| `docker.storage.home` | `192.168.20.225` | Docker host storage-client NIC (eth1 / vmbr2) |
|
||||
| `server.storage.home` | `192.168.20.226` | server VM storage-client NIC (decommissioned — remove DNS record after VM is destroyed) |
|
||||
|
||||
PTR records exist for all five. Remove `server.storage.home`, `server.sweet.home`,
|
||||
and their PTRs from FreeIPA DNS once the server VM is destroyed.
|
||||
The host octet is consistent across subnets for any host that has multiple interfaces — e.g.
|
||||
ha-node1 is always `.228` (LAN: `192.168.2.228`, storage: `192.168.4.228`).
|
||||
|
||||
---
|
||||
|
||||
@@ -128,10 +56,10 @@ All VMs and LXC containers run on pve1.
|
||||
|
||||
| IP | Hostname | Role | Status |
|
||||
|---|---|---|---|
|
||||
| `192.168.2.229` | ha-vip-lan | HA file server LAN floating VIP (Pacemaker `vip-lan`) — LAN iSCSI + NFS | Active |
|
||||
| `192.168.2.228` | ha-node1 | HA file server node 1 — management NIC | Active |
|
||||
| `192.168.2.227` | ha-node2 | HA file server node 2 — management NIC | Active |
|
||||
| `192.168.2.226` | server | Former NFS/ZFS file server — decommissioned | Removed from flake |
|
||||
| `192.168.2.229` | ha-vip | HA file server iSCSI floating VIP (Pacemaker) | Future |
|
||||
| `192.168.2.228` | ha-node1 | HA file server node 1 (DRBD + XFS + iSCSI) | Future |
|
||||
| `192.168.2.227` | ha-node2 | HA file server node 2 (DRBD + XFS + iSCSI) | Future |
|
||||
| `192.168.2.226` | server | Current NFS/ZFS file server — retires when HA is live | Retiring |
|
||||
| `192.168.2.225` | docker | Docker / Traefik stack | Active |
|
||||
| `192.168.2.224` | nix-cache | Nix binary cache + remote builder | Active |
|
||||
| `192.168.2.223` | pxe-boot | PXE / TFTP / HTTP netboot server | Active |
|
||||
@@ -150,40 +78,16 @@ MAC address churn.
|
||||
|
||||
---
|
||||
|
||||
## Cluster network — VLAN 10 — 192.168.10.224/29
|
||||
## Storage network — 192.168.4.0/29
|
||||
|
||||
Internal to pve1 only. Proxmox bridge `vmbr1`, no physical NIC attached.
|
||||
|
||||
| IP | Hostname | Interface role |
|
||||
|---|---|---|
|
||||
| `192.168.10.228` | ha-node1 | DRBD replication + Corosync ring0 (primary heartbeat) |
|
||||
| `192.168.10.227` | ha-node2 | DRBD replication + Corosync ring0 (primary heartbeat) |
|
||||
| `192.168.4.228` | ha-node1 | DRBD replication NIC |
|
||||
| `192.168.4.227` | ha-node2 | DRBD replication NIC |
|
||||
| — | no gateway | Isolated — not routed to LAN or internet |
|
||||
|
||||
Corosync ring1 (backup heartbeat only) uses the LAN IPs (`192.168.2.228` / `192.168.2.227`)
|
||||
over `vmbr0` — no additional bridge needed, and DRBD traffic never crosses ring1.
|
||||
|
||||
---
|
||||
|
||||
## Storage-client network — VLAN 20 — 192.168.20.0/24
|
||||
|
||||
Internal to pve1 only. Proxmox bridge `vmbr2`, no physical NIC attached.
|
||||
|
||||
| IP | Hostname | Interface / role |
|
||||
|---|---|---|
|
||||
| `192.168.20.229` | ha-vip-storage | Pacemaker floating VIP — NFS + iSCSI endpoint |
|
||||
| `192.168.20.228` | ha-node1 | Storage-client NIC (ens20 / vmbr2) |
|
||||
| `192.168.20.227` | ha-node2 | Storage-client NIC (ens20 / vmbr2) |
|
||||
| `192.168.20.226` | server | Storage-client NIC (ens19 / vmbr2) — decommissioned |
|
||||
| `192.168.20.225` | docker | Storage-client NIC (eth1 / vmbr2) — NFS client |
|
||||
| — | no gateway | Isolated — not routed to LAN or internet |
|
||||
|
||||
NFS clients mount from `192.168.20.229` (surviving failover transparently via the VIP).
|
||||
Firewall on each HA node restricts NFS and iSCSI ports to `192.168.20.0/24` — LAN hosts
|
||||
cannot reach either service on this VIP. The `vip-storage` endpoint is not reachable
|
||||
from the workstation directly (internal bridge only); health checks proxy through the
|
||||
active HA node.
|
||||
|
||||
---
|
||||
|
||||
## Migration reference
|
||||
@@ -197,10 +101,9 @@ Current → target IP for every host being renumbered.
|
||||
| pve1 | `192.168.2.250` | `192.168.2.245` | `/etc/network/interfaces` on Proxmox host |
|
||||
| pbs | `192.168.2.108` | `192.168.2.244` | static config on PBS host |
|
||||
| nixos workstation | `192.168.2.119` | `192.168.2.243` | `networking.interfaces` / NetworkManager on guest |
|
||||
| ha-node1 | — | `192.168.2.228` (LAN), `192.168.10.228` (cluster/VLAN 10), `192.168.20.228` (storage/VLAN 20) | active |
|
||||
| ha-node2 | — | `192.168.2.227` (LAN), `192.168.10.227` (cluster/VLAN 10), `192.168.20.227` (storage/VLAN 20) | active |
|
||||
| ha-vip-lan | — | `192.168.2.229` (vmbr0 / Pacemaker `vip-lan`) — NFS endpoint for LAN clients | active |
|
||||
| ha-vip-storage | — | `192.168.20.229` (vmbr2 / Pacemaker `vip-storage`) — iSCSI endpoint for VLAN 20 clients | active |
|
||||
| ha-node1 | — | `192.168.2.228` | future |
|
||||
| ha-node2 | — | `192.168.2.227` | future |
|
||||
| ha-vip | — | `192.168.2.229` | future (Pacemaker resource) |
|
||||
| server | `192.168.2.252` | `192.168.2.226` | static config on guest |
|
||||
| docker | `192.168.2.249` | `192.168.2.225` | static config on guest |
|
||||
| nix-cache | `192.168.2.120` | `192.168.2.224` | static config on guest |
|
||||
|
||||
+1
-83
@@ -336,89 +336,7 @@ temporarily use either.
|
||||
|
||||
---
|
||||
|
||||
## Stage 6 — HA storage cutover (docker NFS remount)
|
||||
|
||||
> **Prerequisites:**
|
||||
> - HA cluster fully deployed and `vip-storage` (`nfs.storage.home` → 192.168.20.229) serving NFS ✓
|
||||
> - DNS configured: `storage.home` zone populated, `nfs.storage.home` resolves to 192.168.20.229 ✓
|
||||
> - docker CT has eth1 on vmbr2 (`docker.storage.home` → 192.168.20.225) ✓
|
||||
> - Final rsync from server.sweet.home to `/srv/ha-data` complete before step 6b
|
||||
|
||||
docker.sweet.home currently NFS-mounts its persistent volumes from `server.sweet.home`
|
||||
(`192.168.2.226:/tank/docker/...`). This stage moves those mounts to the HA cluster's
|
||||
storage VIP so server can be decommissioned.
|
||||
|
||||
### 6a. Final rsync from server to HA cluster
|
||||
|
||||
Run from server.sweet.home (or over SSH from the workstation) to sync any data written
|
||||
since the initial rsync:
|
||||
|
||||
```bash
|
||||
# Confirm active HA node and mount point
|
||||
ssh wayne@192.168.2.228 'sudo findmnt /srv/ha-data' # check which node is active
|
||||
|
||||
# rsync each dataset (adjust source paths to match /tank layout on server)
|
||||
sudo rsync -av --delete /tank/docker/config/ wayne@<active-node-ip>:/srv/ha-data/docker/config/
|
||||
sudo rsync -av --delete /tank/docker/databases/ wayne@<active-node-ip>:/srv/ha-data/docker/databases/
|
||||
sudo rsync -av --delete /tank/docker/volumes/ wayne@<active-node-ip>:/srv/ha-data/docker/volumes/
|
||||
sudo rsync -av --delete /tank/docker/nextcloud-data/ wayne@<active-node-ip>:/srv/ha-data/docker/nextcloud-data/
|
||||
```
|
||||
|
||||
### 6b. Update docker NixOS config to mount from vip-storage
|
||||
|
||||
In `hosts/docker/host.nix` (or wherever the NFS mount fileSystems are declared), change
|
||||
the NFS server from `server.sweet.home` / `192.168.2.226` to `nfs.storage.home`:
|
||||
|
||||
```nix
|
||||
# Before:
|
||||
fileSystems."/mnt/docker/config" = {
|
||||
device = "server:/tank/docker/config"; # or 192.168.2.226:...
|
||||
...
|
||||
};
|
||||
|
||||
# After:
|
||||
fileSystems."/mnt/docker/config" = {
|
||||
device = "nfs.storage.home:/srv/ha-data/docker/config";
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
Using the DNS name (`nfs.storage.home`) rather than the VIP IP means the mount
|
||||
config survives a future VIP renumber without touching the NixOS config.
|
||||
Repeat for all four docker shares (`config`, `databases`, `volumes`, `nextcloud-data`).
|
||||
Then rebuild docker:
|
||||
|
||||
```bash
|
||||
# On the workstation — or via Switch-nix on docker itself
|
||||
sudo nixos-rebuild switch --no-write-lock-file --refresh \
|
||||
--flake "git+https://gitea.lan.ddnsgeek.com/beatzaplenty/nixos.git#lxc-docker"
|
||||
```
|
||||
|
||||
### 6c. Verify mounts and container health
|
||||
|
||||
```bash
|
||||
ssh wayne@192.168.2.225 'findmnt | grep 192.168.20' # mounts should show vip-storage
|
||||
ssh wayne@192.168.2.225 'docker ps' # all containers running
|
||||
```
|
||||
|
||||
Spot-check Nextcloud, Traefik, and any database containers for connectivity.
|
||||
|
||||
### 6d. Decommission server.sweet.home
|
||||
|
||||
Once docker is confirmed healthy on the HA NFS mounts:
|
||||
|
||||
```bash
|
||||
# Stop server VM on pve1
|
||||
ssh wayne@192.168.2.245 'sudo qm stop 101'
|
||||
|
||||
# (Optional) Archive the ZFS pool snapshot before destroying
|
||||
# Then after a settling period:
|
||||
ssh wayne@192.168.2.245 'sudo qm destroy 101 --destroy-unreferenced-disks 1'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 7 — Final cleanup
|
||||
## Stage 6 — Final cleanup
|
||||
|
||||
Once all hosts are at their new IPs and verified:
|
||||
|
||||
|
||||
Generated
+6
-6
@@ -173,11 +173,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785119570,
|
||||
"narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=",
|
||||
"lastModified": 1784350909,
|
||||
"narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d",
|
||||
"rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -259,11 +259,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1785133411,
|
||||
"narHash": "sha256-Yjv0WEg39KRYS0rBdTbu6Fc/or/ihAKk13W9sQ6VWd0=",
|
||||
"lastModified": 1784432872,
|
||||
"narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2f5a153c270b70cb0f8c11f46d96d6d3bc39f4e3",
|
||||
"rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
# (hostName, hostId, per-machine secrets). Every build type except
|
||||
# nix-cache itself consumes the nix-cache substituter and remote
|
||||
# builder.
|
||||
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix, nameSuffix ? "" }:
|
||||
mkTarget = { platform, buildType, hostPath, homeFile ? ./modules/common/home.nix }:
|
||||
let
|
||||
flakeTarget = "${platform}-${buildType}${nameSuffix}";
|
||||
flakeTarget = "${platform}-${buildType}";
|
||||
in
|
||||
nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
@@ -109,6 +109,9 @@
|
||||
proxmox-nix-cache = mkTarget { platform = "proxmox"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
|
||||
lxc-nix-cache = mkTarget { platform = "lxc"; buildType = "nix-cache"; hostPath = ./hosts/nix-cache/host.nix; };
|
||||
|
||||
linode-server = mkTarget { platform = "linode"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
|
||||
proxmox-server = mkTarget { platform = "proxmox"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
|
||||
lxc-server = mkTarget { platform = "lxc"; buildType = "server"; hostPath = ./hosts/server/host.nix; };
|
||||
|
||||
linode-docker = mkTarget { platform = "linode"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
|
||||
proxmox-docker = mkTarget { platform = "proxmox"; buildType = "docker"; hostPath = ./hosts/docker/host.nix; };
|
||||
@@ -128,8 +131,8 @@
|
||||
|
||||
lxc-tor-relay = mkTarget { platform = "lxc"; buildType = "tor-relay"; hostPath = ./hosts/tor-relay/host.nix; };
|
||||
|
||||
proxmox-ha-server-1 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-1/host.nix; nameSuffix = "-1"; };
|
||||
proxmox-ha-server-2 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-2/host.nix; nameSuffix = "-2"; };
|
||||
proxmox-ha-server-1 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-1/host.nix; };
|
||||
proxmox-ha-server-2 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-2/host.nix; };
|
||||
};
|
||||
|
||||
# Auto-install environments (migrated from the former nix-auto-installer
|
||||
|
||||
@@ -5,20 +5,15 @@
|
||||
hostName = "docker";
|
||||
hostId = "007f0200";
|
||||
useDHCP = false;
|
||||
interfaces = {
|
||||
${vars.vmLanInterface}.ipv4.addresses = [{ address = vars.dockerIp; prefixLength = vars.lanPrefixLength; }];
|
||||
${vars.lxcStorageInterface}.ipv4.addresses = [{ address = vars.dockerStorageIp; prefixLength = vars.haClientPrefixLength; }];
|
||||
};
|
||||
interfaces.${vars.vmLanInterface}.ipv4.addresses = [{
|
||||
address = vars.dockerIp;
|
||||
prefixLength = vars.lanPrefixLength;
|
||||
}];
|
||||
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||
nameservers = [ vars.domainControllerIp ];
|
||||
};
|
||||
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";
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
{ vars, ... }:
|
||||
{
|
||||
imports = [
|
||||
(import ../../modules/beszel/host-token.nix {
|
||||
name = "ha-server-1";
|
||||
sopsFile = ../../secrets/ha-server-1.yaml;
|
||||
})
|
||||
];
|
||||
|
||||
networking = {
|
||||
hostName = vars.haServer1Host;
|
||||
hostId = "3a4b5c6d";
|
||||
useDHCP = false;
|
||||
interfaces = {
|
||||
${vars.vmLanInterface}.ipv4.addresses = [{ address = vars.haServer1Ip; prefixLength = vars.lanPrefixLength; }];
|
||||
${vars.vmStorageInterface}.ipv4.addresses = [{ address = vars.haServer1StorageIp; prefixLength = vars.haStoragePrefixLength; }];
|
||||
${vars.vmStorageClientInterface}.ipv4.addresses = [{ address = vars.haServer1ClientIp; prefixLength = vars.haClientPrefixLength; }];
|
||||
};
|
||||
interfaces.${vars.vmLanInterface}.ipv4.addresses = [{
|
||||
address = vars.haServer1Ip;
|
||||
prefixLength = vars.lanPrefixLength;
|
||||
}];
|
||||
interfaces.${vars.vmStorageInterface}.ipv4.addresses = [{
|
||||
address = vars.haServer1StorageIp;
|
||||
prefixLength = vars.haStoragePrefixLength;
|
||||
}];
|
||||
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||
nameservers = [ vars.domainControllerIp ];
|
||||
};
|
||||
|
||||
# Set KEY after pairing this host with the beszel hub; the token is sops-managed.
|
||||
services.beszel.agent.environment.KEY = "";
|
||||
|
||||
system.stateVersion = "26.05";
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
{ vars, ... }:
|
||||
{
|
||||
imports = [
|
||||
(import ../../modules/beszel/host-token.nix {
|
||||
name = "ha-server-2";
|
||||
sopsFile = ../../secrets/ha-server-2.yaml;
|
||||
})
|
||||
];
|
||||
|
||||
networking = {
|
||||
hostName = vars.haServer2Host;
|
||||
hostId = "7e8f9a0b";
|
||||
useDHCP = false;
|
||||
interfaces = {
|
||||
${vars.vmLanInterface}.ipv4.addresses = [{ address = vars.haServer2Ip; prefixLength = vars.lanPrefixLength; }];
|
||||
${vars.vmStorageInterface}.ipv4.addresses = [{ address = vars.haServer2StorageIp; prefixLength = vars.haStoragePrefixLength; }];
|
||||
${vars.vmStorageClientInterface}.ipv4.addresses = [{ address = vars.haServer2ClientIp; prefixLength = vars.haClientPrefixLength; }];
|
||||
};
|
||||
interfaces.${vars.vmLanInterface}.ipv4.addresses = [{
|
||||
address = vars.haServer2Ip;
|
||||
prefixLength = vars.lanPrefixLength;
|
||||
}];
|
||||
interfaces.${vars.vmStorageInterface}.ipv4.addresses = [{
|
||||
address = vars.haServer2StorageIp;
|
||||
prefixLength = vars.haStoragePrefixLength;
|
||||
}];
|
||||
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||
nameservers = [ vars.domainControllerIp ];
|
||||
};
|
||||
|
||||
# Set KEY after pairing this host with the beszel hub; the token is sops-managed.
|
||||
services.beszel.agent.environment.KEY = "";
|
||||
|
||||
system.stateVersion = "26.05";
|
||||
}
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
{ vars, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(import ../../modules/beszel/host-token.nix {
|
||||
name = "nix-cache";
|
||||
sopsFile = ../../secrets/nix-cache.yaml;
|
||||
})
|
||||
(import ../../modules/ipa/client.nix {
|
||||
keytabSopsFile = ../../secrets/nix-cache.keytab;
|
||||
caCertFile = ../../certs/ipa-ca.crt;
|
||||
})
|
||||
];
|
||||
|
||||
networking = {
|
||||
hostName = vars.nixCacheHost;
|
||||
domain = vars.homeDomain;
|
||||
useDHCP = false;
|
||||
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
|
||||
address = vars.nixCacheIp;
|
||||
@@ -12,6 +24,11 @@
|
||||
nameservers = [ vars.domainControllerIp ];
|
||||
};
|
||||
|
||||
services.beszel.agent.environment = {
|
||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||
};
|
||||
|
||||
# Preserved from the pre-refactor `nix-cache` target — stateVersion must
|
||||
# never be bumped on an already-installed machine.
|
||||
system.stateVersion = "25.05";
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
# Optional: set environment vars
|
||||
sessionVariables = {
|
||||
EDITOR = "nano";
|
||||
EDITOR = "vim";
|
||||
SOPS_AGE_KEY_FILE = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
{ vars, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(import ../../modules/ipa/client.nix {
|
||||
keytabSopsFile = ../../secrets/pxe-boot.keytab;
|
||||
caCertFile = ../../certs/ipa-ca.crt;
|
||||
})
|
||||
];
|
||||
|
||||
networking = {
|
||||
hostName = "pxe-boot";
|
||||
domain = vars.homeDomain;
|
||||
useDHCP = false;
|
||||
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
|
||||
address = vars.pxeServerIp;
|
||||
@@ -11,9 +19,7 @@
|
||||
defaultGateway = { address = vars.lanGateway; interface = vars.lxcLanInterface; };
|
||||
nameservers = [ vars.domainControllerIp ];
|
||||
};
|
||||
services.beszel.agent.environment = {
|
||||
# KEY = "";
|
||||
};
|
||||
|
||||
# Preserved from the pre-refactor `pxe-boot` target — stateVersion must
|
||||
# never be bumped on an already-installed machine.
|
||||
system.stateVersion = "25.05";
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{ vars, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(import ../../modules/beszel/host-token.nix {
|
||||
name = "server";
|
||||
sopsFile = ../../secrets/server.yaml;
|
||||
})
|
||||
];
|
||||
|
||||
networking = {
|
||||
hostName = vars.nfsServerHost;
|
||||
hostId = "6689f93e";
|
||||
useDHCP = false;
|
||||
interfaces.${vars.vmLanInterface}.ipv4.addresses = [{
|
||||
address = vars.serverIp;
|
||||
prefixLength = vars.lanPrefixLength;
|
||||
}];
|
||||
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||
nameservers = [ vars.domainControllerIp ];
|
||||
};
|
||||
|
||||
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";
|
||||
};
|
||||
|
||||
# Preserved from the pre-refactor `server` target — stateVersion must never
|
||||
# be bumped on an already-installed machine.
|
||||
system.stateVersion = "25.05";
|
||||
}
|
||||
@@ -1,8 +1,20 @@
|
||||
{ vars, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(import ../../modules/beszel/host-token.nix {
|
||||
name = "tailscale-router";
|
||||
sopsFile = ../../secrets/tailscale-router.yaml;
|
||||
})
|
||||
(import ../../modules/ipa/client.nix {
|
||||
keytabSopsFile = ../../secrets/tailscale-router.keytab;
|
||||
caCertFile = ../../certs/ipa-ca.crt;
|
||||
})
|
||||
];
|
||||
|
||||
networking = {
|
||||
hostName = "tailscale-router";
|
||||
domain = vars.homeDomain;
|
||||
useDHCP = false;
|
||||
interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{
|
||||
address = vars.tailscaleRouterIp;
|
||||
@@ -12,6 +24,10 @@
|
||||
nameservers = [ vars.domainControllerIp ];
|
||||
};
|
||||
|
||||
services.beszel.agent.environment = {
|
||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||
};
|
||||
|
||||
# No networking.hostId: only ZFS-touching hosts (server, docker) need one
|
||||
# for pool-import safety, and this host does neither.
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
{ vars, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
(import ../../modules/beszel/host-token.nix {
|
||||
name = "tor-relay";
|
||||
sopsFile = ../../secrets/tor-relay.yaml;
|
||||
})
|
||||
];
|
||||
|
||||
networking = {
|
||||
hostName = "tor-relay";
|
||||
useDHCP = false;
|
||||
@@ -15,6 +22,10 @@
|
||||
# No networking.hostId: only ZFS-touching hosts need one for pool-import
|
||||
# safety, and this host does neither.
|
||||
|
||||
services.beszel.agent.environment = {
|
||||
KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||
};
|
||||
|
||||
# A genuinely new host (not a pre-refactor carry-over), so it tracks the
|
||||
# flake's current nixpkgs release rather than being pinned to an older one.
|
||||
system.stateVersion = "26.05";
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
{ config, vars, ... }:
|
||||
{ vars, ... }:
|
||||
|
||||
{
|
||||
# Universal token shared by all beszel agents. Add to secrets/common.yaml:
|
||||
# sops secrets/common.yaml
|
||||
# beszel-token: <value from the beszel hub UI>
|
||||
sops.secrets."beszel-token" = { };
|
||||
|
||||
sops.templates."beszel.env".content = ''
|
||||
TOKEN=${config.sops.placeholder."beszel-token"}
|
||||
'';
|
||||
|
||||
services.beszel.agent = {
|
||||
enable = true;
|
||||
environmentFile = config.sops.templates."beszel.env".path;
|
||||
environment = {
|
||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}";
|
||||
KEY = vars.beszelHubKey;
|
||||
};
|
||||
services.beszel.agent.enable = true;
|
||||
services.beszel.agent.environment = {
|
||||
#DOCKER_HOST = "tcp://docker-socket-proxy:2375";
|
||||
HUB_URL = "http://${vars.dockerHost}.${vars.homeDomain}:${toString vars.ports.beszelHub}";
|
||||
};
|
||||
|
||||
# The upstream module runs beszel-agent under DynamicUser with
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{ name, sopsFile }:
|
||||
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
sops.secrets."beszel-token".sopsFile = sopsFile;
|
||||
sops.templates."${name}-beszel.env".content = ''
|
||||
TOKEN=${config.sops.placeholder."beszel-token"}
|
||||
'';
|
||||
services.beszel.agent.environmentFile = config.sops.templates."${name}-beszel.env".path;
|
||||
}
|
||||
@@ -1,17 +1,6 @@
|
||||
{ config, pkgs, lib, inputs, vars, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
../docker/enable-service.nix
|
||||
];
|
||||
|
||||
nixpkgs.overlays = [
|
||||
(final: prev: {
|
||||
docker = prev.docker_29;
|
||||
docker_cli = prev.docker_29;
|
||||
})
|
||||
];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
inputs.nixos-conf-editor.packages.${pkgs.stdenv.hostPlatform.system}.nixos-conf-editor
|
||||
nodejs
|
||||
@@ -29,7 +18,7 @@
|
||||
];
|
||||
|
||||
boot.loader.grub.useOSProber = true;
|
||||
programs.direnv.enable = true;
|
||||
programs.direnv.enable = true;
|
||||
services = {
|
||||
xserver = {
|
||||
enable = true;
|
||||
@@ -81,70 +70,4 @@
|
||||
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 = "nano";
|
||||
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,20 +14,7 @@
|
||||
# the Beszel hub) is not set yet — add it to hosts/ha-server-{1,2}/host.nix
|
||||
# under services.beszel.agent.environment.KEY once the hub accepts the
|
||||
# new agents, following the pattern in hosts/server/host.nix.
|
||||
{ lib, pkgs, vars, ... }:
|
||||
|
||||
let
|
||||
# Generates /etc/exports lines for all nfsShares data entries.
|
||||
# LAN (VLAN 2): NFS via vip-lan (192.168.2.229) for pxe-boot and other LAN clients.
|
||||
# Storage-client (VLAN 20): NFS via vip-storage (192.168.20.229) for docker and
|
||||
# future swarm nodes; firewall restricts these ports to haClientCidr only.
|
||||
mkNfsExports = storageRoot:
|
||||
lib.concatMapStrings
|
||||
(share:
|
||||
" ${storageRoot}/${share.subpath} ${vars.lanCidr}${vars.nfsShares.options}\n" +
|
||||
" ${storageRoot}/${share.subpath} ${vars.haClientCidr}${vars.nfsShares.options}\n")
|
||||
(lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares));
|
||||
in
|
||||
{ lib, vars, ... }:
|
||||
{
|
||||
imports = [
|
||||
../ha/pacemaker-stack.nix
|
||||
@@ -36,19 +23,21 @@ in
|
||||
../beszel/enable-agent.nix
|
||||
];
|
||||
|
||||
# xfsprogs: mkfs.xfs/xfs_info needed by cluster-init.sh.
|
||||
# openiscsi: iscsiadm needed by acceptance-tests.sh T4 (iSCSI discovery check).
|
||||
environment.systemPackages = [ pkgs.xfsprogs pkgs.openiscsi ];
|
||||
|
||||
services.nfs.server = {
|
||||
enable = true;
|
||||
exports = mkNfsExports vars.haStorageRoot;
|
||||
exports = ''
|
||||
${vars.haStorageRoot}/${vars.nfsShares.dockerConfig.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||
${vars.haStorageRoot}/${vars.nfsShares.dockerVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||
${vars.haStorageRoot}/${vars.nfsShares.dockerDatabases.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||
${vars.haStorageRoot}/${vars.nfsShares.nextcloudData.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||
${vars.haStorageRoot}/${vars.nfsShares.raspiVolumes.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||
${vars.haStorageRoot}/${vars.nfsShares.proxmoxIsos.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||
${vars.haStorageRoot}/${vars.nfsShares.proxmoxLxcImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||
${vars.haStorageRoot}/${vars.nfsShares.pxebootImages.subpath} ${vars.lanCidr}${vars.nfsShares.options}
|
||||
'';
|
||||
};
|
||||
|
||||
# Pacemaker controls nfs-server — prevent systemd from starting it at boot
|
||||
# on both nodes (only the Active node should be serving NFS).
|
||||
systemd.services.nfs-server.wantedBy = lib.mkForce [ ];
|
||||
|
||||
# Same reason as server.nix: exports use standard auth, not Kerberos.
|
||||
systemd.services.rpc-svcgssd.enable = false;
|
||||
}
|
||||
|
||||
@@ -314,7 +314,6 @@ in
|
||||
imports = [
|
||||
../pxe-boot/stage-installer-artifacts.nix
|
||||
../pxe-boot/mount-pxe-images.nix
|
||||
../beszel/enable-agent.nix
|
||||
];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
{ vars, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
poolName = lib.removePrefix "/" vars.storageRoot;
|
||||
|
||||
# For each NFS share subpath, generate every ancestor path so ZFS datasets
|
||||
# are created parent-first. e.g. "docker/config" → ["docker" "docker/config"]
|
||||
ancestors = path:
|
||||
let parts = lib.splitString "/" path;
|
||||
in lib.imap1 (i: _: lib.concatStringsSep "/" (lib.take i parts)) parts;
|
||||
|
||||
poolDatasets = lib.unique (
|
||||
lib.concatMap (share: ancestors share.subpath)
|
||||
(lib.filter builtins.isAttrs (lib.attrValues vars.nfsShares))
|
||||
);
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../beszel/enable-agent.nix
|
||||
../services/zfs/enable-service.nix
|
||||
];
|
||||
|
||||
boot.zfs.extraPools = [ poolName ];
|
||||
|
||||
# On a fresh image deploy the data disk (scsi1) starts blank — no pool
|
||||
# exists yet, so zfs-import-tank.service would spin for 60 s and fail.
|
||||
# This service runs first: if the pool is already present it exits instantly;
|
||||
# otherwise it creates it (with all required datasets) so the standard
|
||||
# import service finds it ready on the very first boot.
|
||||
systemd.services."zfs-init-${poolName}" = {
|
||||
description = "Initialize '${poolName}' ZFS pool on first boot if not present";
|
||||
wantedBy = [ "zfs-import-${poolName}.service" ];
|
||||
before = [ "zfs-import-${poolName}.service" ];
|
||||
after = [ "systemd-udev-settle.service" ];
|
||||
unitConfig.DefaultDependencies = false;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
path = [ pkgs.zfs_unstable ];
|
||||
script = ''
|
||||
# Already imported — nothing to do.
|
||||
if zpool list "${poolName}" >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Locate the data disk first — used for both the fallback import
|
||||
# attempt and, only if the disk is genuinely blank, pool creation.
|
||||
DATA_DISK=""
|
||||
for candidate in /dev/disk/by-id/scsi-*drive-scsi1; do
|
||||
[[ "$candidate" == *-part* ]] && continue
|
||||
[ -b "$candidate" ] && DATA_DISK="$candidate" && break
|
||||
done
|
||||
|
||||
if [ -z "$DATA_DISK" ]; then
|
||||
echo "zfs-init-${poolName}: no data disk found (expected /dev/disk/by-id/scsi-*drive-scsi1)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Try importing via the by-id symlink directory first (normal path),
|
||||
# then fall back to scanning the disk directly. The two-step exists
|
||||
# because of a udev race: systemd-udev-settle.service can clear before
|
||||
# /dev/disk/by-id/ entries are fully populated, causing the first
|
||||
# import to fail even when the pool is intact on the disk.
|
||||
if zpool import -d /dev/disk/by-id -N "${poolName}" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
if zpool import -d "$DATA_DISK" -N "${poolName}" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Both import attempts failed. Before creating a new pool, verify the
|
||||
# disk is genuinely blank — if ZFS label metadata is present the import
|
||||
# failed for some other reason and we must not clobber existing data.
|
||||
if zdb -l "$DATA_DISK" 2>/dev/null | grep -q "name: '${poolName}'"; then
|
||||
echo "zfs-init-${poolName}: $DATA_DISK has ZFS pool '${poolName}' metadata but import failed — refusing to overwrite existing data. Run 'zpool import -d $DATA_DISK ${poolName}' manually to investigate." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Disk is genuinely blank: create the pool. -f is intentionally
|
||||
# omitted so that if we somehow reach this point with an existing pool
|
||||
# on the disk, zpool refuses rather than silently destroying data.
|
||||
echo "zfs-init-${poolName}: creating pool on $DATA_DISK"
|
||||
zpool create "${poolName}" "$DATA_DISK"
|
||||
${lib.concatMapStrings (ds: ''
|
||||
zfs create "${poolName}/${ds}"
|
||||
'') poolDatasets}
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.nfs-server = {
|
||||
after = [ "zfs-mount.service" ];
|
||||
requires = [ "zfs-mount.service" ];
|
||||
};
|
||||
|
||||
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}
|
||||
'';
|
||||
};
|
||||
|
||||
# mountd (20048) is needed for showmount/NFSv3 mount protocol — without it
|
||||
# clients can reach portmapper (111) and get the mountd port back, then
|
||||
# time out trying to connect to it. All three ports need TCP and UDP.
|
||||
networking.firewall.allowedTCPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd vars.ports.nfsMountd ];
|
||||
networking.firewall.allowedUDPPorts = [ vars.ports.nfsRpcbind vars.ports.nfsd vars.ports.nfsMountd ];
|
||||
}
|
||||
@@ -1,7 +1,50 @@
|
||||
_:
|
||||
{ config, pkgs, lib, vars, ... }:
|
||||
|
||||
let
|
||||
# Flake attribute names are now <platform>-<buildtype> (e.g. proxmox-docker)
|
||||
# and no longer match networking.hostName, since a host's hostname stays
|
||||
# fixed while the platform backing it can change. Each nixosConfiguration
|
||||
# stamps its own active target name into /etc/flake-target at build time.
|
||||
mySwitchCmd = ''
|
||||
sudo nixos-rebuild switch \
|
||||
--no-write-lock-file \
|
||||
--refresh \
|
||||
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
||||
'';
|
||||
myTestCmd = ''
|
||||
sudo nixos-rebuild test \
|
||||
--no-write-lock-file \
|
||||
--refresh \
|
||||
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
||||
'';
|
||||
|
||||
# lxc-* hosts pre-seed their SSH host key at build time (see
|
||||
# modules/platforms/lxc.nix) so sops-nix's .sops.yaml recipient matches on
|
||||
# first boot -- without it, secrets permanently fail to decrypt (see that
|
||||
# file's comment for the confirmed failure). That requires --impure plus
|
||||
# NIXOS_HOST_KEYS_DIR pointing at the repo's host-keys/ dir, same pattern
|
||||
# docs/auto-installer.md uses for the installer ISO. A function, not a
|
||||
# shellAlias, since the target name has to interpolate into the middle of
|
||||
# the flake attribute path, not just append after it. Must be run from the
|
||||
# repo root, same as every other host-keys/ command in this repo.
|
||||
buildImageFn = ''
|
||||
buildImage() {
|
||||
if [ -z "$1" ]; then
|
||||
echo "usage: buildImage <flake-target> (e.g. lxc-docker)" >&2
|
||||
return 1
|
||||
fi
|
||||
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||
".#nixosConfigurations.$1.config.system.build.tarball"
|
||||
}
|
||||
'';
|
||||
in
|
||||
{
|
||||
# Switch-nix, Test-nix, and buildImage are defined system-wide in
|
||||
# modules/common/configuration.nix so all users (including IPA accounts)
|
||||
# get them. Add any Home-Manager-only per-user shell config here.
|
||||
programs.bash = {
|
||||
enable = true;
|
||||
shellAliases = {
|
||||
"Switch-nix" = mySwitchCmd;
|
||||
"Test-nix" = myTestCmd;
|
||||
};
|
||||
initExtra = buildImageFn;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,45 +1,17 @@
|
||||
{ config, lib, pkgs, vars, ... }:
|
||||
|
||||
let
|
||||
switchCmd = ''
|
||||
sudo nixos-rebuild switch \
|
||||
--no-write-lock-file \
|
||||
--refresh \
|
||||
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
||||
'';
|
||||
testCmd = ''
|
||||
sudo nixos-rebuild test \
|
||||
--no-write-lock-file \
|
||||
--refresh \
|
||||
--flake git+https://${vars.lanDomain}/beatzaplenty/nixos.git#$(cat /etc/flake-target)
|
||||
'';
|
||||
buildImageFn = ''
|
||||
buildImage() {
|
||||
if [ -z "$1" ]; then
|
||||
echo "usage: buildImage <flake-target> (e.g. lxc-docker)" >&2
|
||||
return 1
|
||||
fi
|
||||
NIXOS_HOST_KEYS_DIR="$(pwd)/host-keys" nix build --impure \
|
||||
".#nixosConfigurations.$1.config.system.build.tarball"
|
||||
}
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./set-locale.nix
|
||||
../ipa/client.nix
|
||||
];
|
||||
imports =
|
||||
[
|
||||
# Include the results of the hardware scan.
|
||||
# ./hardware-configuration.nix
|
||||
./set-locale.nix
|
||||
];
|
||||
# Use the GRUB 2 boot loader.
|
||||
# boot.loader.grub.enable = true;
|
||||
#boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
|
||||
|
||||
# System-wide shell config so all users (including IPA accounts) get the
|
||||
# same management aliases as the local nixos user's Home Manager provides.
|
||||
programs.bash = {
|
||||
shellAliases = {
|
||||
"Switch-nix" = switchCmd;
|
||||
"Test-nix" = testCmd;
|
||||
};
|
||||
interactiveShellInit = buildImageFn;
|
||||
};
|
||||
networking.networkmanager.enable = true;
|
||||
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
|
||||
|
||||
# Recommended over the true default (bypasses ZFS's own import safeguards)
|
||||
# per the option's own docs; matches hosts/docker/host.nix and
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Shared activation-script logic to preserve the SSH host key across
|
||||
# nixos-rebuild on platforms that embed the key via environment.etc (lxc and
|
||||
# proxmox). When NIXOS_HOST_KEYS_DIR is not set the key is absent from
|
||||
# environment.etc, and NixOS's etc activation removes any /etc file not in
|
||||
# the new generation — which would destroy the live key and break sops-nix
|
||||
# decryption permanently. These scripts save the key to /run before etc
|
||||
# removes it, then restore it afterward.
|
||||
#
|
||||
# Explicit deps enforce the correct ordering: without them the topological
|
||||
# sort places preserveSshHostKey after etc (confirmed live on lxc-tor-relay:
|
||||
# position 7 vs etc's position 5), so the key is gone before it can be saved.
|
||||
_: {
|
||||
system.activationScripts = {
|
||||
preserveSshHostKey = ''
|
||||
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
|
||||
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
|
||||
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
|
||||
fi
|
||||
'';
|
||||
|
||||
restoreSshHostKey = {
|
||||
deps = [ "etc" ];
|
||||
text = ''
|
||||
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
|
||||
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
|
||||
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
|
||||
fi
|
||||
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
|
||||
'';
|
||||
};
|
||||
|
||||
etc = { deps = [ "preserveSshHostKey" ]; };
|
||||
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
|
||||
};
|
||||
}
|
||||
@@ -1,45 +1,23 @@
|
||||
{ lib, pkgs, vars, ... }:
|
||||
{ pkgs, ... }:
|
||||
|
||||
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
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,19 +10,24 @@ let
|
||||
# non-blocking behavior, so they don't need `nofail` too).
|
||||
automountOpts = if config.boot.isContainer then [ "nofail" ] else [ "x-systemd.automount" ];
|
||||
|
||||
# FQDN in the storage.home zone — resolves to the Pacemaker vip-storage
|
||||
# (192.168.20.229) on docker's eth1/vmbr2 interface. Using the DNS name
|
||||
# rather than the raw IP means a future VIP renumber only requires a DNS
|
||||
# update, not a NixOS rebuild. The storage.home zone is served by the same
|
||||
# FreeIPA nameserver (domainControllerIp) that docker already uses, so
|
||||
# resolution reaches it over eth0 without any extra routing.
|
||||
nfsServer = vars.haStorageNfsFqdn;
|
||||
storageRoot = vars.haStorageRoot;
|
||||
# A bare hostname here never resolves reliably: systemd-resolved only
|
||||
# ever tries LLMNR for single-label names (never DNS, regardless of any
|
||||
# configured search domain), and a *global* search domain (the first fix
|
||||
# attempted here) backfires worse -- confirmed live on lxc-docker, adding
|
||||
# `networking.search` made systemd-resolved prioritize its domain-matched
|
||||
# but server-less global scope over eth0's correctly-configured one for
|
||||
# every "*.sweet.home" query, silently sending them to public fallback
|
||||
# DNS instead. `resolvectl query --interface=eth0 server.sweet.home`
|
||||
# resolved fine throughout, proving the LAN DNS server was never the
|
||||
# problem -- only the ambient, unqualified device string was. Using the
|
||||
# FQDN directly sidesteps all of that, matching the pattern
|
||||
# ../raspi/mount-data.nix already uses for the same reason.
|
||||
nfsServer = "${vars.nfsServerHost}.${vars.homeDomain}";
|
||||
in
|
||||
{
|
||||
fileSystems = {
|
||||
${vars.nfsShares.dockerConfig.mountpoint} = {
|
||||
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerConfig.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
@@ -33,7 +38,7 @@ in
|
||||
};
|
||||
|
||||
${vars.nfsShares.dockerDatabases.mountpoint} = {
|
||||
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerDatabases.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
@@ -44,7 +49,7 @@ in
|
||||
};
|
||||
|
||||
${vars.nfsShares.dockerVolumes.mountpoint} = {
|
||||
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.dockerVolumes.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
@@ -55,7 +60,7 @@ in
|
||||
};
|
||||
|
||||
${vars.nfsShares.nextcloudData.mountpoint} = {
|
||||
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.nextcloudData.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
@@ -66,7 +71,7 @@ in
|
||||
};
|
||||
|
||||
${vars.nfsShares.raspiVolumes.mountpoint} = {
|
||||
device = "${nfsServer}:${storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.raspiVolumes.subpath}";
|
||||
fsType = "nfs";
|
||||
|
||||
options = [
|
||||
|
||||
@@ -11,57 +11,14 @@
|
||||
# Both host keys must be registered via sync-host-keys.sh first so both nodes can decrypt it.
|
||||
#
|
||||
# DRBD fencing:
|
||||
# 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).
|
||||
#
|
||||
# PATH wrapper: when the DRBD kernel module invokes the fence-peer handler
|
||||
# via the UMH (User Mode Helper) mechanism it provides a minimal PATH that
|
||||
# omits /run/current-system/sw/bin. crm-fence-peer.sh calls cibadmin,
|
||||
# crm_mon etc.; if those aren't found a pipeline in the script breaks with
|
||||
# SIGPIPE. A process killed by signal has WEXITSTATUS() == 0, so the kernel
|
||||
# sees exit code 0 and logs "fence-peer helper broken, returned 0", looping
|
||||
# forever. The writeShellScript wrappers below prepend the NixOS sw path
|
||||
# before exec-ing the real handler, giving it a working Pacemaker toolchain.
|
||||
{ lib, pkgs, vars, ... }:
|
||||
let
|
||||
fencePeerWrapper = pkgs.writeShellScript "drbd-fence-peer" ''
|
||||
export PATH="/run/current-system/sw/bin:/run/current-system/sw/sbin:$PATH"
|
||||
exec /run/current-system/sw/lib/drbd/crm-fence-peer.sh "$@"
|
||||
'';
|
||||
unfencePeerWrapper = pkgs.writeShellScript "drbd-unfence-peer" ''
|
||||
export PATH="/run/current-system/sw/bin:/run/current-system/sw/sbin:$PATH"
|
||||
exec /run/current-system/sw/lib/drbd/crm-unfence-peer.sh "$@"
|
||||
'';
|
||||
in
|
||||
# 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.
|
||||
{ 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 = ''
|
||||
@@ -80,16 +37,12 @@ in
|
||||
disk {
|
||||
fencing resource-only;
|
||||
}
|
||||
handlers {
|
||||
fence-peer "${fencePeerWrapper}";
|
||||
unfence-peer "${unfencePeerWrapper}";
|
||||
}
|
||||
}
|
||||
|
||||
resource ha-data {
|
||||
volume 0 {
|
||||
device /dev/drbd0;
|
||||
disk ${vars.haServerDrbdDisk};
|
||||
disk /dev/sdb;
|
||||
meta-disk internal;
|
||||
}
|
||||
|
||||
@@ -122,47 +75,33 @@ in
|
||||
services.corosync = {
|
||||
clusterName = "ha-cluster";
|
||||
nodelist = [
|
||||
# ring0: cluster-internal vmbr1 (primary heartbeat + DRBD path)
|
||||
# ring1: LAN vmbr0 (backup heartbeat only — never carries DRBD)
|
||||
{ nodeid = 1; name = vars.haServer1Host; ring_addrs = [ vars.haServer1StorageIp vars.haServer1Ip ]; }
|
||||
{ nodeid = 2; name = vars.haServer2Host; ring_addrs = [ vars.haServer2StorageIp vars.haServer2Ip ]; }
|
||||
{ nodeid = 1; name = vars.haServer1Host; ring_addrs = [ vars.haServer1StorageIp ]; }
|
||||
{ nodeid = 2; name = vars.haServer2Host; ring_addrs = [ vars.haServer2StorageIp ]; }
|
||||
];
|
||||
};
|
||||
|
||||
networking.firewall = {
|
||||
allowedTCPPorts = [
|
||||
vars.ports.haServerIscsi
|
||||
vars.ports.haServerPacemakerRemoted
|
||||
vars.ports.haServerPcsd
|
||||
vars.ports.haServerDrbd
|
||||
vars.ports.nfsRpcbind
|
||||
vars.ports.nfsd
|
||||
vars.ports.nfsMountd
|
||||
];
|
||||
allowedUDPPorts = [
|
||||
vars.ports.haServerCorosync1
|
||||
vars.ports.haServerCorosync2
|
||||
vars.ports.haServerCorosyncCrypto
|
||||
vars.ports.nfsRpcbind
|
||||
vars.ports.nfsd
|
||||
vars.ports.nfsMountd
|
||||
];
|
||||
# Protocol separation: iSCSI (VLAN 20 / storage clients only),
|
||||
# NFS (VLAN 2 / LAN only). Cluster-internal subnets accepted wholesale
|
||||
# since they are isolated bridges with no external uplink.
|
||||
extraCommands = ''
|
||||
iptables -A nixos-fw -s ${vars.haServer1Ip}/32 -j nixos-fw-accept
|
||||
iptables -A nixos-fw -s ${vars.haServer2Ip}/32 -j nixos-fw-accept
|
||||
iptables -A nixos-fw -s ${vars.haStorageCidr} -j nixos-fw-accept
|
||||
|
||||
iptables -A nixos-fw -p tcp -s ${vars.haClientCidr} --dport ${toString vars.ports.haServerIscsi} -j nixos-fw-accept
|
||||
|
||||
iptables -A nixos-fw -p tcp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsRpcbind} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p udp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsRpcbind} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p tcp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsd} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p udp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsd} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p tcp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsMountd} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p udp -s ${vars.lanCidr} --dport ${toString vars.ports.nfsMountd} -j nixos-fw-accept
|
||||
|
||||
iptables -A nixos-fw -p tcp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsRpcbind} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p udp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsRpcbind} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p tcp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsd} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p udp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsd} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p tcp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsMountd} -j nixos-fw-accept
|
||||
iptables -A nixos-fw -p udp -s ${vars.haClientCidr} --dport ${toString vars.ports.nfsMountd} -j nixos-fw-accept
|
||||
iptables -A INPUT -s ${vars.haServer1Ip}/32 -j ACCEPT
|
||||
iptables -A INPUT -s ${vars.haServer2Ip}/32 -j ACCEPT
|
||||
iptables -A INPUT -s ${vars.haStorageCidr} -j ACCEPT
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
let
|
||||
python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]);
|
||||
targetctl = "${python3}/bin/targetctl";
|
||||
targetctl = "${pkgs.targetcli-fb}/bin/targetctl";
|
||||
|
||||
targetctlStop = pkgs.writeScript "targetctl-stop" ''
|
||||
#!${python3}/bin/python3
|
||||
|
||||
+61
-168
@@ -1,15 +1,34 @@
|
||||
# Fully declarative FreeIPA domain membership.
|
||||
#
|
||||
# Imported by modules/common/configuration.nix — no per-host wiring needed.
|
||||
# Enables itself automatically on any host that has a sops-encrypted keytab
|
||||
# at secrets/<hostname>.keytab; is a no-op for all other hosts.
|
||||
# Configures security.ipa (SSSD, Kerberos, PAM, NSSwitch) and places a
|
||||
# pre-provisioned host keytab via sops-nix so no imperative ipa-client-install
|
||||
# step is needed after deployment.
|
||||
#
|
||||
# To enroll a new host:
|
||||
# 0. scripts/secrets/sync-host-keys.sh <flake-target>
|
||||
# 1. scripts/ipa/create-nixos-ipa-host-account.sh [--ip <addr>] <hostname>
|
||||
# (adds .sops.yaml rule, runs ipa host-add, encrypts keytab in one step)
|
||||
# 2. git add secrets/<hostname>.keytab .sops.yaml && git commit
|
||||
# 3. Deploy — no further steps required.
|
||||
# Usage (in a host.nix imports list):
|
||||
# (import ../../modules/ipa/client.nix {
|
||||
# keytabSopsFile = ../../secrets/<hostname>.keytab;
|
||||
# caCertFile = ../../certs/ipa-ca.crt; # already committed — do not re-fetch
|
||||
# })
|
||||
#
|
||||
# The host.nix networking block must also set:
|
||||
# networking.domain = vars.homeDomain; # needed for Kerberos FQDN
|
||||
# networking.nameservers = [ vars.domainControllerIp ]; # IPA DNS
|
||||
#
|
||||
# One-time operator setup per host (do this BEFORE deploying):
|
||||
#
|
||||
# 0. Generate SSH host keys and the host's age key for sops:
|
||||
# scripts/secrets/sync-host-keys.sh <flake-target>
|
||||
# This must run before step 1 so the host age key is in .sops.yaml
|
||||
# and the keytab can be encrypted for the host to read at boot.
|
||||
#
|
||||
# 1. Add the IPA host account and produce the sops-encrypted keytab:
|
||||
# scripts/ipa/create-nixos-ipa-host-account.sh [--ip <addr>] <hostname>
|
||||
# The script handles ipa host-add, ipa-getkeytab, .sops.yaml patching,
|
||||
# and sops encryption in one step. See the script header for details.
|
||||
#
|
||||
# 2. Wire up the host (see "Usage" above), then deploy:
|
||||
# nixos-rebuild switch (or create-proxmox-resource.sh)
|
||||
# No further manual enrollment steps are required after deployment.
|
||||
#
|
||||
# Manual fallback (if the script isn't usable):
|
||||
# a. On the FreeIPA server: ipa host-add <fqdn> [--ip-address=<ip>] --force
|
||||
@@ -19,146 +38,61 @@
|
||||
# sops -e --input-type binary -i secrets/<host>.keytab
|
||||
# d. Commit secrets/<host>.keytab and the updated .sops.yaml, then deploy.
|
||||
#
|
||||
# vars dependencies: homeDomain, ipaServer, domainControllerIp, ipaUser
|
||||
# vars dependencies: homeDomain, ipaServer, domainControllerIp
|
||||
|
||||
{ keytabSopsFile, caCertFile }:
|
||||
{ config, lib, pkgs, vars, ... }:
|
||||
|
||||
let
|
||||
keytabPath = ../../secrets + "/${config.networking.hostName}.keytab";
|
||||
enabled = builtins.pathExists keytabPath;
|
||||
|
||||
realm = lib.strings.toUpper vars.homeDomain;
|
||||
fqdn = "${config.networking.hostName}.${vars.homeDomain}";
|
||||
# "sweet.home" -> "dc=sweet,dc=home"
|
||||
basedn = lib.strings.concatMapStringsSep "," (c: "dc=${c}") (lib.strings.splitString "." vars.homeDomain);
|
||||
# security.ipa.certificate expects a derivation (package), not a raw path.
|
||||
caCertPkg = pkgs.writeText "ipa-ca.crt" (builtins.readFile ../../certs/ipa-ca.crt);
|
||||
caCertPkg = pkgs.writeText "ipa-ca.crt" (builtins.readFile caCertFile);
|
||||
in
|
||||
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;
|
||||
};
|
||||
|
||||
# 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" ]; }];
|
||||
}];
|
||||
{
|
||||
security.ipa = {
|
||||
enable = true;
|
||||
domain = vars.homeDomain;
|
||||
realm = realm;
|
||||
server = vars.ipaServer;
|
||||
certificate = caCertPkg;
|
||||
basedn = basedn;
|
||||
ipaHostname = fqdn;
|
||||
offlinePasswords = true;
|
||||
cacheCredentials = true;
|
||||
};
|
||||
|
||||
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}";
|
||||
};
|
||||
# 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"
|
||||
];
|
||||
|
||||
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.
|
||||
sops.secrets."ipa-host-keytab" = {
|
||||
sopsFile = keytabPath;
|
||||
sopsFile = keytabSopsFile;
|
||||
format = "binary";
|
||||
path = "/etc/krb5.keytab";
|
||||
owner = "root";
|
||||
@@ -166,45 +100,4 @@ lib.mkIf enabled {
|
||||
mode = "0600";
|
||||
restartUnits = [ "sssd.service" ];
|
||||
};
|
||||
|
||||
# NixOS requires isNormalUser/isSystemUser + group on any entry in
|
||||
# users.users. HM with useUserPackages = true (set in flake.nix) adds a stub
|
||||
# entry for each HM user so it can install packages to
|
||||
# /etc/profiles/per-user/<name>/. This definition satisfies those assertions.
|
||||
# With security.ipa setting "passwd: sss files" in nsswitch, SSSD's IPA entry
|
||||
# takes priority for NSS lookups — this local stub is only a fallback when
|
||||
# SSSD is unreachable (at which point auth fails anyway).
|
||||
users.users.${vars.ipaUser} = {
|
||||
isNormalUser = true;
|
||||
group = "users";
|
||||
extraGroups = [ "wheel" ];
|
||||
createHome = false;
|
||||
# "!" is not a password hash — it is the standard "account locked" marker.
|
||||
# It cannot authenticate anyone locally. It exists solely so NixOS generates
|
||||
# a shadow entry for this stub user; without one pam_unix returns
|
||||
# PAM_AUTHINFO_UNAVAIL before prompting, which means PAM_AUTHTOK is never
|
||||
# set and the subsequent pam_sss use_first_pass call has nothing to work
|
||||
# with — blocking LightDM and su logins even when IPA/SSSD auth succeeds.
|
||||
hashedPassword = "!";
|
||||
};
|
||||
|
||||
# Home Manager config for the IPA primary user, applied on every enrolled
|
||||
# host. Manages what IPA doesn't: dotfiles, user-scoped packages, session
|
||||
# variables. Switch-nix/Test-nix/buildImage are system-wide (configuration.nix)
|
||||
# so they don't need to be repeated here.
|
||||
#
|
||||
# homeDirectory uses mkForce because HM's NixOS integration module sets it to
|
||||
# "/var/empty" for users not found in config.users.users at eval time (SSSD
|
||||
# users aren't visible there).
|
||||
home-manager.users.${vars.ipaUser} = { pkgs, ... }: {
|
||||
home = {
|
||||
username = vars.ipaUser;
|
||||
homeDirectory = lib.mkForce "/home/${vars.ipaUser}";
|
||||
stateVersion = "26.05";
|
||||
packages = with pkgs; [ tmux sshfs ];
|
||||
sessionVariables.EDITOR = lib.mkDefault "nano";
|
||||
};
|
||||
programs.home-manager.enable = true;
|
||||
programs.bash.enable = true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ in
|
||||
# LXC container does).
|
||||
imports = [
|
||||
(modulesPath + "/virtualisation/proxmox-lxc.nix")
|
||||
../common/preserve-ssh-host-key.nix
|
||||
];
|
||||
|
||||
proxmoxLXC = {
|
||||
@@ -106,6 +105,49 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# NixOS's etc activation removes any /etc file that was in the previous
|
||||
# generation's environment.etc but is absent from the current one — even
|
||||
# real (non-symlink) copies. On every routine nixos-rebuild switch/test that
|
||||
# lacks NIXOS_HOST_KEYS_DIR the key is absent from environment.etc, so it
|
||||
# gets removed as "obsolete". sops-nix derives its age decryption key from
|
||||
# /etc/ssh/ssh_host_ed25519_key; deletion cascades into every sops secret
|
||||
# failing with "Error getting data key: 0 successful groups required, got 0".
|
||||
#
|
||||
# Fix: activation scripts that bracket the etc step, with explicit deps
|
||||
# to enforce the correct ordering. Without deps the topological sort places
|
||||
# preserveSshHostKey AFTER etc (confirmed live on a deployed lxc-tor-relay:
|
||||
# position 7 vs etc's position 5) -- the key is already gone by the time it
|
||||
# tries to save it. The etc/setupSecrets entries ADD to existing deps
|
||||
# (types.listOf concatenates across module definitions).
|
||||
system.activationScripts = {
|
||||
# Saves the live key to /run before etc can delete it.
|
||||
preserveSshHostKey = ''
|
||||
if [ -f /etc/ssh/ssh_host_ed25519_key ]; then
|
||||
cp /etc/ssh/ssh_host_ed25519_key /run/sshd-host-key-preserve.tmp
|
||||
cp /etc/ssh/ssh_host_ed25519_key.pub /run/sshd-host-key-preserve.pub.tmp
|
||||
fi
|
||||
'';
|
||||
|
||||
# Reinstalls the key after etc runs if it was removed as "obsolete".
|
||||
# The resulting file is not registered in environment.etc for either
|
||||
# generation, so subsequent rebuilds leave it alone permanently.
|
||||
restoreSshHostKey = {
|
||||
deps = [ "etc" ];
|
||||
text = ''
|
||||
if [ ! -f /etc/ssh/ssh_host_ed25519_key ] && [ -f /run/sshd-host-key-preserve.tmp ]; then
|
||||
install -m 0600 /run/sshd-host-key-preserve.tmp /etc/ssh/ssh_host_ed25519_key
|
||||
install -m 0644 /run/sshd-host-key-preserve.pub.tmp /etc/ssh/ssh_host_ed25519_key.pub
|
||||
fi
|
||||
rm -f /run/sshd-host-key-preserve.tmp /run/sshd-host-key-preserve.pub.tmp
|
||||
'';
|
||||
};
|
||||
|
||||
# Force etc to wait until the key is saved, and sops to wait until the
|
||||
# key is restored. Without these the topological sort breaks the chain.
|
||||
etc = { deps = [ "preserveSshHostKey" ]; };
|
||||
setupSecrets = { deps = [ "restoreSshHostKey" ]; };
|
||||
};
|
||||
|
||||
# virtualisation/proxmox-lxc.nix (imported above) registers the Nix
|
||||
# store DB via a systemd service (register-nix-paths) -- it never runs
|
||||
# an activation script at all. Confirmed live this means neither
|
||||
|
||||
@@ -34,7 +34,6 @@ in
|
||||
../hardware-configuration/vm/proxmox.nix
|
||||
../boot/efi.nix
|
||||
../disko/proxmox.nix
|
||||
../common/preserve-ssh-host-key.nix
|
||||
];
|
||||
|
||||
environment.etc = lib.mkIf hasKeyForThisTarget {
|
||||
@@ -47,4 +46,36 @@ 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" ]; };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{ config, lib, vars, ... }:
|
||||
|
||||
let
|
||||
# FQDN of the LAN NFS VIP (Pacemaker vip-lan, 192.168.2.229). Using the
|
||||
# FQDN rather than a raw IP or bare hostname avoids systemd-resolved LLMNR
|
||||
# quirks and survives a future VIP renumber via a DNS-only update.
|
||||
nfsServer = "ha-vip-lan.${vars.homeDomain}";
|
||||
# Use the same FQDN approach as docker/mount-data.nix — a bare hostname is
|
||||
# unreliable: systemd-resolved only tries LLMNR for single-label names, and
|
||||
# a global search domain causes it to skip the interface-scoped LAN DNS.
|
||||
nfsServer = "${vars.nfsServerHost}.${vars.homeDomain}";
|
||||
in
|
||||
{
|
||||
fileSystems.${vars.nfsShares.pxebootImages.mountpoint} = {
|
||||
device = "${nfsServer}:${vars.haStorageRoot}/${vars.nfsShares.pxebootImages.subpath}";
|
||||
device = "${nfsServer}:${vars.storageRoot}/${vars.nfsShares.pxebootImages.subpath}";
|
||||
fsType = "nfs";
|
||||
options = [
|
||||
"_netdev"
|
||||
|
||||
@@ -19,14 +19,6 @@
|
||||
# tailnet-specific subdomain (vars.tailnetDomain) instead.
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
# NixOS's dnsmasq module defaults resolveLocalQueries to true, which adds
|
||||
# 127.0.0.1 to networking.nameservers and makes dnsmasq bind to
|
||||
# listen-address=127.0.0.1. This instance is not the host's local
|
||||
# resolver — it only serves IPA's conditional forwarder for tailnet names.
|
||||
# The host uses domainControllerIp directly (networking.nameservers in
|
||||
# host.nix). Without this, all host DNS goes through dnsmasq, which has
|
||||
# no upstream for general queries (no-resolv=true), breaking resolution.
|
||||
resolveLocalQueries = false;
|
||||
settings = {
|
||||
# Listen only on the LAN interface — not tailscale0 or loopback.
|
||||
# bind-interfaces prevents dnsmasq from binding to 0.0.0.0 and
|
||||
|
||||
Binary file not shown.
@@ -1,237 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# gc-hosts.sh — Run nix-collect-garbage -d on all live NixOS hosts.
|
||||
#
|
||||
# The host list is rebuilt on every run:
|
||||
# 1. This workstation (nixos) — always first
|
||||
# 2. pve1 — always second (non-NixOS Proxmox node with Nix installed)
|
||||
# 3. Every NixOS guest currently running on pve1 (discovered via pct/qm list)
|
||||
#
|
||||
# nix-cache is excluded: gc-ing the shared binary cache evicts store paths
|
||||
# that other hosts depend on for substitution.
|
||||
#
|
||||
# NixOS hosts: tries "sudo -n nix-collect-garbage -d" first (works when
|
||||
# wheelNeedsPassword = false, e.g. the HA cluster). Falls back to user-level
|
||||
# "nix-collect-garbage -d" if sudo needs a password — still collects
|
||||
# unreferenced store paths and old nixos-user profile generations, but leaves
|
||||
# old system generations in place.
|
||||
# pve1: runs "bash -l -c nix-collect-garbage -d" as the login user so
|
||||
# /etc/profile is sourced and the Nix daemon's PATH is set up automatically.
|
||||
#
|
||||
# Usage (from repo root):
|
||||
# bash scripts/gc-hosts.sh [--dry-run]
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh 2>/dev/null || true
|
||||
source scripts/lib/nix-eval.sh 2>/dev/null || true
|
||||
|
||||
# ── config ────────────────────────────────────────────────────────────────────
|
||||
|
||||
: "${MAX_JOBS:=8}"
|
||||
: "${NIXOS_USER:=nixos}"
|
||||
: "${PVE1_SSH_USER:=${PROXMOX_SSH_USER:-wayne}}"
|
||||
|
||||
# GC connections use BatchMode — no interactive prompts, just succeed or fail.
|
||||
SSH_OPTS=(-o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=10)
|
||||
# Discovery connections do NOT use BatchMode so that sudo can prompt if needed
|
||||
# (pct/qm list require root access on Proxmox).
|
||||
SSH_QUERY_OPTS=(-o StrictHostKeyChecking=no -o ConnectTimeout=10)
|
||||
|
||||
DRY_RUN=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── build the host list ───────────────────────────────────────────────────────
|
||||
|
||||
# ORDERED_HOSTS: names in display/execution order.
|
||||
# HOST_TARGET[name]: SSH target string (user@host).
|
||||
# HOST_TYPE[name]: "nixos" (try sudo gc, fallback user) | "nix" (login-shell gc).
|
||||
declare -a ORDERED_HOSTS=()
|
||||
declare -A HOST_TARGET=()
|
||||
declare -A HOST_TYPE=()
|
||||
declare -A _SEEN_HOSTNAMES=() # dedup tracker
|
||||
|
||||
_add_host() {
|
||||
local name="$1" target="$2" type="$3"
|
||||
if [[ -n "${_SEEN_HOSTNAMES[$name]+_}" ]]; then return; fi
|
||||
_SEEN_HOSTNAMES[$name]=1
|
||||
ORDERED_HOSTS+=("$name")
|
||||
HOST_TARGET[$name]="$target"
|
||||
HOST_TYPE[$name]="$type"
|
||||
}
|
||||
|
||||
# 1. Workstation (hard-wired first)
|
||||
_add_host "nixos" "${NIXOS_USER}@nixos" "nixos"
|
||||
|
||||
# 2. pve1 (hard-wired second; non-NixOS, no system generations)
|
||||
_add_host "pve1" "${PVE1_SSH_USER}@${PVE1_HOST}" "nix"
|
||||
|
||||
# 3. Dynamically discover running NixOS guests on pve1
|
||||
#
|
||||
# create-proxmox-resource.sh names every guest after its NixOS hostname:
|
||||
# pct create ... --hostname <nixos-hostname> (LXC)
|
||||
# qm create ... --name <nixos-hostname> (VM)
|
||||
# So pct/qm list output already contains the NixOS hostname directly.
|
||||
# We validate against the flake to filter out non-NixOS guests on pve1
|
||||
# (e.g. FreeIPA, Proxmox Backup Server) that share the same Proxmox node.
|
||||
echo "Discovering running guests on ${PVE1_HOST}..."
|
||||
|
||||
# Eval the flake once to get the set of hostnames that are actually NixOS.
|
||||
# Values are NixOS hostnames (e.g. "docker"); keys are flake targets ("lxc-docker").
|
||||
nixos_hostnames=""
|
||||
nixos_hostnames="$(
|
||||
nix eval --json "${NIX_EVAL_FLAGS[@]}" .#nixosConfigurations \
|
||||
--apply 'cfgs: builtins.attrValues (builtins.mapAttrs (_: cfg: cfg.config.networking.hostName) cfgs)' \
|
||||
2>/dev/null | jq -r '.[]' | sort -u
|
||||
)" || { echo " warning: flake eval failed — non-NixOS guests will not be filtered" >&2; }
|
||||
|
||||
# SSH_QUERY_OPTS (no BatchMode) so sudo can prompt if needed for pct/qm.
|
||||
if ssh "${SSH_QUERY_OPTS[@]}" "${PVE1_SSH_USER}@${PVE1_HOST}" "true" 2>/dev/null; then
|
||||
running_guests="$(
|
||||
ssh "${SSH_QUERY_OPTS[@]}" "${PVE1_SSH_USER}@${PVE1_HOST}" bash -s <<'DISCOVER'
|
||||
sudo pct list 2>/dev/null | awk 'NR>1 && $2=="running" { print $NF }'
|
||||
sudo qm list 2>/dev/null | awk 'NR>1 && $3=="running" { print $2 }'
|
||||
DISCOVER
|
||||
)" || running_guests=""
|
||||
|
||||
while IFS= read -r hostname; do
|
||||
[[ -z "$hostname" ]] && continue
|
||||
# Exclude nix-cache.
|
||||
case "$hostname" in *nix-cache*) continue ;; esac
|
||||
# Skip if not a flake-managed NixOS host (filters non-NixOS pve1 guests).
|
||||
if [[ -n "$nixos_hostnames" ]] && ! grep -qxF "$hostname" <<< "$nixos_hostnames"; then
|
||||
continue
|
||||
fi
|
||||
# Skip if already in the list (e.g. a proxmox-gui guest whose hostname is nixos).
|
||||
if [[ -n "${_SEEN_HOSTNAMES[$hostname]+_}" ]]; then continue; fi
|
||||
|
||||
echo " + $hostname"
|
||||
_add_host "$hostname" "${NIXOS_USER}@${hostname}" "nixos"
|
||||
done <<< "$(echo "$running_guests" | sort -u)"
|
||||
else
|
||||
echo " warning: ${PVE1_HOST} unreachable — skipping dynamic host discovery" >&2
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Hosts: ${ORDERED_HOSTS[*]}"
|
||||
echo ""
|
||||
|
||||
# ── dry-run ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "[dry-run] commands that would run:"
|
||||
for host in "${ORDERED_HOSTS[@]}"; do
|
||||
target="${HOST_TARGET[$host]}"
|
||||
type="${HOST_TYPE[$host]}"
|
||||
if [[ "$type" == "nixos" ]]; then
|
||||
echo " ssh ${SSH_OPTS[*]} $target 'sudo -n nix-collect-garbage -d'"
|
||||
echo " # fallback: ssh ... $target 'nix-collect-garbage -d'"
|
||||
else
|
||||
echo " ssh ${SSH_OPTS[*]} $target 'bash -l -c nix-collect-garbage -d'"
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── gc worker ─────────────────────────────────────────────────────────────────
|
||||
|
||||
gc_one() {
|
||||
local host="$1" target="${HOST_TARGET[$1]}" type="${HOST_TYPE[$1]}" logfile="$2"
|
||||
|
||||
if ! ssh "${SSH_OPTS[@]}" "$target" "true" 2>>"$logfile"; then
|
||||
echo "unreachable"; return
|
||||
fi
|
||||
|
||||
if [[ "$type" == "nixos" ]]; then
|
||||
if ssh "${SSH_OPTS[@]}" "$target" "sudo -n nix-collect-garbage -d" \
|
||||
>>"$logfile" 2>>"$logfile"; then
|
||||
echo "ok(sudo)"; return
|
||||
fi
|
||||
echo "[sudo needs password — falling back to user-level gc]" >>"$logfile"
|
||||
if ssh "${SSH_OPTS[@]}" "$target" "nix-collect-garbage -d" \
|
||||
>>"$logfile" 2>>"$logfile"; then
|
||||
echo "ok(user)"; return
|
||||
fi
|
||||
else
|
||||
# Non-NixOS node: use a login shell so /etc/profile is sourced and the
|
||||
# Nix daemon's bin dir is on PATH (set up by /etc/profile.d/nix-daemon.sh
|
||||
# which the Nix installer adds to /etc/profile).
|
||||
if ssh "${SSH_OPTS[@]}" "$target" "bash -l -c 'nix-collect-garbage -d'" \
|
||||
>>"$logfile" 2>>"$logfile"; then
|
||||
echo "ok"; return
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "failed:$?"
|
||||
}
|
||||
|
||||
# ── parallel execution ────────────────────────────────────────────────────────
|
||||
|
||||
echo "Running gc on ${#ORDERED_HOSTS[@]} hosts (up to ${MAX_JOBS} parallel)..."
|
||||
echo ""
|
||||
|
||||
TMPDIR_GC="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR_GC"' EXIT
|
||||
|
||||
declare -A LOGS=()
|
||||
job_count=0
|
||||
|
||||
for host in "${ORDERED_HOSTS[@]}"; do
|
||||
logfile="${TMPDIR_GC}/${host}.log"
|
||||
resultfile="${TMPDIR_GC}/${host}.result"
|
||||
LOGS[$host]="$logfile"
|
||||
: > "$logfile"
|
||||
|
||||
( result="$(gc_one "$host" "$logfile")"; echo "$result" > "$resultfile" ) &
|
||||
|
||||
(( job_count++ )) || true
|
||||
if [[ "$job_count" -ge "$MAX_JOBS" ]]; then
|
||||
wait -n 2>/dev/null || wait
|
||||
(( job_count-- )) || true
|
||||
fi
|
||||
done
|
||||
|
||||
wait
|
||||
|
||||
# ── summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "Results:"
|
||||
echo "──────────────────────────────"
|
||||
|
||||
ok_hosts=()
|
||||
warn_hosts=()
|
||||
fail_hosts=()
|
||||
|
||||
for host in "${ORDERED_HOSTS[@]}"; do
|
||||
result="$(cat "${TMPDIR_GC}/${host}.result" 2>/dev/null || echo "failed:missing")"
|
||||
case "$result" in
|
||||
ok|"ok(sudo)"|"ok(user)")
|
||||
printf " %-22s %s\n" "$host" "$result"
|
||||
ok_hosts+=("$host") ;;
|
||||
unreachable)
|
||||
printf " %-22s UNREACHABLE\n" "$host"
|
||||
warn_hosts+=("$host") ;;
|
||||
*)
|
||||
printf " %-22s FAILED (%s)\n" "$host" "$result"
|
||||
fail_hosts+=("$host") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo " ${#ok_hosts[@]} succeeded, ${#warn_hosts[@]} unreachable, ${#fail_hosts[@]} failed"
|
||||
|
||||
for host in "${warn_hosts[@]+"${warn_hosts[@]}"}" "${fail_hosts[@]+"${fail_hosts[@]}"}"; do
|
||||
logfile="${LOGS[$host]}"
|
||||
if [[ -s "$logfile" ]]; then
|
||||
echo ""
|
||||
echo "── $host ──"
|
||||
cat "$logfile"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
[[ "${#fail_hosts[@]}" -eq 0 ]]
|
||||
Executable → Regular
+60
-124
@@ -7,109 +7,49 @@
|
||||
set -euo pipefail
|
||||
|
||||
# ── Configuration ─────────────────────────────────────────────────────────
|
||||
# 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.20.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
|
||||
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
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
RESULTS=()
|
||||
|
||||
# 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"); }
|
||||
pass() { echo " PASS: $1"; ((PASS++)); RESULTS+=("PASS $1"); }
|
||||
fail() { echo " FAIL: $1"; ((FAIL++)); RESULTS+=("FAIL $1"); }
|
||||
|
||||
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; }
|
||||
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; }
|
||||
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo " HA Cluster Acceptance Tests — $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "════════════════════════════════════════════════════"
|
||||
|
||||
# ── Pre-flight: DRBD sync must be complete ────────────────────────────────
|
||||
# Tests that check disk state, XFS mount, and iSCSI will fail or give false
|
||||
# results while the initial full sync is in progress. Block until done.
|
||||
echo ""
|
||||
echo "Pre-flight: verifying DRBD sync is complete..."
|
||||
DRBD_PREFLIGHT=$(n1 "drbdadm dstate ha-data 2>/dev/null" 2>/dev/null || echo "unknown")
|
||||
if ! echo "$DRBD_PREFLIGHT" | grep -q "^UpToDate/UpToDate$"; then
|
||||
echo ""
|
||||
echo " ERROR: DRBD initial sync not complete."
|
||||
echo " Current dstate on $NODE1: $DRBD_PREFLIGHT"
|
||||
echo ""
|
||||
echo " Monitor progress:"
|
||||
echo " ssh nixos@$NODE1_IP 'sudo watch -n3 cat /proc/drbd'"
|
||||
echo ""
|
||||
echo " Re-run this script once dstate shows UpToDate/UpToDate."
|
||||
exit 1
|
||||
fi
|
||||
echo " dstate: $DRBD_PREFLIGHT — ready."
|
||||
|
||||
# ── Detect Active/Standby nodes ────────────────────────────────────────────
|
||||
# Use crm_mon to detect which node holds the Promoted (Primary) DRBD resource.
|
||||
# Pacemaker is authoritative; DRBD role can briefly read as Secondary while
|
||||
# Pacemaker is mid-transition, giving a false Active/Standby swap.
|
||||
# Wait up to 90 s for Pacemaker to settle before giving up.
|
||||
echo ""
|
||||
echo "Detecting Active/Standby nodes (waiting for Pacemaker to settle)..."
|
||||
ACTIVE_NODE=""
|
||||
for i in $(seq 1 30); do
|
||||
# crm_mon -1 output contains "Promoted: [ <node> ]" for the DRBD master.
|
||||
CRM_OUT=$(n1 "crm_mon -1" 2>/dev/null || n2 "crm_mon -1" 2>/dev/null || true)
|
||||
# crm_mon 2.x formats Promoted lines as " * Promoted: [ node ]" — the * bullet
|
||||
# means ^\s*(Promoted|Masters): never matches; filter Unpromoted first instead.
|
||||
ACTIVE_NODE=$(echo "$CRM_OUT" | grep -v 'Unpromoted\|Unmanaged' | grep -E '(Promoted|Masters):' | grep -oE '\b(ha-server-[0-9]+)\b' | head -1 || true)
|
||||
[[ -n "$ACTIVE_NODE" ]] && break
|
||||
sleep 3
|
||||
done
|
||||
|
||||
if [[ -z "$ACTIVE_NODE" ]]; then
|
||||
echo " WARNING: could not determine Active node from crm_mon after 90 s — defaulting to $NODE1"
|
||||
ACTIVE_NODE="$NODE1"
|
||||
fi
|
||||
|
||||
if [[ "$ACTIVE_NODE" == "$NODE1" ]]; then
|
||||
ACTIVE_IP="$NODE1_IP"
|
||||
STANDBY_NODE="$NODE2"; STANDBY_IP="$NODE2_IP"
|
||||
na() { n1 "$@"; }
|
||||
ns() { n2 "$@"; }
|
||||
else
|
||||
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 na "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then
|
||||
if n1 "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then
|
||||
pass "cluster has quorum"
|
||||
else
|
||||
fail "cluster does not have quorum — check corosync on both nodes"
|
||||
fi
|
||||
|
||||
# ── T2: DRBD Primary on Active node, Secondary on Standby ────────────────
|
||||
# ── T2: DRBD Primary on node1, Secondary on node2 ────────────────────────
|
||||
echo ""
|
||||
echo "[T2] DRBD roles"
|
||||
DRBD_ROLE=$(na "drbdadm role ha-data" 2>/dev/null || echo "unknown")
|
||||
DRBD_ROLE=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown")
|
||||
if [[ "$DRBD_ROLE" == "Primary/Secondary" || "$DRBD_ROLE" == "Primary" ]]; then
|
||||
pass "DRBD Primary on $ACTIVE_NODE ($DRBD_ROLE)"
|
||||
pass "DRBD Primary on $NODE1 ($DRBD_ROLE)"
|
||||
else
|
||||
fail "unexpected DRBD role on $ACTIVE_NODE: $DRBD_ROLE (expected Primary/Secondary)"
|
||||
fail "unexpected DRBD role on $NODE1: $DRBD_ROLE (expected Primary/Secondary)"
|
||||
fi
|
||||
|
||||
DRBD_DSTATE=$(na "drbdadm dstate ha-data" 2>/dev/null || echo "unknown")
|
||||
DRBD_DSTATE=$(n1 "drbdadm dstate ha-data" 2>/dev/null || echo "unknown")
|
||||
if echo "$DRBD_DSTATE" | grep -q "UpToDate"; then
|
||||
pass "DRBD disk state UpToDate ($DRBD_DSTATE)"
|
||||
else
|
||||
@@ -119,45 +59,44 @@ fi
|
||||
# ── T3: XFS mounted at haStorageRoot on the Active node ──────────────────
|
||||
echo ""
|
||||
echo "[T3] XFS mount"
|
||||
if na "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||
pass "XFS mounted at ${XFS_MOUNT} on $ACTIVE_NODE"
|
||||
if n1 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||
pass "XFS mounted at ${XFS_MOUNT} on $NODE1"
|
||||
else
|
||||
fail "XFS not mounted at ${XFS_MOUNT} on $ACTIVE_NODE"
|
||||
fail "XFS not mounted at ${XFS_MOUNT} on $NODE1"
|
||||
fi
|
||||
|
||||
if ns "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||
fail "XFS unexpectedly mounted on $STANDBY_NODE (should only be on Active node)"
|
||||
if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||
fail "XFS unexpectedly mounted on $NODE2 (should only be on Active node)"
|
||||
else
|
||||
pass "XFS not mounted on $STANDBY_NODE (correct — Standby)"
|
||||
pass "XFS not mounted on $NODE2 (correct — Secondary)"
|
||||
fi
|
||||
|
||||
# ── T4: iSCSI target visible on Active node ───────────────────────────────
|
||||
# ── T4: iSCSI target visible on both nodes ────────────────────────────────
|
||||
echo ""
|
||||
echo "[T4] iSCSI target"
|
||||
IQN_COUNT=$(na "bash -c 'ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn || true'" 2>/dev/null || echo "0")
|
||||
IQN_COUNT=$(n1 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0")
|
||||
if [[ "$IQN_COUNT" -ge 1 ]]; then
|
||||
pass "iSCSI IQN active on $ACTIVE_NODE ($IQN_COUNT target(s))"
|
||||
pass "iSCSI IQN active on $NODE1 ($IQN_COUNT target(s))"
|
||||
else
|
||||
fail "no iSCSI IQN active on $ACTIVE_NODE"
|
||||
fail "no iSCSI IQN active on $NODE1"
|
||||
fi
|
||||
|
||||
# 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}"
|
||||
# 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}"
|
||||
else
|
||||
fail "iSCSI port 3260 not reachable from $STANDBY_NODE via ${VIP}"
|
||||
fail "iSCSI target not discoverable from $NODE2 via ${VIP}"
|
||||
fi
|
||||
|
||||
# ── T5: Failover — standby Active node, verify resources move to Standby ──
|
||||
# ── T5: Failover — standby node1, verify resources move to node2 ──────────
|
||||
echo ""
|
||||
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..."
|
||||
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..."
|
||||
MOVED=false
|
||||
for i in $(seq 1 120); do
|
||||
if ns "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||
for i in $(seq 1 30); do
|
||||
if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
|
||||
MOVED=true
|
||||
echo " Resources moved in ${i}s"
|
||||
break
|
||||
@@ -166,52 +105,49 @@ for i in $(seq 1 120); do
|
||||
done
|
||||
|
||||
if $MOVED; then
|
||||
pass "XFS mounted on $STANDBY_NODE after failover"
|
||||
IQN_ON_STANDBY=$(ns "bash -c 'ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn || true'" 2>/dev/null || 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"
|
||||
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"
|
||||
else
|
||||
fail "XFS did not mount on $STANDBY_NODE within 120 s — failover incomplete"
|
||||
fail "XFS did not mount on $NODE2 within 30 s — failover incomplete"
|
||||
fi
|
||||
|
||||
# ── T6: Data integrity — file written post-failover readable ─────────────
|
||||
# ── T6: Data integrity — file written pre-failover readable post-failover ─
|
||||
echo ""
|
||||
echo "[T6] Data integrity"
|
||||
# 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.
|
||||
# Write a test file on node2 (now Active) and verify its content
|
||||
TEST_FILE="${XFS_MOUNT}/.acceptance-test-$$"
|
||||
TEST_CONTENT="ha-acceptance-test-$(date +%s)"
|
||||
echo "${TEST_CONTENT}" | ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${STANDBY_IP}" sudo tee "${TEST_FILE}" > /dev/null 2>/dev/null || true
|
||||
READBACK=$(ns cat "${TEST_FILE}" 2>/dev/null || echo "")
|
||||
n2 "echo '${TEST_CONTENT}' > '${TEST_FILE}'" 2>/dev/null || true
|
||||
READBACK=$(n2 "cat '${TEST_FILE}' 2>/dev/null" || echo "")
|
||||
if [[ "$READBACK" == "$TEST_CONTENT" ]]; then
|
||||
pass "test file written and read back correctly on $STANDBY_NODE"
|
||||
pass "test file written and read back correctly on $NODE2"
|
||||
else
|
||||
fail "data integrity check failed (wrote: '$TEST_CONTENT', read: '$READBACK')"
|
||||
fi
|
||||
ns rm -f "${TEST_FILE}" 2>/dev/null || true
|
||||
n2 "rm -f '${TEST_FILE}'" 2>/dev/null || true
|
||||
|
||||
# ── T7: Node rejoin — un-standby original Active, verify cluster is healthy ─
|
||||
# ── T7: Node rejoin — un-standby node1, verify cluster is healthy ─────────
|
||||
echo ""
|
||||
echo "[T7] Node rejoin"
|
||||
na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v off" 2>/dev/null || true
|
||||
na "crm_resource --cleanup" 2>/dev/null || true
|
||||
n1 "crm_standby -N '${MYNODE}' -v off" 2>/dev/null || true
|
||||
n1 "crm_resource --cleanup" 2>/dev/null || true
|
||||
sleep 5
|
||||
|
||||
if na "corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'"; then
|
||||
pass "$ACTIVE_NODE rejoined — cluster has quorum"
|
||||
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"
|
||||
else
|
||||
fail "$ACTIVE_NODE did not rejoin with quorum"
|
||||
fail "$NODE1 did not rejoin with quorum"
|
||||
fi
|
||||
|
||||
DRBD_ROLE_AFTER=$(na "drbdadm role ha-data" 2>/dev/null || echo "unknown")
|
||||
DRBD_ROLE_AFTER=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown")
|
||||
if echo "$DRBD_ROLE_AFTER" | grep -q "Secondary"; then
|
||||
pass "$ACTIVE_NODE is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)"
|
||||
pass "$NODE1 is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)"
|
||||
else
|
||||
fail "unexpected DRBD role on $ACTIVE_NODE after rejoin: $DRBD_ROLE_AFTER"
|
||||
fail "unexpected DRBD role on $NODE1 after rejoin: $DRBD_ROLE_AFTER"
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────
|
||||
|
||||
Executable → Regular
Executable → Regular
+100
-299
@@ -21,33 +21,22 @@
|
||||
set -euo pipefail
|
||||
|
||||
# ── Configuration ─────────────────────────────────────────────────────────
|
||||
# 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.20.229}" # vars.haServerVip (storage-client, vmbr2, VLAN 20)
|
||||
VIP_LAN="${VIP_LAN:-192.168.2.229}" # vars.haServerLanVip (LAN, vmbr0)
|
||||
XFS_MOUNT="${XFS_MOUNT:-/srv/ha-data}" # vars.haStorageRoot
|
||||
ISCSI_IQN="${ISCSI_IQN:-iqn.2026-01.home.sweet:ha-storage}" # vars.haIscsiIqn
|
||||
# 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
|
||||
ISCSI_LUN_FILE="${XFS_MOUNT}/iscsi-lun.img"
|
||||
ISCSI_LUN_SIZE="10G"
|
||||
DRBD_DEVICE="/dev/drbd0"
|
||||
# DRBD backing disk — by-id path that resolves correctly on both nodes
|
||||
# regardless of whether the OS-level name is sda or sdb (Proxmox VM disk
|
||||
# ordering is not guaranteed). Matches haServerDrbdDisk in variables.nix.
|
||||
# Override DRBD_DISK if your hardware uses a different controller/slot path.
|
||||
DRBD_DISK="${DRBD_DISK:-/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi1}"
|
||||
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:-}"
|
||||
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"
|
||||
|
||||
# NFS dataset subdirectories to create under XFS_MOUNT.
|
||||
# Must mirror vars.nfsShares subpath values in variables.nix.
|
||||
@@ -70,44 +59,6 @@ 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
|
||||
|
||||
# drbdmeta lives alongside drbdadm but may not be in PATH when run via sudo.
|
||||
if ! command -v drbdmeta &>/dev/null; then
|
||||
_drbd_bin=$(dirname "$(command -v drbdadm)" 2>/dev/null || true)
|
||||
[[ -n "$_drbd_bin" ]] && export PATH="$_drbd_bin:$PATH" \
|
||||
|| die "drbdmeta not found — is drbd-utils in ha-server environment.systemPackages?"
|
||||
fi
|
||||
|
||||
# Portable 16-hex-char UUID generator (no openssl required).
|
||||
_rand_uuid() {
|
||||
cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-' | cut -c1-16 | tr '[:lower:]' '[:upper:]'
|
||||
}
|
||||
|
||||
# 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
|
||||
@@ -117,20 +68,15 @@ if [[ ! -f "$AUTHKEY" ]]; then
|
||||
chmod 0400 "$AUTHKEY"
|
||||
fi
|
||||
log "Distributing authkey to $NODE2..."
|
||||
n2_ssh "mkdir -p /etc/corosync"
|
||||
n2_scp "$AUTHKEY" "$AUTHKEY"
|
||||
n2_ssh "chmod 0400 '${AUTHKEY}'"
|
||||
ssh "root@${NODE2_IP}" "mkdir -p /etc/corosync"
|
||||
scp -q "$AUTHKEY" "root@${NODE2_IP}:${AUTHKEY}"
|
||||
ssh "root@${NODE2_IP}" "chmod 0400 '${AUTHKEY}'"
|
||||
|
||||
log "Restarting corosync and pacemaker on both nodes..."
|
||||
log "Restarting corosync on both nodes..."
|
||||
systemctl restart corosync
|
||||
n2_ssh "systemctl restart corosync"
|
||||
ssh "root@${NODE2_IP}" "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
|
||||
@@ -153,141 +99,36 @@ for i in $(seq 1 30); do
|
||||
done
|
||||
|
||||
# ── 2. DRBD initialisation ────────────────────────────────────────────────
|
||||
# Put both nodes in Pacemaker standby first so it stops managed resources
|
||||
# cleanly, then enable maintenance-mode so Pacemaker's monitor operations are
|
||||
# suspended. Without maintenance-mode, Pacemaker keeps monitoring: when it
|
||||
# sees DRBD Primary on a standby node (that it didn't start), it triggers a
|
||||
# stop action — killing the initial sync after ~10 s. Maintenance-mode
|
||||
# disables all start/stop/monitor actions for the duration of the sync; it is
|
||||
# cleared after UpToDate/UpToDate is confirmed.
|
||||
log "Setting both nodes to Pacemaker standby for DRBD metadata init..."
|
||||
crm_standby -N "$NODE1" -v on 2>/dev/null || true
|
||||
crm_standby -N "$NODE2" -v on 2>/dev/null || true
|
||||
|
||||
# Wait for Pacemaker to actually stop DRBD (if it was managing it).
|
||||
log "Waiting for DRBD to stop under Pacemaker control..."
|
||||
for i in $(seq 1 30); do
|
||||
n1_role=$(drbdadm role ha-data 2>/dev/null || echo "Unconfigured")
|
||||
n2_role=$(n2_ssh "drbdadm role ha-data 2>/dev/null" 2>/dev/null || echo "Unconfigured")
|
||||
if [[ "$n1_role" == "Unconfigured" ]] && [[ "$n2_role" == "Unconfigured" ]]; then
|
||||
log "DRBD stopped on both nodes"
|
||||
break
|
||||
fi
|
||||
[[ $i -eq 30 ]] && warn "DRBD still active after 60s standby — forcing down anyway"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
log "Enabling Pacemaker maintenance-mode (suspends monitor/start/stop during sync)..."
|
||||
crm_attribute -t crm_config -n maintenance-mode -v true 2>/dev/null || true
|
||||
|
||||
log "Detaching DRBD on $NODE1 (belt-and-suspenders after standby)..."
|
||||
drbdadm down ha-data 2>/dev/null || true
|
||||
log "Detaching DRBD on $NODE2..."
|
||||
n2_ssh "drbdadm down ha-data 2>/dev/null || true"
|
||||
sleep 2
|
||||
|
||||
# Ensure /etc/drbd.conf on both nodes points to DRBD_DISK (the stable by-id
|
||||
# path). VMs built before this fix may have /dev/sda or /dev/sdb hardcoded.
|
||||
# NixOS makes /etc/drbd.conf a symlink into the read-only Nix store, so
|
||||
# sed -i on the symlink target would fail — we break the symlink first with
|
||||
# cp --remove-destination, creating a regular writable copy.
|
||||
# Rebuild+redeploy (--force-rebuild) to make this permanent.
|
||||
_PATCH_DRBD=$(mktemp)
|
||||
cat > "$_PATCH_DRBD" << 'PATCHEOF'
|
||||
#!/bin/bash
|
||||
WANT="$1"
|
||||
conf=/etc/drbd.conf
|
||||
if [[ -L "$conf" ]]; then
|
||||
cp --remove-destination "$(readlink -f "$conf")" "$conf"
|
||||
fi
|
||||
cur=$(drbdadm sh-ll-dev ha-data 2>/dev/null | head -1 || true)
|
||||
if [[ -n "$cur" && "$cur" != "$WANT" ]]; then
|
||||
echo "[cluster-init] WARNING: patching $conf: $cur → $WANT (rebuild to make permanent)"
|
||||
sed -i "s,${cur},${WANT},g" "$conf"
|
||||
fi
|
||||
PATCHEOF
|
||||
chmod +x "$_PATCH_DRBD"
|
||||
bash "$_PATCH_DRBD" "$DRBD_DISK"
|
||||
n2_scp "$_PATCH_DRBD" "/tmp/patch-drbd-disk.sh"
|
||||
n2_ssh "bash /tmp/patch-drbd-disk.sh ${DRBD_DISK}"
|
||||
n2_ssh "rm -f /tmp/patch-drbd-disk.sh"
|
||||
rm -f "$_PATCH_DRBD"
|
||||
|
||||
log "Initialising DRBD metadata on $NODE1..."
|
||||
# Use drbdmeta --force directly for BOTH create-md and write-dev-uuid.
|
||||
# drbdadm create-md --force passes --force to drbdmeta create-md but NOT to
|
||||
# the write-dev-uuid sub-call it makes internally, so write-dev-uuid fails when
|
||||
# the backing disk is still busy and stdin is not a TTY:
|
||||
# "stdin not a TTY, not waiting for confirmation" → exit 20.
|
||||
# Calling drbdmeta --force directly bypasses the exclusive-open confirmation on
|
||||
# both steps without needing a TTY, regardless of whether the device is busy.
|
||||
# Skip metadata creation only if DRBD is UP and fully synced (UpToDate/UpToDate).
|
||||
# When the resource is down, drbdadm dstate reads metadata and returns just
|
||||
# "UpToDate" (no slash) — that must not be treated as "already synced".
|
||||
# Mismatched UUIDs from an interrupted sync cause instant WFConnection→StandAlone,
|
||||
# so we always recreate metadata unless the sync is genuinely complete.
|
||||
if [[ "$(drbdadm dstate ha-data 2>/dev/null)" != "UpToDate/UpToDate" ]]; then
|
||||
UUID1=$(_rand_uuid)
|
||||
drbdmeta --force 0 v08 "${DRBD_DISK}" internal create-md
|
||||
drbdmeta --force 0 v08 "${DRBD_DISK}" internal write-dev-uuid "$UUID1"
|
||||
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..."
|
||||
if [[ "$(n2_ssh "drbdadm dstate ha-data 2>/dev/null" 2>/dev/null)" != "UpToDate/UpToDate" ]]; then
|
||||
UUID2=$(n2_ssh "cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-' | cut -c1-16 | tr '[:lower:]' '[:upper:]'")
|
||||
n2_ssh "drbdmeta --force 0 v08 ${DRBD_DISK} internal create-md"
|
||||
n2_ssh "drbdmeta --force 0 v08 ${DRBD_DISK} internal write-dev-uuid ${UUID2}"
|
||||
fi
|
||||
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
|
||||
"
|
||||
|
||||
log "Bringing up DRBD on both nodes..."
|
||||
drbdadm up ha-data 2>/dev/null || true
|
||||
n2_ssh "drbdadm up ha-data" 2>/dev/null || true
|
||||
ssh "root@${NODE2_IP}" "drbdadm up ha-data 2>/dev/null" || true
|
||||
|
||||
log "Forcing $NODE1 to DRBD Primary for initial sync..."
|
||||
drbdadm primary ha-data --force
|
||||
# NOTE: Pacemaker standby is intentionally kept ON until after the sync
|
||||
# completes. Clearing it here races with the OCF DRBD agent: Pacemaker
|
||||
# sees DRBD in WFConnection/SyncSource and may call drbdadm-down thinking
|
||||
# something went wrong, killing the sync. Standby is cleared below, after
|
||||
# UpToDate/UpToDate is confirmed.
|
||||
|
||||
log "Waiting for DRBD initial sync to complete (32 GB may take 10–20 min)..."
|
||||
log " (monitor with: watch -n3 cat /proc/drbd)"
|
||||
_sync_chars=('|' '/' '-' $'\\')
|
||||
_sync_iter=0
|
||||
while true; do
|
||||
_dstate=$(drbdadm dstate ha-data 2>/dev/null || echo "unknown")
|
||||
if echo "$_dstate" | grep -q "UpToDate/UpToDate"; then
|
||||
printf "\r%-80s\r" ""
|
||||
log "DRBD initial sync complete (dstate: $_dstate)"
|
||||
log "Waiting for DRBD to finish initial sync (this may take several minutes)..."
|
||||
for i in $(seq 1 300); do
|
||||
state=$(drbdadm dstate ha-data 2>/dev/null || echo "unknown")
|
||||
if echo "$state" | grep -q "UpToDate/UpToDate"; then
|
||||
log "DRBD sync complete: $state"
|
||||
break
|
||||
fi
|
||||
# Parse connection state from /proc/drbd (cs:SyncSource, cs:Connected, cs:StandAlone …)
|
||||
_cs=$(grep -oE 'cs:[A-Za-z]+' /proc/drbd 2>/dev/null | head -1 | sed 's/cs://' || echo "unknown")
|
||||
# /proc/drbd uses variable whitespace: "sync'ed: 5.2%" (two spaces).
|
||||
_pct=$(grep -oE "sync'ed:[[:space:]]+[0-9.]+" /proc/drbd 2>/dev/null | grep -oE "[0-9.]+" | head -1 || echo "")
|
||||
_eta=$(grep -oE "finish:[[:space:]]+[0-9:]+" /proc/drbd 2>/dev/null | grep -oE "[0-9:]+$" | head -1 || echo "")
|
||||
_spd=$(grep -oE "speed:[[:space:]]+[0-9,]+" /proc/drbd 2>/dev/null | grep -oE "[0-9,]+$" | head -1 || echo "")
|
||||
_sync_iter=$(( _sync_iter + 1 ))
|
||||
_sc="${_sync_chars[$_sync_iter % 4]}"
|
||||
if [[ "$_cs" == "StandAlone" && $_sync_iter -gt 5 ]]; then
|
||||
printf "\r%-80s\r" ""
|
||||
die "DRBD is StandAlone after 15 s — peer connection lost (dstate: $_dstate). " \
|
||||
"Check corosync/network and re-run cluster-init."
|
||||
elif [[ -n "$_pct" ]]; then
|
||||
printf "\r [%s] syncing: %s%% done — ETA %s @ %s K/s " \
|
||||
"$_sc" "$_pct" "${_eta:-??:??:??}" "${_spd:-?}"
|
||||
else
|
||||
printf "\r [%s] cs:%s dstate:%s — waiting for sync to start " "$_sc" "$_cs" "$_dstate"
|
||||
fi
|
||||
sleep 3
|
||||
[[ $i -eq 300 ]] && warn "DRBD not UpToDate after 300 s — continuing anyway (check drbdadm status)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log "Disabling Pacemaker maintenance-mode and clearing standby — handing DRBD back to Pacemaker..."
|
||||
crm_attribute -t crm_config -n maintenance-mode -v false 2>/dev/null || true
|
||||
crm_standby -N "$NODE1" -v off 2>/dev/null || true
|
||||
crm_standby -N "$NODE2" -v off 2>/dev/null || true
|
||||
|
||||
# ── 3. XFS filesystem ─────────────────────────────────────────────────────
|
||||
log "Creating XFS on ${DRBD_DEVICE}..."
|
||||
if ! xfs_info "${DRBD_DEVICE}" &>/dev/null; then
|
||||
@@ -296,7 +137,7 @@ fi
|
||||
|
||||
log "Mounting ${DRBD_DEVICE} at ${XFS_MOUNT}..."
|
||||
mkdir -p "${XFS_MOUNT}"
|
||||
mountpoint -q "${XFS_MOUNT}" || mount "${DRBD_DEVICE}" "${XFS_MOUNT}"
|
||||
mount "${DRBD_DEVICE}" "${XFS_MOUNT}"
|
||||
|
||||
# ── 4. NFS dataset directories ────────────────────────────────────────────
|
||||
log "Creating NFS dataset directories..."
|
||||
@@ -312,154 +153,116 @@ 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..."
|
||||
n2_scp /etc/target/saveconfig.json /etc/target/saveconfig.json
|
||||
|
||||
scp -q /etc/target/saveconfig.json "root@${NODE2_IP}:/etc/target/saveconfig.json"
|
||||
|
||||
log "Unmounting ${XFS_MOUNT} — Pacemaker manages it..."
|
||||
umount "${XFS_MOUNT}" || { sync; umount -l "${XFS_MOUNT}"; }
|
||||
umount "${XFS_MOUNT}"
|
||||
|
||||
log "Demoting DRBD to Secondary — Pacemaker manages primary role..."
|
||||
drbdadm role ha-data 2>/dev/null | grep -q "^Primary" && drbdadm secondary ha-data || true
|
||||
drbdadm secondary ha-data
|
||||
|
||||
# ── 7. Pacemaker resources ────────────────────────────────────────────────
|
||||
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 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"/>
|
||||
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\"/>
|
||||
</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="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"/>
|
||||
<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\"/>
|
||||
</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="/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"/>
|
||||
<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\"/>
|
||||
</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-storage" class="ocf" type="IPaddr2" provider="heartbeat">
|
||||
<instance_attributes id="vip-storage-attrs">
|
||||
<nvpair id="vip-storage-ip" name="ip" value="192.168.20.229"/>
|
||||
<nvpair id="vip-storage-cidr" name="cidr_netmask" value="24"/>
|
||||
<nvpair id="vip-storage-nic" name="nic" value="ens20"/>
|
||||
<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\"/>
|
||||
</instance_attributes>
|
||||
<operations>
|
||||
<op id="vip-storage-start" name="start" interval="0" timeout="20s"/>
|
||||
<op id="vip-storage-stop" name="stop" interval="0" timeout="20s"/>
|
||||
<op id="vip-storage-monitor" name="monitor" interval="10s" timeout="20s"/>
|
||||
</operations>
|
||||
</primitive>
|
||||
<primitive id="vip-lan" class="ocf" type="IPaddr2" provider="heartbeat">
|
||||
<instance_attributes id="vip-lan-attrs">
|
||||
<nvpair id="vip-lan-ip" name="ip" value="192.168.2.229"/>
|
||||
<nvpair id="vip-lan-cidr" name="cidr_netmask" value="24"/>
|
||||
<nvpair id="vip-lan-nic" name="nic" value="ens18"/>
|
||||
</instance_attributes>
|
||||
<operations>
|
||||
<op id="vip-lan-start" name="start" interval="0" timeout="20s"/>
|
||||
<op id="vip-lan-stop" name="stop" interval="0" timeout="20s"/>
|
||||
<op id="vip-lan-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 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 "Clearing stale Pacemaker failure history..."
|
||||
crm_resource --cleanup 2>/dev/null || true
|
||||
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 "Waiting for resources to start..."
|
||||
for i in $(seq 1 60); do
|
||||
if crm_resource -r vip-storage --locate 2>/dev/null | grep -q "running on"; then
|
||||
log "VIPs are up: $(crm_resource -r vip-storage --locate)"
|
||||
if crm_resource -r vip --locate 2>/dev/null | grep -q "running on"; then
|
||||
log "VIP is up: $(crm_resource -r vip --locate)"
|
||||
break
|
||||
fi
|
||||
[[ $i -eq 60 ]] && { warn "VIPs not up after 120 s — check: crm_mon -1"; break; }
|
||||
[[ $i -eq 60 ]] && { warn "VIP not up after 120 s — check: crm_mon -1"; break; }
|
||||
sleep 2
|
||||
done
|
||||
|
||||
@@ -467,11 +270,9 @@ log ""
|
||||
log "═══════════════════════════════════════════════════════════════"
|
||||
log " HA cluster initialised."
|
||||
log ""
|
||||
log " crm_mon -1 — cluster status"
|
||||
log " iscsiadm -m discovery -t st -p ${VIP} — verify iSCSI (storage net)"
|
||||
log " iscsiadm -m discovery -t st -p ${VIP_LAN} — verify iSCSI (LAN)"
|
||||
log " showmount -e ${VIP} — verify NFS exports (storage net)"
|
||||
log " showmount -e ${VIP_LAN} — verify NFS exports (LAN)"
|
||||
log " crm_mon -1 — cluster status"
|
||||
log " iscsiadm -m discovery -t st -p ${VIP} — verify iSCSI target"
|
||||
log " showmount -e ${VIP} — verify NFS exports"
|
||||
log ""
|
||||
log " To enable STONITH (after deploying fence SSH key):"
|
||||
log " 1. Fill in VMID_NODE1 / VMID_NODE2 in cluster-enable-stonith.sh"
|
||||
|
||||
@@ -1,499 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh — Full lifecycle management for the HA file-server cluster.
|
||||
#
|
||||
# Handles everything from zero (no VMs, no secrets) through a running,
|
||||
# tested cluster, and optionally tears it back down.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/ha/deploy.sh [options]
|
||||
# scripts/ha/deploy.sh --destroy [options]
|
||||
#
|
||||
# Phases (all run by default; skip any with --skip-*):
|
||||
# 1. ensure-bridge Create storage bridge (vmbr1) on the Proxmox node if absent.
|
||||
# 2. sync-keys Generate SSH host keys and register age keys for both nodes.
|
||||
# 3. create-vms Build disk images and create both VMs via create-proxmox-resource.sh.
|
||||
# 4. add-hardware Attach storage NIC (vmbr1) and DRBD data disk to each VM.
|
||||
# 5. boot-wait Start VMs, wait for SSH on both nodes.
|
||||
# 6. cluster-init Form the cluster: DRBD, corosync, Pacemaker, NFS, VIP.
|
||||
# Also encrypts the generated corosync authkey into the repo.
|
||||
# 7. run-tests Run acceptance tests (T1–T7).
|
||||
#
|
||||
# Options:
|
||||
# --node <host> Proxmox host to deploy on (default: pve1.sweet.home)
|
||||
# --vmid1 <n> VMID for ha-server-1 (default: 200)
|
||||
# --vmid2 <n> VMID for ha-server-2 (default: 201)
|
||||
# --storage <pool> Proxmox storage pool (default: local-zfs)
|
||||
# --storage-bridge <br> Bridge for HA storage network (default: vmbr1)
|
||||
# --drbd-disk-gb <n> DRBD data disk size in GB (default: 32)
|
||||
# --memory <MB> RAM per node (default: 4096)
|
||||
# --cores <n> vCPUs per node (default: 4)
|
||||
# --skip-ensure-bridge Skip storage bridge creation/check
|
||||
# --skip-sync-keys Skip sync-host-keys.sh (clan vars already exist)
|
||||
# --skip-create-vms Skip VM creation (VMs already exist)
|
||||
# --skip-add-hardware Skip net1/scsi1 attachment (already attached)
|
||||
# --skip-boot-wait Skip boot/SSH wait (VMs already running)
|
||||
# --skip-refresh-sops-keys Skip scanning running VMs for fresh SSH host keys
|
||||
# --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_REFRESH_SOPS_KEYS=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.10.228"
|
||||
STORAGE_IP2="192.168.10.227"
|
||||
STORAGE_CIDR="192.168.10.224/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-refresh-sops-keys) SKIP_REFRESH_SOPS_KEYS=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}"
|
||||
|
||||
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}..."
|
||||
# Always --force-rebuild: create-proxmox-resource.sh only calls
|
||||
# sync_remote_host_keys (which populates host-keys/ for proxmox.nix to
|
||||
# bake the clan-var SSH key into the disko image) when it actually builds.
|
||||
# Reusing a cached image skips that step, so destroy+recreate would reuse
|
||||
# an image with a stale/random key baked in → sops fails on first boot.
|
||||
run bash "$CREATE" \
|
||||
--type vm \
|
||||
--host "$host_name" \
|
||||
--vmid "$vmid" \
|
||||
--node "$NODE" \
|
||||
--storage "$STORAGE" \
|
||||
--memory "$MEMORY_MB" \
|
||||
--cores "$CORES" \
|
||||
--force-rebuild
|
||||
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 5.5: Refresh sops host-key registrations ───────────────────────────
|
||||
#
|
||||
# Disko builds raw disk images; each new VM boots with a freshly-generated SSH
|
||||
# host key rather than the one pre-seeded in clan vars. This phase scans the
|
||||
# actual running VMs, and if their ed25519 host keys differ from what clan vars
|
||||
# record: updates the clan var pub-key files, rewrites the .sops.yaml age-key
|
||||
# anchors, and re-encrypts all affected sops files so the nodes can decrypt
|
||||
# secrets on the next nixos-rebuild. Safe no-op when keys haven't changed.
|
||||
|
||||
if ! $SKIP_REFRESH_SOPS_KEYS; then
|
||||
if $DRY_RUN; then
|
||||
logn "[dry-run] Would scan VM host keys and refresh .sops.yaml / secrets if needed"
|
||||
else
|
||||
log "Phase 5.5: Refreshing sops host-key registrations (disko key drift fix)"
|
||||
SOPS_UPDATED=false
|
||||
|
||||
for spec in \
|
||||
"${NODE1_IP}:proxmox-ha-server-1:${NODE1_HOST}" \
|
||||
"${NODE2_IP}:proxmox-ha-server-2:${NODE2_HOST}"; do
|
||||
IFS=: read -r node_ip flake_target host_name <<< "$spec"
|
||||
CLAN_PUB="${REPO_ROOT}/vars/per-machine/${flake_target}/openssh/ssh_host_ed25519_key.pub/value"
|
||||
|
||||
logn "Scanning ed25519 host key from ${host_name} (${node_ip})..."
|
||||
RAW=$(ssh-keyscan -t ed25519 "${node_ip}" 2>/dev/null | grep -v "^#") || true
|
||||
if [[ -z "$RAW" ]]; then
|
||||
logn "WARNING: no ed25519 key returned by ssh-keyscan for ${node_ip} — skipping"
|
||||
continue
|
||||
fi
|
||||
# ssh-keyscan returns: <ip> ssh-ed25519 <b64key>
|
||||
SCANNED_TYPE=$(awk '{print $2}' <<< "$RAW")
|
||||
SCANNED_KEY=$(awk '{print $3}' <<< "$RAW")
|
||||
SCANNED_PUBKEY="${SCANNED_TYPE} ${SCANNED_KEY} ${host_name}"
|
||||
|
||||
CURRENT=$(tr -d '\n' < "$CLAN_PUB" 2>/dev/null || true)
|
||||
if [[ "$SCANNED_PUBKEY" == "$CURRENT" ]]; then
|
||||
logn "${host_name}: clan var matches running key — no update needed"
|
||||
continue
|
||||
fi
|
||||
|
||||
logn "${host_name}: key drift detected — updating clan var"
|
||||
logn " old: ${CURRENT}"
|
||||
logn " new: ${SCANNED_PUBKEY}"
|
||||
echo "$SCANNED_PUBKEY" > "$CLAN_PUB"
|
||||
SOPS_UPDATED=true
|
||||
|
||||
# Rewrite the .sops.yaml anchor for this host with the new age key.
|
||||
ANCHOR="${flake_target}" # e.g. proxmox-ha-server-1
|
||||
NEW_AGE=$(echo "$SCANNED_PUBKEY" | \
|
||||
nix run --quiet --no-warn-dirty nixpkgs#ssh-to-age 2>/dev/null)
|
||||
if [[ -z "$NEW_AGE" ]]; then
|
||||
err "ssh-to-age produced no output for ${host_name} — check nixpkgs#ssh-to-age"
|
||||
fi
|
||||
logn " new age key: ${NEW_AGE}"
|
||||
sed -i "/&${ANCHOR} /s| age[a-z0-9]*$| ${NEW_AGE}|" "${REPO_ROOT}/.sops.yaml"
|
||||
done
|
||||
|
||||
if $SOPS_UPDATED; then
|
||||
logn "Running sops updatekeys on affected secrets..."
|
||||
SOPS="nix run --quiet --no-warn-dirty nixpkgs#sops --"
|
||||
(cd "${REPO_ROOT}" && \
|
||||
$SOPS updatekeys -y secrets/common.yaml && \
|
||||
$SOPS updatekeys -y secrets/ha-server-1.yaml && \
|
||||
$SOPS updatekeys -y secrets/ha-server-2.yaml && \
|
||||
$SOPS updatekeys -y secrets/ha-server-1.keytab && \
|
||||
$SOPS updatekeys -y secrets/ha-server-2.keytab)
|
||||
# Note: ha-corosync-authkey is re-generated and re-encrypted by cluster-init below.
|
||||
|
||||
logn "Committing refreshed host keys and re-encrypted secrets..."
|
||||
(cd "${REPO_ROOT}" && \
|
||||
git add \
|
||||
vars/per-machine/proxmox-ha-server-1/openssh/ssh_host_ed25519_key.pub/value \
|
||||
vars/per-machine/proxmox-ha-server-2/openssh/ssh_host_ed25519_key.pub/value \
|
||||
.sops.yaml \
|
||||
secrets/common.yaml \
|
||||
secrets/ha-server-1.yaml \
|
||||
secrets/ha-server-2.yaml \
|
||||
secrets/ha-server-1.keytab \
|
||||
secrets/ha-server-2.keytab && \
|
||||
git commit -m "secrets(ha): refresh sops host-key registrations for new VM instances" || true)
|
||||
logn "Sops keys refreshed and committed."
|
||||
fi
|
||||
fi
|
||||
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.20.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.20.229" \
|
||||
bash "${REPO_ROOT}/scripts/ha/acceptance-tests.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Deploy complete."
|
||||
@@ -1,256 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# failover.sh — graceful HA cluster failover
|
||||
#
|
||||
# Detects which node is active and moves all resources to the other node by
|
||||
# putting the active node into Pacemaker standby. Waits for the XFS mount to
|
||||
# appear on the target before returning.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/ha/failover.sh [--to node1|node2] [--force] [--timeout <s>] [--dry-run]
|
||||
#
|
||||
# --to node1|node2 target node (default: the node that is NOT currently active)
|
||||
# --force skip the interactive confirmation prompt
|
||||
# --timeout <s> seconds to wait for resources to move (default: 120)
|
||||
# --dry-run show what would be done without changing anything
|
||||
set -euo pipefail
|
||||
|
||||
# ── Configuration ─────────────────────────────────────────────────────────
|
||||
NODE1="${NODE1:-ha-server-1}"
|
||||
NODE2="${NODE2:-ha-server-2}"
|
||||
NODE1_IP="${NODE1_IP:-192.168.2.228}"
|
||||
NODE2_IP="${NODE2_IP:-192.168.2.227}"
|
||||
VIP="${VIP:-192.168.20.229}"
|
||||
XFS_MOUNT="${XFS_MOUNT:-/srv/ha-data}"
|
||||
HA_USER="${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; }
|
||||
|
||||
# ── Argument parsing ───────────────────────────────────────────────────────
|
||||
TARGET_NODE=""
|
||||
FORCE=false
|
||||
DRY_RUN=false
|
||||
TIMEOUT=120
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--to)
|
||||
shift
|
||||
case "${1:-}" in
|
||||
node1|ha-server-1) TARGET_NODE="$NODE1" ;;
|
||||
node2|ha-server-2) TARGET_NODE="$NODE2" ;;
|
||||
*) echo "ERROR: --to must be node1, node2, ha-server-1, or ha-server-2"; exit 1 ;;
|
||||
esac
|
||||
;;
|
||||
--force) FORCE=true ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--timeout) shift; TIMEOUT="${1:?--timeout requires a value}" ;;
|
||||
*) echo "Unknown argument: $1"; echo "Usage: $0 [--to node1|node2] [--force] [--timeout <s>] [--dry-run]"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
DRY_PREFIX=""
|
||||
$DRY_RUN && DRY_PREFIX="[dry-run] "
|
||||
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo " HA Cluster Failover — $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
$DRY_RUN && echo " MODE: dry-run — no changes will be made"
|
||||
echo "════════════════════════════════════════════════════"
|
||||
|
||||
# ── Detect active node ─────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Detecting active node..."
|
||||
|
||||
CRM_OUT=""
|
||||
if ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE1_IP}" true 2>/dev/null; then
|
||||
CRM_OUT=$(n1 "crm_mon -1" 2>/dev/null || true)
|
||||
fi
|
||||
if [[ -z "$CRM_OUT" ]]; then
|
||||
if ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE2_IP}" true 2>/dev/null; then
|
||||
CRM_OUT=$(n2 "crm_mon -1" 2>/dev/null || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
# crm_mon 2.x formats Promoted lines as " * Promoted: [ node ]" — the * bullet
|
||||
# means ^\s*(Promoted|Masters): never matches; filter Unpromoted first instead.
|
||||
ACTIVE_NODE=$(echo "$CRM_OUT" | grep -v 'Unpromoted\|Unmanaged' | \
|
||||
grep -E '(Promoted|Masters):' | \
|
||||
grep -oE '\b(ha-server-[0-9]+)\b' | head -1 || true)
|
||||
|
||||
if [[ -z "$ACTIVE_NODE" ]]; then
|
||||
echo ""
|
||||
echo "ERROR: could not determine active node from crm_mon."
|
||||
echo " Is Pacemaker still settling? Try running scripts/ha/health.sh first."
|
||||
echo " If Pacemaker is down on both nodes, manual recovery is required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ACTIVE_NODE" == "$NODE1" ]]; then
|
||||
ACTIVE_IP="$NODE1_IP"
|
||||
STANDBY_NODE="$NODE2"
|
||||
STANDBY_IP="$NODE2_IP"
|
||||
na() { n1 "$@"; }
|
||||
ns() { n2 "$@"; }
|
||||
else
|
||||
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)"
|
||||
|
||||
# ── Validate target ────────────────────────────────────────────────────────
|
||||
if [[ -n "$TARGET_NODE" ]]; then
|
||||
if [[ "$TARGET_NODE" == "$ACTIVE_NODE" ]]; then
|
||||
echo ""
|
||||
echo "ERROR: $TARGET_NODE is already the active node — nothing to do."
|
||||
exit 1
|
||||
fi
|
||||
echo " Target: $TARGET_NODE (as requested)"
|
||||
else
|
||||
echo " Target: $STANDBY_NODE (auto — the other node)"
|
||||
fi
|
||||
|
||||
# ── Pre-checks ─────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Pre-checks..."
|
||||
|
||||
DRBD_DSTATE=$(na "drbdadm dstate ha-data 2>/dev/null" 2>/dev/null || echo "unknown")
|
||||
if ! echo "$DRBD_DSTATE" | grep -q "UpToDate/UpToDate"; then
|
||||
echo ""
|
||||
echo " WARNING: DRBD dstate is '$DRBD_DSTATE' (not UpToDate/UpToDate)."
|
||||
echo " Failing over with a partially-synced disk risks split-brain."
|
||||
if ! $FORCE; then
|
||||
echo " Use --force to proceed anyway (not recommended)."
|
||||
exit 1
|
||||
fi
|
||||
echo " --force specified — proceeding despite non-ideal DRBD state."
|
||||
else
|
||||
echo " DRBD dstate: $DRBD_DSTATE — OK"
|
||||
fi
|
||||
|
||||
QUORUM_OK=$(na "corosync-quorumtool -s 2>/dev/null | grep -c 'Quorate:.*Yes'" 2>/dev/null || echo "0")
|
||||
if [[ "$QUORUM_OK" -lt 1 ]]; then
|
||||
echo " ERROR: cluster does not have quorum — failover would be unsafe."
|
||||
exit 1
|
||||
fi
|
||||
echo " Quorum: OK"
|
||||
|
||||
# ── Confirm ────────────────────────────────────────────────────────────────
|
||||
if ! $FORCE && ! $DRY_RUN; then
|
||||
echo ""
|
||||
echo " This will move all resources from $ACTIVE_NODE → $STANDBY_NODE."
|
||||
echo " VIP and services will be unreachable for ~10–30 seconds."
|
||||
printf " Proceed? [y/N] "
|
||||
read -r ANSWER
|
||||
[[ "${ANSWER,,}" == "y" || "${ANSWER,,}" == "yes" ]] || { echo "Aborted."; exit 0; }
|
||||
fi
|
||||
|
||||
# ── Capture active node's crm_node name ───────────────────────────────────
|
||||
# crm_node -n returns the node name as registered in Pacemaker (may differ
|
||||
# from hostname if Pacemaker was configured with explicit node names).
|
||||
ACTIVE_CRMD_NAME=$(na "crm_node -n 2>/dev/null" 2>/dev/null || echo "$ACTIVE_NODE")
|
||||
|
||||
# ── Perform failover ───────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "${DRY_PREFIX}Putting $ACTIVE_NODE into standby (resources will migrate to $STANDBY_NODE)..."
|
||||
if ! $DRY_RUN; then
|
||||
na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v on" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── Wait for resources to move ─────────────────────────────────────────────
|
||||
echo "${DRY_PREFIX}Waiting up to ${TIMEOUT}s for XFS to mount on $STANDBY_NODE..."
|
||||
MOVED=false
|
||||
SPIN_CHARS=('|' '/' '-' '\')
|
||||
SPIN_I=0
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo " [dry-run] would wait for mountpoint $XFS_MOUNT on $STANDBY_NODE"
|
||||
MOVED=true
|
||||
else
|
||||
for i in $(seq 1 "$TIMEOUT"); do
|
||||
if ns "mountpoint -q '${XFS_MOUNT}' 2>/dev/null" 2>/dev/null; then
|
||||
printf "\r%-80s\r" ""
|
||||
echo " Resources moved in ${i}s"
|
||||
MOVED=true
|
||||
break
|
||||
fi
|
||||
SPIN_I=$(( SPIN_I + 1 ))
|
||||
SC="${SPIN_CHARS[$((SPIN_I % 4))]}"
|
||||
printf "\r [%s] waiting... (%ds) " "$SC" "$i"
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
if ! $MOVED; then
|
||||
echo ""
|
||||
echo "ERROR: XFS did not mount on $STANDBY_NODE within ${TIMEOUT}s."
|
||||
echo ""
|
||||
echo " Current resource state:"
|
||||
na "crm_mon -1 2>/dev/null" 2>/dev/null | grep -E 'Started|Stopped|Promoted|Unpromoted|FAILED' | sed 's/^/ /' || true
|
||||
echo ""
|
||||
echo " Clearing standby to restore $ACTIVE_NODE (undo the failover attempt)..."
|
||||
na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v off" 2>/dev/null || true
|
||||
na "crm_resource --cleanup" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Clear failure history ──────────────────────────────────────────────────
|
||||
echo "${DRY_PREFIX}Clearing Pacemaker failure history..."
|
||||
if ! $DRY_RUN; then
|
||||
ns "crm_resource --cleanup 2>/dev/null" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── Re-enable original active node as standby ─────────────────────────────
|
||||
echo "${DRY_PREFIX}Re-enabling $ACTIVE_NODE (now standby — will not claim resources)..."
|
||||
if ! $DRY_RUN; then
|
||||
na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v off" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── Wait briefly for DRBD resync to begin ─────────────────────────────────
|
||||
if ! $DRY_RUN; then
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# ── Final state ────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Failover complete. Final state:"
|
||||
echo ""
|
||||
|
||||
CRM_OUT_AFTER=""
|
||||
if ! $DRY_RUN; then
|
||||
CRM_OUT_AFTER=$(ns "crm_mon -1" 2>/dev/null || na "crm_mon -1" 2>/dev/null || true)
|
||||
else
|
||||
CRM_OUT_AFTER="$CRM_OUT"
|
||||
fi
|
||||
|
||||
NEW_ACTIVE=$(echo "$CRM_OUT_AFTER" | grep -v 'Unpromoted\|Unmanaged' | \
|
||||
grep -E '(Promoted|Masters):' | \
|
||||
grep -oE '\b(ha-server-[0-9]+)\b' | head -1 || true)
|
||||
|
||||
if [[ -n "$NEW_ACTIVE" ]]; then
|
||||
if [[ "$NEW_ACTIVE" == "$ACTIVE_NODE" ]]; then
|
||||
echo " WARNING: $ACTIVE_NODE is still showing as active in crm_mon."
|
||||
echo " Pacemaker may still be settling — check again in a few seconds."
|
||||
else
|
||||
echo " Active: $NEW_ACTIVE"
|
||||
echo " Standby: $ACTIVE_NODE"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
RESOURCES_AFTER=$(echo "$CRM_OUT_AFTER" | awk '/Full List of Resources/,0' | tail -n +2 || true)
|
||||
[[ -z "$RESOURCES_AFTER" ]] && RESOURCES_AFTER=$(echo "$CRM_OUT_AFTER" | \
|
||||
grep -E 'Started|Stopped|Promoted|Unpromoted|FAILED|Master|Slave' || true)
|
||||
[[ -n "$RESOURCES_AFTER" ]] && echo "$RESOURCES_AFTER" | sed 's/^/ /'
|
||||
|
||||
echo ""
|
||||
echo " (DRBD resync of $ACTIVE_NODE may take a moment; monitor with:"
|
||||
echo " ssh nixos@${ACTIVE_IP} 'sudo watch -n3 cat /proc/drbd')"
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════"
|
||||
Executable → Regular
@@ -1,206 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# health.sh — HA cluster health snapshot (read-only, non-destructive)
|
||||
#
|
||||
# Prints a compact status panel across both nodes: SSH reachability, quorum,
|
||||
# DRBD state, Pacemaker resources, and service ports via the VIP.
|
||||
# Run from any host with SSH access to the HA nodes.
|
||||
set -euo pipefail
|
||||
|
||||
# ── Configuration ─────────────────────────────────────────────────────────
|
||||
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.20.229}" # vars.haServerVip (storage-client, VLAN 20 — internal only)
|
||||
VIP_LAN="${VIP_LAN:-192.168.2.229}" # vars.haServerLanVip (LAN, VLAN 2 — reachable from workstation)
|
||||
XFS_MOUNT="${XFS_MOUNT:-/srv/ha-data}" # vars.haStorageRoot
|
||||
HA_USER="${HA_USER:-nixos}"
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
REACHABLE_1=false
|
||||
REACHABLE_2=false
|
||||
|
||||
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; }
|
||||
|
||||
probe_node() {
|
||||
local ip=$1
|
||||
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${ip}" true 2>/dev/null && echo "ONLINE" || echo "OFFLINE"
|
||||
}
|
||||
|
||||
section() { echo ""; echo "── $* ──"; }
|
||||
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo " HA Cluster Health — $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "════════════════════════════════════════════════════"
|
||||
|
||||
# ── Node reachability ──────────────────────────────────────────────────────
|
||||
section "Nodes"
|
||||
N1_STATUS=$(probe_node "$NODE1_IP")
|
||||
N2_STATUS=$(probe_node "$NODE2_IP")
|
||||
[[ "$N1_STATUS" == "ONLINE" ]] && REACHABLE_1=true
|
||||
[[ "$N2_STATUS" == "ONLINE" ]] && REACHABLE_2=true
|
||||
|
||||
if ! $REACHABLE_1 && ! $REACHABLE_2; then
|
||||
echo " ERROR: both nodes unreachable — cannot continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Detect active node ─────────────────────────────────────────────────────
|
||||
# crm_mon 2.x formats the Promoted line as " * Promoted: [ node ]" — the
|
||||
# bullet * means ^\s*(Promoted|Masters): never matches. Filter out Unpromoted
|
||||
# first, then match anywhere on the line.
|
||||
ACTIVE_NODE=""
|
||||
CRM_OUT=""
|
||||
if $REACHABLE_1; then
|
||||
CRM_OUT=$(n1 "crm_mon -1" 2>/dev/null || true)
|
||||
elif $REACHABLE_2; then
|
||||
CRM_OUT=$(n2 "crm_mon -1" 2>/dev/null || true)
|
||||
fi
|
||||
ACTIVE_NODE=$(echo "${CRM_OUT:-}" | grep -v 'Unpromoted\|Unmanaged' | \
|
||||
grep -E '(Promoted|Masters):' | \
|
||||
grep -oE '\b(ha-server-[0-9]+)\b' | head -1 || true)
|
||||
|
||||
STANDBY_NODE=""
|
||||
if [[ "$ACTIVE_NODE" == "$NODE1" ]]; then
|
||||
STANDBY_NODE="$NODE2"
|
||||
elif [[ "$ACTIVE_NODE" == "$NODE2" ]]; then
|
||||
STANDBY_NODE="$NODE1"
|
||||
fi
|
||||
|
||||
n1_tag=""; n2_tag=""
|
||||
[[ "$ACTIVE_NODE" == "$NODE1" ]] && n1_tag=" [ACTIVE]" || n1_tag=" [STANDBY]"
|
||||
[[ "$ACTIVE_NODE" == "$NODE2" ]] && n2_tag=" [ACTIVE]" || n2_tag=" [STANDBY]"
|
||||
[[ -z "$ACTIVE_NODE" ]] && { n1_tag=""; n2_tag=""; }
|
||||
|
||||
printf " %-14s [%s]%s\n" "$NODE1" "$N1_STATUS" "$n1_tag"
|
||||
printf " %-14s [%s]%s\n" "$NODE2" "$N2_STATUS" "$n2_tag"
|
||||
|
||||
if [[ -z "$ACTIVE_NODE" ]]; then
|
||||
echo ""
|
||||
echo " WARNING: could not determine active node from crm_mon."
|
||||
echo " Pacemaker may still be settling, or both nodes may be in standby."
|
||||
fi
|
||||
|
||||
# ── Quorum ─────────────────────────────────────────────────────────────────
|
||||
section "Quorum"
|
||||
if $REACHABLE_1; then
|
||||
QUORUM=$(n1 "corosync-quorumtool -s 2>/dev/null" 2>/dev/null || echo "")
|
||||
elif $REACHABLE_2; then
|
||||
QUORUM=$(n2 "corosync-quorumtool -s 2>/dev/null" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ -z "${QUORUM:-}" ]]; then
|
||||
echo " corosync-quorumtool: unavailable"
|
||||
else
|
||||
QUORATE=$(echo "$QUORUM" | grep "Quorate:" | awk '{print $2}' || echo "?")
|
||||
VOTES=$(echo "$QUORUM" | grep "Total votes:" | awk '{print $3}' || echo "?")
|
||||
NEEDED=$(echo "$QUORUM" | grep "Quorum votes:" | awk '{print $3}' || echo "?")
|
||||
echo " Quorate: $QUORATE Votes: $VOTES / Expected: $NEEDED"
|
||||
fi
|
||||
|
||||
# ── DRBD ───────────────────────────────────────────────────────────────────
|
||||
section "DRBD (ha-data)"
|
||||
|
||||
drbd_info_from() {
|
||||
local node=$1 run=$2
|
||||
local role dstate cs pct
|
||||
role=$($run "drbdadm role ha-data 2>/dev/null" 2>/dev/null || echo "unknown")
|
||||
dstate=$($run "drbdadm dstate ha-data 2>/dev/null" 2>/dev/null || echo "unknown")
|
||||
cs=$($run "grep -oE 'cs:[A-Za-z]+' /proc/drbd 2>/dev/null | head -1 | sed 's/cs://'" 2>/dev/null || echo "unknown")
|
||||
pct=$($run "grep -oE \"sync'ed:[[:space:]]+[0-9.]+\" /proc/drbd 2>/dev/null | grep -oE '[0-9.]+$' | head -1" 2>/dev/null || echo "")
|
||||
printf " %-14s role: %-22s dstate: %-25s cs: %s" \
|
||||
"$node" "${role:-unknown}" "${dstate:-unknown}" "${cs:-unknown}"
|
||||
[[ -n "$pct" ]] && printf " syncing: %s%%" "$pct"
|
||||
echo ""
|
||||
}
|
||||
|
||||
$REACHABLE_1 && drbd_info_from "$NODE1" n1 || echo " $NODE1 [OFFLINE]"
|
||||
$REACHABLE_2 && drbd_info_from "$NODE2" n2 || echo " $NODE2 [OFFLINE]"
|
||||
|
||||
# ── Pacemaker (full crm_mon output) ───────────────────────────────────────
|
||||
section "Pacemaker"
|
||||
if [[ -n "${CRM_OUT:-}" ]]; then
|
||||
echo "$CRM_OUT" | sed 's/^/ /'
|
||||
else
|
||||
echo " crm_mon returned no output — trying again without suppression:"
|
||||
if $REACHABLE_1; then
|
||||
n1 "crm_mon -1" || true
|
||||
elif $REACHABLE_2; then
|
||||
n2 "crm_mon -1" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── XFS mount ─────────────────────────────────────────────────────────────
|
||||
section "XFS Mount ($XFS_MOUNT)"
|
||||
check_mount() {
|
||||
local node=$1 run=$2
|
||||
local status
|
||||
if $run "mountpoint -q '$XFS_MOUNT' 2>/dev/null" 2>/dev/null; then
|
||||
local usage
|
||||
usage=$($run "df -h '$XFS_MOUNT' 2>/dev/null | tail -1 | awk '{print \$3\"/\"\$2\" used (\"\$5\")\";}'" 2>/dev/null || echo "")
|
||||
status="mounted"
|
||||
[[ -n "$usage" ]] && status="mounted $usage"
|
||||
else
|
||||
status="not mounted"
|
||||
fi
|
||||
printf " %-14s %s\n" "$node" "$status"
|
||||
}
|
||||
|
||||
$REACHABLE_1 && check_mount "$NODE1" n1 || echo " $NODE1 [OFFLINE]"
|
||||
$REACHABLE_2 && check_mount "$NODE2" n2 || echo " $NODE2 [OFFLINE]"
|
||||
|
||||
# ── LAN VIP (NFS) — reachable from workstation ────────────────────────────────
|
||||
section "LAN VIP ($VIP_LAN) — NFS"
|
||||
if ping -c1 -W2 "$VIP_LAN" >/dev/null 2>&1; then
|
||||
echo " Ping OK"
|
||||
else
|
||||
echo " Ping UNREACHABLE"
|
||||
fi
|
||||
if bash -c "echo >/dev/tcp/${VIP_LAN}/2049" 2>/dev/null; then
|
||||
printf " %-10s port %-5s OK\n" "NFS" "2049"
|
||||
else
|
||||
printf " %-10s port %-5s UNREACHABLE\n" "NFS" "2049"
|
||||
fi
|
||||
|
||||
# ── Storage VIP (NFS + iSCSI) — VLAN 20 internal bridge, tested via active node ─
|
||||
section "Storage VIP ($VIP) — NFS + iSCSI (via ${ACTIVE_NODE:-unknown})"
|
||||
|
||||
run_active_raw() {
|
||||
local active_ip=""
|
||||
[[ "$ACTIVE_NODE" == "$NODE1" ]] && active_ip="$NODE1_IP"
|
||||
[[ "$ACTIVE_NODE" == "$NODE2" ]] && active_ip="$NODE2_IP"
|
||||
[[ -z "$active_ip" ]] && return 1
|
||||
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
|
||||
"${HA_USER}@${active_ip}" "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
if [[ -z "$ACTIVE_NODE" ]]; then
|
||||
echo " Cannot determine active node — skipping"
|
||||
else
|
||||
if run_active_raw "ping -c1 -W2 '$VIP' >/dev/null 2>&1"; then
|
||||
echo " Ping OK"
|
||||
else
|
||||
echo " Ping UNREACHABLE"
|
||||
fi
|
||||
if run_active_raw "bash -c 'echo >/dev/tcp/${VIP}/2049' 2>/dev/null"; then
|
||||
printf " %-10s port %-5s OK\n" "NFS" "2049"
|
||||
else
|
||||
printf " %-10s port %-5s UNREACHABLE\n" "NFS" "2049"
|
||||
fi
|
||||
if run_active_raw "bash -c 'echo >/dev/tcp/${VIP}/3260' 2>/dev/null"; then
|
||||
printf " %-10s port %-5s OK\n" "iSCSI" "3260"
|
||||
else
|
||||
printf " %-10s port %-5s UNREACHABLE\n" "iSCSI" "3260"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════"
|
||||
if [[ -n "$ACTIVE_NODE" ]]; then
|
||||
echo " Active: $ACTIVE_NODE Standby: $STANDBY_NODE"
|
||||
else
|
||||
echo " Active: unknown (Pacemaker not settled)"
|
||||
fi
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
@@ -1,274 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# resize-data-disk.sh — online resize of the HA cluster data disk
|
||||
#
|
||||
# Three-phase process (all online-safe, no downtime required):
|
||||
# 1. Proxmox: grow scsi1 on both HA VMs (qm resize)
|
||||
# 2. Guest: rescan block device on both nodes so the kernel sees the new size
|
||||
# 3. DRBD + XFS: drbdadm resize, then xfs_growfs — active node only
|
||||
#
|
||||
# Usage:
|
||||
# scripts/ha/resize-data-disk.sh --size +20G [--force] [--dry-run]
|
||||
#
|
||||
# --size +NNg amount to grow scsi1 by, e.g. +20G, +50G (required)
|
||||
# XFS and DRBD cannot shrink; only positive deltas accepted
|
||||
# --force skip the interactive confirmation prompt
|
||||
# --dry-run show what would be done without changing anything
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=../env.sh
|
||||
source "${SCRIPT_DIR}/../env.sh"
|
||||
|
||||
# ── Configuration ─────────────────────────────────────────────────────────
|
||||
NODE1="${NODE1:-ha-server-1}"
|
||||
NODE2="${NODE2:-ha-server-2}"
|
||||
NODE1_IP="${NODE1_IP:-192.168.2.228}"
|
||||
NODE2_IP="${NODE2_IP:-192.168.2.227}"
|
||||
XFS_MOUNT="${XFS_MOUNT:-/srv/ha-data}"
|
||||
DRBD_RESOURCE="${DRBD_RESOURCE:-ha-data}"
|
||||
DATA_DISK_SLOT="${DATA_DISK_SLOT:-scsi1}" # Proxmox disk name (scsi1 = data disk)
|
||||
HA_USER="${HA_USER:-nixos}"
|
||||
PVE_HOST="${PVE_HOST:-${PVE1_HOST}}"
|
||||
PVE_SSH_USER="${PVE_SSH_USER:-${PROXMOX_SSH_USER}}"
|
||||
PVE_SUDO=""
|
||||
[[ "$PVE_SSH_USER" != "root" ]] && PVE_SUDO="sudo"
|
||||
# By-id symlink for the data disk; basename resolves to the raw block device.
|
||||
# matches variables.nix's haServerDrbdDisk.
|
||||
DATA_DISK_BYID="${DATA_DISK_BYID:-scsi-0QEMU_QEMU_HARDDISK_drive-${DATA_DISK_SLOT}}"
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
pve() { ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 \
|
||||
"${PVE_SSH_USER}@${PVE_HOST}" "$@"; }
|
||||
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; }
|
||||
|
||||
# ── Argument parsing ───────────────────────────────────────────────────────
|
||||
SIZE=""
|
||||
FORCE=false
|
||||
DRY_RUN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--size) shift; SIZE="${1:?--size requires a value (e.g. +20G)}" ;;
|
||||
--force) FORCE=true ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
*) echo "Unknown argument: $1"
|
||||
echo "Usage: $0 --size +NNg [--force] [--dry-run]"
|
||||
exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ -z "$SIZE" ]]; then
|
||||
echo "ERROR: --size is required (e.g. --size +20G)"
|
||||
echo "Usage: $0 --size +NNg [--force] [--dry-run]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only positive deltas — qm resize, DRBD, and XFS all refuse to shrink.
|
||||
if [[ ! "$SIZE" =~ ^\+[0-9]+(G|M|T)$ ]]; then
|
||||
echo "ERROR: --size must be a positive delta like +20G, +50G, +500M, +2T"
|
||||
echo " (qm resize, drbdadm resize, and xfs_growfs can only grow, not shrink)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DRY_PREFIX=""
|
||||
$DRY_RUN && DRY_PREFIX="[dry-run] "
|
||||
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo " HA Data Disk Resize — $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo " Size delta: $SIZE • Proxmox: $PVE_HOST"
|
||||
$DRY_RUN && echo " MODE: dry-run — no changes will be made"
|
||||
echo "════════════════════════════════════════════════════"
|
||||
|
||||
# ── Detect active node ─────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Detecting active node..."
|
||||
|
||||
CRM_OUT=""
|
||||
if ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
|
||||
"${HA_USER}@${NODE1_IP}" true 2>/dev/null; then
|
||||
CRM_OUT=$(n1 "crm_mon -1" 2>/dev/null || true)
|
||||
fi
|
||||
if [[ -z "$CRM_OUT" ]]; then
|
||||
if ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
|
||||
"${HA_USER}@${NODE2_IP}" true 2>/dev/null; then
|
||||
CRM_OUT=$(n2 "crm_mon -1" 2>/dev/null || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
# crm_mon 2.x formats Promoted lines as " * Promoted: [ node ]" (bullet *),
|
||||
# so ^\s*(Promoted|Masters): never matches; filter Unpromoted first instead.
|
||||
ACTIVE_NODE=$(echo "$CRM_OUT" | grep -v 'Unpromoted\|Unmanaged' | \
|
||||
grep -E '(Promoted|Masters):' | \
|
||||
grep -oE '\b(ha-server-[0-9]+)\b' | head -1 || true)
|
||||
|
||||
if [[ -z "$ACTIVE_NODE" ]]; then
|
||||
echo "ERROR: could not determine active node from crm_mon."
|
||||
echo " Is Pacemaker still settling? Try running scripts/ha/health.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ACTIVE_NODE" == "$NODE1" ]]; then
|
||||
ACTIVE_IP="$NODE1_IP"
|
||||
na() { n1 "$@"; }
|
||||
else
|
||||
ACTIVE_IP="$NODE2_IP"
|
||||
na() { n2 "$@"; }
|
||||
fi
|
||||
|
||||
echo " Active node: $ACTIVE_NODE ($ACTIVE_IP)"
|
||||
|
||||
# ── Pre-check DRBD state ───────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Pre-checks..."
|
||||
|
||||
DRBD_DSTATE=$(na "drbdadm dstate ${DRBD_RESOURCE} 2>/dev/null" 2>/dev/null || echo "unknown")
|
||||
if ! echo "$DRBD_DSTATE" | grep -q "UpToDate/UpToDate"; then
|
||||
echo " WARNING: DRBD dstate is '$DRBD_DSTATE' (expected UpToDate/UpToDate)."
|
||||
echo " Resizing with a partially-synced disk may cause issues."
|
||||
if ! $FORCE; then
|
||||
echo " Use --force to proceed anyway."
|
||||
exit 1
|
||||
fi
|
||||
echo " --force specified — proceeding despite non-ideal DRBD state."
|
||||
else
|
||||
echo " DRBD dstate: $DRBD_DSTATE — OK"
|
||||
fi
|
||||
|
||||
# ── Find VMIDs on Proxmox ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Looking up VM IDs on ${PVE_HOST}..."
|
||||
|
||||
QM_LIST=$(pve "$PVE_SUDO qm list 2>/dev/null" || true)
|
||||
VMID1=$(echo "$QM_LIST" | awk -v name="$NODE1" '$0 ~ name {print $1}' | head -1)
|
||||
VMID2=$(echo "$QM_LIST" | awk -v name="$NODE2" '$0 ~ name {print $1}' | head -1)
|
||||
|
||||
if [[ -z "$VMID1" ]]; then
|
||||
echo " ERROR: could not find VMID for $NODE1 on $PVE_HOST"
|
||||
echo " qm list output:"
|
||||
echo "$QM_LIST" | sed 's/^/ /'
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$VMID2" ]]; then
|
||||
echo " ERROR: could not find VMID for $NODE2 on $PVE_HOST"
|
||||
echo " qm list output:"
|
||||
echo "$QM_LIST" | sed 's/^/ /'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " $NODE1: VMID $VMID1"
|
||||
echo " $NODE2: VMID $VMID2"
|
||||
|
||||
# ── Confirm ────────────────────────────────────────────────────────────────
|
||||
if ! $FORCE && ! $DRY_RUN; then
|
||||
echo ""
|
||||
echo " Plan:"
|
||||
echo " Phase 1 — qm resize $VMID1 ${DATA_DISK_SLOT} ${SIZE} (on $PVE_HOST)"
|
||||
echo " qm resize $VMID2 ${DATA_DISK_SLOT} ${SIZE} (on $PVE_HOST)"
|
||||
echo " Phase 2 — block device rescan on $NODE1 and $NODE2"
|
||||
echo " Phase 3 — drbdadm resize + xfs_growfs on $ACTIVE_NODE"
|
||||
echo " No downtime required (all operations are online-safe)."
|
||||
printf " Proceed? [y/N] "
|
||||
read -r ANSWER
|
||||
[[ "${ANSWER,,}" == "y" || "${ANSWER,,}" == "yes" ]] || { echo "Aborted."; exit 0; }
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Phase 1 — Resize both VM data disks in Proxmox
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "── Phase 1 — Proxmox disk resize (${DATA_DISK_SLOT} ${SIZE} on both VMs) ──"
|
||||
|
||||
echo " ${DRY_PREFIX}qm resize $VMID1 ${DATA_DISK_SLOT} ${SIZE} ($NODE1 on ${PVE_HOST})"
|
||||
if ! $DRY_RUN; then
|
||||
pve "$PVE_SUDO qm resize $VMID1 ${DATA_DISK_SLOT} ${SIZE}"
|
||||
fi
|
||||
|
||||
echo " ${DRY_PREFIX}qm resize $VMID2 ${DATA_DISK_SLOT} ${SIZE} ($NODE2 on ${PVE_HOST})"
|
||||
if ! $DRY_RUN; then
|
||||
pve "$PVE_SUDO qm resize $VMID2 ${DATA_DISK_SLOT} ${SIZE}"
|
||||
fi
|
||||
|
||||
echo " Phase 1 done."
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Phase 2 — Rescan block device on both guest nodes
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "── Phase 2 — Block device rescan (both nodes) ──"
|
||||
|
||||
rescan_node() {
|
||||
local node_name=$1 run_fn=$2
|
||||
|
||||
# Resolve block device name from the stable by-id symlink on the guest.
|
||||
# Read-only lookup — safe to run even in dry-run so we show the real device.
|
||||
local blk_dev=""
|
||||
blk_dev=$($run_fn "bash -c 'basename \$(readlink -f /dev/disk/by-id/${DATA_DISK_BYID})'" 2>/dev/null || true)
|
||||
if [[ -z "$blk_dev" ]]; then
|
||||
echo " ERROR: /dev/disk/by-id/${DATA_DISK_BYID} not found on $node_name" >&2
|
||||
echo " Check DATA_DISK_BYID or DATA_DISK_SLOT configuration." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " ${DRY_PREFIX}Rescanning /dev/${blk_dev} on ${node_name}..."
|
||||
if ! $DRY_RUN; then
|
||||
$run_fn "bash -c 'echo 1 > /sys/block/${blk_dev}/device/rescan'" 2>/dev/null
|
||||
local new_size
|
||||
new_size=$($run_fn "lsblk -nd -o SIZE /dev/${blk_dev} 2>/dev/null" 2>/dev/null || echo "unknown")
|
||||
echo " /dev/${blk_dev} on $node_name now reports: $new_size"
|
||||
fi
|
||||
}
|
||||
|
||||
rescan_node "$NODE1" n1
|
||||
rescan_node "$NODE2" n2
|
||||
|
||||
echo " Phase 2 done."
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Phase 3 — Grow DRBD metadata, then XFS (active node only)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
echo ""
|
||||
echo "── Phase 3 — DRBD resize + XFS grow (on active node: $ACTIVE_NODE) ──"
|
||||
|
||||
echo " ${DRY_PREFIX}drbdadm resize ${DRBD_RESOURCE}"
|
||||
if ! $DRY_RUN; then
|
||||
na "drbdadm resize ${DRBD_RESOURCE}"
|
||||
fi
|
||||
|
||||
echo " ${DRY_PREFIX}xfs_growfs ${XFS_MOUNT}"
|
||||
if ! $DRY_RUN; then
|
||||
na "xfs_growfs ${XFS_MOUNT}"
|
||||
fi
|
||||
|
||||
echo " Phase 3 done."
|
||||
|
||||
# ── Verify ────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "── Verify ──"
|
||||
|
||||
if ! $DRY_RUN; then
|
||||
DF_OUT=$(na "df -h '${XFS_MOUNT}' 2>/dev/null" 2>/dev/null || echo "")
|
||||
if [[ -n "$DF_OUT" ]]; then
|
||||
echo " ${XFS_MOUNT}:"
|
||||
echo "$DF_OUT" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
DRBD_DSTATE_AFTER=$(na "drbdadm dstate ${DRBD_RESOURCE} 2>/dev/null" 2>/dev/null || echo "unknown")
|
||||
echo " DRBD dstate: $DRBD_DSTATE_AFTER"
|
||||
if ! echo "$DRBD_DSTATE_AFTER" | grep -q "UpToDate/UpToDate"; then
|
||||
echo " NOTE: DRBD is resyncing — normal immediately after resize."
|
||||
echo " Monitor: ssh nixos@${ACTIVE_IP} 'sudo watch -n3 cat /proc/drbd'"
|
||||
fi
|
||||
else
|
||||
echo " [dry-run] would verify df -h ${XFS_MOUNT} and drbdadm dstate on $ACTIVE_NODE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo " Resize complete."
|
||||
echo " Active node: $ACTIVE_NODE"
|
||||
echo "════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
@@ -81,14 +81,6 @@ if [[ -z "${TARGET}" ]]; then
|
||||
usage 1
|
||||
fi
|
||||
|
||||
# Reject FQDNs passed by mistake — the script appends HOME_DOMAIN itself.
|
||||
# "nixos.sweet.home" → FQDN would become "nixos.sweet.home.sweet.home".
|
||||
if [[ "${TARGET}" == *"."* ]]; then
|
||||
echo "Error: <hostname> must be the short name (e.g. 'nixos'), not a FQDN." >&2
|
||||
echo " The FQDN is derived automatically as ${TARGET}.${HOME_DOMAIN}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FQDN="${TARGET}.${HOME_DOMAIN}"
|
||||
KEYTAB_SECRET="${REPO_ROOT}/secrets/${TARGET}.keytab"
|
||||
# Temp path on the domain controller — use a name that won't collide.
|
||||
|
||||
+118
-137
@@ -1,235 +1,216 @@
|
||||
root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+qh8vlDETtFs/iQdetF/2ZxmANf62SviTNd+Ag0q5JIF1996x7onZGXqxgSMCuVzZLBdUlsO5IR0BslWWz47khYGTe4WkUg4NB1itBfQ==,iv:5Sra5vJ79V8hxQT3g9qJ+dOj2W2sumIhqpitqnHjJdk=,tag:3Igu0+8GeUZHqS3fKUVwog==,type:str]
|
||||
nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,type:str]
|
||||
nix-github-token: ENC[AES256_GCM,data:k1vYz7SqVhzpWa6jTL6NUD8lKOCpHCgTm+HT4IcnbzbSTUZP/bJUYw==,iv:UqAULZnr/4+VcioUDfTwvOSuwM8K9JgGhiApvYQPyoc=,tag:1LKHXhWAO/AHPDIZFBb04A==,type:str]
|
||||
beszel-token: ENC[AES256_GCM,data:OWmSRkZjb11y0Y8GdobqiE9GFwzdHOvvxCbYx69qUghGYARN,iv:i/JhGH0O7ThxPkL0SLAjfN0Fq8prm7tybI5kF2NRNpw=,tag:dBcqxOSHTnD4xngpOog55Q==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiU2IxZ056SUs3SVZiUTlQ
|
||||
NXZEYmg3VzJzeWJPRnZXcUtOMGQ1MElBU0dVCm5xSURZZ0sxTjVxQnhpVVNEU092
|
||||
bDBHbFAzZ3hzejc5NmM2U0FQTkRNSTQKLS0tIC9OOURPRlcvQXIxMVZXR1JjUWRX
|
||||
bE5ja3FoWXFIZ2ZLTklqWHhMRHBTRWcK2Z4sEcdyvx1rdBIGrbNw7bYvEPXdobvF
|
||||
h0aNihn817DvtVr8eeucj22iYCS40etElQNcO5i68t5wab4/d3rUbQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrMGd3ZVNlNXdmOUZMUzdQ
|
||||
TW1acEs0NFA2Q01rM2dkc1h0NHkzQmhiWFZZCjMyK202VWdlaGhsZW04MnVwUVdO
|
||||
alA0Q2FETThsYkhSS0hKdHBaS3VaY28KLS0tIG1Gdk8yalREOUtIZTUyY2p1UHlJ
|
||||
eG5iQnJsaTJBY3Y1dkw1c0VEaDQwdDQKfV04fLy32Lp2ZQ2VnvQ0h/Vsf+qdaJiv
|
||||
DnLXGZ9hE5yzpKWkQIRgqYGBkF8PkH0YC4OIaVkA53wrtjqS4ZHR9Q==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrN0lNNGtYbE8rRTB0U2hL
|
||||
eUNOZUNFRGk1RzE3R1VjV0Q3ckszcDNHWGxJCkhFN25JbDVSaWJIZ09uNzczbkM1
|
||||
ZWZ1Y2wrTHZ6VzlnVWRoT2JOdDZHM0UKLS0tIHQ2ZE9BaFRCeDNqbldRQkpLclI4
|
||||
dXZqOXNaR2Q2QWJtRlZUa0hLaTh2YlkKeCh3+yIt0sjh2Ig8m/YQifhMDHnTpuZE
|
||||
zHBE74SUyWFmqLgtd1b8Rx5gn9vBcS/3SMv3yKWispg4DZ3bCOeNFA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWFNRY2FiK3VkSm1RdHBn
|
||||
cWl1ZStLcTRFZWY5VVI5N0FhODZvR1ltM0FvCnBHZUtTUm9QeHNlbVBoZEx1V3Fa
|
||||
Nk9iMmJKVnhocEpERi9leE1ySUtNMFkKLS0tIDRRYkxnbU90S2RyMHdJNzRJNXBi
|
||||
QjRmZFhVakVic2tYODZHcWtJRmNQTDQK7G8eSJInt11P0DiL9uzNQ/ZHHLVNIYPe
|
||||
bvlhuGkEuQ/+j5sVSKOfSI2Y7CvM7TpE3APyKBcLG3ajYg6F/Ev3SA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5c2luYmpYcVVxRHROQlpk
|
||||
cTM4cm9uNEtVS21pK2pDRVRySmxYS3NwdGg0CnQvS3g4Q2tSdlpVeWZxbzArcTM1
|
||||
YWRMcDAxcWgySFRqRC9XSElsTVorVEEKLS0tIFd6YjdrY2ZXQVBaZ01wMTIvV2sv
|
||||
ZEk0TG0ybENlTlhpZnovMXh0OGd5V1kKEbjrmKP6Su16KRDQ4PMqi0tHRS6+PfuT
|
||||
0v5DEyi8EDtIYSMzzdF7Jk2C0h5XuRux45Z6E6JhKW5c788dGQHz/A==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1S3dOcFdwV2NoMW1oMnY1
|
||||
VGdxeXJVR1lsbzNVZHlGb0NGOVo0SndiakVjCmg0QlNnZDV5RlFja2hCbVRXV1VF
|
||||
S1ZtbC9KU0U1ZW9zeVoyR3hxNW1XTFUKLS0tIFUzWTJhTzM4QnpWV3h1OFU0N3BK
|
||||
RnF1N2k0S0lIVitoNDJLUmZqdHRzZVkKUfNg24p8zxb3749v/A1BOKCNw75AUKpf
|
||||
RUmFCw5DDWF2aNM0mZqcjjVmJ/FRKV2HXwwUGsHPKSOTnKfOUlPNKA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1eW5kd1FrSGJaSDkwTzdY
|
||||
SnJhR2hqRkNTTGtSTWsvYmZpSlVWV0xvYkRZCmtqR1FQOTRQR3dGYU8zNFpMdkUr
|
||||
SWN2KzhXaE1rYWZYNHJsOVVvRDY3R00KLS0tIG1aUldSWHlHeHkrcjF2TENvazR3
|
||||
YXJLUXV3VVNSWEdseFlZQUJPQUF6OFkKOPKH52muUha+MV2leWDiqpuTaIj0ZKzV
|
||||
keEdyqkIck6KKfIjphNKTW5ujiuKjGQbRliquBknfCRGMnRlMNfmkA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzazl5MUpUNEVNMTdLWXBp
|
||||
c1hkaVhjcTZXTUNsWS96QVVWb0RVQlZ4VlNvCnE3TFRySU5jTFk2WjBONUQyQUhl
|
||||
U1lNTDFTRmZhMUFyZmpVY2xpaUVxRW8KLS0tIE9MQ1M2U2ZXQmRCVll2UGRWL1RG
|
||||
K2tnU0NOKzU5dkpiekF0Vk1VM0ZZZDgKK13aFypGAqrKWPOr3UwtXI1EoXf1+UzS
|
||||
rBqcwnX6WPxSKUwWoins4Aojek4QhbhY4R5ei6rRS0KEQeryGxy8bg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmTDExNFpCZHZmWnpwYis4
|
||||
bFJGZXV5UksyS01MT0d3cXE2Wno2Zzh6OFFnCm9hT3MyZ0hTbnkzdXk3NmN1TUVF
|
||||
OGtCZnlJRmZJaWJWaE1pMGcrTzJzZm8KLS0tIFE1L3ZxeUl6WVA2UXN2b0lIcTBl
|
||||
YmtXZnZaMml6ejY1Y3lRUFFtTEVMS0UKZbcuunA9GRqctCfC0Fh2foKt7n3l8KkE
|
||||
FNjjxUzL7OQjNG9M1aAtRMMO5KXL7YKXRUJWOPcpiLeLUNvDLeLXvA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZUpVT1p4TGZyNC9qMm5G
|
||||
UzNHV08rWURjY2lqS3FTWFdoc0FYYTFjb0NBCk1mZ2JzaXk5RmExNE9xWGZ5K0pv
|
||||
UEtqMkltV0dIWll5eVBUVVRNOUNDWUUKLS0tIGpKNVJudUM1UGNvaGl1UDBOeFA1
|
||||
RjUyRlZ6a0Y4SXNsL21zSURVRk9KTFEKU1L6BQ6ZlYQQtqx3uF/uM5CQ1ercmvRT
|
||||
TL3r2/Y07gE7CjRn3pR9z0co8KndGzxV6YR+ubyWptwBS8KQh5stkw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQYlp5U0FvVkdIKzBGZVRl
|
||||
UEtOOG5kVHVLQ0ptVTVpdG9Lb1Y3bzlOODNzCmNYN0Y5ZGNzTGFGYmxJcWFxaExn
|
||||
NkkrT2RLSlZTdmFiNks3RVZXQlBEYnMKLS0tIDdCVlBxUG8vYU05d3c2MXhjQjhM
|
||||
TTVJUGxlWUxWalBRYzFKQUQzd1R6dHcKPz1ciNICph7qnqSH5WYmOjUCNHsIAmTX
|
||||
lRnbFCZdD/ZIFRK8OYAyCGfQ42UYSTf16kH2jDdTFUy7Fk2+O7lNIA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsaWxxWS9xQXViY3VnUEx2
|
||||
V09FbTI0WGtNbW0yclhOSGZDbG5NUTNaTkFFCitkcjJ3OE9BSnN4bjFWcE9nYVBk
|
||||
ZzMyVHlJQ2wwdU5JOXdCQm9oNkhNd2MKLS0tIE9tRzFYS05vSkUwWFRkaTdtc0k0
|
||||
blVoMWV0QklBVkluT0Z4NHYyS1F0blUKO+Uc0of/V77ZUZOsxTzeH8/LmmAOQt+J
|
||||
x/COHxnLCnZ4eWI6q1a0Qn5Br15OJYTxUI2QTV4goTnXBNUDo9wdpQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiemVqWW9LdlV0RjRuRHpF
|
||||
U2MwN3RDeHU5SWhuQ2l4YStRVVE2eU9XSUc4ClNGMkNXQVdkWFkwcys0aFVhZnZv
|
||||
Y3J6RmxwakdaZVo5VWZhb0lVTUdTYzQKLS0tIEQ2NW0yb2lGQ0NLZm1PSWNFVkQ1
|
||||
a0Y1NlYwZ0dnbHhsQVB5eS9kZVdqbmsKgFLUpZMqGywO9Ext5WRwco2bpenVNrzL
|
||||
XrGCqhn/7TW9DrJlPnFe8r4fl4DFP6GWByn7axDl58Qy1OzeBZwnNg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsLzFnZkdVdGQ2dHRwcnRt
|
||||
Ui8rc1NoQzNKSHVzTk0vYU1vMlRxcjBKZ2hNCmhLYXVGSis4RU9HNGVxZkpvUkd5
|
||||
L0xvalhDYTQ4N21OcHRheTlkaUxvTkEKLS0tIFVkRGxtLzhQT0paV2U3ZnNScVdn
|
||||
alZnaVppeGI3OUVscGpONkk3YTRXd3MK61na8x5qX7+dyMHasDz2dj7yeaUlX8me
|
||||
N4/SIk1JDBhv9G7mdKLbKhSF1UJrSY7TJqJqx8/dqEc0uG3vptA1ew==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLRzlzWEcwTUx6M1JNYWgr
|
||||
ZWxnVXhFWkhQVjBrSndGTU9oY2MrOTQxTGhFCjhUdERPc3hzQlkzMjBEa3AvaWVX
|
||||
T0RJWXFFZHdmUVBObGhpQktCQ2NPbWcKLS0tIDN2YkE4bEVtbjJlcndWVmlrQ3ZT
|
||||
dlVvRmt4RklOdUJyZ25ia0ZoYjU4ZHcK5fL1yvRcty6+EMzfSkwwJM3qvrr6h46d
|
||||
d7Zs+EElXrsGDQvL0fevB/zDcICAtudYnVv4nP3Jf8GQnL19zWlwbg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZEtvOUhMU1FRWWpJQjF4
|
||||
NWphNXp5M0dLZXhkZndhT1Y3L09maytHazJVCm1MeEtMWXg1Zjg3bFVnZEorci9J
|
||||
bkNZQU9Ta1dDTFFHaGFWQVBpK3pYRDQKLS0tIEhPVGliRDR3ZTF2aEl3ZnJEYWtR
|
||||
anh0SEpnVW8xdXNkZEZQSjcxU1BHMFEKVRJUA71fi1QawB2TnuTWMYhzQR18u4M2
|
||||
s1V4j4TwYyyKZFoNvt8kOUayjC499c5OBUufYs6G2ciC6gK2A9E0EQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhZElRNXNLK0tVUjQ0ektQ
|
||||
Tzc5NDJReldiYVFFZnRLS3l5dWw3UVQ0WWhJCm1DYVloZGpYeVJtNUNRcEJvVUZE
|
||||
Q0xMcmR0WWdVVTBaWko0dGI5V1hqaUkKLS0tIFBBeXdNY1pFai9QT2t3NWFLV2sw
|
||||
cWpRU2RISSs1UXh3Wkl1dWI3VEk1bXMKP6v3Lm0XZkp3Gn3iJkB0K0Y0E5BeilqR
|
||||
plmPh1rKbT/wkefFwpa8e2Wwmhx80YK+bjn633MH7mLQDf6AiK83Xw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTV2IzSjBEMjUwZjB0dCtj
|
||||
aTdZVlEvYkVRbTNjUC9ZRVdNZVhPQTJEd1ZZClc3NDJiR1BVYkZkdVVxMVZGc0VN
|
||||
dWdSSXBFR2xxR0xrV2thRmUwSS96S0UKLS0tIHRRTXVlUi9UYnFRRlhsU21HZVY4
|
||||
SDFYd0NwVEtVZXNsWUI1a1ZZU2xNRGMKuQUhOq2FRD+PGn5OkdODZItbxCzRKjne
|
||||
E60UOYtHjanuGjJ1svuR9cYsLZz7lLOwItklecYaQYpMRZEwzzBGCQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMVk8yT1FtMVoxYjF2YVBs
|
||||
UTFVbmhzblBCRno2V3YvaERRN0xWcHd2NVE4CmVyK3p1SWtodk5iWjdkQjJRc0dM
|
||||
Vm8zZHczYlFFSG91QThmUGNaWHBoN0UKLS0tIDZMMXoxbUM0TUVaNFUzRExLc2VM
|
||||
OVFrNGZNL256cjg0Q3ZBQW9xUFcxSUEKv4HqaTcTzNYRJCmNEeUQLFfEoFXyOBEC
|
||||
yZrg8D+ROBn98484bxNeoyOjQdcVn7OYiCv1y6utnyeA9sdMUB0ong==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrSjJoUVUyd1JqRm1ZQzZx
|
||||
SFJ2cWNUY0E4b3FndVA5Y0hIOEZnZUlVWlhBCm1vY1luOXZBelRUTmF5Y1NMeDBn
|
||||
cE1BTDErc041UjJCWTBQbnk0Wk80dkEKLS0tIDVSZzd1UktvZGdyanFUMkVORUtl
|
||||
eVB5TnJkMlp6dUpXSTlxRlplZ2NxUlEK0AYOxIbswjM0SUASDfmZ7PqcEU844fgI
|
||||
ycFWVSEPodwUZ6UFoYXhHlJzHFcgpLvwUd1PMktLHe1qrZ7GOQJIMA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1Rkl0UXduS3k3WVU1RzdG
|
||||
N2IybE1TZ1kwVmM4Y2lvUS9DVG0zSkdpZGhRCnFkZnJVS2FBZm9Xa0V4SjRwaGdR
|
||||
TWpaaTh4VEk4cUFrb2pURERzbWtSR0UKLS0tIEsybHdvUWg3UlUvL0lhdHIrUE0y
|
||||
R0pMdnI5QlpIcWVWdGRYQ3JCaElLWTgK5cTfvehj4BPU22tYiR5piZdouUdeh5Gr
|
||||
x/xSbFWqYVauDB6rpzZvhGMI4KCJh2RmDdt7FrlCb/YCB3QoSgvl8w==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPakhQcE92ZU0zYmk2QS9D
|
||||
TG0xc1JPZXZCZ0tZOXA3MGNLVnBlZGVtRFRzCmIvMHhQKzFVWCtpMTQrQUhGVGJp
|
||||
RU1jbldYckw3TXI2SlNpZVBIZHRsWWcKLS0tIGtJTUtJejFxem5jajFQUDFTQWU1
|
||||
VnlxYmVlNG04ay9ETi9FRmVYQXVoRkUK9oFNolI7jRjo9RUs1g4ghrx7aYV4U/ce
|
||||
ZTc2tFh57+7aKgrDi+2W3jwhfkjvBsThk//p5mLlqEEgw2lwlnhvPA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwL0NNbGlGS3lTZjEydGJm
|
||||
MWZRK0Z5YktuNVNudk84MndLUUdXR0RlRFF3ClB4MWRUTFBWYUdwbityeUU2SlR4
|
||||
azZMb012YXQzWHUxZnZzeVp3MmdzcU0KLS0tIFJ2SUNyMTBEY1NWejdVQ3hyUytQ
|
||||
VnlQWHRDYWZFaU1ON2RQTXBwNFNGYUEKwnRUWCdsiStia1jLHJ6UNP/kgEt4Aw8U
|
||||
pA8PKKu6TZVPI8pw7svtI/uvJuW+8ARuNcyb1FdYmX+sOHnOEqHyjg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoVnh6dFAwTkY4cHJpaEs4
|
||||
QnZPeXZHK0tXYWZPNytmYXVsdGRWQVI3RlhRClFVb2I5OVZzZFNrRXFaa0JTUkRJ
|
||||
OC9GQ1V5K0JhWlhkUjU1WStCa1lPV1kKLS0tIEhzdnBBZkRnK0NtV1FuTkVsNlgv
|
||||
QzVEcEVkQm5NL0Z5dUU1U0ZFaTJITnMKaWE9vlrOpQstr6FGP5ObdilsCYk4kYAj
|
||||
/phboR+Ym7QDTyUF9LZXJCU54YJp6vEWkRnlJFqC75UW/v/lgBhBMQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxM0FyaFlTR2FzMkhGTzZx
|
||||
d1d3dWNGOG1ESllETEVxRGJlYWkrTUNIdkYwCmI4andLMUUvNVU3R0N2WCsvWWhV
|
||||
UmlkcHdMSFA3bFp0ZG5OckE3YmJrejAKLS0tIDhEVEJYOFc5VWVHcGcvMm5yanBl
|
||||
eGxaNEFxdEN4NWh3cnlGeElBUTJmUzAKvCSJF+LGf1JqEdkzT/Wk6hEzferseyWO
|
||||
vAYR8Z+u6GDwQi7JdFz9iZKD21LAs0IMzd7cZqXKIXVedyXuaIhPtg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkUHAzWk1KblJxVVZZV3FM
|
||||
WndzRGRtbkw2azRVWnBuZmhKWElqRFk0RENzCk5DMHlMVWpwbXEwVUhkaFZUbkp2
|
||||
QXNlZFV4SjBEdmR6UEw0N1JOUnhNKzAKLS0tIHo5RkNDUk1ESWRHQmV6bzkvSTlP
|
||||
dk1GQ0Y3V0dTRlByb2xUOERVOTVwbVEKY4sAHyAhvGSYJzPuufWUIQD2xZcSt/nX
|
||||
t2ZFXu891/QdEzyUXCIzdwAV+Y/LjvroIlCp5Hkbrk0s7N+ghqsB1A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGZ2M0cG5nMU5nYVliRzlx
|
||||
TVpDcHpwclhXNkxuc2VKanBvcnRxRW04UXh3CmNJeEl3K0FrWWVHQWRwK3Q4Tzk2
|
||||
MEZQbFV1QURLZEhCdEhaMjBRUzllaVUKLS0tIFhSUTJ4SXJwRHhVbkZFc2Rwb3Fh
|
||||
QzAxTnpWSHZaWlF3NUlFMlBHUkRUa2sK3Kim0mp2adSvruI6Necd3UpZHow0EFmT
|
||||
JFOIpIK+3Pc06gsiqrDtDAx2Qtc10Ja4ILTutPlp4va6zzmzrPABiw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBYVkwYXpYSjdmM1FpbDl3
|
||||
QWpycXoyL1AyOEpZUmtpbjl3MFAwTkJoOWpVCmVDd0FBUWxaQmZCU2VmNkZGMk9o
|
||||
TUdLNGtac2N4REg2eVF1eVh0WnNaTE0KLS0tIDJvcFErSjRiWmhPMmpadjROOHdt
|
||||
NXp6Y1JpdHFlSlRoa3JTaEt3emdnalUKjoFfZAiKMPF3noX+K0+vc3+p/XUHnhic
|
||||
k888KdUwcZYl2/dAIc8UDSggbMnncJAJgoezoCHLkj97GNNAD7E+gQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwTUZ1RzJKcVF5bnJGamlU
|
||||
bWJvcU1sTTJuME94YjhMZWRWRWR0aGJ3UUNvCjh6U1BkaEg0TDlCeTNyUlNTRWV6
|
||||
Nnd0NjE3NmplSWFjVWpSRy9qWjBUbnMKLS0tIGdMZUFzbGdMejZoMlFBVUJXSE13
|
||||
TW4rUHFjZk5oVkw0UGtpaG5mOHRRbHcKFX4ZyNQ0TUeqwXEaz1qmAXQRIOctDkvz
|
||||
TIwxJseAjS3qVxCslnxzgaqmCh8DzCfjrRxLUS/HVTnwOaE1YAh0vg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqYnQyYzF5cktZNG5PcThy
|
||||
SEszVEwrUkQ1VVRsM3pSTlRQaHVLN0VuSHhrCmN1Z3pwNlFsbDN2UGI0KzYyallM
|
||||
SHJ5eklQeEIxSlhiYW5PUlpJcG5KNjQKLS0tIEk0QkpMdlBlRjVYMmJaMzJUbDNm
|
||||
K25pZldwd3JoZi9vdURoa3Myb2RQNG8K6N6bO2YKooPfpKihgsYqilfz/yAYCLZD
|
||||
XJ/THgT4URX2VNvSspvBtN8luOiJUVcchp5WtL2m9jARL5txEcDorA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTWStVRDhlblZDMDJ3ejIr
|
||||
MkwyeHRwcnpWQVBmOXY0a3NCU1JnTjkvVGtVClVvVUpLTVhUNGNNLzY4U0o2SDRD
|
||||
bGZteUU1Uzlzc0NjTW56OVNCcnVpUUEKLS0tIC8wN1V2Nkc0cVU3OHNYa1NBVlhq
|
||||
YTV5QUpsQWppNHRpT1dMb2JDQnpoR3MKCoBngnNg5qpktg5j4CcuTUKiooOVI8IN
|
||||
04plE+1lra8q5ZK8q4r1/K8hBM23poyfq+XushO1GZ17dPtWs9wITg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvY0VpeW1nazVvRWJFaGNU
|
||||
RGVzVjRmWWV4dnJzRHdsaW9ENVZYck5lWGtVClNtd3ppelowRjZpRFFSMC9EK09n
|
||||
b2hqNnkzejdrTnhYNGNKblpteDNLRWcKLS0tIE02bWVjazRWNEVKbURITGlQODlR
|
||||
cTJJVnBVdGIrdzBoSXExelNrVk1XcEUKc77o2EX7PCm/HjUo5GsUiQdm488WB2mg
|
||||
wHd/qDbQhF1W75RrVTuIKgtEtrRjZqpmr8toe+aHJizPofcrToUfzw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUU2ZSazhZNWFZVXk0UXdx
|
||||
Mjh3R1NLMThlaHUvWVFMZEQ4Z0NYOW83dWgwCkVnd2dhdzQrZkxaL1NRbDJodERQ
|
||||
SkM5S0kxcVBtY3laNWp5OXpXZHhqZ0UKLS0tIHVOT2VUZU5DZjJjVW1kcU5qS3FD
|
||||
YXNhRkpaREMzRDB5aWgrdVNvUk1qcWsKQnPqE+5xSBGeDMmLfKB0tEBt2eylBOxT
|
||||
oi26mC+EoO5T2pCIpbD+DODAAlWKLattqCYHneUO9wRk9DnhuEXRBg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSMmZnd1pXRWh5NDVSN2xq
|
||||
MVhTN3N1OFpkcUh0a0tjYzJnWUlINGNVd3k0CmZkTnBac0l1dllxazdLY2l0Rzli
|
||||
bE9sNTBVSkJNaWF1T3c0WktoOHl0NU0KLS0tIHEyeWZTUjdQeUN6U2t0d3JwNTBX
|
||||
ZE1Za0tXb0gwc1FSakVYdU9OTHkyd28KkwmlzSYP8XofB0VGag+S18+S2TyQjLrM
|
||||
qaXtbBtLzJGNDhe9FhAKTPFcjTLWbohlG69vxcImyCyCns+QQ+gvug==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZUlFlenlOVDk1NmxQVGUx
|
||||
UHhVK3ZtMHdzaXdtWmE1TXhOekpsTmUwSlJ3CmdncjMxOUw0alBQZkhZZjA0YzJZ
|
||||
bFNVOHUySXRuYW8wVjdHMDN4ZThuQmsKLS0tIGdvUUdHcXlpWlZkL0xBcVpMN1E1
|
||||
NGZCQURYQVY5ZzdDODNNY0o3SG04eDAKiYVuKsJLaSC7/bQRB51tX282/yqpAjWT
|
||||
XNnqPQBQ7H3xuK0tDVdCAUMruOSlqAe9YRZwmiou1irpg9vZYwgTPA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2Q2RMWmZVOEwrOC9VcUxp
|
||||
VVE4R2NqYlZWZlhKSTBHRks4bDNoaUliMWlJCm5FbWdZS05GY0VLc0sxY2x1U21V
|
||||
eExwK29GVVBqYlRPZ0l5RWVXRFhRNlEKLS0tIDl6dVZJQndwVStFVEJnRHRyMW1W
|
||||
NnFqc1F0SGJqT0xmREpaN21EdnlJK3MKRPE5rfFpVnH5wAOkuB5pNMlMd3omcpku
|
||||
do2hFZwyI7t80jxF4+g3J7EolOx8AGjpc9Ba7Gj6IMDjye728q5N+g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnUUlPS3ZPS1lSVmxwV0lq
|
||||
WWE2QXltb1BzU3p3cFRuMzVieGJta05nWno4Cnk1RG1CUXZjM3dNQzlJK1ZGaHpZ
|
||||
b3drMTB2L3hidkpoTXdMQ05vOUtTb28KLS0tIHhyUllpQnlzL093MCs4YWx4VlVn
|
||||
S3Q3QnpVNEx6ek54RGx5bmlqUk5hbVEKHVUvfk/xKtlOk2o73LodweIivSB4Oii/
|
||||
6dwnE8wa+Qx/OU0vDJVelKOUE6JiLwckX/0u2YIUOybkx49n+tYs/Q==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhSG94VE96TVlTNkEyclFE
|
||||
SmdCSkJpeWlVWDFIMDhwUEkxL1RUTDJ5UG5FCjhKRDB6VGtwTVozdUVzbUxGL3BW
|
||||
SnR3cmpSN2RxNnl4QmNvT2lkYmtoVFkKLS0tIHd2V2h2Wk5xOXlISzhjVzBsVkhz
|
||||
VVpRenVnSVpHUWJqV0JHNXNWWXJOdW8Kv7PJSTDbwFOAcl7pynALaJiTXU/87bSF
|
||||
F3HQllYOwOoibGzBCe18H2N+VxyNxoQL9OWe0TvOIR6bgHFIIF0/Dg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3MHlrTTdxYit0MTkxa293
|
||||
eFFIcmRLMUpkZjd3eWdsOXYySEljNDB2NUdNCnAyanVkS0dmenZSa2dpSWZTSkZw
|
||||
Tm5LMkRoWEk3L2ExVktnZDZMM09hZ0EKLS0tIGVGcHFuMjh4OTUxdHNJQSsrR1pq
|
||||
WU1jVG5UN2hMbW9iQ3JmbHlwc2VveFUK4JC32glesv/9nMWBR0lFeX7MPgIt9sIo
|
||||
9HsAkOMTxPTI0NZW8q79+G5hq8qG+QvQhwO6wQtwz/KEHPXntKDX+Q==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsMEZPUExRVHFFb3pSVHNO
|
||||
cGJFN1ZzTDFVNEdneVpMZ253ekFJNjVtYkNnCnRGQjU2Q3dsRGRFV25LQ3pCbTJE
|
||||
OTROaFBiT01xb200S1pUK0NYaTQ3R2sKLS0tIEdiQlZTbi9Vcm0zc2t6bHplZktF
|
||||
ekRySENXcjBuR2psdHZSSUJrR0xUdjgKvBsmnC+cbq5TUDFjXCyImIoPKvh8wsjE
|
||||
7Shk7Act8Jayrhx0lXBDRmfpHRrB4L16rDSmqO0DTE48VhT3TiFyug==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRbEZGNmtlUjJVZ2dlVGZC
|
||||
MGVEQXV1SFRpMjFqWks1Y2F6c29TY25oL1MwCjVFRk90Q2dJRXhUMmIrRVdHVFRD
|
||||
TWgvQS91VWdFWERJWEYrL3VnRXFraUkKLS0tIExUU01VRWlENlpBSGt3V1Izc1dS
|
||||
RFBhSTVKTGV2KzFidDQ3b3lmUHZ3d0UKHWvWO/Om6ahwBkONreGeENqdFG9UI6JB
|
||||
fRA3gRHy98EorEiia1him6lGY+ewYkTtVNo6iEt0abncW5x5xMQywQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxNDlya3RmYzNhU2p6RkVw
|
||||
cHJVWWY0Yi93cW1uZi9FWkVONmtpRWh5SzFzCmtWYTRIY3BkTWU5R2Jsa0ZJK3kz
|
||||
SnZvZ3YwaGtoMVZ3V2laTlBBK3UyTmMKLS0tIFdpZWtqeGlacjlLbmFySHlSUUlj
|
||||
RmRqQWVHK0FUT3VDbFhLbXQ5WDhLeEEKcDkgV34lUFJRIHRoLB8F2IOvGAM93sM+
|
||||
AkmaM4+WRcGeYWQKMG2x6cYCUKFaT1lDXuWZ9kI8Fd7b9gTSnQMs6w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2dFFpeDU2Y2dtQU12cytZ
|
||||
WUJjKzZ5S2RiZmtvYVJScStYVHI5d2FvcmpzCm9COGZmRllyaElqb2NsczhKRjhO
|
||||
ZGhwZCtoaFcrWmNZbGVHMUNLMFQ3Ym8KLS0tIHJQR1JOblZlY0cydnhNUnBCT1Fh
|
||||
aHJiZVpMU3Y3NXd0ajc0MGMxSGp4MmcKky1oaS8yFydszcvZE2pAL90ZUKoN9MnJ
|
||||
1TdqatDSMtmo3PWkdFeHbWxPjTizQGIcKEcsv6GmO/5KCAJDCHDNMg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjeFM4ajhHeVd4bzlqZmJ1
|
||||
MUxYMUpZVjdjR0NGTzVteVB6WFBQeFI0ZkZrCk55TEt0Zjdwbk51RnhYclNzam1H
|
||||
cExKZXQxWFVLa1pDNFpkcGZzcnl6a0kKLS0tIFJ4ZEdJc3JVaEc5RU1aZk1uYm1l
|
||||
d3RHS3hHSkRKRXFnN21FQmh0TlNtNmcKdc2G/1dhTJen6iT9kUWZM5OzCmDVprgx
|
||||
WN1Bl3JzYhLsNKn794887bVAICVqbXqkdpEZztNIS5n/Rw6geKsNvQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrd3QwWE4yaVlIakdiYkNX
|
||||
bFVmR01yNjdWa0d6V29tcWh4M0hWRmQwR3pBCkFRV2gzSVdPQmQ5dXZYc0F1dW1r
|
||||
OFZNTkRwSzRFR0tlUXE2UzFxeU1Ld0kKLS0tIEZ6RlZkMFFiZytPZ05DSEI1Ni9X
|
||||
WEtpUlMycVhkMERsWDRGQ2lrNHdQcDAKRqVAOzQY4ZM1uH7LyZSqX1T6+1PwNhBY
|
||||
5M0RxSuI43E8YKC9axcppvKFJMQI1GAGvgfQxg2J0CEpnmPSVsO5DA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYa0gxNnBwNjNNTTgxKzgv
|
||||
YkQwTjZxb25tQ053Ny9tSW4wNGVYYnlIaFZNCkVvOFNPOFkwQzhYZGxjb0FOZzJ5
|
||||
ZjNXam1ZTWZrS1M4cGhhcHZaT1NjblkKLS0tIEdvL1dDTHpWcWF0S3ZqNkxrQW52
|
||||
VlYwa29sZVloOS9qajJWQWFzY2FKRmsKy074SLdttogXsWycaFX8xso4ek7Cbjph
|
||||
MMEhZd/svmnSiYM81nmeaze7qXEUcsZXuSmZCYATTBEGtx/Srll8aA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvb0xNMloyRW5tTUUvZGph
|
||||
YWxIc0ZhSWI5RHNmaEIvdjZJNHFDM2xBMkNzCldlM2RUeFZCNE9tMVp0MFlWM2sr
|
||||
Sm44MjkrWlNkY0U0eUlyNlE2ODl5SFkKLS0tIEdISElQa2VBZlduajhsV0ZaU3dZ
|
||||
ci9COWE4bldUMS9lSVB1eEk4TG1sVjQKYu9zby36DP+41dTV8sRdtDil3Az40pf0
|
||||
vIhqhCTxQymnJHEwEukiEzM/w3Z85R/W/MbOKyzEZmxzG0a5O5svhg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZlFtOHI0NnlMeWdjZEsr
|
||||
UnQ0UFJXY1pHTm4yLyszY0dBaldtUkIxdzJvCkNQbk9mN0wzK3BtN2pEa3JSWWFk
|
||||
RXNoemFFbDZ1SlUxOEZMUm9mY1RaZW8KLS0tIEk3bCtyaUVGM3MwNUwxS3QyTjA4
|
||||
YVpTTFlOdzljbmI2cnZUNG1nd1NrVFkKxARm3bpBL685Wky4Y06eA18VvUZRkAl3
|
||||
BBYPJMn3lfDgiQPvfXINfhJI6O+bWyjt0WnfodCGFM6EJLARXiTaqA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r
|
||||
lastmodified: "2026-07-29T11:33:40Z"
|
||||
mac: ENC[AES256_GCM,data:hXns2uTM8+bX9J1EWxKcA7v1oYayBMfyB57pMgayEQHV8PvdIWi7rYez+4OCuMBZzBNVAZWn2AfzYxC2VJJnKrajrvwGZ/neJ1qvd2sfY9e9TskZv+c6nqnRuXl0OGi+kP+6EVSmZPiSFUODZd+xtAku8dIMGO8LZeZoGYjCBfs=,iv:EFZZpw/QKiuEtShBhB2bP6qq69T6jlrJk81FpqSt5m8=,tag:VCqctIBaFK2DQqoj1sxv7g==,type:str]
|
||||
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
|
||||
version: 3.13.3
|
||||
version: 3.13.2
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data://9IEHIVCfHjyMNao5sCu2zNlZ/CaW+JyxVpGpD/vab2qvURunCUY7eMfSOyvOx/2WPXnWWlkoVJPCR1ec/yUg09EaSMfxrvqlu4UJI3Sxvu9xNuDszMwMMD/sCulJDiMWFNLp8qaYt7UGzexp4+GlGiqWDxk3ZEu/iLmSApzBrpciTF0lfehT4qblDovo9QXG2KDWFhCt2SwEKmHJ61Yl3pVAQnPLyTWaNhwWD/mYL76mIiDVKq7DJlvi9MBxzi3aYh/ttuHgRCvnCL0C9oUvOyT2cljwra0LXuOrum7FhPIXXboA7WTEbTHJm1LB5yLfxDrnWCrGxQzFQAwNixVHIcjuvjjvA/LifTvbj5FYM8x7an+DwXPfPhruFrDew1paAWsEGNNYXSmAlju2QLI/OwLHFFLuDyLnBKQ6RLtZbCEAOUKf2h4iZj7nH86eb/aGU=,iv:rj76+MJBCpiYyx2Ogut5UxJj3Gn+bygvu2mtuY5hFLA=,tag:yQmTvWVu8ZLug/7fo6RnwA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqcURBS2tIMGl1aWxLMHFQ\nS3ZGVTE0cnYzTmpnQ0daL1F3QnlpemRCMmtjClJiZzIrc2syN29sRXVoaXlPRUNF\nakw4RndmbEduTFg5ZVRKUTVwRWpGRjgKLS0tIHZ5ZFlyODlGWTJoNGpXR3RhY0ZH\nOGpPdDZQVmt6UWZXbHkxQTBoeW1JencKbsfH1V1lUj8mmHyLNj36VaRDgaBojcDU\ndoQWmSEXxjticJqdadbVKb3UABpvzAZxASCy81sa3wH0gT+7zLaNyQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrbFdaMmRjcHgyV3Y0eWZL\neG1uMEtpQVUxQmNTU0Z3aC95Zi9uQlgvMEhRCmJiVlRTR2NocGlnbVU1UmlLVVA2\neERiMXpiQlVPNVhRU09XVGxrY0Ixa2sKLS0tIFRuOHdoelJxOXNRRHBJNnlhSE9j\nUUZHZmQzOVFwRmhFV3pvdnpRMTMraGcKRHBuSUpbHaEzH2tuSBE5MsLJDCuH3vUx\nO0jnDldCWkCw7Wvr/tQAkaDI8axZcYVDUkCEk+xqAdxDozLCPhEO6g==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZMW5taXh2QkJkT0JjaVpq\nQlBjVFpFYUhXTHhOT3prbFcvUFB0YnRGUzMwCjhHT01lY3dPWDE2dDZWZnJza1hN\nS1AvZWIvdjZoYmRjWlliK1hiOEdrT00KLS0tIDljWWY0NlQveS9VR2hOSUNyTUtH\nK2gvbEVibmZSdktPemdEQ2p5U0Y4d2MKKitoTi2vbxJ41IoWMlj4vO91Ahpj0hHP\nrKCPyx7ws/IUMmKGyvDLpZ3pYqHD9jl5pLfB05Hh4Emhv4lA1qtwwQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3K2dVV1BHbDk2Z3FhNmdV\nZEN2Um94K0pma1k4b1V5ZUJxQ1hWU1grZjJNCi9iRVVqOVpEMmtBUi80NThlYldY\nTUtZOVErT2VSWk43NndpVzBlL1lQaTgKLS0tIEpCM3Q4NjM3N1lpUE56N04rTXN0\nZkZ6TkV1TU9OV3dpc25RNWRpVnprM00KItUzKBdShakOfX8Sr+k906nsvYPl8QLb\nge//1GA+ukGsaS9rcChOY89vFdm61JDmj1jXSJ0CN4wLMW9/eblZRw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T01:44:28Z",
|
||||
"mac": "ENC[AES256_GCM,data:efFXIqbauOURR7lrVpK7kqRIPgYjgWfenBYoX7mUrQ/thFc+ApcAx58Fi1zg4biwIs7NarJAgDqAxZi+yKb2Sll8H/wlsEjWDU4iQlLJdIQw7wey9eDW8pgBLd+6E7FXrgn1Vh49dS5GXlC6clAYk1lCiB4NvfzPDy5Ktpl+Chs=,iv:+zCqj5I1MLJfRRiIrqgocYB49PZnlV45PUTH4gYlfrQ=,tag:WxY1sF4kFOTEzbSFcfJFbQ==,type:str]",
|
||||
"version": "3.13.2"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:JwjmYg0qzoer+8/jv7KOfK/BxmpObHOn04GNsUFQd6hSFBWbQlRbODfluL54hnWQ4gsS9MHgwWh8nrLciBhye05kRQCqADB7Crm0CZYubNb65WzM/devh14jmerc3MYIp1M3VmDisML3x5IqhJkFMSqKM/VaNAC3f0otcVR8TTgZw/fJyvPlfqpPTjikGmo5xg++Yk7Cy8A1Dj70kYk10+EQjQ78jf4k/agLoaS+YvMmKXFk4EljtHg8Fe5H6Rn9nfMSSKGTZeAH3Riix2e+fIH+nWEZiPHC0UlPjJrB3SeBqqGRSUUJshVKHFIdKI1Tq2Ea+GDWrAyIRI0qO8e7WQ==,iv:mr39NxbZuYUDHDw0e+fJDshbt9R1hLgVqkUxUVX/+l0=,tag:u31xZRPfvH5j3Sw5U8/nqg==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtVW5ENXlkUW83elc3ZnhE\nOEZ4MWIvQWZ4aXhuUmZGYXZNQnhJemsvOWlVCjIwNmFjS3pJOWEwRnhBOGNRamlM\ncXZid1RNYVBXOFZpMlpnU3ZnVE1scnMKLS0tIGFhWFZFd2JPTFNrV1VHSDVKVHRK\nbmhoeFh6YlUySU1ZRVhHbjZnM1IxMFkKo7aHkz2pEeV64m+OEkBZ2V1e+PUzoChu\nUc/Mnh37dNXSSJtg2KnocHdyzDGi1yQbA72xKTxx6QjYJWrAC/jurg==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBReWRLb09reXUxS0YyMUVV\nQlR2bEdYY08rK0RoR2pIelpLVEtYL0pqNUVNCklRaDNoSjBWT20xbk1UdmNqWHNt\nR3JoSlVwODVMUVBzMERUNktHMVViZm8KLS0tIHc0SXRIS1IreDF1VCthb0FBR0Na\ncGFRWTROUHF3clFtTmY1azEwNUpPNzQKsgCND/BZcMTgBTAcnHunQcT6LG1jNrtO\n+W7Yx7bFtFBajWnRYiNpUZPibQJlv5SE9os47WDH1gs86xftgV+uyA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGbllNTlhxNVVkd01GSUNi\nR0lxQWZoRXI5NWxoTExRUnBSb2FPR1BONnpzCkxCN3pNOXVrTW5GR2J0WFhZUE9r\ncm1QTThINDVVVlZEalJiS1RXcHJRS28KLS0tIHV6aGh5QjB0M1VPWDVMT1k3cTBh\nNTNGY25NV0tWSUl3UHlJSExFQVdpVDQK3ARj8xhFRYU6oqYxNQ5+Ryza86ALNUwX\n7yK/8ATnquYC8/ZIYUbgTPxzsocmXX9lVT0+ktIALqjtG8PgNTlOow==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T16:04:51Z",
|
||||
"mac": "ENC[AES256_GCM,data:9X0dkAEGJiug16LDy/8//QOkHITTzt4zwTKiIU13tRgSRXRpt/7b27+3eDauscXDMamg5wbYo1YF0+VwLl21Ip4icnnwrZBHd6HWleP30HTgef8rmo21SstshnclZz6RH8SEGVDO/vjrMDChaZ3A2WD+nbcomw47DpK3cG3kBIk=,iv:G7CScSD5Z1Z9WMEMydyeGK/RD4W43xA0PlcpmTCxLOc=,tag:DZmby/xkxpinoJztZZRWwA==,type:str]",
|
||||
"version": "3.13.3"
|
||||
}
|
||||
}
|
||||
STUB: run cluster-init.sh to generate, then: sops -e --input-type binary /etc/corosync/authkey > secrets/ha-corosync-authkey
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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+IFgyNTUxOSBRdWFEUlRJMlZId2Yvc29F\nNnkwREkwQkllY25vZnRjV3Nob3BvNE1Zd21NCm56UVg5YUFGbEc2ak5MakxDbCs2\nTFVsRFhjVlRwMXdUbGpMKzQ1N2hBUUEKLS0tIEhmdm9hZEJqUEhRN0dsSk80bDY2\nY1JpajNOVFpEejhibjdKZlNHVjE1bGcKycyDfEBZ1WYc4EfAK2y5x/nKRqq7mnb4\nDlpR1Som4bSt7+B+OCZa48mC0Zv05HbP8PnoZdCe83swGNljvRONcQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0SGFVbXYycTE3amR3ZVhi\nVFJHVVhWNWVQQlJKZUJvZ2p4ditqdnYrWnd3ClZaaUVLUXFKaWlDdDQ4QW5SQUZY\nbG05c0RGNnZUenI1QTBlemVjT0szRncKLS0tIHBWTmNpd2FjWHc3MmR3cWt4SCtH\nWS8xZnVON1lGZVc4YzZFRmRBamdSVlUKdlQTlwiExTUVsiY05MXFG2/IQt1bZwKU\nlFDnuy6YCQzawBLQZuSUAp7WSGedupexlZKQKNUzPf+dis26XEYZoA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6"
|
||||
}
|
||||
],
|
||||
"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]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.13.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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+IFgyNTUxOSByOTkrVVd0TXlBRzdkTktO\nalU3WlVOb2ltc1k5RG5sRE9EN2M2WUlnQVRnClFGZFYvSHdpVlhtNXExSXZMZnU3\ncnl6aHRzU3k2V3R3dU9RVDA3VWp3VkkKLS0tIHcwREM1dDREK0h0UFp0bU5JTW9Q\ncWdyanNMVjY0SnZUWUl3Y2R2SXNyc1UKI1FPIE66to19oQK2TwM5B4snGOVMLFfx\nl6JMEilDVSgy2RR/tSiovmo4NwOsv0hmmF1t34547rbZb+dB5KrMEQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnbWp2UWphSnFibnA4N0ZV\nRHlNUnBNYUNzMzNZakFVc3JXT1d0R3Yyakg0CkowMFlOcEhPcHE5dXd4ZUd3cDAx\nbjQ2T1NqejNRMnFvbTdNRGFGdW1pM2sKLS0tIDhGb1JBQ0RFQ3NxbmpkdXg2WVF6\nS25uZW1hMVo4Q1JGdGNQWWRyWE84b0EK0FmFZB6sN7uZynhzYU932x461zSZIUWU\n8oyDEKNoUuPqpWK8RktgjWlKHtB0oLXC+SLAOJEhtnhWF+GSqloGfw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T11:53:32Z",
|
||||
"mac": "ENC[AES256_GCM,data:EhWEEIamG62xHI6SqO4tzdK6gcEXxUU/UGcF25m+X11kUVXzHd6yLcfiM2DvqcwGLmwmOOf3SpFLABlQUzTka4QRzTMFIAJi7x1rzQV4J0YIGITHk9rgvy0V50TI0loONBb2du+vDt8IlTBNvF9WMbNCki+fHPAjIemyoRBdubo=,iv:wfsx0H4fD3L7WeGzz57SzZtajBr/xTRDljSpz22DD7Y=,tag:naDyGH0WTKkqgd954Ya8jA==,type:str]",
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.13.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# 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
|
||||
@@ -1,3 +1,4 @@
|
||||
beszel-token: ENC[AES256_GCM,data:meuzUP/6wCssJDVTgbC0XwiLZPMGyDl55HEIiON9xOXCD9k6,iv:TDqWcp+8Mxd8wN09r5otQRQXq3XTeQphaTWxvvuLTAs=,tag:cRPZQGlwB/dTguBAheWPQg==,type:str]
|
||||
cache-priv-key: ENC[AES256_GCM,data:6vQKIf7eS0WNL2Eptoi4VWr18SRMZfN/H/aFUUtXdMYQY5LLyBp2EHRKqZcGFuh1nZhUdAxUztq/CVXx+QFxKW+ElHxCxUSp0QqI1fdSkBkKZb8hlit5SoX9JtLzZGg0HBNM3nJu,iv:0J+xmrPJhInHhFR/c41ACjuTfaIoMkQFSfbL2KkgFa8=,tag:f4s9Szs5oprVVRSyXaX48A==,type:str]
|
||||
sops:
|
||||
age:
|
||||
@@ -37,7 +38,7 @@ sops:
|
||||
IqA477+noQ/Rjrszb2hEvxID7keogcDUWMWQzQvdMc22+3mvAr9qIg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||
lastmodified: "2026-07-29T01:59:11Z"
|
||||
mac: ENC[AES256_GCM,data:/nbcfause6G6F8IvMoyPZtkWS1XRLAivhwTFu6y5P0Mm0eCcO6M7/rgioN9dngKzPXrCUl3Dx/EvhrrWKe2/Saq9WEOFgvS2V05pTRbQgYjuugVzW2paPq1fgmoDYNjHz2yFYWAovbIFtjVxMR9tmcASMTe6r/FocvMXbFCJh7Y=,iv:n3KoaPPtDhWK8mxJNJRk7WPVUUNR59nZSA8JyysWNDc=,tag:tYhkNXsj2ohVjHkvxife2w==,type:str]
|
||||
lastmodified: "2026-07-19T23:30:21Z"
|
||||
mac: ENC[AES256_GCM,data:kLGE2xawQT7mx+sfw68hmGk5nCEGiEjZrqTEl9B1dtQmTrMwmoVr/1RISi4LfJrwxy31mDgff4lcIL4wIJuM373uk3X8j4RNyYQNTfKEkORT6r8NHeepNs267O77pKGd7OmcM4MT/BqOnB8ELS7Wlf2ect7CAlvUUVyc8icxgZE=,iv:EYLDsHYHZ1XOQXafOTqHHWpk/OBNq/R6IJnOBYV33E4=,tag:thxrCPC5oGvDjhK7Dz87YA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
version: 3.13.1
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:apiijrtrqd77CTizITg0R35BfCi8PBnufpxIyC+hLYqwoBzP//3z/yjFyHPLG98m/c/qywoi3Kn+zsaTT7MjP++9OMhhX94YKlSHV1/cHB76OkwsNc+ClqWxl6vpaFX29Qvh3gFX9c/NR3xvYQutYwrIrQ9NR+t/M52IMC8hvtR1LQy0ak3VIuXJlSnG2r4kF2Ym1iP7phjuq39Gd245Axzw8OB7yGvOjNxSdTPxW/qL0fMlzNcMrjr9hw15WlqnZfWPOsB1+gZjHXpGfPD5BCbAAMoTRJd75vhKKXP/ERhIffewuuH2x/QHfSFvXVB3QyhBQMxd2b8QEEE5cjvcExOST3tkj6QARkzoUpRT7AE3jhl3XZ0uA2qu9SwyrSvbr0tBRKxCdK0g2E2/hqwcK/Tck5GB1eKb4aN+UkqxOblNDH+B1RfDoyNAuN+KEg==,iv:0p+ScrKpP4kQvO52gBAlwAis6oAzZ0EHFnU74hYPrn4=,tag:ON7qOjztF52xsJWAou7ogg==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlYXB3cVAzb2xEZ2pGa1RJ\nbS9ZVTc4Ums5eUZJUjEvd1g1aGVyNUNramowCnptZXFOZVB3MFRFcUtzSXBEZk1B\neEtKcDdLS0h0b1h3VjRjRXRvV3V5V3MKLS0tIHBDemkyUnV6ZXhTeE5VOVVOMlky\nWWMzVGVzZlAxMjZYUGpQUCs5QmxiYkkKcuBshCgWX4TwfVlQ5lHikzvwWdLEXWD1\n/uSiy0J6yMSiu8u6cg2SxeFrlKJ3j47dDlT6WHCxS0PfeEA0bJb3LA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGa2VTc2NWdkFRNUJxelVR\nWFk4RWxoelYzNHo1UFVhU2ZkLzEySlRWN2xNCmNmcmJod2crL3NMRlVsSmpmVkU2\nMjlXMktjc3piUVNhUXlTdnVGTWJkUTQKLS0tIGQrMUxrNDlNTkRCSUtFWkxRdXgw\nRlV4ZmtYSGhPQU84eWtiQXVqTmxUK3cKk5fn72UZPH68t5ZappfAhZJwzpLkfKmT\ny9TbUPIr4Pbrexau6YiH43QIbDQFdwYPfkBjGkd57zCg8AVo1+MBRw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArNWkwQXFWT1RpaDNvbzYy\nQWc1aHhmNHFEUXVsQjZqb0EzM0wrV0dwN1hnCjFsUFJiT3REK05uSGRWTEw2SFE4\nY1FleE1XVjhBbndiMmZxTWNTYmhYeVEKLS0tIEJiZzJvS3BsYzB3cHIxa2k5N1Ro\nenFFZDVaODNnVGdBZTBOYWJwRjQzc1kKlXJgee8wTSN4Beq4P0t9cYbk0BWHCseQ\nyaWpiPT9aZBEGLFmuEd3zKABc8lrilX/ySTmOG49vRg6CPmr7cT0Wg==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEa0JYenFMNmNzWnVmcXdz\ndG90ZUZ0WWlIU0FCZG9OaWpBM3ZDWnFhZFhNCjhuV1FTOTJ2WVJGa2RuNVV2MjR0\naVNXa3diaWxWUlJtdkNOQXZ2R2NsQkUKLS0tIDcyYXh3N3B2QmNiK3dzemFFMGV1\nNTZpTk5yNGV5YVo3cGswK0NLWFQxQlEKIe0N5OxooWXzt1cUViBmjihmGEe3G6/f\nkz2/IscnG78ZvNgYKjdoG1jlsyje/3zI4C8aWXLq2DnIyxUyAhPgsQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2b0NVVm9QNm9waUUzcXBi\nWnZWU0JETjZRZmprZVRUL1h6ZHB4cXkrdm5rCm9GZ0VnTXB3S1BYSmlGWFJVcDhJ\naUl3RjR0ak9BRmQvVk1GRnQxNmtYM00KLS0tIFlTU1p2OHhWUGlOWngwbE56NEhF\nRW5QSkVVUWZpdDZXWEIxZ1BkbzVwclEK2P25nBgf8255vaKW/+T97aNTecRgNjLu\nedIUiPdXbFATCe3v/YRo6sqzFwIsvM6Bl9yHh/SXo6Ftc7eWZZd8zQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T01:44:24Z",
|
||||
"mac": "ENC[AES256_GCM,data:J0D8bEs5mHLraLS6TvYuCgfiNU1xKM2Yfb5Y0f/q/4wM4LzXufNzv3+SWDHumTe328U8UnNXLqjNHEKL0bZi0coxpU5hVM+BvPcmqD72vscETzbQ2hnU05sfW+XjfZhcN8/ke0bpLt7nP0crD5hsZv3esV1E2UWvzjEiYtWzFHY=,iv:lcmXYG2H469UKBYDndWKMO+GP0mSGLztenm+kBaUdYI=,tag:ujiXbLM9CUsuoFwQQWI84Q==,type:str]",
|
||||
"version": "3.13.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
beszel-token: ENC[AES256_GCM,data:cbQOXhLzNk4g9d6hvm2DH7Q5ApTPCTzsW2txflDT2dD/UPIE,iv:V19MI1GEo5/0205Hrt7JImfkjduFiZ7f9aIkDVaI8mU=,tag:WCArgdrnIOudVe/Tw+oxRw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZeDQyNFh3dm1YZ2FTMjdK
|
||||
cDBWcDE2a1c1S0s1enhWRnVuMlVJc0gyTXk0CmxWNkRJMDhkeGpRTGltaitnVkZS
|
||||
a3Q5TW4zYm5Ja2FETEhJcGF0N2ZKbmcKLS0tIDN0a1FqRGNOY3Y1UWxvUU8zWU1m
|
||||
bE9DVzZESG1HTEhVWUdJOTF0bDhRVGcKP6OoyDAGLB9jQ69jpFyho5eaeK9XtZgN
|
||||
RlSJpBm2Jo19h/crpH9AWXUAIG0BWueyr8mwBu12cQdFIU3IyZT6gg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwRVFQcHpkQkNlSDhhM1Zp
|
||||
bUZyOFduQXg1cDd6ZlNObFgzL2hmUmJhM1JFCnprMldPQXJNVW10dVRqQTdWcGlv
|
||||
RnBYWWFsaVNrMkJpS0pkOGlQQzlJVVUKLS0tIFNsMEEwZTREZ1lwWFJGdE5YSVVU
|
||||
ZEZ1bVpFMEQ5N0g0L2RacUpLMWQrVDQKxPzq6f960purgAmUJw6IZnZSnhkzNE8r
|
||||
CSrFDowKTZI2KRdCtQ5fGhEoWO0ZPgVNxYV0KH7JBttylcpRLm6r5w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0czVoT3ZyYTZhOEQ3RWtR
|
||||
bVl1MW5vNERxUEpmNXV0MGVBRE9ySnBjT3pzCkM2aUVpZjg0SkNVTnRRMlhyMTN6
|
||||
NlFrZDVKV09Yc0tuKzFzR0ZtQ2t6WkkKLS0tIG1mbUNFdHBycS9UOGc2cjNpeHVm
|
||||
NUd1NThRQlZXeG1WbmR5Y3pTYXRKc3MKwSnE+0bGmxOAQUje6jHxuzIIyD6ZAwVz
|
||||
b5AAYwbGRagKj6fimsHBUmi4ohyG1huIGGOU8HiUYpu4PGJgOscztg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlVGRDMWNDZUR6c1VpUjRK
|
||||
WDY3L2lNcWFFcm1UV3RPMjlqYnBGVEJLcFFzCjFxck4wdlp0Wmtzc1RKNS82MXpK
|
||||
ZHBzOGhkc3ZuZUE2UmpUSTgycWdLSGMKLS0tIFUyYjczeUFWU2FyMlBTdzAxMTBE
|
||||
VzhaVzlSL05nZzNmR0ZjNEFPTXYycHcKfiJ0KjdxtLWsXxsWKzAL+H3hYYjHrYO9
|
||||
BjKknq1ZQJM0sB/Tid+GLqDwKi966MQK+AwHF5MqbsHW7eE5bO1nwg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
||||
lastmodified: "2026-07-19T02:30:40Z"
|
||||
mac: ENC[AES256_GCM,data:rKHZjU/MH08ASTlu32HZO9uWmsBYuMCEC6M8gwVhzuWvmablnP05tS2z13XfaWaCEUXk6kmGJKuU0zu5+IKVZgamCF6DAMtxQb6bVCaLsoAm/GSqWQ5VI9eHqgnSSdN/o3ul/33Rf8iBQo4aw8FFAmDVuNz8bfAn0QefFTj0ByI=,iv:JD2gtqRinOY77etg6PUmZNovkYl1Q3F6ZvRi4x7RznQ=,tag:/5IMpWKRVt+l1luCTQE0BA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.1
|
||||
@@ -0,0 +1,43 @@
|
||||
beszel-token: ENC[AES256_GCM,data:qg2eb7Eq/w7vIuiiDeK/s6h9nmHWuooyrd93GvdLD2hGOvka,iv:FWRBQTIY9PzV2V8J7z5ftEb5dvjN/S0hP0H9JV+Uuq8=,tag:WP7DHOu4elfqdLoW7vo9qg==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZmozK3VjcENGb3l2MDNj
|
||||
QVE3VVI3Z3R0MjduSlJGL3BFWkg5NHZmZms0CkNBTWZKZFJVNkJMZ0tpNXhXWW5q
|
||||
WndvbmhuOVh5ZHhJMmJwenh0M2dEVlUKLS0tIGpXQ3dhRXVWUDZzdGg4RHN4V3p0
|
||||
WDZnK1lMUHVIS09hRUEzVHRHQ0EwcWsKEd5NtstHSPH8okXcFLW7BId5xPGmgyC3
|
||||
dAodIGkLBlJlaL1bZuRy6Vaac+mJZL5KJYD/o1kYSXRiHJfNpaulTg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXWWhYOFU3VHZ3S1VuRTF2
|
||||
QlZvMkJUNDZmT1RDWWZsMkxnZjJldjdrRG44CkU0NEJTR0NhQUpjMEY4RXBLSVBJ
|
||||
bDNtZ1UrRDNUbzA0NDJ1cWJKdlBBMWMKLS0tIE9vTmEzTUxpODF4SG1MQ1czM1N5
|
||||
dVgrMERMRXJwVmhtMldTZUlHNUJVTEkKzUf9cETbo0K9bCPkrlbUkgkhRIPvjCHR
|
||||
GFVwgb2fcaOzZHcDwlLDLaP6HbRPtzJfVbLpQ1XU0N/xijSr9ENB6g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlOVdWN0tHNWZmaWlWQ2hJ
|
||||
MVFYd0pMNmJTdTlJQ3h1c0dzbDE3U2VqdzNzCitCcFdhbjRPYnl3Q214WnRxa2Jq
|
||||
eTFnc2VBcXlGcTVYdWpWTFZiWXBMUDAKLS0tIC9YdVkrTU5EeXNlNzliLzBlYUR2
|
||||
Qk1ITW9tMElzUHF4MVJyWS9uRGs1QncKj8OgxWYabf8MccWD6zYLxl+FvYPAvLCW
|
||||
p/FJ8uMa9YWp0RjICvWIwyO0gK7oaxTh/JzfBjABngEZnZazNYP7Uw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsbzFKckFQVDg0VEpJMFlv
|
||||
WSs1ODVoYlZZK3YrSXlMU1hIQjdLTUhNdHhJCkRnbHY5Z2NGQVMvOHV4R2hTd0xu
|
||||
LzdheXdZVVBsTjhVOU8zZGFKN0lPWjQKLS0tIFdaaG5SeElDN3YvMjFxdHVLWlNX
|
||||
M1BQQmtJTUdsK2x5TEo5TmNrMEZidkEKhi4jNKhrkBAPhJoeYNg1d60LQTqmJt+G
|
||||
Z/2X5uWzh12zF5keL8cNHH6sD0z0MOChtd4UF37yr+lb/LFJkia6LQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||
lastmodified: "2026-07-26T01:12:11Z"
|
||||
mac: ENC[AES256_GCM,data:0r68y6XmdiW7p/h9QmTR5h37TxKp+0wF44LP8QfG6cGP0tvaasuvv+V9EC3msnREYjwJhlnHjRd8hJ31HGVkOHbYWI4V5cF4ZnaiGgrZJhsGcJvOJSroAvQwev+vIlXZpAPgZz3Z35FDvOpgb71l5c0so0l4EJhc70JOh2FxhvQ=,iv:Or6l/v/E01+9wtZwyl+pCttiuReR6V0kmBzE9KhsfgQ=,tag:cAEUgj/9tKDToKFr00g5UQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:QvViw/s1N7eIN3CoD27llEjriuSrohOou4Cv310nigcW8xMnA2SDN2id3H3AoKii1JlJ+qWpKn+gUmt5HOM0UlbeNe3os2RVwiX38O7eN9xFH9F5kA3TFs6Umqq3EoI586PwIVmB2LyxDnTeEEXVd7v5PFkBcfu7u8YIcNF7lpcj+6rOyHMB8uxPhrGep3yiKawFd9c9wWD0hlSSatV5tMHA1qmdK8VmDbCU/iuGwIoMzN1eZwGAXzG6LkCA63bUfdxU6yGuTboD+kN2Wbo+GZB0EACmiZoofl2wqlXuiw6qwTvlXkyauc9O5EG//PUkIECzDwiXcX+qSOM9DIBlZNth4ebhtic/PskyF09etL/gICz5YvV4ph5lyrWHq41KxljSU7QXOkGhzagruuMrYzhZb35wFH4Tie1ee2DXbGhreJr8V3Zse/zTMaD+iM57V8bvNcarTzOFXyKfp7Y=,iv:XBPhj2wT0k/yRCRHU4d+BQA/k00ZHWSKOucnZ5+PGys=,tag:Nh5MOxZIUXAYVZY5SZh/JA==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0aUtETXlBMmRlTC9vK3NF\nRW4zS1FrSmo1dDN0OGN4SmV5ODd5MTRVejFJCnpPQkM2MVJ5WFlYRW1NVjduV1hv\nUWUzY0hHV05LV1BROVZVUlM2NldsTlEKLS0tIHFsTzI2SVZzYUtJWTM0MmFiUlVQ\neHEvUXgzc1pxSU1OZFo0cXhSZDdGUVEKpLVfzQEnntluUGsblnkHZJ9Jezu8tFte\nxEoV96GVHxUca6TFWpTLMqdR7NtuQGCkx295W3i1tkp58DP1OzRTUA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
},
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZW5ybVBvTkxibWJMWGRL\nZ1lnWEkxZ3l6d0FWbmxMY2VsTVlzOHJJM3owClNYd3BlQi9pL1lIdzJmekJUVEVi\ncWhSK3ZkZEMyeTNoZlpTT0NMUXZFWVEKLS0tIHVkVlhmQXdRQktTK3J5dXZ4aTNt\ncUw3WCt5dXJhTkdUbVpmeWoxWkoxNnMKj4XtdwmgFVOiVsIJs2Du7QJ09A9tv/Lf\nkFOq8y4tlZe0nCwRjq43sVz7hdCTdQ0rsaWjBGY90LLkJbOA+f+Wrw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T01:44:30Z",
|
||||
"mac": "ENC[AES256_GCM,data:zR2WLWX7NaHA15gi4kX0jDvzUIe9jtz5bMCAggbPW+IXOEedPrddAHZ8OfPErVMfx8O1pJKkAKSzoPTAbEle54FisSLMHXp8fI0297MByJrF9pOsMFpVcDy/L4Q+pBzmB7aS9r7+u7KRVVTZT3QwG1rFWZaDs5dFTP80RhtCbWQ=,iv:ZzPoO+h7ebS+jsSH7tWMx6QK8umpa2/HFQmx9dnJN+Y=,tag:vG9+ifxw4HaBE6YsmOwXcg==,type:str]",
|
||||
"version": "3.13.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
beszel-token: ENC[AES256_GCM,data:gjbT3uROiVKQOJaUeafTxjVknQO1Tvbyx/Pl2bTad7DezByX,iv:3ikf7OaT2omO8yd6G6UwYbaRBSzyvbn+NghxAe5bcgI=,tag:Zuc2EP8rUtdDhr5CzSW2Pw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJL25EUUZack1FMzlnMmdk
|
||||
Z1hnejZRNnMwVHpmWkFNdGcyeHVsNnRsSGtZCk9OdDhhcnR1WW9ZMEZ1OUVYbm1n
|
||||
RmZRVy8wb1J3emJBK3Rrd1d4U1dYUDAKLS0tIGJaaElvSk1sOTBOM0lKck16OUtu
|
||||
NnRZb3U0ZndmaHBZTm8zczhWdE1oaEUKkf6fLomAHoKPhuM4e9q96YmmH+h4VrEj
|
||||
2x0rnwBwOoRzYWutB2MVtlsphAZmZ/PK0tEecT2MM0XXayVG/33qdg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpeEJvaHl4akc1TFdEVGV2
|
||||
czg3OVprU3p4ejNpTktXZEpneDgrOEhrZGlZCmJwT1dhSkZneHE5UmR0WTd5UENq
|
||||
VHJEWG1EekJLY2pRZldtVGtxTHlGaGMKLS0tIEYrWHE0WTgyUlIwdktmNzNIS3FW
|
||||
ZVRvT1dHa1Vzc2RSakVISzdMTlpnVGsKeT+edn4+LUkVtpRUNd/gKX3H1HG2bvNo
|
||||
c8iI6qr/l6oxfP85OrKYFDU9IGvDMxSSdbixHtojPEb5OKVurV0WPQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||
lastmodified: "2026-07-23T23:32:57Z"
|
||||
mac: ENC[AES256_GCM,data:l9a/yNRoxY1hvSkLuR4N7deeKue/1JPlSvZvJfCSNbQ21p1qR433BbSDYvfW+kXQXS8GVcfgXSd9ywNzgVvkA5lR1++uYsZBLbYxJ+s3TKWs6/yECAZ0eM1KBA0BEm7cLSsHTOwd+2WspvmCYir++FDO9XRuS3guiMnQBglDf/E=,iv:eE6GVqexQNSiLYfmTTUdUx5AO//wyjSIsr96xAX1pcI=,tag:CNsoyDZYLUt5Seu7W5wJrw==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
+35
-62
@@ -7,21 +7,21 @@
|
||||
lanGateway = "192.168.2.254"; # LAN default gateway (router)
|
||||
lanPrefixLength = 24; # LAN subnet prefix length (/24 = 255.255.255.0)
|
||||
lxcLanInterface = "eth0"; # LAN NIC name in LXC containers (set by Proxmox --net0 name=eth0)
|
||||
lxcStorageInterface = "eth1"; # storage-client NIC name in LXC containers (vmbr2, --net1)
|
||||
vmLanInterface = "ens18"; # LAN NIC name in Proxmox VMs (virtio, first NIC)
|
||||
vmStorageInterface = "ens19"; # cluster-internal NIC in HA VMs (vmbr1 — DRBD + Corosync only)
|
||||
vmStorageClientInterface = "ens20"; # storage-client NIC in HA VMs (vmbr2 — iSCSI/NFS VIP)
|
||||
vmStorageInterface = "ens19"; # storage NIC name in HA server VMs (virtio, second NIC on vmbr1)
|
||||
pxeServerIp = "192.168.2.223"; # pxe-boot LXC container LAN IP
|
||||
nixCacheIp = "192.168.2.224"; # nix-cache LXC container LAN IP
|
||||
tailscaleRouterIp = "192.168.2.222"; # tailscale-router LXC container LAN IP
|
||||
torRelayIp = "192.168.2.221"; # tor-relay LXC container LAN IP
|
||||
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 — authoritative DNS for sweet.home (not NixOS-managed)
|
||||
domainControllerIp = "192.168.2.253"; # FreeIPA domain controller / primary DNS (not NixOS-managed)
|
||||
ipaServer = "domain-controller.sweet.home"; # FreeIPA server hostname (used by security.ipa and Kerberos; must be a resolvable FQDN, not an IP)
|
||||
|
||||
# Cross-host references (LAN hostnames/users other hosts reach over the network)
|
||||
nixCacheHost = "nix-cache"; # substituter/remote-builder hostname
|
||||
nfsServerHost = "server"; # NFS export source hostname
|
||||
dockerHost = "docker"; # docker-compose stack host
|
||||
|
||||
# Raspberry Pi's own Tailscale hostname (not fronted by `server` — it
|
||||
@@ -40,25 +40,17 @@
|
||||
# from scratch.
|
||||
nixCacheHostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuHUxGNH6ei3BZD+EfZs3l4X8uJNcjQiOsM/G4yo4O/ lxc-nix-cache";
|
||||
|
||||
# Beszel hub's SSH public key — used by every agent to authenticate the
|
||||
# hub's incoming connection. Update if the docker host is ever rebuilt and
|
||||
# the hub generates a new keypair.
|
||||
beszelHubKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFPR9kwtC4TAeTRu46A7+opZsYpxqkRJ+x/ZyB2GWCeG";
|
||||
|
||||
# Public keys authorized to SSH in as remoteBuilderUser on the nix-cache
|
||||
# host (modules/nix-cache/server.nix) — one per client host that's allowed
|
||||
# to use it as a distributed builder.
|
||||
remoteBuilderAuthorizedKeys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK+ioWPhHixlgCB9KIQ0QTHTz6A+Oo2F3uKiINLip5rO root@docker"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDEA1S2ikpObREgbP5uVBWMxIOGbY8B+Wx7VTZK1m6t root@server"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPAYIT9ormlmxZ0SziyDQaUntnKI8HK9/s3Qac1ZKjP2 root@docker"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKKKzoEPl/ZW9KBRHBcp6/ThOngGpwMv5EhkTlgC4aDf root@nixos"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIGtOWOCS+ImHc7NehguoyD7PbonGosKMZqc9+QR3v/h root@nixos"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxXTQxFnArK5HXG7czeoybZebCGfxpUdusJkPn+BCSp root@server"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMJhrfFayLBG+gWtO6oAvgambw5nWWgztiTFEaaaVRH debian@surface"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII/rLceRhnDobVXQYiPceuhDHHvVjFQ1pc9A6un/eUlA root@server"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHj11bLPpRzH2oslnwFzEvY9cSgfEFtSZbLQaDm4nZMK root@pxe-boot"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH4/Sesm8NYpj73R0cbGhI0Ubvz73vIVWAnbEDTlBTdh root@tor-relay"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIBCFUUtePndW7pqtlawft1QCdHmBVs3O/c8EJO+RcXV root@tailscale-router"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBZ9WKKAlP9Z7GQdgaZ1Xgw9C+vja2lqEZO5rJFpVqYN root@ha-server-1"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEGNKlaaMckd8nLWNGz4B2QokXjnnIvM+rEUv+R6h0sp root@ha-server-2"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK+XeMco7OxUpjrjZm54HogMs9QB5xlcKmElASRvrmlW root@nixos"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMseQwPpmaa6cgV5U8KhUsiVSYARG85zGa9rho0LJWks wayne@pve1"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJEj26SL/emsVjW2YhRucJVp2kTz8WgcEQgjBEBLRikk root@claude"
|
||||
];
|
||||
|
||||
# Admin SSH public key, authorized on the primary user of every host and
|
||||
@@ -88,57 +80,38 @@
|
||||
# one-line change.
|
||||
primaryUser = "nixos";
|
||||
|
||||
# Primary IPA/domain user. Home Manager is configured for this user on every
|
||||
# IPA-enrolled host (see modules/ipa/client.nix) to manage the environment
|
||||
# that IPA itself doesn't cover: dotfiles, user packages, session variables.
|
||||
ipaUser = "wayne";
|
||||
|
||||
# GID of the IPA "docker-access" group (GID 50010 on the IPA server).
|
||||
# The local "docker" group is pinned to this GID on every host that runs
|
||||
# Docker so that IPA group membership alone grants docker socket access -
|
||||
# no per-host users.groups.docker.members entry for the IPA user needed.
|
||||
dockerAccessGid = 50010;
|
||||
|
||||
# HA file server cluster
|
||||
# LAN IPs (vmbr0 / ens18) — management only after storage migration.
|
||||
# Cluster IPs (vmbr1 / ens19) — VLAN 10 (192.168.10.x), isolated internal bridge,
|
||||
# DRBD replication and Corosync heartbeat only; never leaves pve1.
|
||||
# Storage-client IPs (vmbr2 / ens20) — VLAN 20 (192.168.20.x), isolated internal
|
||||
# bridge for iSCSI; docker and server VMs connect here instead of crossing vmbr0.
|
||||
# haServerVip: floating virtual IP on vmbr2, managed by Pacemaker IPaddr2;
|
||||
# iSCSI clients connect here regardless of which node is Active.
|
||||
# Protocol separation: iSCSI on storage-client subnet (VLAN 20) only;
|
||||
# NFS on LAN subnet (VLAN 2) only. Enforced by firewall on the HA nodes.
|
||||
# LAN IPs (vmbr0 / ens18) — client-facing: iSCSI initiators, NFS, management.
|
||||
# Storage IPs (vmbr1 / ens19) — isolated internal bridge, used for DRBD
|
||||
# replication and Corosync heartbeat only; never leaves pve1.
|
||||
# haServerVip: floating virtual IP managed by Pacemaker's IPaddr2 resource;
|
||||
# NFS and iSCSI clients connect here regardless of which node is Active.
|
||||
haServer1Host = "ha-server-1";
|
||||
haServer2Host = "ha-server-2";
|
||||
haServer1Ip = "192.168.2.228"; # LAN IP, node 1 (vmbr0 / ens18)
|
||||
haServer2Ip = "192.168.2.227"; # LAN IP, node 2 (vmbr0 / ens18)
|
||||
haServer1StorageIp = "192.168.10.228"; # cluster-net IP, node 1 (vmbr1 / ens19, VLAN 10)
|
||||
haServer2StorageIp = "192.168.10.227"; # cluster-net IP, node 2 (vmbr1 / ens19, VLAN 10)
|
||||
haStorageCidr = "192.168.10.224/29"; # cluster subnet — VLAN 10, internal to pve1 only
|
||||
haStoragePrefixLength = 29; # cluster subnet prefix length (/29)
|
||||
haServer1ClientIp = "192.168.20.228"; # storage-client IP, node 1 (vmbr2 / ens20, VLAN 20)
|
||||
haServer2ClientIp = "192.168.20.227"; # storage-client IP, node 2 (vmbr2 / ens20, VLAN 20)
|
||||
haServerVip = "192.168.20.229"; # storage-client floating VIP on vmbr2 (Pacemaker IPaddr2 vip-storage, VLAN 20)
|
||||
haServerLanVip = "192.168.2.229"; # LAN floating VIP on vmbr0 (Pacemaker IPaddr2 vip-lan) — NFS access
|
||||
dockerStorageIp = "192.168.20.225"; # docker CT storage-client IP (vmbr2 / eth1, VLAN 20)
|
||||
haClientCidr = "192.168.20.0/24"; # storage-client subnet — VLAN 20, internal to pve1 only
|
||||
haClientPrefixLength = 24; # storage-client subnet prefix length (/24)
|
||||
haServer1Ip = "192.168.2.228"; # LAN IP, node 1
|
||||
haServer2Ip = "192.168.2.227"; # LAN IP, node 2
|
||||
haServerVip = "192.168.2.229"; # floating VIP (Pacemaker IPaddr2)
|
||||
haServer1StorageIp = "192.168.4.228"; # storage-net IP, node 1 (vmbr1 / ens19)
|
||||
haServer2StorageIp = "192.168.4.227"; # storage-net IP, node 2 (vmbr1 / ens19)
|
||||
haStorageCidr = "192.168.4.0/29"; # storage subnet — internal to pve1 only
|
||||
haStoragePrefixLength = 29; # storage subnet prefix length (/29)
|
||||
haStorageRoot = "/srv/ha-data"; # XFS-over-DRBD mount point on the Active node
|
||||
haStorageNfsFqdn = "nfs.storage.home"; # NFS VIP FQDN (storage.home zone) — resolves to haServerVip; use this in fileSystems device strings
|
||||
haIscsiIqn = "iqn.2026-01.home.sweet:ha-storage";
|
||||
# DRBD backing disk — identified by SCSI controller path so it resolves to the
|
||||
# correct block device regardless of OS-level naming (sda vs sdb can differ
|
||||
# between Proxmox VMs depending on disk-add order). drive-scsi1 is always the
|
||||
# dedicated data disk on all HA nodes; drive-scsi0 is the OS disk.
|
||||
haServerDrbdDisk = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi1";
|
||||
|
||||
# Storage
|
||||
# NFS share definitions — used by ha-server.nix (exports), docker/mount-data.nix,
|
||||
# and pxe-boot/mount-pxe-images.nix (mounts). `subpath` is relative to
|
||||
# haStorageRoot; `mountpoint` is the absolute local path on each client.
|
||||
# Renaming a share only needs changing it here — exports and all client
|
||||
# mounts follow automatically.
|
||||
storageRoot = "/tank"; # ZFS pool root on `server`
|
||||
|
||||
# NFS datasets exported from `storageRoot` on `nfsServerHost` and mounted
|
||||
# by client hosts. `subpath` is relative to `storageRoot` — combined with
|
||||
# it to build both the export line in modules/build-types/server.nix and
|
||||
# the "<nfsServerHost>:<storageRoot>/<subpath>" device string each client
|
||||
# mount uses in modules/docker/mount-data.nix. `mountpoint` is the
|
||||
# absolute local path clients mount it at, referenced by that same file's
|
||||
# fileSystems attribute name plus every other place that needs to know
|
||||
# where the share lives locally (modules/build-types/docker.nix's
|
||||
# tmpfiles rules, modules/traefik/rotate-logs.nix's log path). Renaming a
|
||||
# dataset or moving where it's mounted only needs changing it here — the
|
||||
# export and every client reference follow automatically.
|
||||
nfsShares = {
|
||||
options = "(rw,sync,no_subtree_check,no_root_squash)";
|
||||
dockerConfig = {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPW/X9Mdrqs0wLR7XbEDTihk7TEkNZ3LcCeXoa2ITSDA linode-server
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:hzI2cJ81+Psv35MMZO06UHpC8F9FLVkfUzn0ReuR+WoUds8D5zhDFbQHeR6ByeHTuK8Hlj712okhoXh6Vm7l3WmhLJZkr8IGLsV9W+P2PYityjuOtBPrphrKUeSqxJfjQ31/EhZLOqw/508XUmQDNcs1/n2g0TtQ2UQSTAuO8r1OlRfRSPizvVdj7lu+Vqg3dDBRETSOJAYIh8XQXoQWl3M4dS7jOYUvgc4EsYOWrvPPPS+8xXzctBPeToasY0IyWtLzoeNajCs5EpH5u7S5K9S40/vFPa45Ic2IxEkb1HHClyN6lXWbnc5QlJ5HPrq+2YBU2y9dJHd9DQiwEbcEBIvR3/Lo9puVDppXx9kez7i4XR1UHU7WVu39nQZuP50BQQazj+eM0/HJapXfjoqtaaq0qAGN4onnmRd0vEmrZZl4WMXmx8m9HhxdNJoZ4C2Wb2skdHZok88tPzjd4r7xKzVhAbB+wqS4LML6FmCNk+m0USQXa25USXs0JDV7lgiTFQJPE/67JGa820WFicGs,iv:YmOeK2Ha3yBXumVO9strgLgqNmPOcnqwUDJv5QNR1WM=,tag:Z7kZvQLDOKx3uX4dRXJqeQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOSWl5ajRSVndlMHJJQTFp\nYzRRU3RQMEVzazB0Sm5FdWZmMzBRaUl5RUFvCjgwRUZWdUpvUjkzWHFVdThIZ1ZV\nM2RMNy9pUWF4VVBCVHBGWHgwakhKQ2cKLS0tIERIOTVVZm55QWkvL25SRGZkZmtI\nT2o3ZEJNQ2hmTDNoRUx2Z3UxcmlyekUKLOajmvRfLdCJL74PKSgBtIXDuAVd8NwM\nh4BtDs0hONOz82JaBqFw8Uz28hVFG/gcS80br1o2klqPd2gN62PCVw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-25T11:45:33Z",
|
||||
"mac": "ENC[AES256_GCM,data:JJC+12gTCsDVvMWRtL5cj50kf1n6Xn2j1hNBnwvZXUA9Pdd96SXt61U/Q80h8GZ7Ycs/slsV7h5f3g6+8tV0PcdBM/vy0vPM5qX1zMySyMv2p+dkJb9MwQpPg2xAQ9jjYM9237p5n9nysgu74h4V7ccBqmBzp764bL9wx6hEzS4=,iv:U6u1GFvoxaqxmHv1zCht24nW0ZMJR2b4pZqCG3bGNzs=,tag:MocwywOBg/1rUuoSADuJVA==,type:str]",
|
||||
"version": "3.13.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILVRdEddGd+AMNe4kXbmA9UXK8JRsPEuxPx1vhNT9ZG8 lxc-server
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:JTmbN++hNMddBdMKIdzqZ6hLD1So9hyAzwbce/QvqWBsa1At85cMRUx+P1Yi4Sq2aXCg1Tf2g8XC9DXsVfMusUGEHrHbJApwkia/xHNMcFOohscUkPltXnm/lRilm4hwJUv6ay6UyLvIqMQAzzC8YWebHkKo00rxRxQWQ77wjmUG4dJeujlK5wMH0zTXUkrf7tIdJ20Zzg1BrAU6s4FYLvRvcUSW2ROu+sF2Sxgy1qVNJfDrRnY4REh9013tTSB5IlgJzUSQLs1vHr7EpIfmA5MCCSkZjplGCKvxkcNHB3aaOkrxkGa18JeWoklRen31UPU9zMUhTjAZ6VbbIagxzaFGKVP27cOiHQPNpEIMXwaRzoprjBX2PJ/Bs6pxe6hBpmiKOb40XdtxFQx2rofPcbQwklTpM5SFwvK+/bgawy1m8O3NHMhcw1qvd+6KjUdqy++/ivFC2Nyy+VfUDyGu0JLK3X+YqU6JXXMcUWOq79pVQIL3q1ofbhVHQ8p17+V3wQQUB3kPo4EEcKikI7Z9,iv:TVYVDOiTsgXaIcuJdnd3djPWXCMDDGjpefAW0MR+7Es=,tag:jIYO1oatbvInp77d4fC8mQ==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5QlNzNEFqSkhKTVhWVisr\nS21zdVBZSnpITDdrb243c0pUdDR5WXl1bFVrCmkxaWR6U01sLzhLL0MwRGh1VG5D\nNEFiU1FtNXRBdGtPellFTXYraitCUTgKLS0tIFdwWGFnMWFCN1ZvK2VBMWR2VTJZ\nRlQ4dEpmczA4bEdvUkNhaC93OXhDTkEK0QBkaLV1mbTMlSnjmN4x+qljGipHM/DQ\nUlmBYyi3nEOrI36I/Mm8yoBZai/qWqdg6IG+sxDE49ZOLp9PhpAWww==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-25T11:45:44Z",
|
||||
"mac": "ENC[AES256_GCM,data:/l6pa3LE7+kFYqH2pv2RIcYBycLrZfpb92Al3SIU2tMeFUUvh+C8q8P9CgmAaiQuQ8S2dfYIJVx67zc7cRqI/UL/dFvDSv0YaMTGQ8Wn4fIXSh01EL0f/QVaIfb+uuvyEsdjy2ScWTWcCf2ICnC/zaMmp+xP+MmR1DRBM8KPIkY=,iv:xsmaYZ6dyHLU3BVfT3jxbfWWeBvYKMT+D9MtRxF9jlo=,tag:VzOiutHQJGHqb5UCI+cxSA==,type:str]",
|
||||
"version": "3.13.2"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN7IABG7x9ejHSENmHsL/BF9MQadGuBnHZa1eUEFMYnH ha-server-1
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:EhYMNPzlMDyby6g3hfvBKisKJnU+YAxZjf326tPdflQ2PcmPTzZ0GjHqJOPrOWyLU6IyvnsLMBV0JF5/UUi2c/H3p2wvoBgsxDMaaMW0YUK2IZFo+PZpWoYJSMrx40sH90W0qL0VBw17JLwPU6CjeUZrn5+sCN9EhQvncD8NoJ6QwZpWKhWLn9/7mxd/PF1uxjEQXzHYFeuUuVpQdKSUfV/BSJIT3pMM/VdH2203jTMtwUbK1/7UtGKtDWdfFbkEvJ6qsV7hnN3EYU16WtdyAiHEZvvZ3gqg6YK2DemojWG7z7VQkQGbGm40yA6/mYGW2dVK5ULzjxALl05G6lo1vzxODbMHhzsfJZVw6ocGx3r8xwaasiX8S3loq8WNUWJ9QgjbxuEX9uGZja6A3nPhcwVGGZExbUoU+syXUk2m+WfIQ0epplaPDyAxrYO5ZPWLdMYoPRxbDx94rx5fMxA3jst++IEL2kQMTieGQYJy3R8veQIe3Jb074zY+Y79EmRxfo8ruR6ZMqORtbZzJOBcd0OObjUVLUqB4rnC,iv:XkCfOKtmZNz+UTZdElLm8L0PpymkDzCM24VkAa9Y/Qo=,tag:hmr9ik0V7mxEKR0DcDB3/w==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqQ3J3VDVwUVBvUjVXSWNz\nU3YrRXZlT2NLRThCNzd3YWNHVTlHeS9xMHpzCi9HVW5kMXZrdjE4bXdML3NhRnI1\nL0NQQkNQUmg2M0NBYzh2cG1vdHA0clkKLS0tIDJZYUwzK0RGNzJWckJJekI0SFg1\nUXE3N05xWThtdjdCZkdJTFJ2YUx6cm8KVANudVL54WBNc9DK9s9h4WQRLMewUqgN\ntu4LdMOmi5oV3LX06lbxhBq79dmsV5uos/qszhJkVGhEZwD1RGBoZA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T06:42:29Z",
|
||||
"mac": "ENC[AES256_GCM,data:Xn3/CWhQJHWtd3QHoqpdwLuJdZTp+oxx43k38j4jspQSBPdVDSG6Ifi7Sb2r1g9YTxpjUto8mv7FH+BqI0wSYUkLfjaxI51xs3dq6e6kl5NWOsvWwIOGmBuhfIwusHGgsfBguxk0J6Ev8Irw674CRFc37jozXTVdpOOzFhPTvFs=,iv:/7OFE9NYkw2kYEetzeZRXah0KXuD5OXmfjQMIeqI75Q=,tag:VamuInZqYGYRwlyU8TtL9g==,type:str]",
|
||||
"version": "3.13.3"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINWuFwPKW2jwQEj6c//xb95DbtandPkJ7M0ceyTNd35G ha-server-2
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:GEebcQrPPrQxLC6mWXLLtytGkIy6Q2FbcQ/Z+ImGa3gahI/nigdNyR/avIz1DAUsOibpoMVlrk0GQUBMuxiEXulIwUEzeTh7sMI3XTZiugujOFHPyJoFeynlgeRNBbl2EVVjGmEmOsR/cZJGyKaet9aNTHTPDJUhodY5d+D/sE3SgpHoch1DI02/DRnqlnlOYiBoujk++BW71EDHLVyz3n9NsurTDYDK4L0Ch5cMgdlvwIuxGuPKtkhU9z6PtGxPbNlYlNP8U+3jG4XWEEY5hZHBGTVHn+bd5FViGY+sMiigO6yaOmddbUqzJoblKeI6F/rfrL3kXETLgVP7uVm3gw9KPEl5JaTdVqkG4hQ9G9/LvRlyEh619mCp9IRXxabm5SQ4NWFzah1ZwqIKp6GoAtqeWRgZNVqijoF+bnb0YmNFmjwpSrSMZvKsrZb5geyGmqah+NJii0xqVebnufB5p8lW9eytBSNgC/XLjj0olwHMw7CWGmnY6Q7lxF1tNYAD/K3dkbxQYFM3kBnmAdweC8WYc+hDjtk0HJul,iv:H4BonLmf3VoW2S9+IUE+SzPb0qiWHuNuGpVIUgUYScs=,tag:5+bq7vC81DMmOk4jn3u8lg==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrQitsaDlqTmlYM2RvR1Ew\nb3NzZTJzR3F2bjM5NHhsa3dCUzZ4TW1pa3pBCkxIeEJtbWdaUFMwMkFyRHpCZ1Bl\nbmM5dGV3b3BDcDEwVlJ4UWV5RFNvRkUKLS0tIFRDd0U3dHBHb0Y3WDhNV3hkd2Ew\nclRDVEZHYjZUSzNVZkFjWGt3SGVQTlUKOULuXiYD9k2uVUmhuC15Kgezrd69rc9P\n7SocPa8kBliffP9IuxxW2S/hPbK8rqEx/sh/Km85ZIi4pG0AUQJ3fA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-28T06:45:10Z",
|
||||
"mac": "ENC[AES256_GCM,data:ZnsqhgELqEdNzlTKJx2+xzcZRMNGTme/0a+PVvpbUv5IApimtif0zkTEC1LJsyLPut3MmO3QGprVlb8ihoRdm/IVvLaQonc36f6vN5cxrpQadru/RWEjLbnge1E8VkN/PVvHqRfLyIOPSq6wtGjAPaC/iHW1scBi50pWr0XO628=,iv:FlqAL1It6JuvoBhZotHWdSz17nckBf2rXnwovl6ZJBA=,tag:tNwcaL1sJakXtgejsB/Sbg==,type:str]",
|
||||
"version": "3.13.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGQHSubxjvaIV9Xp5ABJSKsajCZBGyGmjsdaA2TWCFP8 proxmox-server
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"data": "ENC[AES256_GCM,data:GdVHSekFtRtppf8xxMPsDE0cu8Hwq3ol0iJTfrwKwaAB0IW8YS8TFAeRnfOI5AE3P1JUaGDZ1Od4UXu4Jk2+uYCgwz074qc1WukzUsd8Q3G03ojdrTA13uH8YCUTRsm3bQfB+kmH5qefn9j88UJq7TYiTLe3U0DUU/B83lgyiFIygP8CKWDgFH3jWZOWPmyVa4Z1fMWVsiNhjO4hHduamN/I2FOY7U2TI5+zcYgGeunjnniLHcQdiO0iK9TOP5SckOVPZOy6daKzGcckEq03NV4y+miSucEUHlcQqMUyvsJwETIaCq28xR2MDTyDz42SL2HIodb7FyREtxd91W6gPN1j51yUZxt4WA9R8/WczDcUgmROK+HInPp+ktjWqv2ymXXV5xidxh7m+vrGTPC+YQqyFD4eIp3+h2S/vyzHimBa2rdEp9NNXTQlH3CfL2aLC95FOme+uKTDqWA2PHfB0rQ5uhHAuAgaX5vb/GTiQii5dq4k2O4MDf9u+fdPuOSCaGdlirWmSE45SvaXLJ3M2zQq7M/UTE+wSFyc,iv:EaIsYnGxf42LWQ3hzBU/HsBda/FHInhjZkwsPLcZMEY=,tag:16OuAs3Ram+xd4RfWEBXkw==,type:str]",
|
||||
"sops": {
|
||||
"age": [
|
||||
{
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBETE4vOHhldGRFRjdYaXpi\nSmgzTFlqL3Qzak1CelRBR29jZzR1V0pOQWw0CkhSWDkvbkJWaDVHMzlKNDZUdzhH\nWGoyWXQ1KzJZQU1kQzFvaDk2V2lWQmsKLS0tIHlTY2tua2F1TXBhamsvcDZ0bmI1\nZVFwUGtQWW1HNTM3UUs4bHRZU0xKRDgKBG2iI9JP0lhU5VCWXrpN1b2rYEYk8sOZ\n9FUO14KKMg9QRfSa2iHOa84DByx2hxVRc9wLukBUpkKOAjGSxeGMeg==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2026-07-25T11:45:58Z",
|
||||
"mac": "ENC[AES256_GCM,data:jG7ohweKKndoadidquejYG/1w9iL+9Xb5/IsU/C9fn/Tq5RXEjrxO37COY8sAD7dvQf7iBNsly7upsjtHaMK8ybjQDaa6IQhoGBlfSOA2O968klaJZRQRiLPzCRet252KXzOtvDscrBvyYItvyqjnW5qBbw7lfT4y2J4OA2ieC0=,iv:mMo7Y6XEjQcchNOY6eaw5LOmjFKaQKx84o9bnGNXCKI=,tag:aHkPjs7uVfv8h0REHN8YmA==,type:str]",
|
||||
"version": "3.13.2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user