199 Commits

Author SHA1 Message Date
7ce0dee711 fix: replace ip6tables with disable_ipv6 sysctl in vlan50 egress guard
Some checks failed
validate / lint (push) Failing after 1s
nicolaka/netshoot:v0.11 (the pinned tag actually in use) is built on
Alpine 3.18.0, where ip6tables is packaged separately from iptables;
this image's Dockerfile only installs the latter, so ip6tables does
not exist in the container at all (confirmed live: "ip6tables: not
found", and against Alpine's own v3.18 package index, not master's).

Write 1 to /proc/sys/net/ipv6/conf/all/disable_ipv6 instead - per the
kernel's own ip-sysctl docs this is equivalent to also setting
conf/default/disable_ipv6 and every existing per-interface
disable_ipv6 (lo/eth0/net1) at once, and is strictly stronger than an
iptables DROP policy would have been: no IPv6 activity happens on any
interface at all, not just filtered output. Needs no capability beyond
the NET_ADMIN the guard init container already has.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 22:08:34 +09:00
26894d16ce feat: strengthen VLAN50 canary egress guard and document its lifecycle
Some checks failed
validate / lint (push) Failing after 1s
Diagnosed the vlan50-canary FailedCreatePodSandBox->net1-timeout failure
as a stale Pod artifact of the Multus DaemonSet rollout race (sandbox
created via a transient daemon/shim state mid-rollout; the current,
fully-settled daemon's own logs show no ADD for that UID, only a DEL).
No defect found in 02-daemonset.yaml or 10-nad-vlan50.yaml; both are
unchanged.

Independent of that diagnosis, harden the shared guard script per
review: print safe interface/address/route diagnostics before every
FATAL exit; validate net1 actually carries the workload's expected
static /24 address rather than just existing; add arping-based
duplicate-address and gateway-reachability checks before installing
the net1 default route (exit-code semantics verified against arping's
own source). Requires EXPECTED_VLAN50_IP and NET_RAW (for arping's raw
ARP sockets) on every consumer - wired into 20-canary.yaml now,
qbittorrent.yaml/jdownloader.yaml need the same when they're migrated.

Document the canary's Pod lifecycle: restartPolicy: Never means a
Failed canary never reruns on its own, and Argo "Synced" only reflects
manifest match, not runtime success - recreate it (new UID) after any
Multus/CNI change before trusting its result. Recommend keeping it a
manually recreated, controller-less Pod rather than a Job/Deployment,
since unattended auto-retry risks a duplicate-address race on its
static .100 IP - the exact class of bug this diagnosis just walked
through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 22:02:27 +09:00
f1bdf11f1e fix: install missing CNI reference plugins (macvlan) for Multus on k3s
Some checks failed
validate / lint (push) Failing after 1s
k3s deliberately ships only the plugins its own flannel networking
needs (bandwidth, bridge, firewall, flannel, host-local, loopback,
portmap — confirmed against k3s's own build script; macvlan's absence
is an intentional upstream scope decision, k3s-io/k3s#9224). Multus's
macvlan delegate for the vlan50 NAD therefore had nothing to exec even
after the binDir/mount fixes.

Add a cni-plugins init container mirroring Rancher's own official
rke2-multus Helm chart (the fix k3s's own docs recommend for this
exact gap): rancher/hardened-cni-plugins, digest-pinned, writing into
our existing cnibin mount instead of the image's hardcoded
/opt/cni/bin, with SKIP_CNI_BINARIES=flannel so it doesn't overwrite
k3s's already-working flannel plugin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 21:39:23 +09:00
c7cdcc967d fix: mount k3s CNI data parent dir into multus for symlink resolution
Some checks failed
validate / lint (push) Failing after 0s
binDir alone (previous commit) wasn't sufficient: k3s's plugin bin dir
is a directory of symlinks into a content-hash-named sibling directory,
and the narrow cnibin mount didn't cover that sibling. FindInPath's
os.Stat follows symlinks, so it failed inside the container even though
the same path resolves fine on the real host. Mount the parent
(k3s-cni-data) into the main container instead so the symlinks resolve;
the init container keeps the narrower cnibin mount since it only writes
files and never follows them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 21:25:12 +09:00
d410c05e6d fix: correct Multus binDir/cnibin path for k3s delegate plugin exec
Some checks failed
validate / lint (push) Failing after 0s
Root cause: Multus's ChrootExec resolves a delegate plugin's absolute
path from the daemon container's own filesystem view, then execs that
exact path string after chrooting into the real host root. binDir
defaulted to /opt/cni/bin, which is valid inside the container (bind-
mounted to k3s's real bin dir) but doesn't exist on the actual host, so
every pod attaching to the VLAN 50 NAD failed FailedCreatePodSandBox
trying to delegate to flannel. Confirmed against multus-cni's source at
the exact pinned v4.3.0 tag, not assumed. Sets binDir explicitly to
k3s's real path and matches the cnibin mount path to it on both sides of
the chroot boundary (main container and the install_multus init
container).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 20:06:04 +09:00
e980fcb755 feat: add Multus manifests for VPN VLAN 50 (manual sync)
Some checks failed
validate / lint (push) Failing after 1s
CRD, RBAC, thick-plugin DaemonSet (nik-debian only, k3s CNI paths,
digest-pinned), the VLAN 50 NAD (macvlan bridge on enp1s0.50), the
shared pod egress-guard script, a temporary canary pod, and the
not-yet-deployed browser-vpn-proxy workload (kept in reserved/, outside
this Application's non-recursive source path).

Referenced by argocd/apps/multus.yaml (already pushed) but not deployed
by it — that Application has no syncPolicy.automated, so this still
needs an explicit selective sync per resource.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 18:07:40 +09:00
82137f6b2a feat: register multus Argo CD Application (manual sync)
Some checks failed
validate / lint (push) Failing after 1s
Points at manifests/multus/ — deliberately no syncPolicy.automated, so
app-of-apps picking this up only registers the Application; nothing
under it deploys until selectively synced by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 18:05:32 +09:00
7eb7072ec5 feat: add vlan50-parent role for nik-debian's tagged VLAN 50 interface
Some checks failed
validate / lint (push) Failing after 1s
Creates enp1s0.50 on nik-debian (no L3 address, VLAN 40/node IP/default
route untouched) for Multus to later attach macvlan/ipvlan workloads to.

Verified live: enp1s0.50 is up with no IPv4 address (only the automatic
IPv6 link-local, which is expected and harmless), default route
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 17:56:59 +09:00
325d3bc5c7 feat: add pia-gateway role for minisforum PIA WireGuard egress
Registers minisforum as a PIA WireGuard peer for VPN VLAN 50, with a
boot-ordered kill switch (dedicated PIA-VLAN50 iptables chain + a
terminal unreachable route in a dedicated routing table), multi-region
addKey fallback (Hong Kong -> Taiwan -> JP Tokyo, each region's full
server list, in order), and an observability-only health check.

Verified live against minisforum: registration succeeds, wg-quick@pia-wg
is up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 17:54:16 +09:00
939a7c6ed1 feat: add routed UFW rules for pod traffic and open NFS/SMB on nik-debian
Some checks failed
validate / lint (push) Failing after 1s
Adds explicit UFW routed-allow rules (10.42.0.0/16 pod-to-pod, pod-to-Technitium
DNS) since these nodes default their routed/FORWARD policy to DROP. Also brings
nik-debian's NFS (2049) and SMB (445) ports under Ansible-managed UFW rules via
the existing nfs-server role, scoped to the Lab/Trusted networks that need them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 15:00:45 +09:00
53d02b7522 feat: configure K3s DNS resolver and update firewall rules for new subnet
Some checks failed
validate / lint (push) Failing after 7s
2026-08-23 14:07:49 +09:00
9f9bd04b4b fix: change serviceDns type from LoadBalancer to ClusterIP in Pi-hole configurations
Some checks failed
validate / lint (push) Failing after 8s
2026-08-23 11:46:23 +09:00
0824404f96 fix: update host IP addresses to new subnet 10.10.40.0/24 in documentation
Some checks failed
validate / lint (push) Has been cancelled
2026-08-23 11:42:47 +09:00
fa53b7fee4 fix: disable both Pi-hole instances during DNS migration
Some checks failed
validate / lint (push) Has been cancelled
Set replicaCount: 0 for pihole and pihole-debian so Argo CD's selfHeal
doesn't scale them back up once its git connectivity recovers. Live
deployments already scaled to 0 directly; this just keeps git in sync
with that so it sticks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 11:40:26 +09:00
12b3b48e8d fix: update SERVER_REGIONS environment variable for Gluetun to include additional locations
Some checks failed
validate / lint (push) Has been cancelled
2026-08-23 05:53:12 +09:00
7a8c73d78a fix: update fleet IPs after subnet migration to 10.10.40.0/24
Some checks failed
validate / lint (push) Has been cancelled
minisforum, debian, mac-mini, and gpu-node all moved from
192.168.7.0/24 to 10.10.40.0/24. Updates K3s server/agent config and
node IPs (including gpu-node's host_vars override), NFS export
allow-list and exports template, Pi-hole DNS records and
kube-vip/loadBalancerIP pins, WireGuard's pushed DNS/AllowedIPs, and
the NFS server IP baked into Jellyfin/Kavita/gitea-backup PVs and the
Ollama URL used by ai-gateway/Dashy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 05:52:41 +09:00
abc316d7f8 fix: update Gluetun image version to v3.41 for Gluetun, JDownloader, and qBittorrent deployments
Some checks failed
validate / lint (push) Failing after 1s
2026-08-19 01:23:34 +09:00
5512735770 fix: update VPN server regions to use JP Tokyo format for Gluetun, JDownloader, and qBittorrent
Some checks failed
validate / lint (push) Failing after 1s
2026-08-19 01:13:51 +09:00
db862df82c fix: update VPN server regions to include Japan for Gluetun, JDownloader, and qBittorrent
Some checks failed
validate / lint (push) Failing after 1s
2026-08-19 01:11:27 +09:00
bf91e72429 Revert "fix: remove Ingress and TLS configurations for Watch Party and Portfolio services"
Some checks failed
validate / lint (push) Failing after 8s
This reverts commit 3657519b17c16888fbde0946ba5c65c855330d2b.
2026-08-19 00:38:49 +09:00
e15911305f Revert "feat: move Gitea traffic from gitea.nik4nao.com to gitea.home.arpa"
This reverts commit fd13b8a. The registry-host migration broke image
pulls for home-services/portfolio (internal CA not trusted by
containerd) and has left them Degraded for 11 days; reverting back to
gitea.nik4nao.com pending a decision on how to proceed. Not pushed yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 00:22:27 +09:00
fd13b8aedb feat: move Gitea traffic from gitea.nik4nao.com to gitea.home.arpa
Some checks failed
validate / lint (push) Has been cancelled
The public domain is unreachable while moving, and the cluster had no
Traefik route to Gitea at all (public or internal), leaving every Argo
CD Application stuck in Unknown sync. Add a gitea.home.arpa
Certificate/IngressRoute, repoint Argo CD's repoURL, Gitea's own
DOMAIN/ROOT_URL/SSH_DOMAIN, the container registry references, the
Gitea Actions runner, and the watch-party clone URL at the internal
hostname.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 23:06:02 +09:00
3657519b17 fix: remove Ingress and TLS configurations for Watch Party and Portfolio services
Some checks failed
validate / lint (push) Failing after 1s
2026-08-07 16:22:33 +09:00
36ac94ccf3 fix: increase memory requests and limits for kavita2 deployment
Some checks failed
validate / lint (push) Failing after 0s
2026-08-02 18:10:01 +09:00
0a997367f0 fix: update VPN server regions for JDownloader deployment
Some checks failed
validate / lint (push) Failing after 1s
2026-08-02 03:05:05 +09:00
d31f85f4b3 fix: increase failureThreshold for livenessProbe in Gluetun and qBittorrent deployments
Some checks failed
validate / lint (push) Failing after 0s
2026-08-02 02:32:51 +09:00
bcc697db7e feat: add second Kavita instance with NFS and IngressRoute at books2.home.arpa
Some checks failed
validate / lint (push) Failing after 0s
fix: update Pi-hole configurations to include DNS entry for books2.home.arpa
2026-08-02 02:30:17 +09:00
423d9eacab feat: add Kavita e-book server configuration with NFS and IngressRoute
Some checks failed
validate / lint (push) Failing after 0s
2026-08-02 01:54:23 +09:00
47e3a8992a feat: add custom DNS entries for books.home.arpa in Pi-hole configurations
Some checks failed
validate / lint (push) Failing after 0s
2026-08-02 01:53:59 +09:00
6277c7fa33 alert-bridge: switch ingress from HTTPS to plain HTTP
Some checks failed
validate / lint (push) Failing after 1s
Callers on the LAN (e.g. nik-gpu) don't trust this cluster's internal CA,
and installing it everywhere was judged not worth it for a LAN-only,
bearer-token-authenticated endpoint. Drops the internal-CA Certificate and
switches the IngressRoute from websecure to the web entrypoint.
2026-08-01 23:00:06 +09:00
85cc8494fa feat: add alert bridge service and related configurations
Some checks failed
validate / lint (push) Failing after 1s
2026-08-01 22:51:59 +09:00
d95806cd32 fix: update JDownloader script to include download password in crawljob format
Some checks failed
validate / lint (push) Failing after 1s
2026-07-26 03:10:50 +09:00
c354e6c39f feat: enhance JDownloader script to handle .torrent files and save them to output directory
Some checks failed
validate / lint (push) Failing after 1s
2026-07-26 02:20:30 +09:00
fb3e2976e2 feat: add documentation for JDownloader integration and package naming
Some checks failed
validate / lint (push) Failing after 1s
2026-07-26 01:47:07 +09:00
c625461e30 fix: update JDownloader script to extract package name from URL for crawljob
Some checks failed
validate / lint (push) Failing after 2s
2026-07-26 01:37:03 +09:00
9dc4bbe249 fix: update JDownloader script to write 'text' instead of 'url' for crawljob
Some checks failed
validate / lint (push) Failing after 1s
2026-07-26 01:29:16 +09:00
a70a3a8014 fix: update cnl-bridge container and service ports from 9666 to 9667
Some checks failed
validate / lint (push) Failing after 1s
2026-07-26 01:01:01 +09:00
f3cef5cd1d feat: add alexa-bridge deployment, service, and TLS certificate for mTLS communication
Some checks failed
validate / lint (push) Failing after 5s
2026-07-26 00:51:01 +09:00
710f3be427 feat: add init container for model initialization and change models volume to emptyDir
Some checks failed
validate / lint (push) Failing after 1s
2026-07-25 02:33:18 +09:00
4257e83522 fix: update deployment strategy to Recreate for tts-gateway to prevent GPU deadlock
Some checks failed
validate / lint (push) Failing after 1s
2026-07-25 00:24:47 +09:00
d93aa3541c test: temporarily disable tts-gateway mTLS for Mac-side testing
Some checks failed
validate / lint (push) Failing after 1s
TODO: revert once done - restores plaintext-only gRPC (no client cert
required) so synthesis can be exercised via port-forward + grpcurl
-plaintext from outside the cluster, without extracting cert material.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 00:22:20 +09:00
12718203ac fix: k3s-agent label/taint tasks use undefined ansible_hostname
Some checks failed
validate / lint (push) Failing after 1s
ansible.cfg sets inject_facts_as_vars: False repo-wide (the ollama role
already follows this via ansible_facts['system']), but these two tasks
referenced the bare ansible_hostname magic var, which doesn't exist under
that setting. Surfaced today as a hard failure re-running setup-gpu-node -
previously latent since these nodes' labels/taints were already correct
from whenever they last applied cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 00:13:22 +09:00
69d880186a fix: open Flannel/K3s ports in gpu-node's UFW rules
Some checks failed
validate / lint (push) Failing after 1s
host_vars/gpu-node.yaml's ufw_allowed_ports has overridden (not extended)
the common role's default list since the node was added, silently dropping
the Flannel VXLAN (8472/udp), K3s API (6443/tcp), and Kubelet (10250/tcp)
rules every other node gets. Went unnoticed because kubectl logs/exec/stats
tunnel through the agent's outbound connection to the k3s server rather
than needing a direct inbound path - but real pod dataplane traffic (e.g.
tts-gateway on nik-gpu resolving DNS against CoreDNS on nik-debian) needs
actual VXLAN connectivity and was blackholing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 00:05:54 +09:00
f2261a2676 feat: add tts-gateway service, pinned to nik-gpu for GPU inference
Some checks failed
validate / lint (push) Failing after 1s
Deploys the Go gRPC gateway and Python/libtorch inference sidecar as one
pod on nik-gpu (nodeSelector/toleration/runtimeClassName: nvidia, sidecar
requesting nvidia.com/gpu: 1), matching the existing GPU device plugin.
Model checkpoint/config are bind-mounted from /data/tts-gateway, added to
gpu-node's Ansible data_dirs for consistency with the other hosts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 23:50:40 +09:00
df07062cd1 fix: restore nik4nao.home.arpa DNS entry -- wrongly removed as "orphaned"
Some checks failed
validate / lint (push) Failing after 1s
This points at nik-debian's raw IP (192.168.7.183) for a host-level SMB/Samba
share, not a Kubernetes Service. Stage 3 removed it based on "no Certificate/
Ingress/IngressRoute references it" -- the wrong test for a non-HTTP,
non-Kubernetes service that was never going to have one. Broke real SMB
access. Restored to both Pi-hole values files in their original position.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 12:18:00 +09:00
f006090fea fix: remove Immich entirely
Some checks failed
validate / lint (push) Failing after 1s
Not used enough to justify chasing the stale-NFS-handle issue on its
photos-art mount. Removes manifests/media/immich.yaml (Namespace, PVCs,
Deployments, Service, Certificate, IngressRoute), its secret script and
sealed secret, immich.home.arpa from both Pi-hole values files, its Dashy
dashboard tile, and all other repo references (READMEs, secrets-leak-scanner
scope, regen-sealed-secret example, .env.example).

Per explicit confirmation: this also means the live namespace's PVCs
(immich-library 50Gi, immich-postgres-data 20Gi, immich-ml-cache 10Gi, all
local-path with Delete reclaim policy) are intended to be deleted along with
it -- any photos actually uploaded to Immich's own library are gone once the
namespace is deleted, separately from the read-only NFS folders it browsed
(untouched either way, those live independently on nik-debian).

Git removal alone does not delete the live cluster resources -- the media
Application has prune: false. Manual `kubectl delete namespace immich` still
needed to actually free the namespace/PVCs/data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 20:21:05 +09:00
7fbab2561a fix: revert Gitea DB password to plaintext -- existingSecret doesn't work with this chart
Some checks failed
validate / lint (push) Failing after 1s
Root-causes today's Gitea outage. The official Gitea Helm chart
(dl.gitea.com/charts, gitea/helm-gitea) always builds Gitea's own [database]
config directly from the plaintext postgresql.global.postgresql.auth.password
value, confirmed from templates/gitea/config.yaml:

    {{- $_ := set .Values.gitea.config.database "PASSWD" .Values.postgresql.global.postgresql.auth.password -}}

`existingSecret` only affects the bundled postgresql subchart's own credential
provisioning -- it has no effect on what Gitea itself connects with. Removing
the plaintext field in the earlier Stage 1 "secret hygiene" commit made this
silently fall back to the chart's built-in default password ("gitea", 5
chars), which got re-baked into the persisted app.ini on every pod restart
regardless of what the live Postgres role's real password was -- hence the
repeating CrashLoopBackOff no amount of Postgres-side fixing could resolve.

Restores a plaintext password (the value already live on Postgres from this
incident's recovery), with a comment explaining why, so this doesn't get
"cleaned up" back into the same breakage later. Removes the now-nonfunctional
manifests/gitea/gitea-postgres-secret.sh and its .env.example entry. Getting
this fully off plaintext would need a Config Management Plugin or similar --
not attempted here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 19:56:21 +09:00
8f3e04f179 fix: sealed-secrets chart repo moved to bitnami.github.io
Some checks failed
validate / lint (push) Failing after 1s
bitnami-labs.github.io/sealed-secrets returns 404 -- Bitnami migrated the
sealed-secrets Helm repo to the main bitnami org on 2026-06-15
(bitnami/sealed-secrets#1982), and GitHub Pages doesn't redirect the old
Pages URL. Pre-existing external breakage (last successful Argo CD sync was
2026-05-16, before the migration), not something introduced this session --
the sealed-secrets-controller pod itself kept running fine throughout, this
only blocked Argo CD's ability to diff/compare state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 19:31:54 +09:00
a93818146a fix: qbittorrent/jdownloader crash-loop from today's rollout
Some checks failed
validate / lint (push) Failing after 1s
Both Deployments' rolling update briefly ran an old+new pod pair on the same
node (node-role: storage), sharing the same config storage (qbittorrent's PVC,
jdownloader's hostPath) -- both apps are effectively singletons that lock
their config directory, so the new instance conflicted with the still-running
old one:

- qbittorrent: hit a known qbittorrent:5.2.0 image bug (linuxserver/
  docker-qbittorrent#432) where a stale WebUI lockfile prevents the server
  from ever binding its port while a second instance is present -- surfaced
  as the new pod's readiness probe getting "connection refused" indefinitely.
  On top of that, my livenessProbe (30s/30s) was killing the container
  (exitCode 137) before qbittorrent had any chance to come up at all. Dropped
  the livenessProbe (readiness alone can never kill a container, only mark it
  not-ready) and loosened the readinessProbe timing.
- jdownloader: the new pod's app process detected the old instance's lock in
  the shared /data/jdownloader hostPath and exited cleanly (exitCode 0) rather
  than run a duplicate copy -- nothing to do with probe timing.

Both Deployments now use `strategy: Recreate` instead of the RollingUpdate
default, so any future rollout fully stops the old pod before starting the
new one -- this is the actual fix (matches the pattern immich.yaml already
used). Applying this will briefly restart both currently-running pods to
verify it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 19:15:54 +09:00
9afc7e8005 fix: resolve REPLACE_ME placeholder in argocd/apps/authentik.yaml
Some checks failed
validate / lint (push) Failing after 1s
The placeholder targetRevision reached origin/main and broke the Application's
comparison (ComparisonError: "improper constraint: REPLACE_ME"), showing as a
broken card in the Argo CD UI. The actual Authentik pods were never affected
(sync policy has no `automated` block, so nothing was ever applied against the
live release) -- confirmed all authentik namespace pods stayed Running 1/1
throughout.

Fixed using the real deployed chart version read off the live authentik-server
pod's helm.sh/chart=authentik-2026.2.1 label, not a guess.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 18:47:51 +09:00