Aller au contenu principal

GitOps Secret Delivery (ArgoCD + Vault)

Secret Manager Integration covers the general pattern — DuoKey consumes secrets from whatever secret manager you already operate, through the External Secrets Operator (ESO). This page is the detailed, step-by-step version of that pattern for the specific combination many enterprise platform teams already standardize on: ArgoCD as the GitOps controller and a HashiCorp Vault (or OpenBao — see OpenBao vs. Vault below) as the secret store.

The core rule

Git holds pointers, never values. ArgoCD's job is to reconcile declarative manifests from your Git repository into the cluster. Those manifests describe where a secret lives and who may read it — the actual secret bytes (a database password, a signing key) never pass through Git, a CI pipeline, or an ArgoCD diff. They flow directly from Vault to the pod at runtime, brokered by ESO.

What goes where​

Lives in Git (ArgoCD-managed)Lives only in Vault
ServiceAccount for the ESO reader identityThe actual secret values (DB password, signing/encryption keys, client secrets)
SecretStore / ClusterSecretStore (points at your Vault address + auth role)—
ExternalSecret (declares which Vault path/key maps to which Kubernetes Secret key)—
RBAC / NetworkPolicy scoping the above—

Because the manifests contain no secret material, they are safe to review, diff, and audit through your normal Git pull-request process — exactly like any other GitOps-managed workload.

Delivery flow​

GitOps secret delivery
Git repositorySecretStore · ExternalSecret manifests — no secret values
sync (reconcile)
ArgoCDGitOps controller
applies manifests
OpenShift cluster
External Secrets OperatorServiceAccount auth · no sidecar
Kubernetes auth · fetch on refreshInterval
HashiCorp Vault / OpenBaosource of truth
materializes
K8s Secrettmpfs · in-memory
envFrom
DuoKey pod
GitOps (Git + ArgoCD)Secret brokerRuntime (in-cluster)

ArgoCD syncs the SecretStore/ExternalSecret manifests (and the ESO controller itself) from Git into OpenShift. ESO then authenticates to Vault directly using a Kubernetes ServiceAccount token, fetches only the values an ExternalSecret declares, and materializes a native, in-memory Kubernetes Secret. The DuoKey pod consumes that Secret exactly like any other — it never talks to Vault, ArgoCD, or Git itself.

  1. Your platform team commits the SecretStore/ExternalSecret manifests (and the ESO Helm release itself, if you manage it the same way) to your GitOps repository.
  2. ArgoCD detects the change and applies it to the cluster — the same reconciliation loop it already uses for every other workload.
  3. ESO, running as a controller in its own namespace, authenticates to Vault using the Kubernetes auth method: it presents the projected ServiceAccount token named in the SecretStore, Vault validates it against the OpenShift API, and issues a short-lived Vault token scoped to the policy you configured.
  4. On the ExternalSecret's refreshInterval, ESO reads the declared path and writes (or updates) a native Kubernetes Secret.
  5. The DuoKey pod consumes that Secret through a normal envFrom — it has no awareness that Vault, ArgoCD, or Git exist.
Why not a Vault Agent Injector sidecar?

The sidecar pattern (a mutating webhook injecting an init container + sidecar into your application pod) renders secrets to files and expects a shell inside the image to source them into environment variables. DuoKey's container images run as a fixed non-root user with no shell and no package manager, so that bridge doesn't exist — and OpenShift's restricted SCCs add extra friction to injected containers anyway (arbitrary UID, no default privileged mode). The controller/pull pattern above needs nothing inside the DuoKey pod at all, so it works unchanged on these minimal images.

Vault-side setup​

Your Vault administrator enables the Kubernetes auth method once per cluster and scopes a policy to exactly the path DuoKey needs — nothing broader:

vault auth enable kubernetes

vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"

vault policy write duokey-read - <<'EOF'
path "secret/data/duokey/*" {
capabilities = ["read"]
}
EOF

vault write auth/kubernetes/role/duokey \
bound_service_account_names=duokey \
bound_service_account_namespaces=duokey \
policies=duokey-read \
ttl=1h

No Vault token or root credential is ever stored as a Kubernetes Secret for ESO itself — the trust anchor is the ServiceAccount's projected token, which OpenShift already rotates automatically.

OpenShift manifests (ArgoCD-managed)​

These are the objects your GitOps repository holds; ArgoCD applies them exactly like any other Application.

ClusterSecretStore​

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "https://vault.corp.example.local:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "duokey"
serviceAccountRef:
name: duokey
namespace: duokey

ExternalSecret​

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: duokey-bootstrap-secrets
namespace: duokey
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: duokey-bootstrap-secrets
data:
- secretKey: DATABASE_URL
remoteRef:
key: duokey/database
property: url
- secretKey: JWT_SECRET
remoteRef:
key: duokey/signing
property: jwt_secret
- secretKey: ENCRYPTION_KEY
remoteRef:
key: duokey/signing
property: encryption_key
- secretKey: HMAC_KEY
remoteRef:
key: duokey/signing
property: hmac_key

The DuoKey Deployment then references duokey-bootstrap-secrets via envFrom — see Cockpit Configuration for the full list of variables it can carry.

Two different secret scopes — don't conflate them

This page covers DuoKey's own platform bootstrap secrets (database DSN, signing/encryption keys) — a small, tightly-scoped Vault path delivered once at pod start. It is a separate concern from tenant-configured secret managers (a tenant's own Vault/HSM credentials, entered from inside the DuoKey Cockpit console for their KMS integrations) — that data path is covered generically on Secret Manager Integration and never goes through this ArgoCD-managed flow.

Prefer the Cockpit to talk to Vault directly instead?

Rather than the ESO pattern above, DuoKey can alternatively be configured to dial Vault/OpenBao itself at process start using a bootstrap AppRole credential. Both patterns are fail-closed and fully supported — see Cockpit Configuration → Secrets sourcing to compare the two and pick the one that matches your platform team's preference.

OpenShift-specific notes​

  • SCC — ESO's own pods run fine under the default restricted/ restricted-v2 SCC (no privileged mode, no host access needed); nothing special beyond letting its ServiceAccount use the cluster's default SCC.
  • Webhook certificate — ESO's admission webhook needs a TLS certificate. Either let ESO's built-in cert-controller manage it (the default) or wire it to cert-manager if your cluster already runs one.
  • etcd exposure — the materialized Secret is a native Kubernetes object and lands in etcd like any other Secret. If your cluster already enables etcd encryption, that covers it. If your policy is stricter — "must never touch etcd" — use the Secrets Store CSI Driver with the HashiCorp Vault CSI provider instead, which mounts the value to the pod's tmpfs only and skips the native Secret object entirely; ask your DuoKey representative to scope that variant.
  • Network — Vault must be reachable from the namespace ESO runs in (a ClusterIP Service and a NetworkPolicy allow rule, or a Route if Vault lives outside the cluster). There is no sidecar and therefore no localhost shortcut: the controller dials Vault directly, so this path needs the same TLS and network hardening as any other in-cluster HTTP client — see Network Security.

OpenBao vs. Vault​

Identical setup. ESO's Vault provider works against OpenBao unchanged, because OpenBao is Vault-API-compatible — only spec.provider.vault.server changes to point at your OpenBao address. There is no separate OpenBao provider in ESO; pointing the same vault provider block at an OpenBao endpoint is the standard, supported way to use it.

Rotation & auditing​

  • Rotate the Vault duokey role's TTL and the underlying policy on your normal secret-rotation cadence — ESO re-authenticates on every fetch, so a narrowed policy takes effect on the next refreshInterval with no pod restart required for the authorization change (a rotated secret value still requires whatever your application does to pick up a changed env var — see Cockpit Configuration for DuoKey's own behavior).
  • Every Vault read is logged by Vault's own audit device — enable one if you haven't already; it is your authoritative record of what ESO fetched and when, independent of DuoKey's own audit trail.
  • Keep the duokey-read policy scoped to exactly secret/data/duokey/* (or your equivalent path) — least privilege here means a compromised ESO ServiceAccount token can read only DuoKey's own secrets, nothing else in your Vault.

Continue to Identity, SSO & Access Control, or back to Secret Manager Integration for the general, any-secret-manager version of this pattern.