
Secrets Management Without the Bootstrap Chicken-and-Egg
TL;DR
Secrets management is a five-job problem — storage, distribution, rotation, audit, and the bootstrap chicken-and-egg — and the six contenders in 2026 (Infisical, ESO + SOPS-in-Git, HashiCorp Vault, Sealed Secrets, AWS Secrets Manager + CSI, and the null-hypothesis plaintext-in-Git) each treat one or two of those jobs as primary and demand a tax from the operator for the rest.
Frank runs Infisical + External Secrets Operator, with SOPS-encrypted
Secrets in Git as the bootstrap layer. The scars came in the seams:
the ExternalSecret data: [] admission-webhook rejection, the
envFrom.secretRef without optional: true wedge, and the
three-ArgoCD-app split forced by an Infisical chart bug.
Frank’s answer does not generalize. ≤20 secrets → SOPS in Git. Compliance audit → Vault. Single cloud → cloud Secrets Manager + CSI.
§1 — The capability
A change is ready to ship. The Helm chart wants a database password. The controller wants an API token. The cron job wants a service-account JWT. The dashboard wants an OIDC client secret. The new app does not even have an admin password yet — it needs one minted, written to a secret store, and made available to the pod the moment it starts. And the chart that ships the secret store itself wants its own admin password, its own encryption key, its own database connection string. Where do those come from?
That is the capability under examination. Not “secrets” as a generic
term, not “encryption” in the abstract — Kubernetes already has a
Secret resource and the cluster API can encrypt secrets at rest. The
capability is the chain of custody from the moment a credential is
generated to the moment a container reads it: where the source of truth
lives, who is allowed to read it, how a fresh cluster bootstraps the
system, and what tax is paid every time a key rotates.
flowchart LR
G["GitOps (ArgoCD)"] --> SS["Secret store"]
SS --> ST["Storage"]
SS --> DI["Distribution"]
SS --> RO["Rotation"]
SS --> AU["Audit log"]
SS --> BO["Bootstrap"]
BO -.-> SO["SOPS / out-of-band"]
Five jobs in one capability — storage, distribution, rotation, audit, and bootstrap. The vendor space splits on which job is primary and which dependency is mandatory. Some options optimise for storage and let the others fall out of the design; others assume a cloud KMS and refuse to exist without it; one of them is “the secret store itself needs secrets to exist”, and the only honest answer to that is an out-of-band loop the declarative-everything principle reluctantly carves out as the single exception it accepts.
I run Infisical with External Secrets Operator on top, with SOPS-encrypted secrets in Git for the bootstrap layer. That choice was not made on the abstract merits of feature matrices — it was made on the specific shape of a homelab cluster that wants a UI for day-one ergonomics, native Kubernetes Secret resources for app compatibility, and an explicit, documented bootstrap loop that pretends to no magic.
§2 — The landscape
Six options dominate secrets management on Kubernetes in 2026, and they split on two axes. The horizontal axis is where the source of truth lives — cloud-managed on the left (you outsource the server, IAM gates access), self-hosted on the right (you run the server, your keys, your storage). The vertical axis is credential dynamism — plain key-value storage on the bottom (the secret is what you put in), dynamic credentials on the top (the store mints a short-lived credential on read, signs a PKI certificate, or rotates a backend password on a schedule).
quadrantChart
title Secrets management — 2026
x-axis "Cloud managed" --> "Self-hosted"
y-axis "Plain KV" --> "Dynamic credentials"
quadrant-1 "Self-hosted · Dynamic"
quadrant-2 "Cloud-managed · Dynamic"
quadrant-3 "Cloud-managed · Plain"
quadrant-4 "Self-hosted · Plain"
"Infisical": [0.80, 0.40]
"ESO + SOPS-in-Git": [0.85, 0.15]
"HashiCorp Vault": [0.70, 0.90]
"Sealed Secrets": [0.90, 0.10]
"AWS Secrets Manager + CSI": [0.10, 0.55]
"Plaintext in Git": [0.95, 0.05]
| Feature | Infisical | ESO + SOPS-in-Git | HashiCorp Vault | Sealed Secrets | AWS Secrets Manager + CSI | Plaintext in Git |
|---|---|---|---|---|---|---|
| Self-hosted | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Web UI | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ |
| Audit log | ✅ | 🟡 | ✅ | ❌ | ✅ | ❌ |
| Built-in rotation | 🟡 | ❌ | ✅ | ❌ | ✅ | ❌ |
| Dynamic credentials | ❌ | ❌ | ✅ | ❌ | 🟡 | ❌ |
| K8s-native sync | ✅ | ✅ | 🟡 | ✅ | ✅ | 🟡 |
| Bootstrap chicken-and-egg | ✅ | ❌ | ✅ | 🟡 | ❌ | ❌ |
| OSS / no contract required | ✅ | ✅ | 🟡 | ✅ | ❌ | ✅ |
The matrix grades the options on self-hosting, UI, audit log, built-in rotation, dynamic credentials, Kubernetes-native sync, bootstrap loop, and licensing. The bootstrap-loop column is the one that does the most work; it is also the one most vendor docs mention only after you have read the install guide and discovered that the server cannot start without secrets that do not yet exist anywhere.
Infisical optimises for day-one ergonomics. A web UI exists from the first deploy; projects, environments, and identities map onto the mental model most teams already have; the Helm chart is one app, the ClusterSecretStore is one CR, ESO does the syncing. The trade is the bootstrap loop — Infisical needs an admin password, an encryption key, and a database URL before it can run, and those have to come from somewhere that is not Infisical.
ESO + SOPS-in-Git is the no-server answer. External Secrets Operator is one component (the operator itself) and SOPS-encrypted YAML in Git is another; together they cover storage (Git), distribution (ESO sync), and bootstrap (SOPS-decrypt-and-apply). There is no separate server, no UI, no chicken-and-egg loop. The trade is that everything else — audit log, rotation, dynamic credentials — is also missing.
HashiCorp Vault is the heritage answer. Predates the Kubernetes operators by years; built for environments where the answer to “how many features does the secret store need?” is “all of them”. KV storage, PKI, transit encryption, dynamic database credentials, audit log, token-based auth, namespaces — it is in there. The vendor docs are explicit about the design:
The storage backend is untrusted, and is used only to durably store encrypted data.HashiCorp Vault — Architecture
The trade is operational weight. Vault has Shamir-split unseal keys, a multi-step initialisation ceremony, a separate audit-log subsystem, and its own Helm chart with enough configurable surface that the production deployment guide is multi-page. For a homelab, that is the wrong shape. For a regulated bank, it is the right shape.
Bitnami Sealed Secrets is the GitOps-purist answer. A controller
runs in the cluster with an asymmetric key pair; you encrypt secrets
locally with the public key and commit the resulting SealedSecret CR;
the controller decrypts on the cluster side and materialises the
Kubernetes Secret. There is no separate server, no UI, no API. The
trade is everything that depends on a live secret store: no audit
log of who-read-what-when, no rotation, no dynamic credentials. Rotate
the controller’s private key and every SealedSecret in Git needs to be
re-encrypted.
AWS Secrets Manager + Secrets Store CSI Driver is the cloud-native answer. AWS holds the secrets; IAM gates access; the CSI driver mounts them as files in pod volumes. The bootstrap loop is solved by the cloud identity — pods authenticate via IRSA (IAM Roles for Service Accounts) which is itself trust-chained back to the cluster’s OIDC issuer. The trade is the AWS dependency and the fact that secrets do not become Kubernetes Secret objects (they are tmpfs files), which is more secure and less ergonomic.
Plaintext in Git is the null hypothesis. The .env file is committed.
The Helm values include the database password. The README says “remember
to remove this before shipping”. Its purpose in this paper is to mark
the lower bound — and to be loudly disqualified by the empirical evidence
that 12.8 million such secrets leaked in public GitHub commits in 2023
alone. The community talk frames the entire decision as a trust-boundary
problem:
The trade-off between encrypt-in-Git (Sealed Secrets, SOPS) and sync-from-external-store (ESO, CSI) is fundamentally an axis of where you put the trust boundary.KubeCon NA 2022 — The State of Kubernetes Secrets Management
Plaintext-in-Git puts the trust boundary at “everyone who has ever cloned the repo”, which is exactly the wrong answer.
§3 — How each option handles the hard part
The hard part of secrets management is bootstrapping the secret store itself without trusting any one machine, any one engineer, or any one Git history line. Every option on this list has an answer; the answers diverge enough that they need separate diagrams. The diagrams below use a shared visual language — squares for controllers and servers, rounded rectangles for Kubernetes Secret resources, diamonds for decision points and auth gates, cylinders for persistent storage, dashed edges for bootstrap and out-of-band paths, solid edges for runtime sync.
Infisical + ESO + SOPS-for-bootstrap (Frank’s stack)
flowchart TD
subgraph BOOT["Bootstrap (out-of-band)"]
G_SOPS[("secrets/ in Git\nSOPS-encrypted")]
OP{"Engineer: sops --decrypt | kubectl apply"}
K1(("K8s Secret\ninfisical-secrets"))
K2(("K8s Secret\ninfisical-db-uri"))
K3(("K8s Secret\ninfisical-credentials"))
end
subgraph RUN["Runtime"]
I["Infisical server"]
DB[("Postgres")]
R[("Redis")]
ESO["External Secrets Operator"]
CSS{"ClusterSecretStore\ninfisical"}
ES{"ExternalSecret CR"}
K_APP(("K8s Secret\napp-secret"))
end
G_SOPS -.-> OP
OP -.-> K1
OP -.-> K2
OP -.-> K3
K1 --> I
K2 --> DB
I --> DB
I --> R
K3 --> CSS
CSS --> I
ES --> ESO
ESO --> CSS
ESO --> K_APP
Three SOPS-encrypted Secrets seed the system: infisical-secrets
(application env vars including ENCRYPTION_KEY and AUTH_SECRET),
infisical-db-uri (the Postgres connection string), and
infisical-credentials (the Machine Identity ESO uses to authenticate).
All three are applied manually with sops --decrypt | kubectl apply -f -
before ArgoCD ever touches the Infisical app. Once they exist,
ArgoCD takes over: Postgres, Redis, and Infisical itself each become
separate ArgoCD apps (a chart bug, documented in §5, forced the split).
ESO reads infisical-credentials, opens a Universal Auth session with
Infisical, and from that point forward every workload’s secrets are
materialised by an ExternalSecret CR pointing at the
ClusterSecretStore.
The failure mode is the bootstrap step. If secrets/ is lost, the
cluster cannot be rebuilt without rotating every credential. The SOPS
master keys (held in ~/.config/sops/age/keys.txt on a small set of
operator laptops) are the actual root of trust, and Paper 09 will not
pretend otherwise.
ESO + SOPS-in-Git (no Infisical)
flowchart TD
subgraph SRC["Source of truth"]
G_SOPS[("secrets/ in Git\nSOPS-encrypted")]
end
subgraph RUN["Runtime"]
DEC["sops-decrypt sidecar / kustomize plugin"]
ESO["External Secrets Operator"]
K(("K8s Secret"))
end
G_SOPS --> DEC
DEC -.-> ESO
ESO --> K
The simplest possible secret-store: Git is the database. SOPS-encrypted
YAML files live alongside the app manifests; a kustomize plugin or a
sidecar decrypts at apply time; ESO is optional (you can also pipe
straight to kubectl apply). There is no server to bootstrap, no
chicken-and-egg loop. The trade is that the audit log is git log,
rotation is “edit the file and commit”, and dynamic credentials are
not on the menu. For a one-cluster homelab with a small operator pool,
this is often the right answer.
HashiCorp Vault
flowchart TD
subgraph SEAL["Vault unseal ceremony (manual)"]
SK1{"Shamir key share 1"}
SK2{"Shamir key share 2"}
SK3{"Shamir key share 3"}
UNSEAL{"Unseal ≥3 of N"}
end
subgraph RUN["Runtime"]
V["vault server"]
KV[("KV / transit / PKI backends")]
AUDIT[("Audit log")]
AUTH{"Kubernetes auth method"}
ESO["ESO or Vault Agent Injector"]
K(("K8s Secret / file mount"))
end
SK1 -.-> UNSEAL
SK2 -.-> UNSEAL
SK3 -.-> UNSEAL
UNSEAL -.-> V
V --> KV
V --> AUDIT
AUTH --> V
ESO --> AUTH
ESO --> K
Vault’s bootstrap is the heaviest in the landscape and the most principled. Storage backend is untrusted; at startup, the master key is reconstructed from Shamir shares held by separate operators; only after enough shares are presented does Vault unseal and begin serving requests. Kubernetes workloads authenticate via the Kubernetes auth method (a ServiceAccount JWT exchanged for a Vault token), and ESO or the Vault Agent Injector materialises the secrets.
The failure mode is the unseal ceremony. After every restart, every node reboot, every Helm upgrade, Vault is sealed and refuses to serve. Auto-unseal with a cloud KMS solves this at the cost of trusting the cloud KMS — a trade that defeats some of the reason to run Vault on-prem in the first place.
Bitnami Sealed Secrets
flowchart TD
subgraph LOCAL["Engineer's laptop"]
SC["kubeseal CLI"]
PUBKEY{"Controller public key"}
end
subgraph GIT["Git repo"]
SS[("SealedSecret CR\n(encrypted)")]
end
subgraph RUN["Runtime"]
CTRL["sealed-secrets controller"]
PRIVKEY{"Controller private key"}
K(("K8s Secret"))
end
SC --> SS
PUBKEY --> SC
SS --> CTRL
PRIVKEY -.-> CTRL
CTRL --> K
Sealed Secrets inverts the bootstrap problem by making the controller
the only thing that needs a key at startup. The controller generates
its key pair on first install and stores the private key in a
Kubernetes Secret in its own namespace. Engineers encrypt locally with
the public key (via kubeseal), commit the SealedSecret CR to Git,
and the controller decrypts on the cluster side. There is no server,
no API, no audit log of reads — only an audit log of applies, via
git log.
The failure mode is private-key rotation. The controller’s key pair is the only thing that can decrypt the SealedSecrets in Git; lose it and every SealedSecret in Git is now a glorified base64-encoded payload. Rotation is supported but expensive: re-encrypt every SealedSecret in the repo, commit, redeploy.
AWS Secrets Manager + Secrets Store CSI Driver
flowchart TD
subgraph AWS["AWS"]
SM[("Secrets Manager")]
IAM{"IAM (IRSA)"}
OIDC[("Cluster OIDC issuer")]
end
subgraph K8S["Kubernetes"]
SA["ServiceAccount\n(IRSA-annotated)"]
POD["Pod"]
CSI["Secrets Store CSI Driver"]
VOL[("tmpfs volume\n/mnt/secrets")]
end
OIDC -.-> IAM
SA -.-> IAM
IAM --> SM
SA --> POD
POD --> CSI
CSI --> SM
CSI --> VOL
The cloud-native answer has no bootstrap chicken-and-egg because the cloud’s identity system is the bootstrap. Pods authenticate to AWS via IRSA — the ServiceAccount token is OIDC-trusted by AWS IAM, IAM grants the role’s Secrets Manager permissions, the CSI driver fetches the secret and mounts it as a tmpfs file in the pod’s volume. There is no Kubernetes Secret object in etcd; there is no server to install on-cluster.
The trade is the AWS lock-in — and the fact that secrets are files,
not env vars or Secret objects. This is more secure (no etcd snapshot
leak) and less ergonomic (no envFrom, no secretRef). Helm charts
that assume Secret objects need to be patched.
§4 — What scale changes
Three scale axes flip vendor rankings. The first two are quantitative; the third is operational.
Secret count. A cluster with ten secrets can run any option in this
matrix without strain — SOPS-in-Git works, Sealed Secrets works, even
manual kubectl create secret works. At a hundred secrets the
operational cost diverges sharply. SOPS-in-Git’s per-rotation cost
becomes “find every consumer of this credential by grep”. Sealed
Secrets’ re-encrypt-on-controller-key-rotation becomes a multi-hour
ceremony. Infisical’s UI starts paying for itself the moment two
engineers need to read the same secret from different terminals on
the same Tuesday. Vault’s namespaces and ACLs start mattering. The
crossover is not a number — it is “how many secrets can one engineer
hold in their head simultaneously?” Below the threshold, simpler is
better; above it, simpler becomes the bottleneck.
Rotation cadence. Quarterly rotation is fine on every option in the matrix. Per-incident rotation — one breach, every credential in the cluster — flips the ranking entirely. Static credentials (KV in Infisical, Sealed Secrets, SOPS) require an operator to mint a new value and propagate it to every consumer; even with ESO doing the sync, the consumer pods must restart to pick up the new env var, and “restart every pod in the cluster” is rarely a free operation. Dynamic credentials (Vault’s database secrets engine, AWS Secrets Manager’s auto-rotate) sidestep this entirely: each pod gets a fresh short-lived credential on a schedule the secret store controls. For a regulated environment with a 24-hour rotation SLO, Vault and AWS Secrets Manager are the only options that can keep up.
Audit-log demand. No compliance framework, no auditor, no
quarterly access review: SOPS+age in Git is enough — git log is the
audit log, every read of a secret is a git checkout event with a
timestamp and an author. SOC 2 / ISO 27001 / HIPAA / PCI-DSS / FedRAMP:
you need an audit log of who-read-what-when at runtime, and that is
where the secret-store-as-server options separate themselves from the
secret-store-as-Git-history options. Infisical, Vault, and AWS Secrets
Manager all log reads to a structured audit stream with retention
measured in years. Sealed Secrets and SOPS in Git do not. The
empirical scale of the failure mode this audit log catches is in
GitGuardian’s annual report:
In 2023, we detected 12.8 million secrets exposed in public GitHub commits — a 28% increase over 2022.GitGuardian — The State of Secrets Sprawl 2024
That number is not the cost of running a self-hosted secret store; it is the cost of not running one, paid by everyone in aggregate. At audit time, “we encrypt everything in Git” is a defensible posture; “we keep nothing in Git” is a much stronger one.
§5 — Frank’s choice, and what happened
I run Infisical with External Secrets Operator, with SOPS-encrypted
secrets in Git as the bootstrap layer. The full chain: SOPS-encrypted
YAML in secrets/ applied out-of-band to seed three Secrets
(infisical-secrets, infisical-db-uri, infisical-credentials);
Infisical running at 192.168.55.204 with its own Postgres and Redis
running as separate ArgoCD apps; ESO reading from Infisical via a
ClusterSecretStore and materialising native Kubernetes Secrets for
every workload that wants one.
I did not pick Infisical over Vault on the merits in the abstract. I
picked it because the UI is good enough that a single operator can
keep a hundred secrets sorted across dev and prod environments
without losing track, and because the API and the Helm chart are
documented well enough that the ESO integration is a single CR. I
ruled out Vault because the unseal ceremony is not justifiable at
homelab scale, and ruled out plain SOPS-in-Git because once the secret
count crossed forty I stopped being able to keep a mental index of
which app needed which credential. Sealed Secrets stayed in the
running until the moment I considered controller-key rotation.
The honesty of that choice is what makes the resulting scars worth writing down. A different vendor would have produced different scars; a managed cloud secret store would have hidden them all.
ENCRYPTION_KEY, a database
connection string, and a working Redis URL before it can start. The
Helm chart cannot materialise any of those from thin air. ArgoCD cannot
apply a Secret it does not know yet exists. The only accepted exception
to the declarative-everything principle is SOPS-encrypted secrets that
live in secrets/, applied manually via
sops --decrypt secrets/infisical/infisical-secrets.yaml | kubectl apply -f -
before the Infisical app is synced. Three Secrets, three commands,
one per cluster rebuild. Everything else after that is ArgoCD-managed.
The bootstrap exception is the tax for declarative-everything;
pretending it does not exist is what causes half the production
secret-store incidents in the field. The lesson: name the exception,
write it down, and apply it the same way every time.ExternalSecret’s data: array and
left an empty data: []. The ESO admission webhook rejected the CR
with a schema-validation error. ArgoCD did not infer this from the
values-diff and parked the app in OutOfSync forever. The fix, once
diagnosed, is one of two: either delete the ExternalSecret entirely
(if no keys remain), or conditionally render the manifest behind a
{{- if .Values.secrets }} Helm guard. The lesson:
“the last key was removed” is not a state most operators consider
possible. The admission webhook does — and the consequence of not
considering it is an ArgoCD app that looks busted for the wrong
reason. The first ten minutes of the incident were spent looking at
the values diff and the Application; the next ten were spent looking
at the ESO controller logs, where the rejection was perfectly clear.A Deployment used envFrom.secretRef for a Secret that did not exist
yet — because ESO had not synced from Infisical on the first deploy.
Without optional: true on the envFrom block, every rolling update
wedged on CreateContainerConfigError for the entire chart’s pods,
with no visible signal except a stuck ReplicaSet. The fix is a
one-line annotation per envFrom block:
envFrom:
- secretRef:
name: app-secret
optional: trueEasy when you know it, invisible when you don’t. The lesson is that
the Secret resource is a runtime dependency, not a build-time
dependency, and the chart that consumes it has to encode that
correctly. Otherwise the first deploy of every new app becomes a
ten-minute race between ArgoCD and ESO that ArgoCD usually loses.
A fourth scar is worth a paragraph instead of a callout — the
Infisical chart’s two-source DB_CONNECTION_URI bug. The upstream
chart ships both postgresql.enabled and useExistingPostgresSecret
with no else branch between them; set both and the env var is
injected twice, the second binding silently wins, and the bundled
Postgres goes unused while the app talks to the external one. The fix
was to split one Infisical app into three ArgoCD apps — infisical,
infisical-postgresql, infisical-redis — each managed independently.
The three-app split is not the conventional Helm shape; it is the
shape forced by the chart’s missing else.
Visible evidence (cluster-side capture deferred):

The four scars share a shape. None of them are bugs in Infisical itself, or in ESO, or in SOPS. All of them are emergent properties of running a self-hosted secret store whose bootstrap loop is real and whose interfaces with the declarative machinery of the cluster need to be exact. The seams are where the scars live, and the seams are exactly where the marketing material does not look.
§6 — When Frank’s answer doesn’t generalize
Frank’s answer — Infisical + ESO + SOPS-for-bootstrap — is one leaf of a four-leaf tree. The other three are real.
flowchart TD
A["Solo dev · one cluster · ≤20 secrets?"] -- "Yes" --> L1["SOPS+age in Git · decrypt-on-deploy"]
A -- "No" --> B["Compliance / audit-log mandated?"]
B -- "Yes · regulated industry" --> L4["HashiCorp Vault"]
B -- "No" --> C["Cloud-native · single cloud?"]
C -- "Yes · AWS" --> L3["AWS Secrets Manager + CSI"]
C -- "No · self-hosted" --> L2["Infisical + ESO (Frank's pick)"]
The first branch is whether the secret count justifies a server at all.
A solo developer with one cluster and twenty secrets does not need a
UI, an API, or an audit log of reads — git log is enough. SOPS+age
in Git with decrypt-on-deploy is the right answer; running Infisical
in that context is operational overhead in exchange for nothing the
developer needs.
The second branch is the compliance question. A regulated industry — finance, healthcare, government — needs an audit log of who-read-what-when with retention measured in years, and needs dynamic credentials so that per-incident rotation is a knob and not a multi-day ceremony. Vault is the only option in the matrix that has both, and at that compliance load the operational tax of the unseal ceremony is amortised against the audit fines avoided.
The third branch is cloud lock-in. A workload running exclusively on AWS has the cloud’s identity system available for free, IRSA solves the bootstrap loop without any out-of-band step, and Secrets Manager
- CSI is the lowest-friction answer in the landscape — provided you are happy to be on AWS forever. The same applies in shape (with different vendor names) on Azure and GCP.
Frank’s leaf — Infisical + ESO + SOPS-bootstrap — is the answer for the cluster that is none of the above. Self-hosted (no cloud lock-in), small-but-not-tiny (a UI starts mattering), unregulated (no Vault-level ceremony justified). If you are reading this from a fifty-secret multi-cloud SOC 2 environment, the right answer for you is one of the other leaves. Frank’s answer is correct for Frank and is documented here so that anyone considering the same trade understands the rest of the leaves before picking it.
§7 — Roadmap & where this space is going
Three trends are worth naming. None are settled; all affect the next few years of secrets management on Kubernetes.
OIDC-trusted workload identity is eating static credentials. Cloud KMS providers, Vault, and Infisical are all converging on “the pod authenticates with its ServiceAccount JWT and the secret store mints a short-lived credential”. This is the secret-store-as-OIDC-RP model. AWS already does it via IRSA; Vault has the Kubernetes auth method; Infisical’s Machine Identity supports JWT auth. In eighteen months the “store a long-lived API key in a bootstrap Secret” pattern may look as quaint as the “long-lived AWS access key” pattern looks today. The bootstrap loop does not disappear, but it shrinks — the only long-lived credential becomes the cluster’s OIDC signing key, which is the cluster’s by definition.
CSI driver vs ESO is the architectural fork to watch. The Secrets
Store CSI Driver mounts secrets directly as tmpfs files; ESO
materialises them as native Kubernetes Secret objects. CSI is more
secure (no Secret object in etcd, no etcd-snapshot leak path) but less
ergonomic (no envFrom, no secretRef, harder Helm portability). ESO
is winning adoption but CSI is winning the security-first crowd. The
two are not strictly exclusive — some teams run both — but the long
arc bends toward CSI for high-security environments and ESO for
day-one ergonomics. Watch what the OpenShift and EKS defaults pick.
Sealed Secrets is becoming a deployment-only artefact. Encrypt-in-Git is great for bootstrap and disaster recovery; it is mediocre for day-two operations. Expect Sealed Secrets (and SOPS) to live on as the GitOps disaster-recovery layer beneath a real secret store — the same role SOPS plays in Frank’s stack. Day-to-day operations move to Infisical or Vault; rebuild-from-cold-Git stays in SOPS or SealedSecret form because it is the only thing that survives the secret store being unavailable.
The space is not done evolving. Frank will revisit this paper when the answers change — most likely when the OIDC-trusted-identity story matures enough that the bootstrap loop becomes a Kubernetes-native operation rather than an out-of-band one.
References
- vendor-docs
External Secrets Operator — ClusterSecretStore API
The ClusterSecretStore is a cluster scoped SecretStore that can be referenced by all ExternalSecrets from all namespaces.
It defines how the operator authenticates against the secret provider — for Infisical, via universalAuthCredentials referencing a clientId and clientSecret stored in Kubernetes Secrets.
Definitive vendor documentation of the ClusterSecretStore CR Frank actually deploys. Grounds the §3 architecture comparison in ESO's own model and underwrites the §5 description of `apps/infisical/manifests/cluster-secret-store.yaml`.
- vendor-docs
Infisical — Self-hosting overview
Infisical is an open-source platform for managing secrets, certificates, and configurations across teams and infrastructure.
You can self-host Infisical on your own infrastructure using Docker, Kubernetes, or any other container-orchestration platform.
Vendor's authoritative statement of Infisical's self-hosting model and the projects/environments/identities architecture. Anchors §3's Infisical diagram and the §5 narrative of why a UI-first secret store earns its operational cost at small homelab scale.
- paper
HashiCorp Vault — Architecture
Vault is a complex system that has many different pieces. To help both users and developers of Vault build a mental model of how it works, this page documents the system architecture.
The storage backend is untrusted, and is used only to durably store encrypted data.
Canonical architecture document for the heavyweight in the space. Anchors §2's 'self-hosted · dynamic-credentials' quadrant and the §4 scale claim that Vault's storage-backend-is-untrusted model is what enables the audit-log + transit + PKI feature surface that compliance teams pay for.
- vendor-docs
Mozilla SOPS — README and design
sops is an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, age, and PGP.
Only the values are encrypted, the keys are still readable. That allows you to do diffs that are meaningful and preserve the structure of your file.
Vendor design document for SOPS — the only sane way to ship encrypted bootstrap secrets in Git. Anchors the §3 SOPS-in-Git architecture diagram and the §5 explanation of why `secrets/` is applied out-of-band before Infisical exists.
- postmortem
Frank — Storage / Secrets / SSA gotcha registry
SOPS-encrypted secrets must NOT be ArgoCD-managed; apply out-of-band from `secrets/`.
ESO: empty `data: []` is rejected; delete the ExternalSecret if all keys are removed.
`envFrom.secretRef` without `optional: true` blocks rolling updates when the Secret is missing.
Frank's own running incident catalogue, codified as one-liner gotchas with per-topic prose under `docs/runbooks/frank-gotchas/storage-secrets-ssa.md`. Direct evidence for the §5 scar callouts; also the load-bearing claim that every scar in Paper 09 came from a real incident, not a hypothetical.
- vendor-docs
Secrets Store CSI Driver — Overview
Secrets Store CSI Driver for Kubernetes Secrets — Integrates secrets stores with Kubernetes via a CSI volume.
The Secrets Store CSI Driver secrets-store.csi.k8s.io allows Kubernetes to mount multiple secrets, keys, and certs stored in enterprise-grade external secrets stores into their pods as a volume.
Vendor docs for the CSI-driver alternative to ESO. Anchors the §7 roadmap claim about 'CSI driver vs ESO is the architectural fork to watch' and the §3 architecture diagram for AWS Secrets Manager + CSI.
- vendor-docs
External Secrets Operator — GitHub repo
External Secrets Operator is a Kubernetes operator that integrates external secret management systems like AWS Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault, IBM Cloud Secrets Manager, Akeyless, CyberArk Conjur, Pulumi ESC, and many more.
The operator reads information from external APIs and automatically injects the values into a Kubernetes Secret.
The canonical project repo. Anchors the §2 'ESO + SOPS-in-Git' placement on the landscape and the §3 architecture flow for how ESO sync into native K8s Secret resources is the same regardless of upstream store.
- benchmark
GitGuardian — The State of Secrets Sprawl 2024
In 2023, we detected 12.8 million secrets exposed in public GitHub commits — a 28% increase over 2022.
Generic high-entropy strings, generic passwords, and Google API keys remain the top three leak types.
The closest thing to an industry-wide benchmark on the cost of NOT running a secret-store: 12.8M leaks in public Git in one year, the empirical pricetag of 'plaintext in Git'. Anchors §4's scale claim that the rotation-cadence axis flips the ranking when secret count grows past a single human's mental cache, and provides the headline number for §6's argument that the null-hypothesis leaf is not safe at any scale where you ship code from more than one machine.
- talk
KubeCon NA 2022 — The State of Kubernetes Secrets Management
There is no single 'right' way to manage secrets in Kubernetes — the right answer depends on whether you trust your cluster API, your storage backend, your CI system, and your operators, and the answer to each is rarely the same.
The trade-off between encrypt-in-Git (Sealed Secrets, SOPS) and sync-from-external-store (ESO, CSI) is fundamentally an axis of where you put the trust boundary.
A community talk that frames the entire space as a trust-boundary decision, which is exactly the framing §6's decision-tree adopts. Cited as the canonical 'there is no single right answer' source — useful when the Paper resists the temptation to declare one vendor universally correct.
