From 1a657ad0a4f4596b12320ec0d5642e5c64572a0f Mon Sep 17 00:00:00 2001 From: Scooby Husky Date: Mon, 17 Aug 2026 22:28:10 -0500 Subject: [PATCH] Phase 2: deploy Authentik warm standby on the VPS CNPG operator on the VPS (argocd-apps/vps-standby/cnpg-operator.yaml), plus a CNPG 'replica cluster' for pg-authentik (infrastructure/ vps-standby/authentik/manifests/cnpg-cluster.yaml) that continuously replays WAL shipped from home's pg-authentik via VPS MinIO's cnpg-backups/pg-authentik bucket path - CNPG's native replica-cluster mode, not a custom restore script like Vault needed (Vault has no equivalent built-in continuous-replication-into-object-store feature). The Authentik app itself runs at replicas: 0 in normal operation - the replica DB is read-only until a deliberate manual promotion (spec.replica.enabled: false), and a pod trying to write against a read-only DB would just crashloop uselessly. values.yaml's authentik: block is copied byte-identical from home's on purpose: the chart deterministically derives its generated Secret's AUTHENTIK_SECRET_KEY from these literal values.yaml strings, so both clusters land on the same key without manually copying it - required since that key decrypts things stored encrypted in the replicated DB. --- argocd-apps/vps-standby/authentik.yaml | 33 ++++++ argocd-apps/vps-standby/cnpg-operator.yaml | 27 +++++ .../authentik/manifests/cnpg-cluster.yaml | 68 ++++++++++++ .../authentik/manifests/presync-job.yaml | 87 +++++++++++++++ .../vps-standby/authentik/values.yaml | 101 ++++++++++++++++++ 5 files changed, 316 insertions(+) create mode 100644 argocd-apps/vps-standby/authentik.yaml create mode 100644 argocd-apps/vps-standby/cnpg-operator.yaml create mode 100644 infrastructure/vps-standby/authentik/manifests/cnpg-cluster.yaml create mode 100644 infrastructure/vps-standby/authentik/manifests/presync-job.yaml create mode 100644 infrastructure/vps-standby/authentik/values.yaml diff --git a/argocd-apps/vps-standby/authentik.yaml b/argocd-apps/vps-standby/authentik.yaml new file mode 100644 index 0000000..83e6430 --- /dev/null +++ b/argocd-apps/vps-standby/authentik.yaml @@ -0,0 +1,33 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vps-authentik + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "3" # after vps-cnpg (wave 1) and vps-minio (wave 2) + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: vps-standby + sources: + - repoURL: https://charts.goauthentik.io + chart: authentik + targetRevision: 2026.2.1 + helm: + valueFiles: + - $values/infrastructure/vps-standby/authentik/values.yaml + - repoURL: https://gitlab.kube.huskypup.net/Scooby/Homelabv4.git + targetRevision: main + ref: values + - repoURL: https://gitlab.kube.huskypup.net/Scooby/Homelabv4.git + targetRevision: main + path: infrastructure/vps-standby/authentik/manifests + destination: + name: vps-standby + namespace: authentik + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/argocd-apps/vps-standby/cnpg-operator.yaml b/argocd-apps/vps-standby/cnpg-operator.yaml new file mode 100644 index 0000000..eb5772a --- /dev/null +++ b/argocd-apps/vps-standby/cnpg-operator.yaml @@ -0,0 +1,27 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vps-cnpg + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" # before minio/vault/gitea (wave 2) and the CNPG-backed standby apps (wave 3) + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: vps-standby + source: + repoURL: https://cloudnative-pg.github.io/charts + chart: cloudnative-pg + targetRevision: "*" # matches home's argocd-apps/infrastructure/cnpg.yaml (also unpinned) + destination: + name: vps-standby + namespace: cnpg-system + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - ServerSideDiff=true + - RespectIgnoreDifferences=true diff --git a/infrastructure/vps-standby/authentik/manifests/cnpg-cluster.yaml b/infrastructure/vps-standby/authentik/manifests/cnpg-cluster.yaml new file mode 100644 index 0000000..14cbb16 --- /dev/null +++ b/infrastructure/vps-standby/authentik/manifests/cnpg-cluster.yaml @@ -0,0 +1,68 @@ +--- +# CNPG "replica cluster" - continuously replays WAL shipped from home's +# pg-authentik cluster (infrastructure/authentik/manifests/cnpg-cluster.yaml) +# via VPS MinIO's cnpg-backups/pg-authentik bucket path. Unlike Vault's +# raft-restore CronJob (Vault has no native continuous-replication-into- +# object-store feature), CNPG's replica-cluster mode is a first-class +# built-in mechanism - no custom restore scripting needed, matches the +# plan's intent directly. +# +# While spec.replica.enabled is true, this cluster is a read-only standby +# in continuous recovery - the -rw service exists but is NOT writable +# (same "nothing accepts writes in normal operation" discipline as Vault/ +# Gitea). Promoting it to a real writable primary during an actual +# incident is a deliberate manual step: +# kubectl -n authentik patch cluster pg-authentik --type merge \ +# -p '{"spec":{"replica":{"enabled":false}}}' +# then scale the authentik Deployment(s) up from 0. +# +# vps-minio-secret is a plain Secret copied here manually (kubectl, not +# git) from the vps-minio-root-secret in the minio namespace - this +# cluster has no Vault/ESO of its own: +# kubectl -n authentik create secret generic vps-minio-secret \ +# --from-literal=accesskey= \ +# --from-literal=secretkey= +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: pg-authentik + namespace: authentik +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:16 + instances: 1 + + resources: + requests: + memory: "256Mi" + cpu: "25m" + limits: + memory: "1Gi" + cpu: "250m" + + storage: + size: 5Gi + storageClass: local-path + + bootstrap: + recovery: + source: home-backup + + externalClusters: + - name: home-backup + barmanObjectStore: + destinationPath: s3://cnpg-backups/pg-authentik + endpointURL: http://vps-minio.minio.svc.cluster.local:9000 + s3Credentials: + accessKeyId: + name: vps-minio-secret + key: accesskey + secretAccessKey: + name: vps-minio-secret + key: secretkey + + replica: + enabled: true + source: home-backup + + monitoring: + enablePodMonitor: false # no Prometheus on the VPS cluster diff --git a/infrastructure/vps-standby/authentik/manifests/presync-job.yaml b/infrastructure/vps-standby/authentik/manifests/presync-job.yaml new file mode 100644 index 0000000..6d3eaa8 --- /dev/null +++ b/infrastructure/vps-standby/authentik/manifests/presync-job.yaml @@ -0,0 +1,87 @@ +--- +# Same idempotent secret-bootstrap pattern as home's +# infrastructure/authentik/manifests/presync-job.yaml, scoped to a +# dedicated ServiceAccount here (no shared argocd-hook-sa exists on the +# vps-standby cluster the way it does at home). +apiVersion: v1 +kind: ServiceAccount +metadata: + name: authentik-hook + namespace: authentik +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: authentik-hook + namespace: authentik +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create"] + - apiGroups: ["postgresql.cnpg.io"] + resources: ["clusters"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: authentik-hook + namespace: authentik +subjects: + - kind: ServiceAccount + name: authentik-hook + namespace: authentik +roleRef: + kind: Role + name: authentik-hook + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: authentik-presync + namespace: authentik + annotations: + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation +spec: + backoffLimit: 3 + template: + spec: + serviceAccountName: authentik-hook + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 65534 + seccompProfile: + type: RuntimeDefault + containers: + - name: presync + image: alpine/k8s:1.32.13 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + command: + - /bin/bash + - -c + - | + set -euo pipefail + echo "=== Authentik (VPS) PreSync ===" + + # NOTE: does NOT wait for pg-authentik to be "Healthy" the way + # home's presync-job does - a replica cluster in continuous + # recovery never reports Healthy in that sense (it's + # perpetually catching up / read-only), and server/worker are + # at replicas: 0 anyway so nothing actually needs the DB yet. + + if ! kubectl -n authentik get secret authentik >/dev/null 2>&1; then + echo "Creating authentik secret (literal env:// placeholder -" + echo "matches home's values.yaml exactly, see values.yaml comment)..." + kubectl -n authentik create secret generic authentik \ + --from-literal=AUTHENTIK_SECRET_KEY="env://AUTHENTIK_SECRET_KEY" + else + echo "authentik secret already exists" + fi + + echo "=== Done ===" diff --git a/infrastructure/vps-standby/authentik/values.yaml b/infrastructure/vps-standby/authentik/values.yaml new file mode 100644 index 0000000..04a30f1 --- /dev/null +++ b/infrastructure/vps-standby/authentik/values.yaml @@ -0,0 +1,101 @@ +# Authentik warm standby on the VPS - Phase 2. The CNPG replica cluster +# (manifests/cnpg-cluster.yaml) continuously replays WAL from home in the +# background; the app itself is NOT run in normal operation (replicas: 0) +# since the replica DB is read-only until a deliberate manual promotion - +# an Authentik pod trying to write against a read-only DB would just +# crashloop uselessly. Promotion runbook: flip the CNPG cluster's +# spec.replica.enabled to false (see cnpg-cluster.yaml), then scale +# server/worker up from 0. +# +# The `authentik:` block below (secret_key/postgresql "env://" indirection) +# is copied VERBATIM from infrastructure/authentik/values.yaml on purpose - +# the authentik chart deterministically renders its own generated Secret +# from these literal values.yaml strings (not randomly), so keeping this +# block byte-identical between home and VPS means both clusters +# independently arrive at the same AUTHENTIK_SECRET_KEY without needing to +# manually copy it - required for promotion to work, since that key +# decrypts things (stored provider secrets, cert private keys) that live +# encrypted in the DB being replicated. +authentik: + secret_key: env://AUTHENTIK_SECRET_KEY + postgresql: + host: env://AUTHENTIK_POSTGRESQL__HOST + port: 5432 + name: env://AUTHENTIK_POSTGRESQL__NAME + user: env://AUTHENTIK_POSTGRESQL__USER + password: env://AUTHENTIK_POSTGRESQL__PASSWORD + +global: + envFrom: + - secretRef: + name: authentik + - secretRef: + name: pg-authentik-app + prefix: AUTHENTIK_POSTGRESQL__ + env: + - name: AUTHENTIK_URL + value: "https://auth.kube.huskypup.net" + - name: AUTHENTIK_POSTGRESQL__HOST + valueFrom: + secretKeyRef: + name: pg-authentik-app + key: host + - name: AUTHENTIK_POSTGRESQL__NAME + valueFrom: + secretKeyRef: + name: pg-authentik-app + key: dbname + - name: AUTHENTIK_POSTGRESQL__USER + valueFrom: + secretKeyRef: + name: pg-authentik-app + key: username + - name: AUTHENTIK_POSTGRESQL__PASSWORD + valueFrom: + secretKeyRef: + name: pg-authentik-app + key: password + - name: AUTHENTIK_POSTGRESQL__PORT + value: "5432" + +server: + replicas: 0 + containerSecurityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + resources: + requests: + cpu: 50m + memory: 512Mi + limits: + memory: 1Gi + metrics: + enabled: false + +worker: + replicas: 0 + containerSecurityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + resources: + requests: + cpu: 50m + memory: 512Mi + limits: + memory: 1Gi + metrics: + enabled: false + +# Disable bundled Postgres - using the CNPG replica cluster instead +postgresql: + enabled: false