diff --git a/argocd-apps/vps-standby/vps-eso.yaml b/argocd-apps/vps-standby/vps-eso.yaml new file mode 100644 index 0000000..35b5c5f --- /dev/null +++ b/argocd-apps/vps-standby/vps-eso.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vps-eso + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" # before any Application whose secrets it manages (authentik is wave 3) + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: vps-standby + source: + repoURL: https://gitlab.kube.huskypup.net/Scooby/Homelabv4.git + targetRevision: main + path: infrastructure/vps-eso/manifests + destination: + name: vps-standby + namespace: external-secrets + syncPolicy: + automated: + prune: true + selfHeal: true diff --git a/infrastructure/authentik/manifests/ha-postgres-app-pushsecret.yaml b/infrastructure/authentik/manifests/ha-postgres-app-pushsecret.yaml new file mode 100644 index 0000000..dd3f9b6 --- /dev/null +++ b/infrastructure/authentik/manifests/ha-postgres-app-pushsecret.yaml @@ -0,0 +1,36 @@ +# Multi-site active failover pilot (see +# /home/scooby/.claude/plans/jiggly-snacking-iverson.md) - closes the gap +# the user correctly flagged: manually kubectl/ssh-copying secrets between +# home and the VPS defeats the entire point of having Vault. This is the +# first piece of a real Vault -> VPS pipeline (infrastructure/vps-eso/ has +# the other half, ESO running on the VPS itself pulling this back down). +# +# pg-authentik-app is CNPG-generated, not Vault-native - each cluster +# (home and VPS) independently generates its own random password for the +# `app` role at bootstrap time. Since VPS's Postgres now REPLICATES from +# home (WAL includes role/password changes), the two clusters' actual live +# passwords are only in sync because of that replication - but the two +# clusters' K8S SECRET OBJECTS never resync on their own (confirmed live +# 2026-08-20: VPS's copy was stale). Pushing home's value into Vault, with +# VPS's ESO pulling it back down on refreshInterval, makes this self- +# healing instead of a manual one-time fix that goes stale again on the +# next password rotation. +apiVersion: external-secrets.io/v1alpha1 +kind: PushSecret +metadata: + name: pg-authentik-app-to-vps + namespace: authentik +spec: + refreshInterval: 5m + secretStoreRefs: + - name: vault-backend + kind: ClusterSecretStore + selector: + secret: + name: pg-authentik-app + data: + - match: + secretKey: password + remoteRef: + remoteKey: vps/pg-authentik-app + property: password diff --git a/infrastructure/vps-eso/manifests/clustersecretstore.yaml b/infrastructure/vps-eso/manifests/clustersecretstore.yaml new file mode 100644 index 0000000..ba2ee9a --- /dev/null +++ b/infrastructure/vps-eso/manifests/clustersecretstore.yaml @@ -0,0 +1,84 @@ +# Multi-site active failover pilot (see +# /home/scooby/.claude/plans/jiggly-snacking-iverson.md) - closes the gap +# flagged 2026-08-20: manually kubectl/ssh-copying secrets between home +# and the VPS defeats the point of having Vault at all. This gives the +# VPS its own real Vault -> ESO pipeline instead. +# +# External Secrets Operator itself is installed directly via helm +# (out-of-band, like k3s/cert-manager/Netbird - see +# scripts/vps-bootstrap.sh's existing pattern for why those aren't +# GitOps-managed either): +# helm repo add external-secrets https://charts.external-secrets.io +# helm install external-secrets external-secrets/external-secrets \ +# --namespace external-secrets --create-namespace \ +# --version 0.20.4 --set installCRDs=true +# (0.20.4 matches home's version - see argocd-apps/infrastructure/ +# external-secrets.yaml) +# +# Auth: AppRole, not Kubernetes auth - home's existing vault-backend +# ClusterSecretStore (infrastructure/vault/manifests/clustersecretstore.yaml) +# uses Vault's kubernetes auth method, which verifies a ServiceAccount JWT +# against THAT cluster's own API server - doesn't work for the VPS, it's a +# completely separate k3s cluster with no federation to home's API server. +# AppRole is the standard way to authenticate an external/non-native +# client to Vault instead. +# +# Reachable via https://vault.kube.huskypup.net - a NEW public Cloudflare +# A record (home was previously deliberately kept off the public +# internet - this was an explicit, confirmed decision, not a default). +# Goes through the same Istio ingress gateway that already serves other +# public *.kube.huskypup.net hosts, so no new UniFi port-forward/NodePort +# needed, and no Istio PeerAuthentication/AuthorizationPolicy change +# either - unlike the ha-authentik-postgres NodePort case, this traffic +# arrives already wrapped in a normal in-mesh call from the ingress +# gateway's own identity, not raw external TCP straight to a pod. +# +# NOTE: the AppRole's issued token/secret_id CANNOT be IP-bound +# (token_bound_cidrs / secret_id_bound_cidrs) over this path - confirmed +# live 2026-08-20 that Vault only ever sees the ingress gateway's own pod +# IP for any request arriving this way, never the VPS's real source IP. +# Security boundary here is AppRole credential secrecy + the narrow +# read-only secret/vps/* policy (vps-eso-reader), not network-level +# restriction - this is the standard/expected shape of AppRole auth for +# external clients generally, CIDR-binding is normally extra +# defense-in-depth on top rather than the primary mechanism. +# +# One-time manual bootstrap on Vault's side (already done 2026-08-20, not +# scripted - matches every other Vault policy/auth-method setup in this +# repo, which are also applied by hand via `vault` CLI, not GitOps): +# vault auth enable approle +# vault policy write vps-eso-reader - <<'EOF' +# path "secret/data/vps/*" { capabilities = ["read", "list"] } +# path "secret/metadata/vps/*" { capabilities = ["read", "list"] } +# EOF +# vault write auth/approle/role/vps-eso token_policies="vps-eso-reader" \ +# token_ttl=1h token_max_ttl=4h secret_id_num_uses=0 +# vault read auth/approle/role/vps-eso/role-id # -> roleId below +# vault write -f auth/approle/role/vps-eso/secret-id # -> secret_id +# +# vault-approle-creds is a plain Secret created manually on the VPS +# (kubectl, not git - same reasoning as every other VPS secret): +# kubectl -n external-secrets create secret generic vault-approle-creds \ +# --from-literal=role_id= \ +# --from-literal=secret_id= +# Rotate the secret_id periodically by writing a new one and patching this +# Secret - role_id is stable and not sensitive on its own (useless without +# a valid secret_id). +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: + name: vault-backend +spec: + provider: + vault: + server: https://vault.kube.huskypup.net + path: secret # KV v2 mount, same as home's vault-backend + version: v2 + auth: + appRole: + path: approle + roleId: ecaf2eda-d922-f7cf-1143-690bbbb4d8ea # not sensitive alone, see note above + secretRef: + name: vault-approle-creds + namespace: external-secrets + key: secret_id diff --git a/infrastructure/vps-standby/authentik/manifests/pg-authentik-app-externalsecret.yaml b/infrastructure/vps-standby/authentik/manifests/pg-authentik-app-externalsecret.yaml new file mode 100644 index 0000000..e76c23b --- /dev/null +++ b/infrastructure/vps-standby/authentik/manifests/pg-authentik-app-externalsecret.yaml @@ -0,0 +1,29 @@ +# Multi-site active failover pilot - VPS's half of the Vault pipeline +# (infrastructure/authentik/manifests/ha-postgres-app-pushsecret.yaml has +# home's half, which pushes the authoritative password into Vault at +# secret/vps/pg-authentik-app). Pulls it back down here. +# +# Merge (not Owner/Replace): only overwrites the `password` key, leaving +# CNPG's own generated host/dbname/username fields on this secret intact +# - those are correctly LOCAL to each site (this secret's `host` key +# points at the VPS's own local -rw service, which CNPG itself still +# needs internally, even though the app tier reads a different value via +# the pg-authentik.ha.huskypup.net override in values.yaml). +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: pg-authentik-app-password + namespace: authentik +spec: + refreshInterval: 5m + secretStoreRef: + name: vault-backend + kind: ClusterSecretStore + target: + name: pg-authentik-app + creationPolicy: Merge + data: + - secretKey: password + remoteRef: + key: vps/pg-authentik-app + property: password