# 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, and # consistent regardless of "how sensitive" a given field is on its own - # role_id and secret_id both live in the same Secret, neither in git): # 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. 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 roleRef: name: vault-approle-creds namespace: external-secrets key: role_id secretRef: name: vault-approle-creds namespace: external-secrets key: secret_id