Commit Graph
102 Commits
Author SHA1 Message Date
Scooby Husky f2213b56b4 Fix Gitea mirror: use service=git instead of service=gitlab
service=gitlab invokes Gitea's GitLab-API-based downloader (for issues/
PRs/releases metadata), which calls the source's /api/v4/projects/...
REST API rather than doing a plain git clone. That was 404ing against
home GitLab and getting swallowed into a generic 'InternalServerError:
404 Not Found' with no further detail. We only want a plain pull-mirror
of the git repo itself, so service=git forces the plain git-clone
downloader instead.
2026-08-17 22:16:49 -05:00
Scooby HuskyandClaude Sonnet 5 777edf3895 Phase 2: deploy Gitea warm standby on the VPS
Pull-mirrors home GitLab's Homelabv4 repo on Gitea's own built-in mirror
scheduler (6h interval) - no custom sync job needed, per the original
plan. SQLite instead of the chart's default HA Postgres + Valkey cluster
(single-instance standby holding one small repo, not worth the extra
moving parts). A PostSync Job creates the mirror once, idempotently;
Gitea's scheduler handles all ongoing pulls after that.

Also added a public Cloudflare CNAME for gitlab.kube.huskypup.net ->
home.kube.huskypup.net: the VPS has no route to home's LAN via Netbird
(none of the mesh peers advertise that subnet, confirmed live), so
GitLab needs to be reachable the same way any other internet client
reaches it - home's public IP already has port 443 forwarded to
istio-ingressgateway from earlier this session.

Admin and GitLab-mirror credentials are plain Secrets created directly
on the VPS cluster (kubectl, not git) - same pattern as vault-unseal-key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 22:05:24 -05:00
Scooby HuskyandClaude Sonnet 5 d6e8c2aa2d Optimize vault-restore: don't wait on condition=Ready (verified working already)
Confirmed end-to-end tonight: condition=Ready correctly times out every
cycle since Vault can't be Ready while sealed (the unseal step comes
right after this wait) - harmless via the existing || true fallback, but
wastes up to 2 minutes per restore cycle waiting on a condition that can
never be met at this point. Poll for the container process merely being
started instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:57:52 -05:00
Scooby HuskyandClaude Sonnet 5 fb124201bb Fix vault-restore RBAC: add watch verb for kubectl wait
Without it, kubectl wait's internal watch retry loop never terminates
cleanly even with --timeout set - confirmed live, it kept retrying well
past the stated 120s timeout. The restore/unseal steps themselves still
succeed regardless (verified end-to-end with a manual unseal), this only
affects the job's own graceful detection of the pod coming back.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:54:21 -05:00
Scooby HuskyandClaude Sonnet 5 e2bdab8940 Fix vault-restore: authenticate raft snapshot restore with a real token
raft snapshot restore is a privileged operation - unsealing alone isn't
enough, it needs an authenticated token. Missed this on first pass, caught
live: 'Code: 403. Errors: * permission denied'.

First run: uses the throwaway init's own fresh root token. Steady state
(already restored at least once): uses a copy of home's real root token,
which becomes valid on this Vault the moment the first restore completes
(its auth data becomes byte-for-byte home's as of that snapshot). Stored
the same way as the unseal key - kubectl directly on the VPS, not git.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:46:23 -05:00
Scooby HuskyandClaude Sonnet 5 62751f7d75 Fix vps-vault restore script: pod name is vps-vault-0, not vault-0
StatefulSet pod naming follows the Helm release name (vps-vault, matching
the ArgoCD Application name), not the chart's default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:43:26 -05:00
Scooby HuskyandClaude Sonnet 5 d0c5563671 Phase 2: deploy Vault warm standby on the VPS
Single-node Vault (raft storage, standalone mode) plus a restore CronJob
that pulls the latest raft snapshot from the VPS's own local MinIO
(already receiving snapshots every 6h from home) and restores it every
6h, offset 30 minutes after the home-side snapshot job.

Not an independent root of trust: raft snapshot restore replaces the
entire storage backend including the keyring, so after every restore
this Vault is sealed with HOME's actual keyring - unsealing it needs a
copy of home's real unseal key, stored directly on the VPS cluster
(kubectl, not git, same pattern as vps-minio-root-secret). First-run
bootstrap uses a throwaway single-Shamir-key init just to get through
the very first restore, then is irrelevant from then on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:42:09 -05:00
Scooby HuskyandClaude Sonnet 5 8aed0bf98e Fix vault-raft-snapshot: mc download needs -L to follow redirects
Without -L, curl silently saved dl.min.io's redirect response body
(HTML/text) as the mc binary instead of the actual binary, failing at
runtime with a confusing shell-script-interpretation error. Confirmed
live while testing the leader-detection fix end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:15:40 -05:00
Scooby HuskyandClaude Sonnet 5 7206022a2b Pin VPS MinIO root credentials to a real Secret, fix regeneration bug
rootUser/rootPassword were left unset, relying on the chart's
auto-generation - but ArgoCD renders Helm via 'helm template', which
doesn't support the lookup() function charts use to preserve an existing
generated secret across upgrades. With selfHeal: true, every single sync
generated a BRAND NEW random root password, immediately invalidating
whatever credentials were stored in the home cluster's Vault for the
backup/CronJob consumers (vault-raft-snapshot, 3x CNPG barmanObjectStore,
nextcloud-pvc-sync) - confirmed live: all of them failing with 'Access
Key Id does not exist' right after being fixed, because the password had
already rotated out from under them again.

Fixed by pointing at a pre-existing Secret created directly on the VPS
cluster (kubectl, not git - this cluster has no Vault/ESO of its own).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:12:37 -05:00
Scooby HuskyandClaude Sonnet 5 7d73542645 Fix nextcloud-pvc-sync: add fsGroup for data access, fix restic cache dir
Now that the endpoint/credentials are fixed and this job can actually
reach the VPS, it surfaced two more real bugs: no fsGroup (couldn't read
the PVC data at all - nextcloud's real Deployment uses fsGroup 33/www-data,
this job never matched it) and no writable cache dir for runAsUser 65534
(restic defaults to $HOME/.cache).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:05:38 -05:00
Scooby HuskyandClaude Sonnet 5 7a6ecd14fe Bump rancher memory limit 2Gi -> 3Gi, was OOMKilling during restart recovery
Repeated restarts from tonight's network disruption kept OOMKilling
rancher - each restart's startup reconciliation is memory-hungry, and 2Gi
wasn't enough headroom for that burst even though steady-state usage is
normally fine. Nodes have ample free memory (26-36% used).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 21:01:48 -05:00
Scooby HuskyandClaude Sonnet 5 2fc7fb5b59 Fix Nextcloud Redis config: use externalRedis, not redis (bundled subchart toggle)
redis.enabled: true was unintentionally deploying the chart's BUNDLED redis
subchart (redis-21.1.3, StatefulSet nextcloud-redis-replicas) alongside
the actual redis-operator instance - that key's schema is pure subchart
passthrough (auth/image/master/replica), it has no host field. The
host/existingSecret keys previously set there were silently ignored.
Found while investigating why nextcloud-redis-replicas-0 was
crash-looping on failed liveness probes (confirmed via : the correct key for pointing at an external Redis
is the separate externalRedis: block).

This means Nextcloud's config.php may never have been correctly pointed
at the redis-operator instance despite apps/nextcloud/manifests/redis-cr.yaml
existing and being healthy - worth confirming file-locking/caching
actually engages after this deploys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 20:54:28 -05:00
Scooby HuskyandClaude Sonnet 5 e2c8ef0fde Commit crowdsec-firewall-bouncer headless-service fix (was uncommitted)
This fix for the EPERM ClusterIP-from-hostNetwork issue (Cilium socketLB
hostNamespaceOnly + hostNetwork bouncer + ClusterIP VIP) was made and
verified earlier but never committed - ArgoCD's selfHeal kept reverting
the live ConfigMap back to the broken crowdsec-service URL against git's
stale version, causing a fresh bouncer pod to crash-loop with the original
EPERM error again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 20:51:47 -05:00
Scooby HuskyandClaude Sonnet 5 018eb3570f Build real pod-egress routing to the VPS, fix everything blocked by its absence
Root cause of tonight's earlier CrowdSec/CNPG-backup workarounds: no node
in the cluster had any route into the Netbird mesh CIDR (100.108.0.0/16)
for pod-originated traffic. The per-namespace netbird 'router' pods are
inbound-only infrastructure (external peers reaching K8s services); their
own architecture has no reverse path.

Fix, in two parts:

1. infrastructure/netbird/manifests/egress-daemonset.yaml - one netbird
   client per node, hostNetwork so its wt0 interface lives in the node's
   real network namespace, plus a sidecar that adds a host route sending
   100.108.0.0/16 out via it. hostNetwork requires a scoped Kyverno
   PolicyException (infrastructure/kyverno/policies/netbird-egress-exception.yaml)
   to the disallow-host-namespaces STIG policy - narrowly for this one
   DaemonSet by name, not a namespace-wide exclusion.

2. Discovered the route alone wasn't enough for k3s NodePort traffic
   (vps-minio:30900): Netbird manages its own nftables ACLs independent of
   iptables/Kyverno, and its forward chain (netbird-rt-fwd) only permits
   *established* connections through a peer acting as a router - never new
   ones, by design, unless a Netbird 'Network Route' policy is explicitly
   configured (it isn't, for this VPS). Locally-terminated connections
   (tinyproxy) go through a separate, already-permissive ACL chain, which
   is why the CrowdSec proxy fix from earlier tonight worked. Replicated
   that working pattern for MinIO: minio-forward.service on the VPS host
   (systemd, socat) forwards 100.108.113.41:9000 -> MinIO's ClusterIP,
   avoiding the NodePort path entirely.

Re-enabled everything that was disabled/suspended earlier tonight because
of this gap, pointed at the new endpoint:
- CrowdSec CAPI/console-enroll (removed DISABLE_ONLINE_API, restored the
  VPS proxy env vars)
- n8n/nextcloud/authentik CNPG backup.barmanObjectStore
- vault-raft-snapshot CronJob (unsuspended)
- nextcloud PVC content sync CronJob endpoint

vps-minio.netbird.internal is retired everywhere - it was never actually
resolvable (Netbird has no DNS configured) even before today's routing
fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 20:36:13 -05:00
Scooby HuskyandClaude Sonnet 5 07a59d3b34 Fix vault-raft-snapshot: detect active leader, suspend until VPS egress exists
vault operator raft snapshot save fails against a standby Vault node with
'incomplete snapshot, unable to read SHA256SUMS.sealed file' - it must run
directly against the active HA leader. The job was hardcoded to vault-0,
which has been a standby since boot, so it had been failing on every run.

Fixed to detect the actual leader at runtime via vault status is_self.
Also suspended the CronJob: the upload step targets
vps-minio.netbird.internal, unreachable from any pod in this cluster (same
missing egress-route gap as tonight's CrowdSec/CNPG-backup fixes). The
leader-detection fix is kept since it's correct and independent - just
unsuspend once real egress routing exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 19:56:12 -05:00
Scooby HuskyandClaude Sonnet 5 e18308b750 Fix pg-n8n's long-recurring CNPG status-extraction error - missing AuthorizationPolicy rule
n8n's allow-n8n-access AuthorizationPolicy only allowed traffic from the
n8n/istio-system/prometheus namespaces - unlike nextcloud/authentik's
policies, it was missing the broader ipBlocks: 10.244.0.0/16 fallback
rule. The CNPG operator (cnpg-system namespace) polling pg-n8n instances'
status API (port 8000) wasn't covered by any rule, so Istio's L4
authorization enforcement (via ztunnel - this traffic is direct pod-IP,
not routed through a Service, so waypoint/L7 never sees it) rejected the
connection outright, surfacing as a TCP reset to the operator.

This was the actual cause of pg-n8n's long-recurring 'Instance Status
Extraction Error' - confirmed 100% reproducible tonight (every single
operator poll attempt failed identically), not the intermittent
HBONE-tunnel issue it resembled from the outside. Also labeled the n8n
namespace istio.io/use-waypoint=waypoint (was in the enrollment script's
namespace list but the label was missing live - namespace parity fix,
though not itself the fix for this specific issue since this traffic
bypasses the waypoint).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 19:50:29 -05:00
Scooby HuskyandClaude Sonnet 5 2c8d29c51f Disable VPS MinIO backup on n8n/nextcloud/authentik CNPG clusters
Discovered while debugging pg-n8n's recurring 'Instance Status Extraction
Error': the root cause is the same missing pod-egress route to the Netbird
mesh found tonight while fixing CrowdSec - continuousArchiving kept failing
to connect to vps-minio.netbird.internal, and CNPG correctly holds
Ready=False while archiving is broken (a real condition, not cosmetic).
That's what was driving these three apps' ArgoCD health flapping.

No node in the cluster has any route into 100.108.0.0/16 for
pod-originated traffic - the per-namespace netbird 'router' pods
(gitlab, vault, argocd, etc.) are inbound-only infrastructure, not egress
gateways. gitlab's own CNPG backup is unaffected (points at a local
in-cluster MinIO, not the VPS).

Commented out rather than deleted - re-enable once real pod-egress
routing exists, tracked as a separate task. No data loss: this is WAL
archiving/backup, not the live database.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 19:45:04 -05:00
Scooby HuskyandClaude Sonnet 5 ff638250d6 Disable CrowdSec online API - CAPI unreachable from home, egress routing gap found
LAPI treated a failed CAPI/console-enroll attempt as fatal at startup,
crashing the whole server (and taking agent + bouncer down with it via
their LAPI dependency) on every restart. Root cause investigation tonight
found two separate real issues:

1. Home's public IP gets a 403 from AWS WAF in front of api.crowdsec.net
   on any request, unrelated to credentials - ENROLL_KEY is valid and
   correctly wired but can't get past this.
2. Attempted routing around it via a VPS-side proxy over Netbird; found no
   node in the cluster has any route into the Netbird mesh CIDR for
   pod-originated egress traffic - the per-namespace netbird router pods
   (gitlab, vault, argocd, etc.) are inbound-only infrastructure. A proper
   fix needs either a Talos-native static route (fragile against pod IP
   churn) or a deliberate STIG exception for hostNetwork - out of scope
   for tonight, tracked separately.

DISABLE_ONLINE_API=true lets LAPI start cleanly with local-only detection
(agent + bouncer functional again). ENROLL_KEY stays set in Vault/values -
flip this back once real egress routing exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 19:37:33 -05:00
Scooby HuskyandClaude Sonnet 5 c8643ca025 Add dedicated netbird router for crowdsec namespace
Every other major namespace (gitlab, vault, argocd, nextcloud, etc.) has
its own 3-replica netbird-router deployment auto-provisioned from an
NBRoutingPeer CR; crowdsec never got one since it never previously needed
mesh connectivity. Without it, crowdsec pods had no dedicated mesh routing
path, causing intermittent connection resets/hangs reaching the VPS proxy
used to route around CrowdSec's WAF block on home's public IP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 19:27:39 -05:00
Scooby HuskyandClaude Sonnet 5 33b0d018f9 Route CrowdSec CAPI/console-enroll traffic through VPS proxy
Confirmed 2026-08-17: home's public IP gets a 403 ForbiddenException from
AWS WAF in front of api.crowdsec.net, even on a bare unauthenticated test
request - the VPS's IP gets a normal 401 on the identical request. This is
an IP-level block unrelated to credentials, which is why LAPI kept hitting
403 on both anonymous auto-registration and console enroll with a fresh,
valid enrollment key.

Fix: HTTPS_PROXY/HTTP_PROXY pointed at a tinyproxy instance on the VPS
(bound to its Netbird interface only, not internet-reachable), reachable
from home over the existing Netbird mesh via netbird-cluster-router.
cscli/crowdsec's Go HTTP client honors standard proxy env vars.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 18:39:23 -05:00
Scooby HuskyandClaude Sonnet 5 bbb80687ca Fix netbird-operator namespace reference in istio mesh policies
allow-netbird-operator-access (AuthorizationPolicy) and the netbird-operator
PeerAuthentication both targeted namespace 'netbird-operator', which has
never existed - the actual netbird-operator pod runs in the 'netbird'
namespace. This permanently blocked istio-mesh-config from going Synced
(kubectl apply failed: namespaces "netbird-operator" not found on every
sync attempt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 18:20:00 -05:00
Scooby HuskyandClaude Sonnet 5 3226169af8 Fix Vault kubernetes-auth reviewer token expiring 1hr after every boot
vault-init-and-store.sh minted the auth/kubernetes/config reviewer JWT via
'kubectl create token vault-auth' with no --duration, defaulting to a 1hr
TTL, then wrote it into Vault as a static token_reviewer_jwt. ~1hr after
every cluster boot/hook rerun it silently expired, breaking TokenReview
for every kubernetes-auth login (including ESO's) with a generic 403 that
logged nothing at INFO/ERROR. This was the actual root cause of today's
broad ArgoCD Degraded wave across ~14 apps - not Vault's seal state, which
was fine the whole time.

Fix: clear token_reviewer_jwt and rely on Vault's local-JWT auto-detection
(disable_local_ca_jwt=false default), which reads the vault pod's own
kubelet-refreshed SA token from disk on every call instead of a static
copy. The vault SA already has system:auth-delegator via the existing
vault-server-binding ClusterRoleBinding.

Applied live directly against Vault to unblock immediately; confirmed
ClusterSecretStore vault-backend flipped to Ready=True and previously
broken ExternalSecrets (guacamole, teslamate, unpoller, netbird x3) all
resynced successfully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 18:19:13 -05:00
Scooby Husky 9bb4ada1f5 Fix Redis CR permission denied on PVC (needs fsGroup)
redis-nextcloud-0 was CrashLoopBackOff: "mkdir: can't create directory
'/node-conf': Permission denied" and same for appendonlydir - the pod
had no podSecurityContext, so it couldn't write to its own PVC-backed
data directories. Added fsGroup/runAsUser: 1000 matching the
opstree/redis image's default non-root user. Verified live: Redis now
starts cleanly ("Ready to accept connections").
2026-08-17 17:20:16 -05:00
Scooby Husky a8b16fa4cb Fix Redis CR apiVersion/schema - v1beta2 doesn't exist, blocked whole nextcloud sync
apps/nextcloud/manifests/redis-cr.yaml used redis.redis.opstreelabs.in/v1beta2
(guessed, flagged as unverified when written) - the actually-installed CRD
only has v1beta1. ArgoCD validates all resource API groups/versions before
syncing anything, so this one bad apiVersion blocked the ENTIRE nextcloud
Application from syncing at all (every resource showed OutOfSync, not just
this one).

Also fixed against the live CRD schema: redisSecret belongs nested under
kubernetesConfig, not top-level, and redisExporter.image is required by the
schema even when enabled: false.

Verified live: kubectl apply succeeded and the Redis resource was created
before committing this.
2026-08-17 16:56:20 -05:00
Scooby Husky 7b3669e9a6 Fix CNPG operator -> instance status communication + add CNPG health check
Two related fixes, found while working through why authentik/n8n/gitlab
ArgoCD Applications showed Unknown/stuck health despite pods being fine:

1. ArgoCD has no built-in health check for CNPG's Cluster CRD, so it
   always reported "Unknown" - and since app-level health rolls up to
   the worst resource status, every app with a CNPG Cluster showed
   Unknown/Progressing regardless of actual state. Added a Lua health
   check (resource.customizations.health.postgresql.cnpg.io_Cluster)
   reading .status.conditions[Ready] / .status.phase.

2. Once that started reporting real status instead of masking it,
   pg-authentik showed a genuine problem: CNPG's operator couldn't
   reach its Postgres instances' status API (port 8000) - "Cannot
   extract Pod status ... context deadline exceeded" - because:
   a) authentik-ingress's CiliumNetworkPolicy never allowed cnpg-system
      as a source (crowdsec-ingress already had this exception,
      authentik-ingress was just missing it - inconsistency, not
      deliberate).
   b) Even after fixing (a), still blocked - pg-authentik's pods are
      ambient-mesh-enrolled, so the connection actually goes through
      ztunnel's HBONE tunnel (port 15008) first, same underlying issue
      as the argocd-redis fix from earlier today. Rather than keep
      finding and patching this per-namespace as it recurs, added a
      cluster-wide CiliumClusterwideNetworkPolicy allowing HBONE
      broadly - ztunnel's own mTLS/SPIFFE identity verification is the
      real security boundary for mesh traffic; Cilium blocking the
      tunnel port itself was only breaking legitimate traffic, not
      adding meaningful protection on top.

Verified live: CNPG operator immediately went from failing status
extraction to successfully reconciling (recreating a pod to reattach
its PVC) once both fixes were in place.
2026-08-17 16:41:11 -05:00
Scooby Husky 99b8ca6be9 Fix Nextcloud OIDC client_id/secret - was hardcoded empty
apps/nextcloud/values.yaml had oidc_login_client_id/secret as literal
empty strings, causing "client_id is missing or invalid" on login.
The matching Authentik blueprint (infrastructure/authentik/nextcloud-
blueprint.yaml) already defines fixed credentials for this OAuth2
provider - just never got copied over. Filled in to match.

Also found and left alone: an orphaned nextcloud-oidc-config ConfigMap
(from January) that already had the correct values but isn't actually
mounted by the current Deployment (which uses nextcloud-config instead)
- harmless dead resource, not the active config path.
2026-08-17 16:33:47 -05:00
Scooby Husky 8372887ce1 Fix two long-standing bugs in the vault-init PreSync hook script
Both pre-existing (not from today's work), found while cleaning up a
recurring stale-pod pattern (a fresh vault-init Job failing on every
sync of the vault Application):

1. Init-detection was always broken: `vault status -format=json`
   pretty-prints with a space after colons ("initialized": true), but
   the parser (grep -o '"initialized":[a-z]*' | cut -d: -f2) required
   no space - it always matched empty, so `initialized`/`sealed` were
   always blank regardless of real state. Every run concluded "not
   initialized" and tried to re-init, which correctly fails once Vault
   already has been ("Vault is already initialized"). Switched to jq.
   Separately, the old `|| echo '{}'` fallback also discarded valid
   JSON on vault status's normal non-zero exit codes (2 = sealed) -
   fixed too, though the parsing bug was the actual blocker.

2. `vault_exec` wraps `kubectl exec` without `-i`, so the heredoc piped
   into `vault policy write NAME -` never reached the remote command -
   vault saw an empty policy body. Never previously reached in
   practice since bug #1 always failed the script earlier. Added -i.

Verified end-to-end with a one-off Job run: script now correctly
detects the already-initialized state and completes successfully,
including the policy/role writes that were previously unreachable.
2026-08-17 16:13:48 -05:00
Scooby Husky ee71e4f46f Fix DDNS/failover Cloudflare zone name and add restricted PSS securityContext
- ZONE_NAME was "kube.huskypup.net" in both the home-ip-ddns CronJob and
  the VPS failover watcher - that's a record, not a Cloudflare zone (the
  actual zone is the parent "huskypup.net"). Caused home-ip-ddns to fail
  every run (zone lookup returned zero results, curl -f exit 22) -
  confirmed live and fixed.
- Added seccompProfile/non-root/dropped-capabilities securityContext to
  the three CronJobs added this session that were missing it (flagged by
  the cluster's "restricted" PodSecurity admission). Repointed the raft
  snapshot job's mc binary install from /usr/local/bin to /tmp so it
  still works running as non-root.
2026-08-17 15:55:47 -05:00
Scooby Husky 983671ef50 Fix ArgoCD Redis connectivity broken by ambient mesh HBONE NetworkPolicy gap
Root cause (confirmed via ztunnel logs): the argo-cd chart's default
NetworkPolicies only allow each component's application port (e.g. 6379
for redis), but Istio ambient mode routes ALL pod-to-pod traffic through
ztunnel's HBONE tunnel on port 15008 first - so the tunnel itself was
being blocked even though the "real" port was allowed. Every inter-pod
connection in the argocd namespace hung for exactly 10s then reset;
ztunnel's own log named it directly ("connection timed out, maybe a
NetworkPolicy is blocking HBONE port 15008"). This broke argocd-server's
Redis-backed session/cluster-info caching cluster-wide and was silently
preventing the root Application from picking up new child Applications.

Fix: additive NetworkPolicy allowing ingress on 15008 for all argocd
pods (NetworkPolicies union across multiple policies selecting a pod,
so this doesn't touch/replace the chart's rendered ones - safe across
Helm upgrades).

Also: dropped argocd from the namespace-enrollment job's waypoint list.
argocd's only AuthorizationPolicy (allow-argocd-access) is a plain
source-namespace/IP match with no L7 rules - its own status shows
"attached to ztunnel", not waypoint - so forcing L7 waypoint processing
onto the namespace was unnecessary overhead, not a security requirement.
ztunnel's mTLS still fully covers it. (This was a red herring for the
HBONE bug itself, not the fix, but a valid simplification found along
the way.)
2026-08-17 15:08:33 -05:00
Scooby Husky 7990f1fa47 Add VPS warm-standby/backup site (Phase 0-1b)
Foundation for a DR/backup path using an always-on VPS as a second
ArgoCD-managed cluster, plus DB/backup standardization work that fell
out of it:

- vps-standby ArgoCD cluster destination + AppProject, MinIO backup
  receiver, VPS bootstrap script (k3s, Netbird, cert-manager)
- Dual-site DNS failover watcher + home-IP DDNS CronJob, Cloudflare
  token moved out of git into Vault+ExternalSecret
- Nextcloud migrated from ad-hoc MariaDB to CNPG + redis-operator
  (matches n8n/Authentik/GitLab's backup-native pattern)
- Authentik's CNPG manifests moved into the actual ArgoCD-synced
  manifests/ path (were present but never wired into the sync path)
- Vault raft-snapshot CronJob, CNPG barmanObjectStore backups
  (Authentik/n8n/Nextcloud), Nextcloud file-PVC restic sync - all
  targeting the new VPS MinIO receiver

See VPS Warm-Standby plan doc for full design rationale.
2026-08-17 14:59:26 -05:00
Scooby HuskyandClaude Opus 4.6 5163403e24 Scale down non-critical replicas for homelab resource savings
Reduce replicas to 1 for workloads that don't need HA in a homelab:
- Prometheus 2→1, Alertmanager 2→1 (~4.4GB RAM saved)
- cert-manager 3→1
- GitLab: registry 2→1, kas 2→1, sidekiq 2→1, praefect 2→1,
  pgbouncer-ro 3→1
- Guacamole + guacd 2→1
- Kiali 2→1, ArgoCD server 2→1
- Kyverno background-controller 2→1
- Scylla operator 2→1
- ext-authz-proxy 2→1, netbird-cluster-router 2→1

Kept multi-replica: coredns, envoy-gateway, kyverno admission,
vault, argocd-repo-server, gitlab-webservice, istiod, rook-ceph CSI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 23:00:48 -05:00
Scooby HuskyandClaude Opus 4.6 fa23e5084c Reduce GitLab latency: increase Gitaly CPU limit and Puma workers
Gitaly CPU limit was 250m causing throttling on git operations (485ms
p99 to webservice). Increased to 1 core with 100m request. Also added
workerProcesses: 3 to webservice for better request concurrency
(was default 2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:47:39 -05:00
Scooby HuskyandClaude Opus 4.6 a8b9064248 Set Grafana deployment strategy to Recreate for RWO PVC compatibility
RollingUpdate with a ReadWriteOnce PVC causes a Multi-Attach deadlock
where the new pod can't start because the old pod still holds the
volume. Recreate strategy ensures the old pod is terminated before
the new one is created.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:33:16 -05:00
Scooby HuskyandClaude Opus 4.6 ff00f666a0 Add privileged PodSecurity label to 8 namespaces for NetBird router
NetBird router deployments require NET_ADMIN capability, which is
blocked by the default baseline PodSecurity standard. Add
managedNamespaceMetadata with pod-security.kubernetes.io/enforce:
privileged to argocd, frigate, gitlab, grafana, guacamole,
home-assistant, teslamate, and vault ArgoCD Applications.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:27:39 -05:00
Scooby HuskyandClaude Opus 4.6 02632d06a4 Fix NetBird service annotations: use netbird.io/expose for operator
The operator watches for netbird.io/expose (presence-based), not
netbird.io/resource which was from the old Helmfile bootstrap script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 16:25:35 -05:00
Scooby HuskyandClaude Opus 4.6 2edc8a156e Fix netbird-operator: add self-hosted management URL
The operator was missing managementURL, causing it to default to the
NetBird SaaS cloud (api.netbird.io) instead of the self-hosted instance.
This resulted in an infinite reconcile loop and no networks/resources
being created.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 16:08:51 -05:00
Scooby HuskyandClaude Opus 4.6 0c16785116 Fix Guacamole OIDC session persistence via Istio DestinationRule
Traffic goes through Istio (not Envoy Gateway), so cookie-based
consistent hashing must be on the Istio DestinationRule. Removes
the unused Envoy Gateway BackendTrafficPolicy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 15:11:15 -05:00
Scooby HuskyandClaude Opus 4.6 f71dbdb689 Add Envoy Gateway cookie-based session persistence for Guacamole OIDC
Fixes OIDC nonce validation failures with 2 replicas by using a
BackendTrafficPolicy with cookie-based consistent hashing instead of
Service-level ClientIP affinity (which doesn't work behind Envoy proxy).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:55:53 -05:00
Scooby HuskyandClaude Opus 4.6 b9fd5a9461 Add session affinity to Guacamole service for OIDC flow
With 2 replicas, OIDC callbacks can hit a different pod than the
one that generated the state/nonce, causing auth failures. Session
affinity ensures the same client always reaches the same pod.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:47:25 -05:00
Scooby HuskyandClaude Opus 4.6 e29eb9611b Make Authentik the primary login method for Guacamole
Change EXTENSION_PRIORITY from "*,openid" to "openid,*" so
unauthenticated users are redirected to Authentik OIDC instead
of seeing the database login form.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:24:53 -05:00
Scooby HuskyandClaude Opus 4.6 987f91c877 Enable CephFS CSI driver for SMB gateway and CephFS volumes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 12:27:00 -05:00
Scooby HuskyandClaude Opus 4.6 7eae427d1a Scale services to 2+ replicas for HA
- ArgoCD: server and repo-server → 2 replicas
- GitLab: webservice, sidekiq, registry, KAS, praefect → min 2 replicas
- Guacamole: client and guacd → 2 replicas
- Kiali: 1 → 2 replicas
- Alertmanager: add 2 replicas
- TeslaMate CNPG: 1 → 2 instances

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 03:08:49 -05:00
Scooby HuskyandClaude Opus 4.6 38b1c16c1e Update rook-ceph-operator chart to v1.19.2 and remove stale comment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 21:25:22 -05:00
Scooby HuskyandClaude Opus 4.6 5d09fb86e7 Add GitLab Redis initializer ordering fix
Fixes NameError: uninitialized constant Gitlab::Redis::ALL_CLASSES
caused by 7_prometheus_metrics.rb loading before 7_redis.rb
(alphabetically 'p' < 'r'). The ConfigMap provides a 0_redis_early.rb
initializer that requires gitlab/redis before other initializers run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 20:26:24 -05:00
Scooby HuskyandClaude Opus 4.6 c0fd93c73b Upgrade Ceph to v19.2.3 (Squid)
Includes security fixes (CVE-2023-43040) and RBD/RADOS improvements.
Rook operator upgraded to v1.19.2 via Helm.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 18:38:10 -05:00
Scooby HuskyandClaude Opus 4.6 a93df9375a Add talos-cp-02 back to Rook-Ceph nodes list
LVM filter now applied via Talos machine config on all nodes to prevent
lvs hang on BIOS partitions. talos-cp-02 can safely run OSD prepare jobs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 16:31:13 -05:00
Scooby HuskyandClaude Opus 4.6 2643d5e0bf Fix TeslaMate DB corruption and Rook-Ceph reconcile hang
- Scale CNPG pg-teslamate to 1 instance: on-disk storage corruption
  (corrupted sequences, system catalog indexes) means replicas cloned
  via pg_basebackup inherit corruption and can't open any database.
  Primary works from cached catalogs. Sequences already rebuilt live.

- Remove talos-cp-02 from Rook-Ceph nodes list: ceph-volume inventory
  hangs on BIOS partition via nsenter lvs in host namespace. Talos
  read-only /etc prevents LVM filter fix. OSD 1 keeps running from
  its existing deployment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 14:53:31 -05:00
Scooby HuskyandClaude Opus 4.6 8df30c11ee Re-enable Ceph dashboard SSL to fix SAML http/https URL mismatch
X-Forwarded-Proto approach didn't work - Ceph dashboard SAML doesn't
read it. Re-enabling native SSL so the dashboard sees https:// URLs
directly. Updated both Istio and Envoy Gateway backends for TLS with
insecureSkipVerify for the self-signed cert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 13:56:33 -05:00
Scooby HuskyandClaude Opus 4.6 fb0ed0071f Set X-Forwarded-Proto header for Ceph dashboard SAML authentication
Dashboard runs with ssl: false behind TLS-terminating proxy, so SAML
library sees http:// URLs and rejects responses expecting https://.
Explicitly set X-Forwarded-Proto: https on both Istio VirtualService
and Envoy Gateway HTTPRoute so the SAML ACS URL matches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 13:48:35 -05:00
Scooby HuskyandClaude Opus 4.6 7c184f9d14 Fix Ceph dashboard DestinationRule: disable TLS to match dashboard config
Dashboard has ssl: false (plain HTTP on port 7000), but the DestinationRule
had tls.mode: SIMPLE which caused Istio to attempt TLS to the backend,
resulting in WRONG_VERSION_NUMBER errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 13:41:57 -05:00