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.
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 identity | The 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
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.
- Your platform team commits the
SecretStore/ExternalSecretmanifests (and the ESO Helm release itself, if you manage it the same way) to your GitOps repository. - ArgoCD detects the change and applies it to the cluster — the same reconciliation loop it already uses for every other workload.
- 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. - On the
ExternalSecret'srefreshInterval, ESO reads the declared path and writes (or updates) a native KubernetesSecret. - The DuoKey pod consumes that
Secretthrough a normalenvFrom— it has no awareness that Vault, ArgoCD, or Git exist.
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.
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.
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-v2SCC (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-managerif your cluster already runs one. - etcd exposure — the materialized
Secretis a native Kubernetes object and lands inetcdlike any otherSecret. 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'stmpfsonly and skips the nativeSecretobject entirely; ask your DuoKey representative to scope that variant. - Network — Vault must be reachable from the namespace ESO runs in (a
ClusterIPService and aNetworkPolicyallow rule, or aRouteif Vault lives outside the cluster). There is no sidecar and therefore nolocalhostshortcut: 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
duokeyrole'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 nextrefreshIntervalwith 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-readpolicy scoped to exactlysecret/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.