Archived
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2741451642 | ||
|
|
9f1fed26c2 | ||
|
|
3b6ac30946 | ||
|
|
cee33aa2d8 | ||
|
|
ac9099a1f2 | ||
|
|
c96752e5d0 | ||
|
|
43314b6aa3 | ||
|
|
db84a0bcc1 | ||
|
|
419b0f706b | ||
|
|
89e1c5dc9a |
+20
@@ -21,6 +21,8 @@ keys:
|
|||||||
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
- &proxmox-tailscale-router age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||||
- &proxmox-ha-server-1 age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6
|
- &proxmox-ha-server-1 age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6
|
||||||
- &proxmox-ha-server-2 age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r
|
- &proxmox-ha-server-2 age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r
|
||||||
|
- &proxmox-ha-docker-1 age16y0vfts5v2k20e2rj23qa5lc5gm96gm64uwsqsr0y688xl0ne46qcfr0sm
|
||||||
|
- &proxmox-ha-docker-2 age1tm3zj5lp3elw2j832az4nj9xxmhqd66ag3cqcv3vskvmd7qdmqmsdpdcn0
|
||||||
|
|
||||||
creation_rules:
|
creation_rules:
|
||||||
# Shared across every currently-deployed host: root/nixos password hash,
|
# Shared across every currently-deployed host: root/nixos password hash,
|
||||||
@@ -52,6 +54,8 @@ creation_rules:
|
|||||||
- *proxmox-tailscale-router
|
- *proxmox-tailscale-router
|
||||||
- *proxmox-ha-server-1
|
- *proxmox-ha-server-1
|
||||||
- *proxmox-ha-server-2
|
- *proxmox-ha-server-2
|
||||||
|
- *proxmox-ha-docker-1
|
||||||
|
- *proxmox-ha-docker-2
|
||||||
|
|
||||||
- path_regex: secrets/nix-cache\.yaml$
|
- path_regex: secrets/nix-cache\.yaml$
|
||||||
key_groups:
|
key_groups:
|
||||||
@@ -199,3 +203,19 @@ creation_rules:
|
|||||||
- *admin
|
- *admin
|
||||||
- *proxmox-ha-server-2
|
- *proxmox-ha-server-2
|
||||||
# proxmox-ha-server-2 added by sync-host-keys.sh
|
# proxmox-ha-server-2 added by sync-host-keys.sh
|
||||||
|
|
||||||
|
# Host keytab for ha-docker-1 FreeIPA enrollment (binary sops file).
|
||||||
|
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
|
||||||
|
- path_regex: secrets/ha-docker-1\.keytab$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
- *proxmox-ha-docker-1
|
||||||
|
|
||||||
|
# Host keytab for ha-docker-2 FreeIPA enrollment (binary sops file).
|
||||||
|
# Generated by scripts/ipa/create-nixos-ipa-host-account.sh.
|
||||||
|
- path_regex: secrets/ha-docker-2\.keytab$
|
||||||
|
key_groups:
|
||||||
|
- age:
|
||||||
|
- *admin
|
||||||
|
- *proxmox-ha-docker-2
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
# Docker Swarm Cutover Plan
|
||||||
|
|
||||||
|
Migration guide for moving containerised services from the existing single-host
|
||||||
|
Docker LXC container (CT 105, `docker.sweet.home`, 192.168.2.225) to the new
|
||||||
|
Docker Swarm cluster (`ha-docker-1` / `ha-docker-2`, 192.168.2.230–231).
|
||||||
|
|
||||||
|
CT 105 stays running throughout. Services migrate one stack at a time.
|
||||||
|
Roll back any stack by restarting it on CT 105 if anything goes wrong.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Swarm cluster deployed and healthy (`scripts/docker-swarm/deploy.sh`).
|
||||||
|
- Both nodes show `Ready / Active / Manager` in `docker node ls`.
|
||||||
|
- NFS mounts healthy on both swarm nodes (`/mnt/docker/config`, `/mnt/docker/databases`, `/mnt/docker/volumes`).
|
||||||
|
- Access to FreeIPA DNS admin to update A records during cutover.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Traefik — switch to Docker log rotation
|
||||||
|
|
||||||
|
**Current state (CT 105):** Traefik writes access logs to the NFS volume at
|
||||||
|
`/mnt/docker/volumes/traefik-data/logs/`. `modules/traefik/rotate-logs.nix`
|
||||||
|
rotates those files via `logrotate`.
|
||||||
|
|
||||||
|
**Swarm approach:** Remove file-based access logging from Traefik's static
|
||||||
|
config and rely on Docker's json-file log driver with built-in rotation.
|
||||||
|
Traefik container logs (including access events) then live under
|
||||||
|
`/var/lib/docker/containers/<id>/` on the node running Traefik.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
**1a.** In the Traefik stack definition, add logging config to the service:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "100m"
|
||||||
|
max-file: "20"
|
||||||
|
```
|
||||||
|
|
||||||
|
**1b.** In `traefik.yml` (Traefik's static config), remove the `accessLog`
|
||||||
|
file path if present. To keep structured access logs, use Traefik's
|
||||||
|
`accessLog.format: json` with no `filePath` — logs then go to stdout and are
|
||||||
|
captured by the json-file driver above.
|
||||||
|
|
||||||
|
**1c.** Deploy Traefik to the swarm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On either swarm manager:
|
||||||
|
docker stack deploy -c /mnt/docker/config/traefik/docker-compose.yml traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
Traefik should be deployed as a **global mode** service so it runs on all
|
||||||
|
swarm nodes and handles ingress on whichever node a request arrives at:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
deploy:
|
||||||
|
mode: global
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == manager
|
||||||
|
```
|
||||||
|
|
||||||
|
**1d.** After confirming Traefik works on the swarm, remove
|
||||||
|
`traefik/rotate-logs.nix` from the `docker` build type in
|
||||||
|
`modules/build-types/docker.nix` and rebuild CT 105.
|
||||||
|
|
||||||
|
**DNS:** Update `docker.sweet.home` and any service FQDNs that point at
|
||||||
|
192.168.2.225 to a swarm VIP or round-robin A records once Traefik is running
|
||||||
|
on the swarm. See section 8 (DNS cutover).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Nextcloud — migrate cron job to sidecar container
|
||||||
|
|
||||||
|
**Current state (CT 105):** `modules/docker/nextcloud-cron-job.nix` runs a
|
||||||
|
systemd timer every 5 minutes that calls:
|
||||||
|
```bash
|
||||||
|
docker exec nextcloud-webapp php ./cron.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Swarm problem:** `docker exec` only works against the local daemon. If
|
||||||
|
Nextcloud is scheduled on the other swarm node, the exec fails silently and
|
||||||
|
cron never runs.
|
||||||
|
|
||||||
|
**Swarm approach:** Add a `nextcloud-cron` sidecar container to the Nextcloud
|
||||||
|
stack definition, pinned to the same node as the main Nextcloud container via
|
||||||
|
placement constraints.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
**2a.** Choose which swarm node will host Nextcloud (e.g. `ha-docker-1`).
|
||||||
|
Label that node:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On either swarm manager:
|
||||||
|
docker node update --label-add nextcloud=true ha-docker-1
|
||||||
|
```
|
||||||
|
|
||||||
|
**2b.** In the Nextcloud stack compose file, add the sidecar and pin both
|
||||||
|
services to the labelled node:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
nextcloud-webapp:
|
||||||
|
image: nextcloud:production # pin same version as CT 105
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.labels.nextcloud == true
|
||||||
|
# ... existing volumes, env, networks ...
|
||||||
|
|
||||||
|
nextcloud-cron:
|
||||||
|
image: nextcloud:production # same image, different entrypoint
|
||||||
|
entrypoint: /cron.sh
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.labels.nextcloud == true # must co-locate with webapp
|
||||||
|
volumes:
|
||||||
|
# Same data volume as nextcloud-webapp so cron sees the same files.
|
||||||
|
- nextcloud-data:/var/www/html
|
||||||
|
# No ports exposed — cron only runs PHP inside the container.
|
||||||
|
```
|
||||||
|
|
||||||
|
`/cron.sh` is Nextcloud's built-in cron entrypoint. It runs
|
||||||
|
`php -f /var/www/html/cron.php` in a loop, sleeping for 5 minutes between
|
||||||
|
runs — identical to the current systemd timer.
|
||||||
|
|
||||||
|
**2c.** Migrate Nextcloud's data volume to the swarm:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mnt/docker/volumes/nextcloud-data/ → already on NFS, no migration needed
|
||||||
|
/mnt/docker/databases/nextcloud/ → already on NFS, no migration needed
|
||||||
|
```
|
||||||
|
|
||||||
|
The NFS paths are identical on the swarm nodes (`mount-data.nix` mounts the
|
||||||
|
same shares from the same VIP). Stop Nextcloud on CT 105, deploy on the
|
||||||
|
swarm, confirm it starts cleanly.
|
||||||
|
|
||||||
|
**2d.** Remove `nextcloud-cron-job.nix` from `modules/build-types/docker.nix`
|
||||||
|
and rebuild CT 105 after confirming Nextcloud works on the swarm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. docker-health-to-gotify — update for swarm awareness
|
||||||
|
|
||||||
|
**Current state (CT 105):** The script at
|
||||||
|
`/home/nixos/docker/monitoring/gotify/docker-health-to-gotify.sh` runs every
|
||||||
|
minute, calls `docker ps --filter health=unhealthy`, and notifies Gotify.
|
||||||
|
|
||||||
|
**Swarm behaviour:** The same script runs on both swarm nodes independently,
|
||||||
|
each monitoring its own local Docker daemon. This gives per-node coverage
|
||||||
|
across the swarm.
|
||||||
|
|
||||||
|
**Changes needed in the script** (edit the copy on the NFS volume — it takes
|
||||||
|
effect on both nodes simultaneously on the next timer fire):
|
||||||
|
|
||||||
|
### 3a. Strip the Swarm task suffix from service names
|
||||||
|
|
||||||
|
In swarm mode, `docker ps --format '{{.Names}}'` returns names like
|
||||||
|
`nextcloud-webapp.1.abc123xyz`. The notification should show `nextcloud-webapp`,
|
||||||
|
not the full task name.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Before:
|
||||||
|
CONTAINER_NAME=$(docker ps --format '{{.Names}}' ...)
|
||||||
|
|
||||||
|
# After:
|
||||||
|
CONTAINER_NAME=$(docker ps --format '{{.Names}}' ... | cut -d. -f1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3b. Include the reporting node in the Gotify message
|
||||||
|
|
||||||
|
Add `$(hostname)` to the notification payload so you know which swarm node
|
||||||
|
detected the problem:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MESSAGE="[$(hostname)] ${CONTAINER_NAME} is unhealthy"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3c. Extend to catch swarm service replica failures
|
||||||
|
|
||||||
|
`docker ps` only shows what's running locally. If a service has zero healthy
|
||||||
|
replicas (task crash-looping) it may not show up on either node's `docker ps`
|
||||||
|
at the same moment. Add a swarm-level check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run only on managers (both ha-docker nodes are managers):
|
||||||
|
if docker info --format '{{.Swarm.ControlAvailable}}' 2>/dev/null | grep -q true; then
|
||||||
|
# Find services where running replicas < desired replicas
|
||||||
|
docker service ls --format '{{.Name}}\t{{.Replicas}}' | \
|
||||||
|
awk -F'\t' '$2 !~ /^[0-9]+\/[0-9]+$/ || split($2,a,"/") && a[1] < a[2] { print $1, $2 }' | \
|
||||||
|
while read -r svc_name replicas; do
|
||||||
|
# Send Gotify notification for degraded service
|
||||||
|
curl -s -X POST "${GOTIFY_URL}/message" \
|
||||||
|
-H "X-Gotify-Key: ${GOTIFY_TOKEN}" \
|
||||||
|
-d "title=Swarm service degraded" \
|
||||||
|
-d "message=[$(hostname)] ${svc_name}: ${replicas} replicas"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
This catches the case where a service's desired replicas are not running
|
||||||
|
(e.g. OOM kill, image pull failure) — a failure mode that doesn't produce a
|
||||||
|
Docker health event on any node.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Passbolt migration
|
||||||
|
|
||||||
|
Passbolt has strict data integrity requirements. Migrate with care:
|
||||||
|
|
||||||
|
1. **Backup first** — `docker exec passbolt-webapp php /usr/share/php/passbolt/bin/cake passbolt export_keys` and a database dump.
|
||||||
|
2. Database is on NFS (`/mnt/docker/databases/passbolt/`) — no data copy needed.
|
||||||
|
3. Pin Passbolt to a specific node: `docker node update --label-add passbolt=true ha-docker-1`
|
||||||
|
4. Add placement constraint `node.labels.passbolt == true` to the Passbolt stack.
|
||||||
|
5. Stop on CT 105, deploy on swarm, verify login works.
|
||||||
|
6. Test email delivery and 2FA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Gitea migration
|
||||||
|
|
||||||
|
Gitea's data directory is on NFS (`/mnt/docker/volumes/gitea-data/`).
|
||||||
|
|
||||||
|
1. Stop Gitea on CT 105: `docker stop gitea`
|
||||||
|
2. Deploy to swarm with placement constraint (pin to `ha-docker-1` initially).
|
||||||
|
3. Verify web UI and SSH clone/push work.
|
||||||
|
4. Update DNS: `gitea.lan.ddnsgeek.com` → swarm Traefik endpoint.
|
||||||
|
5. Update the flake remote URL in `variables.nix` (`giteaDomain`) if the address changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Other services
|
||||||
|
|
||||||
|
Deploy remaining services (Grafana, InfluxDB, NodeRed, Prometheus, etc.)
|
||||||
|
as swarm stacks. Most have no special migration concern — they use NFS
|
||||||
|
volumes already on the shared storage.
|
||||||
|
|
||||||
|
Services with stateful databases (PostgreSQL, MariaDB) should follow the
|
||||||
|
pattern: stop on CT 105, confirm NFS database directory is intact, deploy on
|
||||||
|
swarm, verify.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Monitoring — Beszel
|
||||||
|
|
||||||
|
The Beszel hub runs on CT 105 (`docker.sweet.home:8090`). Both swarm nodes
|
||||||
|
run `beszel-agent` (from `modules/beszel/enable-agent.nix`), pointing at the
|
||||||
|
existing hub URL.
|
||||||
|
|
||||||
|
No migration needed for Beszel itself during the container migration. Once
|
||||||
|
all services are on the swarm, you may wish to move the Beszel hub too (as a
|
||||||
|
swarm service with a placement constraint) but this is optional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. DNS cutover
|
||||||
|
|
||||||
|
When a service is confirmed working on the swarm, update the FreeIPA DNS
|
||||||
|
A record from the CT 105 IP (192.168.2.225) to a swarm node IP or, when a
|
||||||
|
shared Traefik frontend is in place, to a round-robin record across both nodes.
|
||||||
|
|
||||||
|
**Recommended approach — Traefik as the single entry point:**
|
||||||
|
|
||||||
|
```
|
||||||
|
service.lan.ddnsgeek.com → Traefik on swarm (global mode)
|
||||||
|
docker.sweet.home → keep as 192.168.2.225 (CT 105) until fully decommissioned
|
||||||
|
```
|
||||||
|
|
||||||
|
For LAN-only services using `*.sweet.home` names, update FreeIPA directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On domain-controller (or via SSH):
|
||||||
|
ipa dnsrecord-mod sweet.home nextcloud --a-rec=192.168.2.230
|
||||||
|
# Add 192.168.2.231 as a second A record for round-robin (optional):
|
||||||
|
ipa dnsrecord-add sweet.home nextcloud --a-rec=192.168.2.231
|
||||||
|
```
|
||||||
|
|
||||||
|
Services behind Traefik don't need their own DNS updates — only Traefik's
|
||||||
|
own entry point IPs need to change.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. NixOS cleanup — CT 105
|
||||||
|
|
||||||
|
Once all services are migrated:
|
||||||
|
|
||||||
|
**Remove from `modules/build-types/docker.nix`:**
|
||||||
|
- `../docker/nextcloud-cron-job.nix` — replaced by sidecar container
|
||||||
|
- `../traefik/rotate-logs.nix` — replaced by Docker log driver
|
||||||
|
|
||||||
|
**Keep in `modules/build-types/docker.nix` until CT 105 is decommissioned:**
|
||||||
|
- `../docker/docker-health-to-gotify.nix` — still monitors CT 105's own daemon
|
||||||
|
- Everything else
|
||||||
|
|
||||||
|
**When decommissioning CT 105:**
|
||||||
|
1. Confirm all NFS volumes are in use only by swarm services (not CT 105).
|
||||||
|
2. Stop CT 105: `pct stop 105` on pve1.
|
||||||
|
3. Archive/remove the `lxc-docker` and `proxmox-docker` targets from `flake.nix`.
|
||||||
|
4. Remove `hosts/docker/`, `modules/build-types/docker.nix`, and `modules/docker/`.
|
||||||
|
5. Update `variables.nix` to remove `dockerIp`, `dockerStorageIp`, `dockerHost`
|
||||||
|
(or reassign `dockerHost` to point at a swarm node for Beszel hub resolution).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Any stack can be rolled back to CT 105 independently:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On CT 105:
|
||||||
|
docker start <service-name>
|
||||||
|
# Update DNS A record back to 192.168.2.225
|
||||||
|
ipa dnsrecord-mod sweet.home <service> --a-rec=192.168.2.225
|
||||||
|
```
|
||||||
|
|
||||||
|
CT 105 remains running throughout the cutover. Only decommission it after
|
||||||
|
every service is confirmed stable on the swarm and you have run one full
|
||||||
|
backup cycle from the new hosts.
|
||||||
+47
-5
@@ -7,6 +7,10 @@
|
|||||||
| LAN | 2 (native/untagged) | `192.168.2.0/24` | General LAN — clients and infrastructure | Yes (gateway .254) |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
| Swarm cluster | 30 | `192.168.30.0/24` | Docker Swarm gossip (TCP/UDP 7946) + VXLAN overlay (UDP 4789) | No — internal `vmbr3` only, no uplink |
|
||||||
|
|
||||||
|
When expanded to a second Proxmox node, VLAN 10 (cluster), VLAN 20 (storage-client), and VLAN 30 (swarm) all share
|
||||||
|
the same inter-node trunk NIC via 802.1q VLAN tagging — different VLAN IDs, same physical cable.
|
||||||
|
|
||||||
The cluster and storage-client subnets never leave pve1. `vmbr1` and `vmbr2` are Proxmox Linux
|
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.
|
bridges with no physical port attached; traffic between guests on each bridge stays in-kernel.
|
||||||
@@ -27,9 +31,9 @@ is always `.228`: `192.168.2.228` (LAN), `192.168.10.228` (cluster), `192.168.20
|
|||||||
|
|
||||||
## DNS Zones
|
## DNS Zones
|
||||||
|
|
||||||
FreeIPA (domain-controller.sweet.home) is authoritative for all zones. Three
|
FreeIPA (domain-controller.sweet.home) is authoritative for all zones.
|
||||||
zones correspond to the three subnets — one per VLAN. All zones are internal
|
|
||||||
only; no external delegation.
|
Four zones correspond to the four subnets. All zones are internal only; no external delegation.
|
||||||
|
|
||||||
### sweet.home — VLAN 2 (192.168.2.x)
|
### sweet.home — VLAN 2 (192.168.2.x)
|
||||||
|
|
||||||
@@ -132,12 +136,14 @@ All VMs and LXC containers run on pve1.
|
|||||||
| `192.168.2.228` | ha-node1 | HA file server node 1 — management NIC | 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.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.226` | server | Former NFS/ZFS file server — decommissioned | Removed from flake |
|
||||||
| `192.168.2.225` | docker | Docker / Traefik stack | Active |
|
| `192.168.2.225` | docker | Docker / Traefik stack (CT 105 — existing single-host) | Active |
|
||||||
| `192.168.2.224` | nix-cache | Nix binary cache + remote builder | 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 |
|
| `192.168.2.223` | pxe-boot | PXE / TFTP / HTTP netboot server | Active |
|
||||||
| `192.168.2.222` | tailscale-router | Tailscale exit node / router | Active |
|
| `192.168.2.222` | tailscale-router | Tailscale exit node / router | Active |
|
||||||
| `192.168.2.221` | tor-relay | Tor relay | Active |
|
| `192.168.2.221` | tor-relay | Tor relay | Active |
|
||||||
| `192.168.2.220` | pdm | Proxmox Deploy Manager | Active |
|
| `192.168.2.220` | pdm | Proxmox Deploy Manager | Active |
|
||||||
|
| `192.168.2.231` | ha-docker-2 | Docker Swarm node 2 — management NIC | Active |
|
||||||
|
| `192.168.2.230` | ha-docker-1 | Docker Swarm node 1 — management NIC | Active |
|
||||||
|
|
||||||
### Client DHCP pool (.10–.59)
|
### Client DHCP pool (.10–.59)
|
||||||
|
|
||||||
@@ -175,7 +181,9 @@ Internal to pve1 only. Proxmox bridge `vmbr2`, no physical NIC attached.
|
|||||||
| `192.168.20.228` | ha-node1 | Storage-client NIC (ens20 / vmbr2) |
|
| `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.227` | ha-node2 | Storage-client NIC (ens20 / vmbr2) |
|
||||||
| `192.168.20.226` | server | Storage-client NIC (ens19 / vmbr2) — decommissioned |
|
| `192.168.20.226` | server | Storage-client NIC (ens19 / vmbr2) — decommissioned |
|
||||||
| `192.168.20.225` | docker | Storage-client NIC (eth1 / vmbr2) — NFS client |
|
| `192.168.20.225` | docker | Storage-client NIC (eth1 / vmbr2) — NFS client (CT 105) |
|
||||||
|
| `192.168.20.231` | ha-docker-2 | Storage-client NIC (ens19 / vmbr2) — NFS client |
|
||||||
|
| `192.168.20.230` | ha-docker-1 | Storage-client NIC (ens19 / vmbr2) — NFS client |
|
||||||
| — | no gateway | Isolated — not routed to LAN or internet |
|
| — | no gateway | Isolated — not routed to LAN or internet |
|
||||||
|
|
||||||
NFS clients mount from `192.168.20.229` (surviving failover transparently via the VIP).
|
NFS clients mount from `192.168.20.229` (surviving failover transparently via the VIP).
|
||||||
@@ -184,3 +192,37 @@ cannot reach either service on this VIP. The `vip-storage` endpoint is not reach
|
|||||||
from the workstation directly (internal bridge only); health checks proxy through the
|
from the workstation directly (internal bridge only); health checks proxy through the
|
||||||
active HA node.
|
active HA node.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Swarm cluster network — VLAN 30 — 192.168.30.0/24
|
||||||
|
|
||||||
|
Internal to pve1 only. Proxmox bridge `vmbr3`, no physical NIC attached.
|
||||||
|
Carries Docker Swarm inter-node traffic only: Raft consensus (TCP 2377),
|
||||||
|
Serf gossip (TCP/UDP 7946), and VXLAN overlay data path (UDP 4789).
|
||||||
|
Docker Swarm is initialised with `--advertise-addr` and `--data-path-addr`
|
||||||
|
both pointing to this subnet so all cluster traffic stays on `vmbr3` and
|
||||||
|
never crosses the LAN.
|
||||||
|
|
||||||
|
| IP | Hostname | Interface / role |
|
||||||
|
|---|---|---|
|
||||||
|
| `192.168.30.231` | ha-docker-2 | Swarm cluster NIC (ens20 / vmbr3) |
|
||||||
|
| `192.168.30.230` | ha-docker-1 | Swarm cluster NIC (ens20 / vmbr3) |
|
||||||
|
| — | no gateway | Isolated — not routed to LAN or internet |
|
||||||
|
|
||||||
|
### DNS zone: `swarm.home` — VLAN 30 (192.168.30.x)
|
||||||
|
|
||||||
|
| Hostname | A record | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `ha-docker-1.swarm.home` | `192.168.30.230` | Swarm NIC — debugging only |
|
||||||
|
| `ha-docker-2.swarm.home` | `192.168.30.231` | Swarm NIC — debugging only |
|
||||||
|
|
||||||
|
Operators reach the Docker API on the LAN IPs (`192.168.2.230`/`.231`), not these addresses.
|
||||||
|
The `swarm.home` records exist for diagnostic convenience (e.g. confirming `vmbr3` routing).
|
||||||
|
|
||||||
|
### Multi-node Proxmox expansion
|
||||||
|
|
||||||
|
When a second Proxmox node (pve2) is added, VLAN 10 (cluster), VLAN 20 (storage-client),
|
||||||
|
and VLAN 30 (swarm) all extend to pve2 via 802.1q VLAN tagging on the inter-node trunk
|
||||||
|
link. All three internal networks share the same physical NIC between hypervisors —
|
||||||
|
VLAN tags provide the logical separation.
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,9 @@
|
|||||||
|
|
||||||
proxmox-ha-server-1 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-1/host.nix; nameSuffix = "-1"; };
|
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-2 = mkTarget { platform = "proxmox"; buildType = "ha-server"; hostPath = ./hosts/ha-server-2/host.nix; nameSuffix = "-2"; };
|
||||||
|
|
||||||
|
proxmox-ha-docker-1 = mkTarget { platform = "proxmox"; buildType = "ha-docker"; hostPath = ./hosts/ha-docker-1/host.nix; nameSuffix = "-1"; };
|
||||||
|
proxmox-ha-docker-2 = mkTarget { platform = "proxmox"; buildType = "ha-docker"; hostPath = ./hosts/ha-docker-2/host.nix; nameSuffix = "-2"; };
|
||||||
};
|
};
|
||||||
|
|
||||||
# Auto-install environments (migrated from the former nix-auto-installer
|
# Auto-install environments (migrated from the former nix-auto-installer
|
||||||
@@ -200,7 +203,11 @@
|
|||||||
(modulesPath + "/installer/netboot/netboot-minimal.nix")
|
(modulesPath + "/installer/netboot/netboot-minimal.nix")
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
{ networking.hostName = "nixos-minimal"; }
|
{
|
||||||
|
networking.hostName = "nixos-minimal";
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
boot.zfs.forceImportRoot = false;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{ vars, ... }:
|
||||||
|
{
|
||||||
|
networking = {
|
||||||
|
hostName = vars.haDocker1Host;
|
||||||
|
hostId = "a1d0c4e1";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces = {
|
||||||
|
# ens18 — LAN management NIC (vmbr0, 192.168.2.0/24)
|
||||||
|
${vars.vmLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haDocker1Ip;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
# ens19 — storage-client NIC (vmbr2, 192.168.20.0/24) — NFS from HA cluster
|
||||||
|
${vars.haDockerStorageInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haDocker1StorageIp;
|
||||||
|
prefixLength = vars.haClientPrefixLength;
|
||||||
|
}];
|
||||||
|
# ens20 — swarm cluster NIC (vmbr3, 192.168.30.0/24) — Docker gossip + VXLAN
|
||||||
|
${vars.haDockerSwarmInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haDocker1SwarmIp;
|
||||||
|
prefixLength = vars.haDockerSwarmPrefixLength;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Only register the LAN IP with IPA DNS. Without this, sssd dyndns
|
||||||
|
# would also register Docker bridge IPs (172.x.x.x) and the storage/swarm
|
||||||
|
# NIC IPs as A records for ha-docker-1.sweet.home.
|
||||||
|
security.ipa.dyndns.interface = vars.vmLanInterface;
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{ vars, ... }:
|
||||||
|
{
|
||||||
|
networking = {
|
||||||
|
hostName = vars.haDocker2Host;
|
||||||
|
hostId = "a2d0c4e2";
|
||||||
|
useDHCP = false;
|
||||||
|
interfaces = {
|
||||||
|
# ens18 — LAN management NIC (vmbr0, 192.168.2.0/24)
|
||||||
|
${vars.vmLanInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haDocker2Ip;
|
||||||
|
prefixLength = vars.lanPrefixLength;
|
||||||
|
}];
|
||||||
|
# ens19 — storage-client NIC (vmbr2, 192.168.20.0/24) — NFS from HA cluster
|
||||||
|
${vars.haDockerStorageInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haDocker2StorageIp;
|
||||||
|
prefixLength = vars.haClientPrefixLength;
|
||||||
|
}];
|
||||||
|
# ens20 — swarm cluster NIC (vmbr3, 192.168.30.0/24) — Docker gossip + VXLAN
|
||||||
|
${vars.haDockerSwarmInterface}.ipv4.addresses = [{
|
||||||
|
address = vars.haDocker2SwarmIp;
|
||||||
|
prefixLength = vars.haDockerSwarmPrefixLength;
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
defaultGateway = { address = vars.lanGateway; interface = vars.vmLanInterface; };
|
||||||
|
nameservers = [ vars.domainControllerIp ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Only register the LAN IP with IPA DNS — same reasoning as ha-docker-1.
|
||||||
|
security.ipa.dyndns.interface = vars.vmLanInterface;
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Docker Swarm node build type.
|
||||||
|
#
|
||||||
|
# Produces NixOS hosts that form a Docker Swarm manager cluster. Two nodes
|
||||||
|
# (ha-docker-1, ha-docker-2) are both managers so either can accept Docker
|
||||||
|
# API and `docker stack` commands.
|
||||||
|
#
|
||||||
|
# Key differences from the existing `docker` build type (used by CT 105):
|
||||||
|
# - nextcloud-cron-job.nix is EXCLUDED — `docker exec` breaks in swarm
|
||||||
|
# because the target container may be on the other node. The cron job
|
||||||
|
# is replaced by a nextcloud-cron sidecar in the Nextcloud stack.
|
||||||
|
# See docs/internal/docker-swarm-cutover.md.
|
||||||
|
# - traefik/rotate-logs.nix is EXCLUDED — log rotation moves to Docker's
|
||||||
|
# json-file log driver (max-size/max-file on the Traefik service
|
||||||
|
# definition). See docs/internal/docker-swarm-cutover.md.
|
||||||
|
# - raspi/mount-data.nix is EXCLUDED — specific to CT 105's backup role.
|
||||||
|
# - Swarm firewall ports (2377/tcp, 7946/tcp+udp, 4789/udp) are opened
|
||||||
|
# on the swarm NIC (ens20/vmbr3) only.
|
||||||
|
# - checkReversePath = "loose" is required for the Swarm ingress routing
|
||||||
|
# mesh: VXLAN return traffic is asymmetric (arrives ens20, exits ens18).
|
||||||
|
# - beszel-agent is enabled for host-level monitoring.
|
||||||
|
{ pkgs, vars, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Pin Docker Engine to version 29, matching CT 105, so image layers cached
|
||||||
|
# on NFS volumes remain compatible across old and new hosts.
|
||||||
|
nixpkgs.overlays = [
|
||||||
|
(final: prev: {
|
||||||
|
docker = prev.docker_29;
|
||||||
|
docker_cli = prev.docker_29;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
../docker/enable-service.nix
|
||||||
|
../docker/mount-data.nix
|
||||||
|
../docker/docker-health-to-gotify.nix
|
||||||
|
../beszel/enable-agent.nix
|
||||||
|
../services/enable-rpcbind.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
nfs-utils
|
||||||
|
];
|
||||||
|
|
||||||
|
boot.supportedFilesystems = [ "nfs" ];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
# Symlink ~/docker → NFS config mount so the docker-health-to-gotify
|
||||||
|
# script (and operator convenience) resolves ~/docker/... correctly.
|
||||||
|
"L+ /home/${vars.primaryUser}/docker - - - - ${vars.nfsShares.dockerConfig.mountpoint}"
|
||||||
|
"d /mnt/docker 0755 ${vars.primaryUser} users -"
|
||||||
|
];
|
||||||
|
|
||||||
|
users.users.${vars.primaryUser}.extraGroups = [ "docker" ];
|
||||||
|
|
||||||
|
networking.firewall = {
|
||||||
|
# LAN-facing service ports — same as the existing docker build type.
|
||||||
|
allowedTCPPorts = [
|
||||||
|
vars.ports.dockerHttp
|
||||||
|
vars.ports.dockerHttps
|
||||||
|
vars.ports.dockerExtra
|
||||||
|
vars.ports.beszelHub
|
||||||
|
];
|
||||||
|
|
||||||
|
# Swarm inter-node ports restricted to the swarm NIC (ens20/vmbr3).
|
||||||
|
# vmbr3 is an isolated internal bridge — no LAN reachability.
|
||||||
|
interfaces.${vars.haDockerSwarmInterface} = {
|
||||||
|
allowedTCPPorts = [
|
||||||
|
vars.ports.dockerSwarmMgmt # 2377 — Raft + cluster management
|
||||||
|
vars.ports.dockerSwarmDisc # 7946 — Serf gossip (TCP half)
|
||||||
|
];
|
||||||
|
allowedUDPPorts = [
|
||||||
|
vars.ports.dockerSwarmDisc # 7946 — Serf gossip (UDP half)
|
||||||
|
vars.ports.dockerSwarmVxlan # 4789 — VXLAN overlay data path
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Docker Swarm ingress routing mesh creates asymmetric routes: a request
|
||||||
|
# arrives on ens18 (LAN) for a container that lives on ens20's VXLAN
|
||||||
|
# overlay; the return path differs from the incoming interface. Strict
|
||||||
|
# rp_filter drops these packets. "loose" allows them.
|
||||||
|
checkReversePath = "loose";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,13 +5,13 @@ let
|
|||||||
sudo nixos-rebuild switch \
|
sudo nixos-rebuild switch \
|
||||||
--no-write-lock-file \
|
--no-write-lock-file \
|
||||||
--refresh \
|
--refresh \
|
||||||
--flake git+https://${vars.giteaDomain}/${vars.giteaRepoPath}.git#$(cat /etc/flake-target)
|
--flake git+https://${vars.giteaDomain}/${vars.giteaRepoPath}.git?dir=${vars.giteaRepoFlakePath}#$(cat /etc/flake-target)
|
||||||
'';
|
'';
|
||||||
testCmd = ''
|
testCmd = ''
|
||||||
sudo nixos-rebuild test \
|
sudo nixos-rebuild test \
|
||||||
--no-write-lock-file \
|
--no-write-lock-file \
|
||||||
--refresh \
|
--refresh \
|
||||||
--flake git+https://${vars.giteaDomain}/${vars.giteaRepoPath}.git#$(cat /etc/flake-target)
|
--flake git+https://${vars.giteaDomain}/${vars.giteaRepoPath}.git?dir=${vars.giteaRepoFlakePath}#$(cat /etc/flake-target)
|
||||||
'';
|
'';
|
||||||
buildImageFn = ''
|
buildImageFn = ''
|
||||||
buildImage() {
|
buildImage() {
|
||||||
@@ -71,18 +71,19 @@ in
|
|||||||
"root-hashedPassword".neededForUsers = true;
|
"root-hashedPassword".neededForUsers = true;
|
||||||
"nixos-hashedPassword".neededForUsers = true;
|
"nixos-hashedPassword".neededForUsers = true;
|
||||||
"nix-github-token" = { };
|
"nix-github-token" = { };
|
||||||
|
"nix-gitea-token" = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
# nix.conf has no *File-style option for access-tokens, so the token is
|
# nix.conf has no *File-style option for access-tokens, so tokens are
|
||||||
# rendered into a runtime-only file (never touches the Nix store) and
|
# rendered into a runtime-only file (never touches the Nix store) and
|
||||||
# pulled in via nix.conf's native !include directive.
|
# pulled in via nix.conf's native !include directive.
|
||||||
templates."nix-github-token.conf".content = ''
|
templates."nix-access-tokens.conf".content = ''
|
||||||
access-tokens = github.com=${config.sops.placeholder."nix-github-token"}
|
access-tokens = github.com=${config.sops.placeholder."nix-github-token"} ${vars.giteaDomain}=${config.sops.placeholder."nix-gitea-token"}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
nix.extraOptions = ''
|
nix.extraOptions = ''
|
||||||
!include ${config.sops.templates."nix-github-token.conf".path}
|
!include ${config.sops.templates."nix-access-tokens.conf".path}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
users = {
|
users = {
|
||||||
|
|||||||
Executable
+595
@@ -0,0 +1,595 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy.sh — Full lifecycle management for the Docker Swarm HA cluster.
|
||||||
|
#
|
||||||
|
# Provisions two NixOS Proxmox VMs (ha-docker-1, ha-docker-2) as dual-manager
|
||||||
|
# Docker Swarm nodes sharing NFS storage from the existing HA file-server
|
||||||
|
# cluster. Both nodes are managers so either can accept Docker API and
|
||||||
|
# `docker stack` commands.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/docker-swarm/deploy.sh [options]
|
||||||
|
# scripts/docker-swarm/deploy.sh --destroy [options]
|
||||||
|
#
|
||||||
|
# Phases (all run by default; skip any with --skip-<phase>):
|
||||||
|
# 1. ensure-bridge Create vmbr3 (swarm cluster bridge) on the Proxmox node.
|
||||||
|
# 2. sync-keys Generate SSH host keys for both nodes (clan vars).
|
||||||
|
# 3. ipa-hosts Create IPA host objects + sops-encrypted keytabs.
|
||||||
|
# 4. create-vms Build NixOS disk images and create VMs via create-proxmox-resource.sh.
|
||||||
|
# 5. add-hardware Attach vmbr2 (storage) and vmbr3 (swarm) NICs; start VMs.
|
||||||
|
# 6. boot-wait Wait for SSH on both LAN IPs.
|
||||||
|
# 7. refresh-sops-keys Detect disko key drift; re-encrypt secrets; commit.
|
||||||
|
# 8. init-swarm docker swarm init on node1; manager join on node2; label nodes.
|
||||||
|
# 9. dns Register storage.home and swarm.home A records in FreeIPA.
|
||||||
|
# 10. verify docker node ls; NFS mount check; swarm health.
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --node <host> Proxmox host (default: pve1.sweet.home)
|
||||||
|
# --vmid1 <n> VMID for ha-docker-1 (default: 202)
|
||||||
|
# --vmid2 <n> VMID for ha-docker-2 (default: 203)
|
||||||
|
# --storage <pool> Proxmox storage pool (default: local-zfs)
|
||||||
|
# --swarm-bridge <br> Bridge for Docker Swarm cluster network (default: vmbr3)
|
||||||
|
# --storage-bridge <br> Bridge for NFS storage network (default: vmbr2)
|
||||||
|
# --memory <MB> RAM per node (default: 4096)
|
||||||
|
# --cores <n> vCPUs per node (default: 4)
|
||||||
|
# --skip-ensure-bridge Skip vmbr3 creation/check
|
||||||
|
# --skip-sync-keys Skip sync-host-keys.sh (clan vars already exist)
|
||||||
|
# --skip-ipa-hosts Skip IPA host account creation (keytabs already exist)
|
||||||
|
# --skip-create-vms Skip VM creation (VMs already exist)
|
||||||
|
# --skip-add-hardware Skip NIC attachment (already attached)
|
||||||
|
# --skip-boot-wait Skip boot/SSH wait (VMs already running)
|
||||||
|
# --skip-refresh-sops-keys Skip sops host-key drift fix
|
||||||
|
# --skip-init-swarm Skip swarm initialisation (already initialised)
|
||||||
|
# --skip-dns Skip FreeIPA DNS record creation
|
||||||
|
# --skip-verify Skip post-deploy health checks
|
||||||
|
# --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).
|
||||||
|
# - SSH access to domain-controller.sweet.home as $PROXMOX_SSH_USER for DNS phase.
|
||||||
|
# - For --skip-sync-keys: clan vars already in vars/per-machine/proxmox-ha-docker-{1,2}/.
|
||||||
|
# - For --skip-ipa-hosts: secrets/ha-docker-{1,2}.keytab already exist and are committed.
|
||||||
|
|
||||||
|
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="${PVE1_HOST}" # deploy.sh targets pve1 by default (authorised for this cluster)
|
||||||
|
VMID1=202
|
||||||
|
VMID2=203
|
||||||
|
STORAGE="${PROXMOX_STORAGE:-local-zfs}"
|
||||||
|
SWARM_BRIDGE="vmbr3"
|
||||||
|
STORAGE_BRIDGE="vmbr2"
|
||||||
|
MEMORY_MB=4096
|
||||||
|
CORES=4
|
||||||
|
|
||||||
|
SKIP_ENSURE_BRIDGE=false
|
||||||
|
SKIP_SYNC_KEYS=false
|
||||||
|
SKIP_IPA_HOSTS=false
|
||||||
|
SKIP_CREATE_VMS=false
|
||||||
|
SKIP_ADD_HARDWARE=false
|
||||||
|
SKIP_BOOT_WAIT=false
|
||||||
|
SKIP_REFRESH_SOPS_KEYS=false
|
||||||
|
SKIP_INIT_SWARM=false
|
||||||
|
SKIP_DNS=false
|
||||||
|
SKIP_VERIFY=false
|
||||||
|
FORCE_REBUILD=false
|
||||||
|
DESTROY=false
|
||||||
|
DRY_RUN=false
|
||||||
|
|
||||||
|
# ── Variables from repo (mirrors variables.nix) ───────────────────────────────
|
||||||
|
|
||||||
|
NODE1_HOST="ha-docker-1"
|
||||||
|
NODE2_HOST="ha-docker-2"
|
||||||
|
NODE1_LAN_IP="192.168.2.230"
|
||||||
|
NODE2_LAN_IP="192.168.2.231"
|
||||||
|
NODE1_SWARM_IP="192.168.30.230"
|
||||||
|
NODE2_SWARM_IP="192.168.30.231"
|
||||||
|
NODE1_STORAGE_IP="192.168.20.230"
|
||||||
|
NODE2_STORAGE_IP="192.168.20.231"
|
||||||
|
SWARM_CIDR="192.168.30.0/24"
|
||||||
|
STORAGE_CIDR="192.168.20.0/24"
|
||||||
|
STORAGE_ZONE="storage.home"
|
||||||
|
SWARM_ZONE="swarm.home"
|
||||||
|
SSH_USER="${PROXMOX_SSH_USER:-wayne}"
|
||||||
|
DC_HOST="${IPA_SERVER:-domain-controller.sweet.home}"
|
||||||
|
|
||||||
|
# ── 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 ;;
|
||||||
|
--swarm-bridge) SWARM_BRIDGE="$2"; shift 2 ;;
|
||||||
|
--storage-bridge) STORAGE_BRIDGE="$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-ipa-hosts) SKIP_IPA_HOSTS=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-init-swarm) SKIP_INIT_SWARM=true; shift ;;
|
||||||
|
--skip-dns) SKIP_DNS=true; shift ;;
|
||||||
|
--skip-verify) SKIP_VERIFY=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() {
|
||||||
|
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() {
|
||||||
|
# Read-only probe — always executes even in dry-run.
|
||||||
|
ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" "sudo $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
SWARM_USER="nixos"
|
||||||
|
|
||||||
|
n1() {
|
||||||
|
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
|
||||||
|
"${SWARM_USER}@${NODE1_LAN_IP}" "$@" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
n2() {
|
||||||
|
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
|
||||||
|
"${SWARM_USER}@${NODE2_LAN_IP}" "$@" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
dc() {
|
||||||
|
# Run ipa commands on domain-controller as $SSH_USER.
|
||||||
|
if $DRY_RUN; then
|
||||||
|
echo "[dry-run] ssh ${SSH_USER}@${DC_HOST} $*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${DC_HOST}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "${SWARM_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 Docker Swarm 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 — swarm VMs destroyed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 1: Ensure swarm bridge ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_ENSURE_BRIDGE; then
|
||||||
|
log "Phase 1: Ensuring swarm bridge ${SWARM_BRIDGE} on ${NODE}"
|
||||||
|
if pve_check "test -d /sys/class/net/${SWARM_BRIDGE}" &>/dev/null; then
|
||||||
|
logn "${SWARM_BRIDGE} already exists — skipping."
|
||||||
|
else
|
||||||
|
logn "Creating isolated internal bridge ${SWARM_BRIDGE} (no upstream port, ${SWARM_CIDR})"
|
||||||
|
BRIDGE_CONF="auto ${SWARM_BRIDGE}
|
||||||
|
iface ${SWARM_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/${SWARM_BRIDGE}.conf and ifup it"
|
||||||
|
else
|
||||||
|
ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" \
|
||||||
|
"echo '${BRIDGE_CONF}' | sudo tee /etc/network/interfaces.d/${SWARM_BRIDGE}.conf > /dev/null && sudo ifup ${SWARM_BRIDGE}"
|
||||||
|
logn "${SWARM_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 swarm targets"
|
||||||
|
for target in proxmox-ha-docker-1 proxmox-ha-docker-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 3: IPA host accounts ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_IPA_HOSTS; then
|
||||||
|
log "Phase 3: Creating IPA host accounts and keytabs"
|
||||||
|
IPA_SCRIPT="${REPO_ROOT}/scripts/ipa/create-nixos-ipa-host-account.sh"
|
||||||
|
for host in "${NODE1_HOST}" "${NODE2_HOST}"; do
|
||||||
|
KEYTAB="${REPO_ROOT}/secrets/${host}.keytab"
|
||||||
|
if [[ -f "$KEYTAB" ]]; then
|
||||||
|
logn "Keytab for ${host} already exists — skipping."
|
||||||
|
else
|
||||||
|
logn "Creating IPA host account and keytab for ${host}..."
|
||||||
|
run bash "$IPA_SCRIPT" "$host"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! $DRY_RUN; then
|
||||||
|
# Keytabs must be committed and pushed before VMs rebuild from Gitea.
|
||||||
|
CURRENT_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
|
||||||
|
logn "Committing keytabs and pushing to Gitea (branch: ${CURRENT_BRANCH})..."
|
||||||
|
(cd "${REPO_ROOT}" && \
|
||||||
|
git add secrets/ha-docker-1.keytab secrets/ha-docker-2.keytab .sops.yaml && \
|
||||||
|
git commit -m "secrets(ha-docker): add IPA keytabs for ha-docker-1 and ha-docker-2" || true && \
|
||||||
|
git push origin "${CURRENT_BRANCH}")
|
||||||
|
logn "Pushed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 3.5: Prepare Proxmox node for building ─────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_CREATE_VMS && ! $DRY_RUN; then
|
||||||
|
CURRENT_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
|
||||||
|
|
||||||
|
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..."
|
||||||
|
local_ssh "sudo chown -R ${SSH_USER} /nix"
|
||||||
|
logn "Done."
|
||||||
|
fi
|
||||||
|
unset -f local_ssh
|
||||||
|
|
||||||
|
# Use PROXMOX_REMOTE_REPO_DIR (from env.sh) so the build path is consistent
|
||||||
|
# with what create-proxmox-resource.sh will use. The default is
|
||||||
|
# /home/<user>/nixos (the standalone nixos repo clone on pve1), but can be
|
||||||
|
# overridden to e.g. /home/<user>/infrastructure/nixos when the infrastructure
|
||||||
|
# mono-repo is checked out on pve1 instead.
|
||||||
|
REMOTE_REPO="${PROXMOX_REMOTE_REPO_DIR:-/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 repo (${REMOTE_REPO}) is on '${REMOTE_BRANCH}', switching to '${CURRENT_BRANCH}'..."
|
||||||
|
if ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${NODE}" \
|
||||||
|
"cd ${REMOTE_REPO} && git fetch origin && git checkout '${CURRENT_BRANCH}' && git pull --ff-only" 2>&1; then
|
||||||
|
logn "Done."
|
||||||
|
else
|
||||||
|
logn "WARNING: branch switch failed — proceeding anyway (create-proxmox-resource.sh will retry)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
logn "Remote repo ${REMOTE_REPO} not found on ${NODE} — create-proxmox-resource.sh will clone it."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 4: Create VMs ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_CREATE_VMS; then
|
||||||
|
log "Phase 4: Building and creating swarm VMs on ${NODE}"
|
||||||
|
CREATE="${REPO_ROOT}/scripts/proxmox/create-proxmox-resource.sh"
|
||||||
|
REBUILD_FLAG=""
|
||||||
|
$FORCE_REBUILD && REBUILD_FLAG="--force-rebuild"
|
||||||
|
|
||||||
|
for spec in "${VMID1}:${NODE1_HOST}:proxmox-ha-docker-1" "${VMID2}:${NODE2_HOST}:proxmox-ha-docker-2"; do
|
||||||
|
IFS=: read -r vmid host_name flake_target <<< "$spec"
|
||||||
|
log "Creating ${flake_target} (VMID ${vmid}) on ${NODE}..."
|
||||||
|
# --force-rebuild is always passed: create-proxmox-resource.sh only calls
|
||||||
|
# sync_remote_host_keys (which bakes the clan-var SSH key into the disk
|
||||||
|
# image) when it actually builds. Reusing a cached image skips that step
|
||||||
|
# and leaves the VM unable to decrypt sops secrets 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 5: Add NICs and start VMs ──────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_ADD_HARDWARE; then
|
||||||
|
log "Phase 5: Attaching storage (${STORAGE_BRIDGE}) and swarm (${SWARM_BRIDGE}) NICs"
|
||||||
|
for vmid in "$VMID1" "$VMID2"; do
|
||||||
|
logn "VMID ${vmid}: stopping to add NICs..."
|
||||||
|
pve "qm stop ${vmid} --skiplock 1 2>/dev/null; sleep 3" || true
|
||||||
|
|
||||||
|
logn "Adding net1 (${STORAGE_BRIDGE} — NFS storage)..."
|
||||||
|
pve "qm set ${vmid} --net1 virtio,bridge=${STORAGE_BRIDGE},firewall=0"
|
||||||
|
|
||||||
|
logn "Adding net2 (${SWARM_BRIDGE} — Docker Swarm)..."
|
||||||
|
pve "qm set ${vmid} --net2 virtio,bridge=${SWARM_BRIDGE},firewall=0"
|
||||||
|
|
||||||
|
logn "Starting VMID ${vmid}..."
|
||||||
|
pve "qm start ${vmid}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 6: Wait for SSH ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_BOOT_WAIT; then
|
||||||
|
log "Phase 6: Waiting for both nodes to come up on LAN IPs"
|
||||||
|
wait_for_ssh "$NODE1_LAN_IP" "$NODE1_HOST"
|
||||||
|
wait_for_ssh "$NODE2_LAN_IP" "$NODE2_HOST"
|
||||||
|
logn "Both nodes are SSHable."
|
||||||
|
sleep 10 # let systemd finish activation
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 7: Refresh sops host-key registrations ─────────────────────────────
|
||||||
|
#
|
||||||
|
# Disko builds raw disk images: each new VM boots with a freshly-generated SSH
|
||||||
|
# host key, not the one pre-seeded in clan vars. Scan the running VMs; if
|
||||||
|
# their ed25519 keys differ from the clan var, update the clan var, rewrite
|
||||||
|
# the .sops.yaml anchor, and re-encrypt all affected sops files.
|
||||||
|
|
||||||
|
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 7: Refreshing sops host-key registrations (disko key drift fix)"
|
||||||
|
SOPS_UPDATED=false
|
||||||
|
|
||||||
|
for spec in \
|
||||||
|
"${NODE1_LAN_IP}:proxmox-ha-docker-1:${NODE1_HOST}" \
|
||||||
|
"${NODE2_LAN_IP}:proxmox-ha-docker-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 for ${node_ip} — skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
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
|
||||||
|
|
||||||
|
ANCHOR="${flake_target}"
|
||||||
|
NEW_AGE=$(echo "$SCANNED_PUBKEY" | \
|
||||||
|
nix run --quiet --no-warn-dirty nixpkgs#ssh-to-age 2>/dev/null)
|
||||||
|
[[ -z "$NEW_AGE" ]] && err "ssh-to-age produced no output for ${host_name}"
|
||||||
|
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-docker-1.keytab && \
|
||||||
|
$SOPS updatekeys -y secrets/ha-docker-2.keytab)
|
||||||
|
|
||||||
|
logn "Committing refreshed host keys and re-encrypted secrets..."
|
||||||
|
(cd "${REPO_ROOT}" && \
|
||||||
|
git add \
|
||||||
|
vars/per-machine/proxmox-ha-docker-1/openssh/ssh_host_ed25519_key.pub/value \
|
||||||
|
vars/per-machine/proxmox-ha-docker-2/openssh/ssh_host_ed25519_key.pub/value \
|
||||||
|
.sops.yaml \
|
||||||
|
secrets/common.yaml \
|
||||||
|
secrets/ha-docker-1.keytab \
|
||||||
|
secrets/ha-docker-2.keytab && \
|
||||||
|
git commit -m "secrets(ha-docker): refresh sops host-key registrations for new VM instances" || true)
|
||||||
|
logn "Sops keys refreshed and committed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 8: Initialise Docker Swarm ─────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_INIT_SWARM; then
|
||||||
|
log "Phase 8: Initialising Docker Swarm"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
logn "[dry-run] Would run: docker swarm init --advertise-addr ${NODE1_SWARM_IP} --data-path-addr ${NODE1_SWARM_IP} on ${NODE1_HOST}"
|
||||||
|
logn "[dry-run] Would join ${NODE2_HOST} as manager"
|
||||||
|
logn "[dry-run] Would label both nodes"
|
||||||
|
else
|
||||||
|
# Check if node1 is already a swarm manager.
|
||||||
|
if n1 "docker info --format '{{.Swarm.LocalNodeState}}'" 2>/dev/null | grep -qx "active"; then
|
||||||
|
logn "${NODE1_HOST} is already in a swarm — skipping init."
|
||||||
|
else
|
||||||
|
logn "Initialising swarm on ${NODE1_HOST} (advertise: ${NODE1_SWARM_IP})..."
|
||||||
|
n1 "docker swarm init \
|
||||||
|
--advertise-addr ${NODE1_SWARM_IP} \
|
||||||
|
--data-path-addr ${NODE1_SWARM_IP}"
|
||||||
|
logn "Swarm initialised on ${NODE1_HOST}."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if node2 is already joined.
|
||||||
|
if n2 "docker info --format '{{.Swarm.LocalNodeState}}'" 2>/dev/null | grep -qx "active"; then
|
||||||
|
logn "${NODE2_HOST} is already in the swarm — skipping join."
|
||||||
|
else
|
||||||
|
logn "Fetching manager join token from ${NODE1_HOST}..."
|
||||||
|
JOIN_TOKEN=$(n1 "docker swarm join-token manager -q")
|
||||||
|
[[ -z "$JOIN_TOKEN" ]] && err "Failed to get swarm manager join token from ${NODE1_HOST}"
|
||||||
|
|
||||||
|
logn "Joining ${NODE2_HOST} as manager (advertise: ${NODE2_SWARM_IP})..."
|
||||||
|
n2 "docker swarm join \
|
||||||
|
--token ${JOIN_TOKEN} \
|
||||||
|
--advertise-addr ${NODE2_SWARM_IP} \
|
||||||
|
--data-path-addr ${NODE2_SWARM_IP} \
|
||||||
|
${NODE1_SWARM_IP}:2377"
|
||||||
|
logn "${NODE2_HOST} joined as manager."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Label nodes for service placement constraints.
|
||||||
|
logn "Labelling swarm nodes..."
|
||||||
|
n1 "docker node update --label-add node=${NODE1_HOST} ${NODE1_HOST}" || true
|
||||||
|
n1 "docker node update --label-add node=${NODE2_HOST} ${NODE2_HOST}" || true
|
||||||
|
logn "Labels applied."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 9: DNS registration ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_DNS; then
|
||||||
|
log "Phase 9: Registering DNS records in FreeIPA"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
logn "[dry-run] Would create/verify ${SWARM_ZONE} zone and add A records"
|
||||||
|
else
|
||||||
|
# Check for and create the swarm.home zone if absent.
|
||||||
|
if ! dc "ipa dnszone-show ${SWARM_ZONE}" >/dev/null 2>&1; then
|
||||||
|
logn "Creating ${SWARM_ZONE} DNS zone..."
|
||||||
|
dc "ipa dnszone-add ${SWARM_ZONE} \
|
||||||
|
--name-server=${DC_HOST}. \
|
||||||
|
--admin-email=hostmaster@${SWARM_ZONE}"
|
||||||
|
# Reverse zone for 192.168.30.x
|
||||||
|
dc "ipa dnszone-add 30.168.192.in-addr.arpa \
|
||||||
|
--name-server=${DC_HOST}. \
|
||||||
|
--admin-email=hostmaster@${SWARM_ZONE}" 2>/dev/null || \
|
||||||
|
logn " (reverse zone 30.168.192.in-addr.arpa already exists or skipped)"
|
||||||
|
else
|
||||||
|
logn "${SWARM_ZONE} zone already exists."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# storage.home A records (zone already exists from HA cluster setup).
|
||||||
|
for spec in "${NODE1_HOST}:${NODE1_STORAGE_IP}" "${NODE2_HOST}:${NODE2_STORAGE_IP}"; do
|
||||||
|
IFS=: read -r hostname ip <<< "$spec"
|
||||||
|
logn "Adding ${hostname}.${STORAGE_ZONE} → ${ip}"
|
||||||
|
dc "ipa dnsrecord-add ${STORAGE_ZONE} ${hostname} --a-rec=${ip} --a-create-reverse" 2>/dev/null || \
|
||||||
|
logn " (record already exists or reverse zone missing — continuing)"
|
||||||
|
done
|
||||||
|
|
||||||
|
# swarm.home A records.
|
||||||
|
for spec in "${NODE1_HOST}:${NODE1_SWARM_IP}" "${NODE2_HOST}:${NODE2_SWARM_IP}"; do
|
||||||
|
IFS=: read -r hostname ip <<< "$spec"
|
||||||
|
logn "Adding ${hostname}.${SWARM_ZONE} → ${ip}"
|
||||||
|
dc "ipa dnsrecord-add ${SWARM_ZONE} ${hostname} --a-rec=${ip} --a-create-reverse" 2>/dev/null || \
|
||||||
|
logn " (record already exists — continuing)"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 10: Verify ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! $SKIP_VERIFY; then
|
||||||
|
log "Phase 10: Verifying swarm health"
|
||||||
|
|
||||||
|
if $DRY_RUN; then
|
||||||
|
logn "[dry-run] Would verify swarm node list and NFS mounts"
|
||||||
|
else
|
||||||
|
logn "Swarm node list:"
|
||||||
|
n1 "docker node ls" || err "docker node ls failed on ${NODE1_HOST}"
|
||||||
|
|
||||||
|
logn "Checking swarm state on both nodes..."
|
||||||
|
for spec in "${NODE1_LAN_IP}:${NODE1_HOST}" "${NODE2_LAN_IP}:${NODE2_HOST}"; do
|
||||||
|
IFS=: read -r ip hostname <<< "$spec"
|
||||||
|
STATE=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||||
|
"${SWARM_USER}@${ip}" "docker info --format '{{.Swarm.LocalNodeState}}'" 2>/dev/null)
|
||||||
|
if [[ "$STATE" != "active" ]]; then
|
||||||
|
err "${hostname} swarm state is '${STATE}', expected 'active'"
|
||||||
|
fi
|
||||||
|
logn " ${hostname}: swarm=${STATE} ✓"
|
||||||
|
done
|
||||||
|
|
||||||
|
logn "Checking NFS mounts on both nodes..."
|
||||||
|
for spec in "${NODE1_LAN_IP}:${NODE1_HOST}" "${NODE2_LAN_IP}:${NODE2_HOST}"; do
|
||||||
|
IFS=: read -r ip hostname <<< "$spec"
|
||||||
|
NFS_OK=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
|
||||||
|
"${SWARM_USER}@${ip}" "df -h /mnt/docker/config 2>/dev/null | grep -c nfs || echo 0" 2>/dev/null)
|
||||||
|
if [[ "$NFS_OK" -ge 1 ]]; then
|
||||||
|
logn " ${hostname}: /mnt/docker/config NFS mount ✓"
|
||||||
|
else
|
||||||
|
logn " WARNING: ${hostname}: /mnt/docker/config does not appear to be NFS-mounted"
|
||||||
|
logn " (automount may still be pending — try: ssh nixos@${ip} 'ls /mnt/docker/config')"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
logn "Checking overlay network..."
|
||||||
|
NETWORKS=$(n1 "docker network ls --filter driver=overlay --format '{{.Name}}'")
|
||||||
|
if echo "$NETWORKS" | grep -q "ingress"; then
|
||||||
|
logn " ingress overlay network present ✓"
|
||||||
|
else
|
||||||
|
logn " WARNING: ingress overlay network not found — swarm may not be fully initialised"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Deploy complete. Both nodes are ready for 'docker stack deploy'."
|
||||||
|
log "Connect to either manager:"
|
||||||
|
log " ssh nixos@${NODE1_LAN_IP} (${NODE1_HOST})"
|
||||||
|
log " ssh nixos@${NODE2_LAN_IP} (${NODE2_HOST})"
|
||||||
+131
-139
@@ -2,234 +2,226 @@ root-hashedPassword: ENC[AES256_GCM,data:Kp0nOZI7vDoLhJHiOJBwJn0rQZ5yhnwapGnAcA+
|
|||||||
nixos-hashedPassword: ENC[AES256_GCM,data:pT7tVRN6X4a+DNUgB7fIUUE3CbnetkjxmoSL1PxSU+ktsFU+fB0mEvJjA1uujsGH5Rcztg7YM815+M0Z67ILmHaXbza5DtFacrqhi4/b277xly0SHRX4yOvBwQh6mJG1jn/0O/wvUUIYdw==,iv:bp2nfhC8nFbk6o5iWDAugvbzu7J/a1xayFnBEtkhNpE=,tag:HqWgkIpSrSM/K9OK2WO+VQ==,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]
|
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]
|
beszel-token: ENC[AES256_GCM,data:OWmSRkZjb11y0Y8GdobqiE9GFwzdHOvvxCbYx69qUghGYARN,iv:i/JhGH0O7ThxPkL0SLAjfN0Fq8prm7tybI5kF2NRNpw=,tag:dBcqxOSHTnD4xngpOog55Q==,type:str]
|
||||||
|
nix-gitea-token: ENC[AES256_GCM,data:HRQ8ymx/D8pLcL/pYIhcSTv3tlBCKOU5nhXPp/W6rNI+t+yxfOoRPQ==,iv:0Av3lrxQew2bDFf67nX/UM+cD0/8CtbQ2fZSaKWHAzM=,tag:0K+atsB/YmYd/qCN3hMdBw==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiU2IxZ056SUs3SVZiUTlQ
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyR3lnUnZwUnlJTkFaeVdz
|
||||||
NXZEYmg3VzJzeWJPRnZXcUtOMGQ1MElBU0dVCm5xSURZZ0sxTjVxQnhpVVNEU092
|
T3Q2eW52cWI2eTlhSFpHNDRHTEVNVGZJYm1NCldaMmlHaCt1K3lWRHhEZExHK3lo
|
||||||
bDBHbFAzZ3hzejc5NmM2U0FQTkRNSTQKLS0tIC9OOURPRlcvQXIxMVZXR1JjUWRX
|
azZFck1URVg5ejBaRVdCWjdFMTc4dGsKLS0tIG9mNkxsZXI1N1hCRWk1NTZKcUUr
|
||||||
bE5ja3FoWXFIZ2ZLTklqWHhMRHBTRWcK2Z4sEcdyvx1rdBIGrbNw7bYvEPXdobvF
|
SlJoWGdWbXhqZEJHM3IzZmZSQ25QbzAKPzBIA/IJiZr5NpOhB6IPkUSDGQzPwpTU
|
||||||
h0aNihn817DvtVr8eeucj22iYCS40etElQNcO5i68t5wab4/d3rUbQ==
|
vgFLMze8OSEviaGXKLt/ZwTXHsr5As7V9yGvJKJHhS/hzuSLRjs8rQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
recipient: age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrN0lNNGtYbE8rRTB0U2hL
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpYmZHNWVCL0ErNEF3OTlG
|
||||||
eUNOZUNFRGk1RzE3R1VjV0Q3ckszcDNHWGxJCkhFN25JbDVSaWJIZ09uNzczbkM1
|
TTJDT1hZMXJNNUdLZ3ByUDNndVpLWVVGUm1RCkZSRmJmd3NyeHc0V0tpZEdYakF1
|
||||||
ZWZ1Y2wrTHZ6VzlnVWRoT2JOdDZHM0UKLS0tIHQ2ZE9BaFRCeDNqbldRQkpLclI4
|
WXhyWUJ0V1hTYzhrSUo1ZUNzR2J3OVUKLS0tIEthYXVvNVRibDVSQ2N0NWt3TmNP
|
||||||
dXZqOXNaR2Q2QWJtRlZUa0hLaTh2YlkKeCh3+yIt0sjh2Ig8m/YQifhMDHnTpuZE
|
cTlteEtRYmxnUmdIUlBTREZzQXBBSVEKxQq10KDseuoVPe0cLBbk1+weuq0y6di+
|
||||||
zHBE74SUyWFmqLgtd1b8Rx5gn9vBcS/3SMv3yKWispg4DZ3bCOeNFA==
|
wJjaMShqVQBUI+MFWiQokBhPt8gZS7cs33LkWf3BegNALLDd6HXoOg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n
|
recipient: age19m0m7vdfg86yqy8l5mmle5jdd0unrn3f55t232w8h5ey42cqw34sfpt32n
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5c2luYmpYcVVxRHROQlpk
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArOE9oU210Vkk1Z1VKMS8x
|
||||||
cTM4cm9uNEtVS21pK2pDRVRySmxYS3NwdGg0CnQvS3g4Q2tSdlpVeWZxbzArcTM1
|
YTJGbFlSYmx6eWgvMXExZ243SndXWkZ0ekY4ClJVUTFSVkRERjY1UVZMZjlFeE1P
|
||||||
YWRMcDAxcWgySFRqRC9XSElsTVorVEEKLS0tIFd6YjdrY2ZXQVBaZ01wMTIvV2sv
|
Vm40WHY4UjMwZWxVa05lblBMUW9iNEEKLS0tIGVlbDVxakR4MlBEaFlrM0tNOWRX
|
||||||
ZEk0TG0ybENlTlhpZnovMXh0OGd5V1kKEbjrmKP6Su16KRDQ4PMqi0tHRS6+PfuT
|
elIvMnp5NmhnYVBYOFA0aUdtOE5DbEUKy+soKNLlRe0SC8kcnwrpKqvSrTGE114/
|
||||||
0v5DEyi8EDtIYSMzzdF7Jk2C0h5XuRux45Z6E6JhKW5c788dGQHz/A==
|
FaX2829gQWm0bYI0M4ixeTc5ME2O2Ct2tvYlzfZQPnAuub+jVx2k8g==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
|
recipient: age1rrxqea6q6pn39sw8y5te63h2py8jgjl9v0jyper86w3ggtn67upqg3ah39
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1eW5kd1FrSGJaSDkwTzdY
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzR0R1VEc2eTRIOWNxaVpa
|
||||||
SnJhR2hqRkNTTGtSTWsvYmZpSlVWV0xvYkRZCmtqR1FQOTRQR3dGYU8zNFpMdkUr
|
RXVaNkxjaDVDTmxMVWJEU0dWNkxDeE56c2swCjdkZGVQM3V0SnhGMXFxSEFEeEpP
|
||||||
SWN2KzhXaE1rYWZYNHJsOVVvRDY3R00KLS0tIG1aUldSWHlHeHkrcjF2TENvazR3
|
OFNwTWQwYTY0S0trMy9FMjNjbWFxajAKLS0tIHhjUW1lZ1EycUh2Sm1yanhzS3dP
|
||||||
YXJLUXV3VVNSWEdseFlZQUJPQUF6OFkKOPKH52muUha+MV2leWDiqpuTaIj0ZKzV
|
WFlMcndzOThUVVVDZlJJeGFBT2JLTEUKHsJ6cwSPcO0IB1CQe2RqKeid8Q92BTNF
|
||||||
keEdyqkIck6KKfIjphNKTW5ujiuKjGQbRliquBknfCRGMnRlMNfmkA==
|
RfURqE7Curj1yaFB45mzv2ThBgTKN5FE6y5BWgBnF6+szdMMXRw4uQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
recipient: age1adur9g330gua4l6ndk8cqjg35qc8yxwgme6wrl2hpylcc7vxm38q05ejuy
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmTDExNFpCZHZmWnpwYis4
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlQkVPTHBYWkdMM3Jyd3NW
|
||||||
bFJGZXV5UksyS01MT0d3cXE2Wno2Zzh6OFFnCm9hT3MyZ0hTbnkzdXk3NmN1TUVF
|
bkNGa3VtYXNBanZPc1puSGdwUEN5ejIwRkFJCjlNNXFlTEV1eEZNVTFxWnNJZG5O
|
||||||
OGtCZnlJRmZJaWJWaE1pMGcrTzJzZm8KLS0tIFE1L3ZxeUl6WVA2UXN2b0lIcTBl
|
UG5hci9XVmMyeXF1ZHF2Rm5DUVdPcFEKLS0tIDBEZUtZNXR2R093OVNNYzRnbXlj
|
||||||
YmtXZnZaMml6ejY1Y3lRUFFtTEVMS0UKZbcuunA9GRqctCfC0Fh2foKt7n3l8KkE
|
S3R2UzFoTmhZT3Y0K1d4STFjU2RRamsKU9LcaOLLjmcarmdir9Hnt/qaNvlxvSsE
|
||||||
FNjjxUzL7OQjNG9M1aAtRMMO5KXL7YKXRUJWOPcpiLeLUNvDLeLXvA==
|
RdXIdOKuaQqJyJ1VEpuDCfuZgtIdkr7OG1360giXFUIEUDliM9OPiA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
|
recipient: age17e89ty6p0fw24daanen57wg8uald9s025t3wwxsw269svwpmgvrshfvfvt
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQYlp5U0FvVkdIKzBGZVRl
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWUFJETUk2TGZTdVJqUTcz
|
||||||
UEtOOG5kVHVLQ0ptVTVpdG9Lb1Y3bzlOODNzCmNYN0Y5ZGNzTGFGYmxJcWFxaExn
|
MUZvUWhmWGR4SGVxOW8xcy9Pd1A2ZnhPcXpzCnhvL1lPTmZJclJ3OHVqbGtGZFMw
|
||||||
NkkrT2RLSlZTdmFiNks3RVZXQlBEYnMKLS0tIDdCVlBxUG8vYU05d3c2MXhjQjhM
|
VysxdnY5ZzgwUEFiVUxpYStUb3NsTEUKLS0tIHRpS0dpb1ZNMXBiRUhFVVZiSHQ2
|
||||||
TTVJUGxlWUxWalBRYzFKQUQzd1R6dHcKPz1ciNICph7qnqSH5WYmOjUCNHsIAmTX
|
UTRoTXFjNVFOeE84WXdsT25UQUprbE0K5U8S5xojEgUn8pAgY6X+Njllv/jqm/Qp
|
||||||
lRnbFCZdD/ZIFRK8OYAyCGfQ42UYSTf16kH2jDdTFUy7Fk2+O7lNIA==
|
tqeTbMw+w/afAhRxY80x/mTJdCAxUh4guLTKO6eojHYcFvwT6eZA4w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
recipient: age1hrx8qj02fj2ea6d4g9vqhyj9hl7fppkjqfdx2l37py3h6pdkr95s8n8rvs
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiemVqWW9LdlV0RjRuRHpF
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpY1FUZ3RKU080RVNPZFZ6
|
||||||
U2MwN3RDeHU5SWhuQ2l4YStRVVE2eU9XSUc4ClNGMkNXQVdkWFkwcys0aFVhZnZv
|
cDExYmhLdlU4K2xWU213MkZtUWY4elVxdlF3Ci9LK0cxV2hKbXZEMEd5dzVCWGVU
|
||||||
Y3J6RmxwakdaZVo5VWZhb0lVTUdTYzQKLS0tIEQ2NW0yb2lGQ0NLZm1PSWNFVkQ1
|
SjErTmpYbDhmcDlSSENhNnRrN1QrcTgKLS0tIFRlbVlKOW51R3ZiZ2pLd0JXdks0
|
||||||
a0Y1NlYwZ0dnbHhsQVB5eS9kZVdqbmsKgFLUpZMqGywO9Ext5WRwco2bpenVNrzL
|
b0FsZ3VETmREVEhqTGI4Vm5KQnZaWjAKXq+8u2Qk84Vt+eDUxzE6sDk4DDm78P7H
|
||||||
XrGCqhn/7TW9DrJlPnFe8r4fl4DFP6GWByn7axDl58Qy1OzeBZwnNg==
|
KVnrZfhAmwP3X7dSuBhW+dK8in8D3jaqRK50d/eHUUWX0NIqniUMKg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
recipient: age1e7l8dusgmgfzd2cxrrzwepzjxt69hzqj4epee0cs27u6yg4kxcuqm34ncx
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLRzlzWEcwTUx6M1JNYWgr
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZU2R4VVVkWTdqS1dxbDhv
|
||||||
ZWxnVXhFWkhQVjBrSndGTU9oY2MrOTQxTGhFCjhUdERPc3hzQlkzMjBEa3AvaWVX
|
K21KeWs2NVo4UTBvNEF6SDRYdE9WUldYUmpRClloR1lBRU9UYkpRSzYxY0NsR2c0
|
||||||
T0RJWXFFZHdmUVBObGhpQktCQ2NPbWcKLS0tIDN2YkE4bEVtbjJlcndWVmlrQ3ZT
|
WHlXT0MycWJPdHhHaThWUHltdGZYQXcKLS0tIGJuaTEwSnpmMnFGVml4UUs1ZCtT
|
||||||
dlVvRmt4RklOdUJyZ25ia0ZoYjU4ZHcK5fL1yvRcty6+EMzfSkwwJM3qvrr6h46d
|
U1V3U2NNN0l4L2JPRHFDOWpWWE9HaVkK/i5m6YFiAR6xtms/pbcDNhKaZreqIpjT
|
||||||
d7Zs+EElXrsGDQvL0fevB/zDcICAtudYnVv4nP3Jf8GQnL19zWlwbg==
|
8tvnqHz2HDSuCMAjAnZfluvuP1USHvjJQGZpBfreZ/XGhW0oqa7D7w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
recipient: age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhZElRNXNLK0tVUjQ0ektQ
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKNklENW8yZ3VlMytlVHJs
|
||||||
Tzc5NDJReldiYVFFZnRLS3l5dWw3UVQ0WWhJCm1DYVloZGpYeVJtNUNRcEJvVUZE
|
dUhDcDI0amdZWlhmN3UzbFJyR2x4RGVOdFNVCmovVzFlYnh6YWxYTnYxVzBNejVT
|
||||||
Q0xMcmR0WWdVVTBaWko0dGI5V1hqaUkKLS0tIFBBeXdNY1pFai9QT2t3NWFLV2sw
|
MGFhdjZzU2hnMW42d1RucENGeFdXa28KLS0tIGhIZy8rZ0pFM3o0cDBKTXQzaFNO
|
||||||
cWpRU2RISSs1UXh3Wkl1dWI3VEk1bXMKP6v3Lm0XZkp3Gn3iJkB0K0Y0E5BeilqR
|
bG9XNzFGdVNyczRhWjRqQXlxNHFvL1UK8NMj76782tmIdJJ4qIzLicFytNhj6ZMk
|
||||||
plmPh1rKbT/wkefFwpa8e2Wwmhx80YK+bjn633MH7mLQDf6AiK83Xw==
|
HIIOJGCrBnqgCtcKTiCrTGRhGbqGzhkId1oJkZkhMFRoU7kdSvwD8Q==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1sweerhrga9yf8x6sv0apz4ed4g48rnlcq34rpv20t0rcelwgpgeqwvndzz
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMVk8yT1FtMVoxYjF2YVBs
|
|
||||||
UTFVbmhzblBCRno2V3YvaERRN0xWcHd2NVE4CmVyK3p1SWtodk5iWjdkQjJRc0dM
|
|
||||||
Vm8zZHczYlFFSG91QThmUGNaWHBoN0UKLS0tIDZMMXoxbUM0TUVaNFUzRExLc2VM
|
|
||||||
OVFrNGZNL256cjg0Q3ZBQW9xUFcxSUEKv4HqaTcTzNYRJCmNEeUQLFfEoFXyOBEC
|
|
||||||
yZrg8D+ROBn98484bxNeoyOjQdcVn7OYiCv1y6utnyeA9sdMUB0ong==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
recipient: age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1Rkl0UXduS3k3WVU1RzdG
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzbFU5YWREdFpsK3NScXNU
|
||||||
N2IybE1TZ1kwVmM4Y2lvUS9DVG0zSkdpZGhRCnFkZnJVS2FBZm9Xa0V4SjRwaGdR
|
NUFMMml2VEVOQU5pWkIrUjkzM2tZOG9vcTNRCmR6Vzc5OWRQaGdvU1Naam51dXhD
|
||||||
TWpaaTh4VEk4cUFrb2pURERzbWtSR0UKLS0tIEsybHdvUWg3UlUvL0lhdHIrUE0y
|
ME1yNFJObktNNTA2eXFsNExTL3JNeHMKLS0tIE13L0xYMCt5TE5ac3FEbkxVWmVv
|
||||||
R0pMdnI5QlpIcWVWdGRYQ3JCaElLWTgK5cTfvehj4BPU22tYiR5piZdouUdeh5Gr
|
V3NCSk9LUCtPZFZCbVNmSjR5QkFKTmMK5qFJXtZCKLjOCg1r+sVQpMKl75GNcrbI
|
||||||
x/xSbFWqYVauDB6rpzZvhGMI4KCJh2RmDdt7FrlCb/YCB3QoSgvl8w==
|
Dum/K/3HU03wv5reG51UDsQ1tMrsFDsaFh2fjR+LxLKSGlG7b3Rz4w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
recipient: age17jqc66x9yeshfgd9v78mj483r4zzarqdtuxtrkxe4x5mw679gphshd94th
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwL0NNbGlGS3lTZjEydGJm
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEWFdCNGpPYU5pSXkzd3lG
|
||||||
MWZRK0Z5YktuNVNudk84MndLUUdXR0RlRFF3ClB4MWRUTFBWYUdwbityeUU2SlR4
|
TGU1WkhuUEhmaDdybzFUbVVaVmVqU0FyQXhBCnRmRngrbXlsTVVMSDV6dkhBS1lD
|
||||||
azZMb012YXQzWHUxZnZzeVp3MmdzcU0KLS0tIFJ2SUNyMTBEY1NWejdVQ3hyUytQ
|
MmNUd0NjTE5ybFEweFhkWXdGbDNwTmcKLS0tIGU5Sk5hM3Y2bUVsS3pTaDZiaTBi
|
||||||
VnlQWHRDYWZFaU1ON2RQTXBwNFNGYUEKwnRUWCdsiStia1jLHJ6UNP/kgEt4Aw8U
|
S21vU1ZVbjZ1SHE0WStab1VHQUNLRUUKBDW9hwI90Yn+B2mB7LUNTVxFGbwEFSw3
|
||||||
pA8PKKu6TZVPI8pw7svtI/uvJuW+8ARuNcyb1FdYmX+sOHnOEqHyjg==
|
LGGqgZdvnN5p6NMigBbJz+kSwOk1gQ/yo86HtRj8Ejllp7P7jRsfmw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
recipient: age1px0h5l9zp2dww0m8fncrc82kfdmzplsfv2ltat7sna28xpg09pqqcl3s2k
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxM0FyaFlTR2FzMkhGTzZx
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaMWdPVkV4WnNHNmU1dTZZ
|
||||||
d1d3dWNGOG1ESllETEVxRGJlYWkrTUNIdkYwCmI4andLMUUvNVU3R0N2WCsvWWhV
|
ekVoVTJWY3laQ1ROM1NLUnlZY2FKTnJwNkFFCm8wVWtoNFpxMS91RkxzQWRhOWRs
|
||||||
UmlkcHdMSFA3bFp0ZG5OckE3YmJrejAKLS0tIDhEVEJYOFc5VWVHcGcvMm5yanBl
|
V3k2STdvN1JRdjIrSHVFaFpSYmlmT00KLS0tIExnSmw2anNtdzVGMjRYdzdpcFRq
|
||||||
eGxaNEFxdEN4NWh3cnlGeElBUTJmUzAKvCSJF+LGf1JqEdkzT/Wk6hEzferseyWO
|
alFrUTJVckpTVEVIcUxTK3pPbWNpUzAKSe3Z9V5u8+om2s+HqUcx6qXIaTmQUgOT
|
||||||
vAYR8Z+u6GDwQi7JdFz9iZKD21LAs0IMzd7cZqXKIXVedyXuaIhPtg==
|
zIrU8T0tLOHzdS1SAdh1Yb50yevN+P2+5TORdLjrtigpyzeYc2U/zQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
recipient: age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGZ2M0cG5nMU5nYVliRzlx
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtWDI1Z2hVRG9ieHkyZnlP
|
||||||
TVpDcHpwclhXNkxuc2VKanBvcnRxRW04UXh3CmNJeEl3K0FrWWVHQWRwK3Q4Tzk2
|
SVQ3VHNsSlZxMjdIMUsxQWdIclNQQk5OUWhzCnZSMzFZUGkvYlNSRTJiT1ZMU0h3
|
||||||
MEZQbFV1QURLZEhCdEhaMjBRUzllaVUKLS0tIFhSUTJ4SXJwRHhVbkZFc2Rwb3Fh
|
L0RIWkt4R1F5bUlWNVlQQklKc0tGa3cKLS0tIEJpTXNzZkFUOWp1ZnFocjJEVExa
|
||||||
QzAxTnpWSHZaWlF3NUlFMlBHUkRUa2sK3Kim0mp2adSvruI6Necd3UpZHow0EFmT
|
dGlqRDRLd1BDYzYwSWZXaG1ueXhrSzAKdJ8yA+igZKyetNuQdoN2Woi2bl2I3Xj7
|
||||||
JFOIpIK+3Pc06gsiqrDtDAx2Qtc10Ja4ILTutPlp4va6zzmzrPABiw==
|
dDBVAn8bvxx5ZG1eyDXXcHSh76xUp5ZPRlLOFCvRK9oOLWd8FntBmw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
recipient: age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwTUZ1RzJKcVF5bnJGamlU
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkb3RrRDNqTGZGYlR1eTls
|
||||||
bWJvcU1sTTJuME94YjhMZWRWRWR0aGJ3UUNvCjh6U1BkaEg0TDlCeTNyUlNTRWV6
|
Y2FYQWxncmJBQ1ZiNWFpMTBMUmtlSnJzemhVCk5NTWZRcTJMWkIrTnVsbVAzcndB
|
||||||
Nnd0NjE3NmplSWFjVWpSRy9qWjBUbnMKLS0tIGdMZUFzbGdMejZoMlFBVUJXSE13
|
MVp2NHJSazJjMVN5UXJHOWpnc0dLK1EKLS0tIGJlelUvbEZZZkFKd3BpdWx4VVNH
|
||||||
TW4rUHFjZk5oVkw0UGtpaG5mOHRRbHcKFX4ZyNQ0TUeqwXEaz1qmAXQRIOctDkvz
|
cHJSVmJ4ZWlVTlA3VXpWeE5DNkphcWsKTGpdWU/cE8vC/43lwmnwJDh4IPqHQoVV
|
||||||
TIwxJseAjS3qVxCslnxzgaqmCh8DzCfjrRxLUS/HVTnwOaE1YAh0vg==
|
yjnUdZnPDGyQHwwrydVgun2fdaAKH1zxAut8TZlH9pxD4ir0B69G+g==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1nruncs4l0ufk7yuc4des8p99c0alfndl0lhsws8tycl5pplfp56s30af5f
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTWStVRDhlblZDMDJ3ejIr
|
|
||||||
MkwyeHRwcnpWQVBmOXY0a3NCU1JnTjkvVGtVClVvVUpLTVhUNGNNLzY4U0o2SDRD
|
|
||||||
bGZteUU1Uzlzc0NjTW56OVNCcnVpUUEKLS0tIC8wN1V2Nkc0cVU3OHNYa1NBVlhq
|
|
||||||
YTV5QUpsQWppNHRpT1dMb2JDQnpoR3MKCoBngnNg5qpktg5j4CcuTUKiooOVI8IN
|
|
||||||
04plE+1lra8q5ZK8q4r1/K8hBM23poyfq+XushO1GZ17dPtWs9wITg==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
recipient: age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUU2ZSazhZNWFZVXk0UXdx
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaZ0tVT09Sd1hXVHZPd0tD
|
||||||
Mjh3R1NLMThlaHUvWVFMZEQ4Z0NYOW83dWgwCkVnd2dhdzQrZkxaL1NRbDJodERQ
|
YmZKaStnUXg3cERMTjRmOVh2RmxFbG1FZ0hJCjNMWWJmaUFqTURoL2NvRnJIdzNW
|
||||||
SkM5S0kxcVBtY3laNWp5OXpXZHhqZ0UKLS0tIHVOT2VUZU5DZjJjVW1kcU5qS3FD
|
OWFmSFAxWVRrdit6S2lpaFdERGNrOWcKLS0tIEJlVVFBSE5PK3k0dTRTRlZYMGZt
|
||||||
YXNhRkpaREMzRDB5aWgrdVNvUk1qcWsKQnPqE+5xSBGeDMmLfKB0tEBt2eylBOxT
|
elcxVlltUnJVd256NHQ4TithUmFnSkkKkbFSRRktuO+tNxlfIEuJ27C7LWalHJMb
|
||||||
oi26mC+EoO5T2pCIpbD+DODAAlWKLattqCYHneUO9wRk9DnhuEXRBg==
|
aCnjE9301XMceGB8SstPtGi5SJRSHD4WrJleeJbKXYMronfvWnYDSg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
recipient: age16kqfmvz4e23hmdlqresnyw69ej604s320mmd49h4hm3fhqchtgyqrws0k2
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZUlFlenlOVDk1NmxQVGUx
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEa3E2ek9BcHZRRUR1cUpK
|
||||||
UHhVK3ZtMHdzaXdtWmE1TXhOekpsTmUwSlJ3CmdncjMxOUw0alBQZkhZZjA0YzJZ
|
UEFERzc1ZEtrYjc4bEdEeDRnSDNkWHV6elhFCjljdnJLa2syR0UzYTRveGhPYVk1
|
||||||
bFNVOHUySXRuYW8wVjdHMDN4ZThuQmsKLS0tIGdvUUdHcXlpWlZkL0xBcVpMN1E1
|
cGNnWkNMNUVwbXFrOUZ1aE5UelFyMGsKLS0tIFNyQ29ieUpReW96OGhJaE4yU2Ni
|
||||||
NGZCQURYQVY5ZzdDODNNY0o3SG04eDAKiYVuKsJLaSC7/bQRB51tX282/yqpAjWT
|
Y3VWNUFYTU5LdHlmd0krK0NyQk9mMVEKfh7I/9+V9+0DLkzTf6n4sBKs+oZlMlTx
|
||||||
XNnqPQBQ7H3xuK0tDVdCAUMruOSlqAe9YRZwmiou1irpg9vZYwgTPA==
|
ar203b95cR/jjsUekF0NoZjj6MW1IZSV7BkaDoizVwrAxh7WS0vwqQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
recipient: age1arhf2q45zw6wf2uevju4savp575x3m2tfvved5zzq3ay92ynua9s3cm92c
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnUUlPS3ZPS1lSVmxwV0lq
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtSUcxeE5pM3NZVHlhcGNt
|
||||||
WWE2QXltb1BzU3p3cFRuMzVieGJta05nWno4Cnk1RG1CUXZjM3dNQzlJK1ZGaHpZ
|
d1EwODM3Wm5BOGNybkNMdnM0SnJmTG5ScEhNCk5yZHhieVN4QnNPMkxzbUZ3SS9G
|
||||||
b3drMTB2L3hidkpoTXdMQ05vOUtTb28KLS0tIHhyUllpQnlzL093MCs4YWx4VlVn
|
aG5WVENIbVBYRGI5ZzQ3QndUTG12NjgKLS0tIFF6ZFp5YWJ6UzlqMWRsb3pTS3l0
|
||||||
S3Q3QnpVNEx6ek54RGx5bmlqUk5hbVEKHVUvfk/xKtlOk2o73LodweIivSB4Oii/
|
ODRZZG9pRkNxL1Mxa1dwUG9vdkJZZTgKYrjkSkjNQCU2wMIuZMvJnd/regVplzMi
|
||||||
6dwnE8wa+Qx/OU0vDJVelKOUE6JiLwckX/0u2YIUOybkx49n+tYs/Q==
|
3kPGRBMWzO1t4CSFtf2PMqv5AHw754I+vVNs5CpTXBuxVUcQDcGJCA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
recipient: age19mn8zrxl8zpps9yvrh4euquvygpp4fp8queg7xc6qhtnl4ng8c9qx02qwn
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3MHlrTTdxYit0MTkxa293
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGNHY0Wm9oNk1BWFBiYTRm
|
||||||
eFFIcmRLMUpkZjd3eWdsOXYySEljNDB2NUdNCnAyanVkS0dmenZSa2dpSWZTSkZw
|
OXBxNU9ETDNyTEhyMnBxZFRnZmFjVGVHN0RjCkdPNWpvRkVMTCtWdDhlR3Q0U3Nw
|
||||||
Tm5LMkRoWEk3L2ExVktnZDZMM09hZ0EKLS0tIGVGcHFuMjh4OTUxdHNJQSsrR1pq
|
QUh6KzBVMU1ONXh2TTYrd0FaWlhCU28KLS0tIEhsaGxvcGFtaVF5T1NIc1NUOFBt
|
||||||
WU1jVG5UN2hMbW9iQ3JmbHlwc2VveFUK4JC32glesv/9nMWBR0lFeX7MPgIt9sIo
|
d2NGeW9YNVVxdUpYbFRoVnhUM2VTUGsK4adI9pgC2PipcAY4zXMRf9hPv5kilTvc
|
||||||
9HsAkOMTxPTI0NZW8q79+G5hq8qG+QvQhwO6wQtwz/KEHPXntKDX+Q==
|
BsUNz0Qr4YdRfVfrPlPBzMCPOTDofTp6qBv+Gc8FE4hvwxtbjIcJ3A==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
recipient: age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRbEZGNmtlUjJVZ2dlVGZC
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBT25YN3BtMVZrVnhDazhO
|
||||||
MGVEQXV1SFRpMjFqWks1Y2F6c29TY25oL1MwCjVFRk90Q2dJRXhUMmIrRVdHVFRD
|
OXpzOEd0Wk5KUnZ4a2JjRGI3U0RsMExXN1FZClM3UFoyeUIzY3FVNnlkTU1NSCtn
|
||||||
TWgvQS91VWdFWERJWEYrL3VnRXFraUkKLS0tIExUU01VRWlENlpBSGt3V1Izc1dS
|
dXhNb3c4ZFlMTU5VTFc3blBUV0c5MkkKLS0tIHUzUzliL1NEUSt3cWx2d0tjRHcv
|
||||||
RFBhSTVKTGV2KzFidDQ3b3lmUHZ3d0UKHWvWO/Om6ahwBkONreGeENqdFG9UI6JB
|
a1Nmbm1MZEptQjJuZ2ROWGUzN01zbDQKy3dPKE5oYwsTwE2vnhUi3auqJ/KBOBPX
|
||||||
fRA3gRHy98EorEiia1him6lGY+ewYkTtVNo6iEt0abncW5x5xMQywQ==
|
vSKOROUuS/uEfsdo8NoVxuZ2RaJHetrbHAYPHFHOMgpe/X72pCcQNQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
recipient: age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2dFFpeDU2Y2dtQU12cytZ
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2Q24xSkJaVDVUZlBWcW11
|
||||||
WUJjKzZ5S2RiZmtvYVJScStYVHI5d2FvcmpzCm9COGZmRllyaElqb2NsczhKRjhO
|
cWFOdjhXZEtuNkJtaUhQTEN3QkVUQnRRRjBZCnY4V1lCOTBNM3Mrc09naWNTQndn
|
||||||
ZGhwZCtoaFcrWmNZbGVHMUNLMFQ3Ym8KLS0tIHJQR1JOblZlY0cydnhNUnBCT1Fh
|
VXU2ZWt5Zi9CbjBFa3YrejFHM041VFEKLS0tIHd5L0hRN00wdmdsSUhkdFpTcUNS
|
||||||
aHJiZVpMU3Y3NXd0ajc0MGMxSGp4MmcKky1oaS8yFydszcvZE2pAL90ZUKoN9MnJ
|
YUNiUGhVR2M2clFrOUkxNW5idkR2cnMKXO02RJh4ew22tf5GWH4lNdLlhf4bWyef
|
||||||
1TdqatDSMtmo3PWkdFeHbWxPjTizQGIcKEcsv6GmO/5KCAJDCHDNMg==
|
+hW9R4TGCJOnIO1xSGpBX0wJsM3oiW4qy/1tgfQw2ejVih5qUr7JRg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1529taqdwr6t0w7cvzmty0d5y5593wffl0krt48j6uc4u39k56g2qf6ywtp
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrd3QwWE4yaVlIakdiYkNX
|
|
||||||
bFVmR01yNjdWa0d6V29tcWh4M0hWRmQwR3pBCkFRV2gzSVdPQmQ5dXZYc0F1dW1r
|
|
||||||
OFZNTkRwSzRFR0tlUXE2UzFxeU1Ld0kKLS0tIEZ6RlZkMFFiZytPZ05DSEI1Ni9X
|
|
||||||
WEtpUlMycVhkMERsWDRGQ2lrNHdQcDAKRqVAOzQY4ZM1uH7LyZSqX1T6+1PwNhBY
|
|
||||||
5M0RxSuI43E8YKC9axcppvKFJMQI1GAGvgfQxg2J0CEpnmPSVsO5DA==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
recipient: age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvb0xNMloyRW5tTUUvZGph
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1SnhhMEpwOS9vUWVuT0Iz
|
||||||
YWxIc0ZhSWI5RHNmaEIvdjZJNHFDM2xBMkNzCldlM2RUeFZCNE9tMVp0MFlWM2sr
|
bWFCUzE4djFyUzN0alRvOG94bGJldUhlb1ZnCkxHd1pUU3B4b0ZhRklnczUyRzBR
|
||||||
Sm44MjkrWlNkY0U0eUlyNlE2ODl5SFkKLS0tIEdISElQa2VBZlduajhsV0ZaU3dZ
|
djF1bS9scjBlQXJEbmtiUFgzTndKQVEKLS0tIDRpUnhWYTM2U3lLcWZyVFkwcVNL
|
||||||
ci9COWE4bldUMS9lSVB1eEk4TG1sVjQKYu9zby36DP+41dTV8sRdtDil3Az40pf0
|
NFpldXQraURraWZNdUNqZFNwVFY0WlEKhV8IZYbBXKGb0x+2E5pJkoMavH2ox4qp
|
||||||
vIhqhCTxQymnJHEwEukiEzM/w3Z85R/W/MbOKyzEZmxzG0a5O5svhg==
|
dK/XRlBpEG0SCUkKZ5sDegzz/HyqbxRF26jC3IOL1uR8A5nsnwC2Cw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6
|
recipient: age1k73g8x47hs93wcv7qh92n3htz8pl295g49hyvlrf3570mts0hgys5g04d6
|
||||||
- enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCZlFtOHI0NnlMeWdjZEsr
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEWmIvVG1GWEZmbnpFU252
|
||||||
UnQ0UFJXY1pHTm4yLyszY0dBaldtUkIxdzJvCkNQbk9mN0wzK3BtN2pEa3JSWWFk
|
bFhOajA2dnJXNlFGMmF0TDRuR1YrYmZqc0hnCk85WGFUMGZXdElWU2tvdFV3eE1v
|
||||||
RXNoemFFbDZ1SlUxOEZMUm9mY1RaZW8KLS0tIEk3bCtyaUVGM3MwNUwxS3QyTjA4
|
czEyTHBkZXFnSi9MVWFQVHJabEpUb1UKLS0tIC9TNGZiQ0xYR1dQWHl6NTAxbzlD
|
||||||
YVpTTFlOdzljbmI2cnZUNG1nd1NrVFkKxARm3bpBL685Wky4Y06eA18VvUZRkAl3
|
YmNhQkFVZTZidWFtbURqYnY2eGMxZ2sKaUPhe5mQ4QSyxuQMqLNI1jqfkCUBnBWc
|
||||||
BBYPJMn3lfDgiQPvfXINfhJI6O+bWyjt0WnfodCGFM6EJLARXiTaqA==
|
HR5ck3H80/H0pMwzP5uaBnN/hD5L4CT2FcZBgUygbyNz4G3VgbjaMA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r
|
recipient: age1fefy6dk8zn5c3edwmrs9vwx79quftnt784m628t9e34q3ft3cehqz8u72r
|
||||||
lastmodified: "2026-07-29T11:33:40Z"
|
- enc: |
|
||||||
mac: ENC[AES256_GCM,data:hXns2uTM8+bX9J1EWxKcA7v1oYayBMfyB57pMgayEQHV8PvdIWi7rYez+4OCuMBZzBNVAZWn2AfzYxC2VJJnKrajrvwGZ/neJ1qvd2sfY9e9TskZv+c6nqnRuXl0OGi+kP+6EVSmZPiSFUODZd+xtAku8dIMGO8LZeZoGYjCBfs=,iv:EFZZpw/QKiuEtShBhB2bP6qq69T6jlrJk81FpqSt5m8=,tag:VCqctIBaFK2DQqoj1sxv7g==,type:str]
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlU3BybGcyckNaT2dQR0ZO
|
||||||
|
dDVXa2NtNGFTS1RqZ0ViLzVJSk5PbWdYU3lVCkRZNTJ4MytYYVpON0JBQTh4SWRY
|
||||||
|
ay9xdkViektHNGRuSFNSeVoyMFdrNE0KLS0tIGJ1QW5RUjhWR2NlclR2dnh4SUQ5
|
||||||
|
SlJEWHhBTFI5Rm1rZVFjTGZlUWhZMGcKReNE3l+U35iutlQ07AZ+3fOFF4YdVbdx
|
||||||
|
bR/Sz3NqpqmZqBEmgjUjjjQI4h4Xturv4tT8/JUzEmVnEyDsqCbhHQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age16y0vfts5v2k20e2rj23qa5lc5gm96gm64uwsqsr0y688xl0ne46qcfr0sm
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTMjhleFpqRGRBMGIrVmU0
|
||||||
|
aXcrclh1TmtIV1JZSlNWRXRRc2RCYThyUVMwCmZOdXptWnhvajYxeERlOEJFVTU4
|
||||||
|
WTY0R2NZSXplMDNNSzZ6eCtEeTN1NjgKLS0tIFIvYmY3VHM4T1RTZzljR2RnV0ND
|
||||||
|
Z0ZhY2JpL0psb0RJcEJNN3FHUHJVQlEKJdoXKdvm6fGA+C21lsxY4Zq+VVPt/6k4
|
||||||
|
vH0IirsPd9Cg33+tBzrySwkF+GVNtYLtekup2L61pFQ6/Y/necfQ2A==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1tm3zj5lp3elw2j832az4nj9xxmhqd66ag3cqcv3vskvmd7qdmqmsdpdcn0
|
||||||
|
lastmodified: "2026-07-30T07:49:07Z"
|
||||||
|
mac: ENC[AES256_GCM,data:D5Y+zyLvs3gz8BWS3+lrra/tc1TsDIk7FykxSPiz0Er1jUB5LOe4IKircytMpwGqdLcVhUKgXYdRA+C0KAXaI9KpQp5qKfRDvX2jX24jveVNFPgbvxA2LidYasVMzA7ayTU0I+oirPcEtj+5VH/ahSjDz9YtarTDeHMpXvBZMLU=,iv:KcIGEs+61yS8Ul734fqsrC95iA82wIinf72DE2bLk/A=,tag:RzL48FqVcMro2W6VAgIxzQ==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.3
|
version: 3.13.3
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:81JCCVaOeEYNyqTT3vXkFDDV1oSAlOrElGmvN+1Jy+U+dF6EaCSeFYT0U2i2BvUu9VAYEiY6NRAXwWUgZXeQXsI65eziB6d/8NKTr7UWb0eQxvhHbthgzxoCfdCvofZH49DdGuCEl8kU6hppSeM+wnDIIYvEgWljO4JQJr4/qnQRegMqJ33564+ZvhiYqrbfojcBYpTjVBzh+4aitqZEAYoFx4xhFAtdBfmAA1W6FwDvZLrX09bbdFT5jOqnSSkPunvTHiyWtyAfLgaNzG656F1U+3eRDv4eZQYOaLXXo9H4BaBMC5jyJFV1rDgl5s3diCuQggSyDbWUnwSS3Prcixj3dkrF8h8heNk/LDV5lGBtGEV3BQsEAlCPl01PIe3O5McfsRaR8IqwD7wQQ8ULyA56fHHAYJR1xe9bNGgdLryhqLDBFWi06Ztidi1MQXFLnNFJxoIhvANIAmt//TBWimkHnXidkg==,iv:WNChOcSG+QiZOMUi8jpb2gmOCLonM3zK7vcZCrXt4Yg=,tag:zq6Zvf3xu4kofaCechUpcg==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxMzVHK0tOazNoazU0RWZF\nT1h2N01NNktHRjVMQ0pEOWR4RDIxNVlQTWlZCkV2a2pQS0o0VFZWN1lyaklLWllp\nZXJvZEt6Zng1Q09xQTFkNnVtTDNNVDQKLS0tIFhnNW1sY09ZcWpVcGdmM1hrK0o3\nZ3VscjBXMVFEa0xtbVlZM0lMN1dHS2sKcYtCKhw6D1ax3Isf5Vk93cDteUEjx79j\n1fheqgOjytY3W9t8U2NOEpUP1hT7zWYswnaq+agm8nfEjf1fbwR9MQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4K3VSL1NrSm9rMm5WNTk1\nVEpudHhJUkJUeWxieEtKSUdCL2c1YkNHWVdNCk9EeGNoWHdLOVNUR2dRY2YrMVlJ\nblliN08yQW1xdXN5VTRjbm9vZUxZaDgKLS0tICtLaldjemJHSkZzY2R1MHlySEsy\ncXJ6UGdZMi9hTnNjRHRTVW5UMlNMQkkKWhLTte9gTpppGCmA0lf/FEYu5b/ZaGYT\njn9j9qj3Lets0gNj9qBVaHUkCoJ2TAGORdkhnLEdY/Tm/J1w3XfjNQ==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age16y0vfts5v2k20e2rj23qa5lc5gm96gm64uwsqsr0y688xl0ne46qcfr0sm"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-30T09:34:38Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:oC5OPZyZ4kEh0A8Mmwoi5oZ3+fBNvEDN3A+o3n099RdPREn6mcH5QC+XD9BXvWVxp9BzQEwRBmExlyZBEoRuwrB6DQy5Fn8TneEjQoCvZrudpbXIB66/EvORk1utpUsbAov3wn9A8tXUDAUJaRsY93/Pe1mTc8KzIOGv/QTMR9w=,iv:+p87aL7dKsbyAfCWxc0pecVB36yUUa+ltfzQZc4oovk=,tag:tNRRC056bqMToaHz1UrqQQ==,type:str]",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:6Is5g/gqFdQ9aTE6dpe80g7kSgDbhRr3yIwffv6tT5CLqsQLA6i+V5XPAqqhH0zbpXnT7k5Rc2tA2880MDa0eVoI+x7qDnhadfLKVgErWZNwQ4Nz9ukovf+lDfoKXn9AWNFxbiBckpCoxNOms3zMv6kqf1gbEwV7eRIoiq/i2xypke4nOJSmRH22dqFUldfQOOUL1D0H6RFQpNK+5O/Okb7URxIzdUgmq6/9zIsZTOlX5j1V7kzpx8U4P+YPyoPoRSLTOxpK/5gh+Gx1BQbXVTf8z5lm42tJxuQAt5kXqu0Tu8aXZzUUcMKf1oGVbo4TwRaQN+9tFgkhegVMSS/l2don3X3vWPimBcYHTfXfvvCh3krH3hoSmej8es2jCG8AesObDEmCyMNu9hdqhGGdCWspakrXVEuefCJ2Tq8fJfOn5XQhh4AX1wAf04Us0FJO4NjByNrgW8Bo1344wocAStxBxuD3rw==,iv:kkk5muigMC6iTIiTHKwXFreCuJGl7CWmwp1W7ILmq7U=,tag:W49PPiOISdFjk54wroD8ZQ==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQN0NWRllWaEtPdTdlOHFz\nUXJBbG1IRXlPbnNzMEJhT0FuazJLYVZHWUdFCnJXdUtvV2RvUU05eDBaaVdxSlBw\nUVh1azM0T2dFZlpMMFhsU2tWbm4vSVUKLS0tIGNlUXMrbUlDYWpiRTQzV2svcXJH\nd1RIcmpGbkYvSlZwYkwxanBZajVTMUEKt4HCEEPjsSDKvp6XuSSrQjVXFQLQHwk0\n1Fi/HrkhhdkIO1f4DyTOOznvK5bc+Z4JKT5lrOkKQln92uAho2ECiw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBock0vOGVLMC9HZ0NYckxr\nZWRwK0NhbkVaV1FVVndDNmRoNWhGVXlUOEdjCk50NFVtdmdxbEFuZE92YU1IUDlN\nbVJ6QnN1TEk5c1VmWDQwNXJReFdCSXMKLS0tIEl6TW1iV1NvTk5OeUkyOTRtT0ZZ\nR3NsV1lwa00rdHdTRUkwbzdrc1VaVkkK2ioVnzacNrQD6cpNOomKz9WfRRq+B3oK\nnabokq5rEOoNsSFNip5TBeDjo/34kOTXZCKXpFYwmBSoHCrkdFHvYg==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1tm3zj5lp3elw2j832az4nj9xxmhqd66ag3cqcv3vskvmd7qdmqmsdpdcn0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-30T09:34:40Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:QOvDbiPiQBPYBGg3BzJNZzLBv//UccDfOnoOVqnpdskOpPw88uQOcpK0mcF/unW8o+B0AJe4wzr8/XhlhtlLSRi6buES2uZr8pjgCqrgFMCUX8WP+4rxiSq9DkZtmLO1XBtBtjJy37PgFr8bwUw1p8fNdhnOEJBYR75MDC8JG38=,iv:YCCfUyncprAkIbvxWwMGGZZywNHLTrr5bYjBBdQp2OI=,tag:mHyHw11ok4jm79iYn6L74Q==,type:str]",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-1
@@ -7,7 +7,10 @@ rec {
|
|||||||
|
|
||||||
# Org/repo path within Gitea, combined with giteaDomain to form the
|
# Org/repo path within Gitea, combined with giteaDomain to form the
|
||||||
# git+https:// URL used by Switch-nix / Test-nix.
|
# git+https:// URL used by Switch-nix / Test-nix.
|
||||||
giteaRepoPath = "beatzaplenty/nixos";
|
giteaRepoPath = "beatzaplenty/infrastructure";
|
||||||
|
|
||||||
|
# internal repo path to flake
|
||||||
|
giteaRepoFlakePath = "nixos";
|
||||||
|
|
||||||
# ── Network ───────────────────────────────────────────────────────────────
|
# ── Network ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -185,6 +188,46 @@ rec {
|
|||||||
# the data disk; drive-scsi0 is the OS disk.
|
# the data disk; drive-scsi0 is the OS disk.
|
||||||
haServerDrbdDisk = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi1";
|
haServerDrbdDisk = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi1";
|
||||||
|
|
||||||
|
# ── Docker Swarm cluster ──────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Three network segments, all internal to pve1:
|
||||||
|
# LAN vmbr0 192.168.2.0/24 — management; SSH + external service traffic
|
||||||
|
# Storage-client vmbr2 192.168.20.0/24 — NFS from HA cluster VIP (shared with HA nodes)
|
||||||
|
# Swarm cluster vmbr3 192.168.30.0/24 — Docker Swarm gossip + VXLAN overlay
|
||||||
|
#
|
||||||
|
# Host octet consistent across subnets: node1 = .230, node2 = .231.
|
||||||
|
# IPs from the .230–.239 expansion buffer documented in docs/ip-addressing.md.
|
||||||
|
#
|
||||||
|
# Docker Swarm uses --advertise-addr and --data-path-addr on the swarm NIC
|
||||||
|
# (ens20/vmbr3) so all inter-node cluster traffic stays on the isolated
|
||||||
|
# internal bridge and never crosses the LAN.
|
||||||
|
#
|
||||||
|
# When expanding to a second Proxmox node, vmbr3 (VLAN 30) and vmbr1
|
||||||
|
# (VLAN 10) share the same inter-node trunk NIC via VLAN tagging — same
|
||||||
|
# physical wire, different VLAN IDs.
|
||||||
|
|
||||||
|
haDocker1Host = "ha-docker-1";
|
||||||
|
haDocker2Host = "ha-docker-2";
|
||||||
|
|
||||||
|
haDocker1Ip = "192.168.2.230"; # LAN management NIC (ens18, vmbr0)
|
||||||
|
haDocker2Ip = "192.168.2.231";
|
||||||
|
|
||||||
|
haDocker1StorageIp = "192.168.20.230"; # storage-client NIC (ens19, vmbr2)
|
||||||
|
haDocker2StorageIp = "192.168.20.231";
|
||||||
|
|
||||||
|
haDocker1SwarmIp = "192.168.30.230"; # swarm cluster NIC (ens20, vmbr3)
|
||||||
|
haDocker2SwarmIp = "192.168.30.231";
|
||||||
|
|
||||||
|
haDockerSwarmCidr = "192.168.30.0/24";
|
||||||
|
haDockerSwarmPrefixLength = 24;
|
||||||
|
|
||||||
|
# NIC names for ha-docker VMs. ens19/ens20 occupy the same guest bus
|
||||||
|
# positions as vmStorageInterface/vmStorageClientInterface on ha-server VMs
|
||||||
|
# but are attached to different bridges — storage (vmbr2) and swarm (vmbr3)
|
||||||
|
# respectively. Kept as named variables to avoid bare literals in modules.
|
||||||
|
haDockerStorageInterface = "ens19"; # vmbr2 — NFS client
|
||||||
|
haDockerSwarmInterface = "ens20"; # vmbr3 — Docker Swarm gossip + VXLAN
|
||||||
|
|
||||||
# ── Storage / NFS ─────────────────────────────────────────────────────────
|
# ── Storage / NFS ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# NFS share definitions — used by ha-server.nix (exports), docker/mount-data.nix,
|
# NFS share definitions — used by ha-server.nix (exports), docker/mount-data.nix,
|
||||||
@@ -260,6 +303,13 @@ rec {
|
|||||||
dockerHttps = 443;
|
dockerHttps = 443;
|
||||||
dockerExtra = 8080;
|
dockerExtra = 8080;
|
||||||
|
|
||||||
|
# Docker Swarm inter-node ports (modules/build-types/ha-docker.nix).
|
||||||
|
# Firewalled to haDockerSwarmCidr only — vmbr3 is an isolated bridge
|
||||||
|
# with no physical uplink, so these ports are unreachable from LAN.
|
||||||
|
dockerSwarmMgmt = 2377; # TCP — Raft consensus + cluster management
|
||||||
|
dockerSwarmDisc = 7946; # TCP+UDP — Serf gossip (container network discovery)
|
||||||
|
dockerSwarmVxlan = 4789; # UDP — VXLAN overlay data path
|
||||||
|
|
||||||
# Beszel monitoring hub on docker.sweet.home, reached by every agent
|
# Beszel monitoring hub on docker.sweet.home, reached by every agent
|
||||||
# (modules/beszel/enable-agent.nix, hosts/nixos/home.nix)
|
# (modules/beszel/enable-agent.nix, hosts/nixos/home.nix)
|
||||||
beszelHub = 8090;
|
beszelHub = 8090;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIoCzxgfFjE3scsLzTitDcrmJ/Bh4MQ+3cEH8YOQ665z ha-docker-1
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:fd++6Dyfyq7JfeGflnUXkmAKWhTG9Z0tvMCmVreczaoVtPN9+t+atIIa9EB3I2PWbOEYoIKelO0fDuK3ZrY1mrp8K8Stb3vJdB8lz97dBB/Gn5BonRAMYszMpfp9+rgf8pZqc89KWt3xz7z9EFKjTE7Kf8n08K1pHyqVjZ9W+bH9Zxh4WEWGeaKAyH4nlmVLjZ7rKch9ErY28bMVBwiiXIDFeXy/TIkSk7pVODFeCqMcuXFFnPememm7vXme+wtepqNc6WfrKTQOuQFe9SDK/A36Cl/92hSqjooTLU9XXjQNrEf1xAOkZhpTpbvsF32O103ixtqDY72ZMmC/RExAvaTT+Jf+Utfnq0S8Gna+d2rHyzbbvtdUC68vqA6QhL9qJe3HHwyAPbcGJ2YONgTsKot9DR3zYEWVwTjydBL8Z3/JTw62A+sLve4WuXgq9yooYGF4s/SVX1+a0e1MkQ2ahn7a6rIEyj+YK2bX71G5Kw+KBXwHzFW6duJs4mN23Yu9N+xrU7RdAT0O3hWGeBgmLfp8ko4uWOxm2Kin,iv:+5am0O58f/8tqrSEiFld4Xu2GZNDsEobjFPDgNyv768=,tag:GLERbrGo1rRKUXZBnUXEvw==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxcDErcVphYUFWUnN0WlBa\nVU9jUlZkMzArZFlYbENRNmZFQzN5Q1ZNa2k4CmlYa21yYXIrZG1QUVVNSy9oWE40\nK2t1dCtrR0R3cVQ5cVcwakphUFRvQ2cKLS0tIFdVL1dwNmMxR0xJOHh2ZlUzQmNm\nV1NNMldMRGd4NDAxbU1nbGZ0bWJKTDgKnyzQCimHCBke8dM3kdkjVu4lDcfvkvOi\n+k4GJVCuTQ60rwWXPsB8kiPMDp/+8tNO09LG/fR3vcGRgX65KcjMSA==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-30T09:22:17Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:MjPZqcKTTO3uoZk/u7fdfQhgpcHsjKggzu+/pJanXUgIDWtCiKfFbRsQmwH5nYiRyjHOJdX4xMcFRmaYEN6oz/hzDR5ty+0mSuVNIemBz8OpShwjGY2E+pAkdxDgWLsc/Q+CyywGscLslbgHSAKuKPdPlyzhuRyy1QTyk/ty7Xk=,iv:MSQsNuENPgde3YitmHTyDjTYR5ZuhzwRgcWb5DAPY6g=,tag:Yo231g12u6aMPUauIf65hg==,type:str]",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPwfEcHZRbdI5GK9cn59SDrLruNOJJEjVfJXZve25nnE ha-docker-2
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"data": "ENC[AES256_GCM,data:3sWc8kfLREvxWRf0dtZFgubyxrAnfgiOT8b7nGtAQyQVaoy3moi3TGhPVdeihwbEOaEQX+YHvLWIzfxtLQxTPDxjvu3v2+dAYrjobIDHJaon3Th9vLfF9dxl/NVQB0wy/uL26z1Q4bhYj2yOU2V9wIzJ4G/94OvnttZKUub2iKmiUji36KsZkq6FVL0e0fUcv134dbo65yTIjmVB6U4SSB4nCrVKNg7z5EeXBBZodulK4kJ5zsspgTHLZbI43cW/tXmnNoW2NTFZen7Q1rLhlB0ISkXSXp0Sn6rIQsSZpywdc24cZ0fenqER38UI4PhwUPXd34Mv/LxQ6D5TnblANDQn1rsfxjjFY2JHXWO7kydccS5Us0vBHWMHbgZbC50+D1VIvvrK9SoEnZ7LW5WRJc1QaCvAz5eVbaB1V1UQ+eJjZToBQY1yvCg3vyj2nlX0A+BAPdZZMZgRazUp5CZaspcvcszIjTK8TDwqMeefOmUfNkC2Hgz83ij2lu+Kr9/Z1BurvIMotPDFER7qqYFodZJR4HMQ40rS9egO,iv:qh4tRPY6wZfOHdk1bDzQme/QqBVfMSfh2gZVTy1BEN0=,tag:8UcO2ia1nlzZPHNKuMYrrw==,type:str]",
|
||||||
|
"sops": {
|
||||||
|
"age": [
|
||||||
|
{
|
||||||
|
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBK1h5RUtPdjQwWm9vWGJK\nVzZHbU1GU2dVN3NKSWxqOEhtTUZwbDkzbTA4CjREek1WbnZrVkdQZGU3djFVVjg4\nOUNkNzk3eXIvN2hDMVNsc3dZS3hJb28KLS0tIFgxdlhwUG9PdUlsRFdRdmJkOGhh\nVU1uWkpwSVhKL1VRTktoQmxEQVk5bjgKQHofvV/y5tPG0AJUoC1/oXBqAIlX6FB/\nAHKB7k3JxbgrzkR40uKuA9y6qmokGkVPoD3p7N5fd5G3aZY3fsYtjw==\n-----END AGE ENCRYPTED FILE-----\n",
|
||||||
|
"recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastmodified": "2026-07-30T09:22:26Z",
|
||||||
|
"mac": "ENC[AES256_GCM,data:bICdSPh5zGwWW1MI4rZZgKOZWM6X2h2OEL+uK3EQNGsGoRnW2JicBdIjqBkQsin5Ke4a0i0n/LudIB/G90+NkQYXu8/ixve3vN6VvfnoBb53BuDp/Hn7hJkvvn/1iXDmYsg1S75IM6xTm4RkY8nbLTuBltgiZvrN6mYMrxcXc9s=,iv:rGnu8V9P3vOrHA06GDq3Ru9l3+x93hfrVVZtN+hR9e4=,tag:rYZ1h95YoZzLg2ZaRxJmQA==,type:str]",
|
||||||
|
"version": "3.13.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user