mirror of
https://gitlab.kube.huskypup.net/Scooby/Homelabv4.git
synced 2026-08-21 05:26:49 +00:00
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>
157 lines
6.0 KiB
YAML
157 lines
6.0 KiB
YAML
---
|
|
# Periodic Vault raft snapshot, shipped to the VPS MinIO backup receiver.
|
|
# This is the DR path for Vault's data independent of the unseal-key custody
|
|
# story - a Ceph/cluster-loss disaster is recovered by standing up a fresh
|
|
# Vault and `vault operator raft snapshot restore`ing the latest one of these,
|
|
# not by anything to do with the unseal key itself.
|
|
#
|
|
# Requires a one-time manual step after VPS bootstrap: store the VPS MinIO
|
|
# root credentials (see infrastructure/vps-standby/minio/values.yaml) into
|
|
# this cluster's Vault so ESO can hand them to the CronJob:
|
|
# vault kv put secret/vps-minio-credentials \
|
|
# access-key=<minio-root-user> secret-key=<minio-root-password>
|
|
apiVersion: external-secrets.io/v1
|
|
kind: ExternalSecret
|
|
metadata:
|
|
name: vps-minio-credentials
|
|
namespace: vault
|
|
spec:
|
|
refreshInterval: 1h
|
|
secretStoreRef:
|
|
kind: ClusterSecretStore
|
|
name: vault-backend
|
|
target:
|
|
name: vps-minio-credentials
|
|
creationPolicy: Owner
|
|
data:
|
|
- secretKey: access-key
|
|
remoteRef:
|
|
key: vps-minio-credentials
|
|
property: access-key
|
|
- secretKey: secret-key
|
|
remoteRef:
|
|
key: vps-minio-credentials
|
|
property: secret-key
|
|
---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: vault-raft-snapshot-script
|
|
namespace: vault
|
|
data:
|
|
snapshot.sh: |
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
# VPS's Netbird address - replace with the actual peer IP/hostname once
|
|
# the VPS is bootstrapped and joined to the mesh (scripts/vps-bootstrap.sh).
|
|
# 100.108.113.41:9000 is a locally-terminated socat forward on the VPS
|
|
# host (minio-forward.service) to MinIO's ClusterIP, not the NodePort.
|
|
# vps-minio.netbird.internal was never resolvable (Netbird has no DNS
|
|
# configured) and the NodePort itself is unreachable from other peers -
|
|
# Netbird's own ACL model only permits established forwarded
|
|
# connections through a peer, never new ones. See egress-daemonset.yaml
|
|
# for the pod-egress route this now travels over.
|
|
VPS_MINIO_ENDPOINT="${VPS_MINIO_ENDPOINT:-100.108.113.41:9000}"
|
|
BUCKET="vault-raft-snapshots"
|
|
SNAP_NAME="vault-raft-$(date -u +%Y%m%dT%H%M%SZ).snap"
|
|
|
|
ROOT_TOKEN="$(kubectl -n vault get secret vault-init-keys -o jsonpath='{.data.VAULT_ROOT_TOKEN}' | base64 -d)"
|
|
|
|
# `vault operator raft snapshot save` fails against a standby node with
|
|
# "incomplete snapshot, unable to read SHA256SUMS.sealed file" - it must
|
|
# run directly against the active HA leader, not a proxied standby.
|
|
# Found 2026-08-17: this job had been failing every run since it was
|
|
# added, hardcoded to vault-0 (frequently a standby). Detect the actual
|
|
# leader at runtime instead of hardcoding a pod name.
|
|
echo "==> Finding the active Vault leader..."
|
|
LEADER_POD=""
|
|
for pod in vault-0 vault-1 vault-2; do
|
|
is_leader="$(kubectl -n vault exec "$pod" -- env VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN="$ROOT_TOKEN" \
|
|
vault status -format=json 2>/dev/null | jq -r '.is_self // false')"
|
|
if [ "$is_leader" = "true" ]; then
|
|
LEADER_POD="$pod"
|
|
break
|
|
fi
|
|
done
|
|
if [ -z "$LEADER_POD" ]; then
|
|
echo "ERROR: could not find an active Vault leader among vault-0/1/2" >&2
|
|
exit 1
|
|
fi
|
|
echo "==> Taking raft snapshot from ${LEADER_POD} (active leader)..."
|
|
kubectl -n vault exec "$LEADER_POD" -- env VAULT_TOKEN="$ROOT_TOKEN" \
|
|
vault operator raft snapshot save "/tmp/${SNAP_NAME}"
|
|
|
|
echo "==> Copying snapshot out of ${LEADER_POD}..."
|
|
kubectl -n vault cp "${LEADER_POD}:/tmp/${SNAP_NAME}" "/tmp/${SNAP_NAME}"
|
|
kubectl -n vault exec "$LEADER_POD" -- rm -f "/tmp/${SNAP_NAME}"
|
|
|
|
echo "==> Installing mc (MinIO client)..."
|
|
curl -sf https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc
|
|
chmod +x /tmp/mc
|
|
export MC_CONFIG_DIR=/tmp/.mc
|
|
/tmp/mc alias set vps-minio "http://${VPS_MINIO_ENDPOINT}" \
|
|
"${MINIO_ACCESS_KEY}" "${MINIO_SECRET_KEY}" >/dev/null
|
|
|
|
echo "==> Uploading ${SNAP_NAME} to vps-minio/${BUCKET}..."
|
|
/tmp/mc cp "/tmp/${SNAP_NAME}" "vps-minio/${BUCKET}/${SNAP_NAME}"
|
|
rm -f "/tmp/${SNAP_NAME}"
|
|
|
|
echo "==> Pruning snapshots older than 30 days..."
|
|
/tmp/mc find "vps-minio/${BUCKET}" --older-than 30d --exec "/tmp/mc rm {}" || true
|
|
|
|
echo "==> Done: ${SNAP_NAME}"
|
|
---
|
|
apiVersion: batch/v1
|
|
kind: CronJob
|
|
metadata:
|
|
name: vault-raft-snapshot
|
|
namespace: vault
|
|
spec:
|
|
schedule: "0 */6 * * *" # every 6 hours
|
|
# Re-enabled 2026-08-18: real pod-egress routing to the VPS now exists
|
|
# (netbird-egress DaemonSet) and the endpoint above was fixed to use the
|
|
# locally-terminated socat forward instead of the unreachable NodePort.
|
|
concurrencyPolicy: Forbid
|
|
successfulJobsHistoryLimit: 3
|
|
failedJobsHistoryLimit: 3
|
|
jobTemplate:
|
|
spec:
|
|
backoffLimit: 2
|
|
template:
|
|
spec:
|
|
serviceAccountName: argocd-hook-sa # already has kubectl exec rights in this namespace (see vault-init-job.yaml)
|
|
restartPolicy: Never
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 65534
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
- name: raft-snapshot
|
|
image: alpine/k8s:1.32.13
|
|
command: ["/bin/bash", "/scripts/snapshot.sh"]
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
env:
|
|
- name: MINIO_ACCESS_KEY
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: vps-minio-credentials
|
|
key: access-key
|
|
- name: MINIO_SECRET_KEY
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: vps-minio-credentials
|
|
key: secret-key
|
|
volumeMounts:
|
|
- name: scripts
|
|
mountPath: /scripts
|
|
volumes:
|
|
- name: scripts
|
|
configMap:
|
|
name: vault-raft-snapshot-script
|
|
defaultMode: 0755
|