Secrets Architecture (Vault + ESO)

How secrets reach OpenShift workloads on the v6 fleet: HashiCorp Vault on a VM, External Secrets Operator on each cluster, a single platform ClusterSecretStore, per-tenant SecretStores, ExternalSecrets, and the kubernetes-provider bridge for OBC-backed operands.

This page is the orientation map for everything else in §10. Read it once, then jump to the page covering the specific CR or flow you need (operator install, ClusterSecretStore, per-tenant SecretStore, ExternalSecret, OBC bridge, rotation). The wider §3 sections (image supply, GitOps, ACM) often touch secrets at the edges; the design point is that those sections never have to author Secret YAML — they reference what this layer materializes.

What this layer does

External Secrets Operator (ESO) is the “secret delivery” plane for the v6 fleet. Its job is one sentence: take desired-state references to secrets stored in a system of record (HashiCorp Vault on a VM, or another Kubernetes Secret in the case of the OBC bridge), and materialize them as Kubernetes Secret objects in the namespaces that consume them — without ever putting the secret value into Git.

The system of record is:

  • Vault (VM-based, lab /24 — addresses redacted) — the canonical store for tokens, passwords, robot accounts, pull credentials, certificates, and any opaque key/value that an operator or app needs. KV-v2 mount at secret/.
  • NooBaa ObjectBucketClaim Secret + ConfigMap — the local cluster-side store for in-cluster S3 access credentials produced by ODF. Loki, Tempo, and Quay consume these.

The delivery plane is:

  • External Secrets Operator (Red Hat distribution, openshift-external-secrets-operator v1.1.0). Provides the controllers, the webhook, and the four CR kinds shown in the diagram.
  • ClusterSecretStore — exactly one, named vault-platform. Cluster-wide; used only by platform/operator namespaces. Defined under ADR 0019 conventions.
  • SecretStore (per tenant) — one per apps-<division>-<app> namespace, named vault-apps. Namespace-scoped; cannot read another division’s Vault paths.
  • ExternalSecret — the request CR. References either a ClusterSecretStore or a tenant SecretStore, declares a target Secret shape, and (optionally) templates the value.
  • ClusterExternalSecret — used once, for the cluster-wide app-registry-pull Secret that needs to fan out to every namespace labelled apps.platform/tenant=true. Documented separately.

Architecture

Reading the diagram:

  1. Top row — platform path. The ESO operand authenticates to Vault via the cluster’s Kubernetes auth mount (auth/kubernetes-<cluster>). The vault-platform ClusterSecretStore is read by ExternalSecrets in operator namespaces (openshift-cert-manager, openshift-pipelines, openshift-gitops, stackrox, etc.); their materialized Secrets are the operator/system credentials.
  2. Middle row — tenant path. Each tenant namespace runs its own app-eso ServiceAccount and its own namespaced SecretStore vault-apps. The Vault role behind it is per-division (apps-<cluster>-<division>) with a namespace-glob bind of apps-<division>-*. Tenant ExternalSecrets materialize the actual app credentials, robot tokens, and DB passwords.
  3. Bottom row — OBC bridge. ODF/NooBaa creates an ObjectBucketClaim, which yields a Secret (with AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) and a ConfigMap (with BUCKET_HOST/BUCKET_NAME/BUCKET_PORT). Loki/Tempo/Quay expect a Secret with different lowercase keys (endpoint, bucketnames, access_key_id, access_key_secret). An ExternalSecret using ESO’s kubernetes provider templates the bridge Secret in the shape the operand expects.

Three reasons this is one diagram, not three:

  • The same operand reconciler runs all three flows. Sizing, NetworkPolicy egress, and observability are shared.
  • The CR vocabulary is identical. A reviewer reading any ExternalSecret page can predict the others.
  • Failure modes overlap. The “Vault auth times out” symptom in the platform path is the same NetworkPolicy egress fix as the tenant path.

Why this design (and not the alternatives)

Alternative consideredWhy we did not pick it
Vault Agent InjectorPod-side sidecar that mounts secret files. Requires per-Pod annotations and reissues at restart only; conflicts with Argo-managed manifests and forces ServiceAccount-bound annotations into app repos. ESO keeps the indirection in CRs the platform owns.
Sealed SecretsThe secret value is in Git (encrypted). Recovery, audit, and rotation are clumsy at fleet scale; an encrypted Secret blob in a public-ish-feeling repo still gives auditors heartburn.
oc apply with a Secret out of bandDefeats GitOps. Breaks the “everything reconciles back” property under ADR 0025.
Cluster-wide ClusterSecretStore for tenantsA single CSS would let any namespace’s ESO request read any Vault path. We want a hard cross-tenant boundary, so tenants get a namespace-scoped SecretStore with a role that refuses to issue tokens for namespaces outside apps-<division>-*.
Vault Kubernetes auth with one shared roleOne leaked token would read every division. Per-division role + per-division policy keeps blast radius bounded.

ADR 0014 (developer-readiness contract) requires “approved pull-secret delivery”; ADR 0019 requires that image-pull credentials come from a controlled source rather than being committed; ADR 0025 forbids oc apply mutations of secrets in the live cluster. The Vault + ESO + per-tenant SecretStore composition satisfies all three.

Inventory at a glance

ConceptIdentifierWhere it lives
Vault auth methodauth/kubernetes-<cluster>Vault VM
Platform Vault policyocp-<cluster>-esoVault VM
Platform Vault roleocp-<cluster>-esoauth/kubernetes-<cluster>/role/...
Tenant Vault policyapps-<division>-readVault VM (cluster-agnostic)
Tenant Vault roleapps-<cluster>-<division>auth/kubernetes-<cluster>/role/...
ESO operator namespaceopenshift-external-secrets-operatorOperatorHub install
ESO operand namespaceexternal-secretscreated by ESO operator
Platform ClusterSecretStorevault-platformper cluster, in clusters/<cluster>/secrets/eso/
Tenant SecretStorevault-apps (in each tenant ns)per tenant overlay, from tenants/_template/secretstore-vault-apps.yaml
Vault path tree (platform)secret/ocp/<cluster>/..., secret/ocp/platform/...KV-v2
Vault path tree (tenant)secret/apps/<division>/<app>/<env>/<key>KV-v2

What lives in each Vault subtree

SubtreeOwnerExample contents
secret/ocp/<cluster>/...platform admincluster-specific bootstrap secrets, ESO smoke-test data, ODF route credentials
secret/ocp/platform/rhacs-init-bundleplatform adminRHACS sensor init-bundle materials (delivered via §11)
secret/ocp/<cluster>/registries/app-registry-pullplatform admincluster-wide app-registry dockerconfigjson (see §6)
secret/ocp/<cluster>/quay/config-bundleplatform adminQuay registry operand config bundle (consumed via OBC bridge sibling pattern)
secret/apps/<division>/<app>/<env>/<key>tenant team (write) / platform (audit)app credentials, DB passwords, OIDC client secrets
secret/apps/<division>/<app>/ci/quay-robotplatform admin (creates) / tenant (consumes)Tekton Path B push-robot dockerconfigjson

Cross-references

  • 02-eso-operator-and-policies.mdx — install path, the operator/operand split, default-deny NetworkPolicy gotcha.
  • 03-vault-clustersecretstore.mdx — the vault-platform shape, CA bundle, auth wiring.
  • 04-tenant-secretstore-pattern.mdx — per-tenant vault-apps + role/policy/namespace-glob.
  • 05-externalsecret-and-templates.mdx — request shapes, templating, refresh interval choices.
  • 06-obc-to-operand-bridge.mdx — the kubernetes-provider Loki/Tempo/Quay bridge.
  • 07-rotation-and-revocation.mdx — Vault-side rotation, ESO refresh semantics, revocation playbook.

References

  • connection-details/vault-app-secrets.md — tenant path convention (DEV-OCP-0.4 / #174).
  • connection-details/app-registry-pullsecret.mdClusterExternalSecret-fanned pull credential (DEV-OCP-0.2 / #172).
  • ADRs: 0014 (developer readiness), 0019 (Nexus-only supply chain), 0025 (GitOps-only operations).

Last reviewed: 2026-05-11