
Secrets Management — Infisical + External Secrets Operator
Layer 8 established that Secrets OPerationSMozilla's tool for encrypting the *values* in a YAML file while leaving the keys readable, so an encrypted secret still reviews as a sensible diff.-encrypted secrets cannot live in ArgoCD-managed manifest paths. The fix — apply them out-of-band with sops --decrypt | kubectl apply — works for bootstrap secrets that exist before anything else runs. It does not work for runtime secrets that applications consume daily.
Layer 9 replaces the runtime half. The goal: secrets live in a versioned, audited store; applications consume them as standard Kubernetes Secrets; no engineer ever touches a plaintext credential. The tooling is Infisical for the store and External Secrets Operator (External Secrets OperatorThe operator that pulls secrets from an external store into Kubernetes Secrets. On Frank it mints short-lived GitHub App tokens, so no long-lived credential is ever committed.) to materialize secrets into the cluster.
But deploying Infisical’s standalone chart surfaced three bugs in quick succession — duplicate environment variables, hardcoded Redis password paths, and Bitnami image registry issues — that forced splitting one ArgoCD application into three.
flowchart TD
subgraph Infisical[Infisical — Secret Store]
PG[PostgreSQL<br/>separate ArgoCD app]
Redis[C<br/>separate ArgoCD app]
App[Infisical Standalone<br/>v1.7.2, sub-charts disabled]
end
subgraph ESO[External Secrets Operator]
CSS[ClusterSecretStore<br/>infisical]
ES[ExternalSecret<br/>per app per namespace]
end
subgraph Bootstrap[Bootstrap Secrets]
SOPS[SOPS-encrypted<br/>applied out-of-band]
end
SOPS -->|PG password| PG
SOPS -->|Redis password| Redis
SOPS -->|Infisical env vars| App
App -->|Machine Identity auth| CSS
CSS --> ES
ES -->|materializes| K8sSecret[Kubernetes Secret]
The Architecture
Two components:
Infisical is a self-hosted secret manager. Secrets live in projects, scoped to environments (dev, staging, prod). Access is controlled per Machine Identity. There is an audit log. The self-hosted version is free.
External Secrets Operator (ESO) reads from external secret stores and materializes them as native Kubernetes Secrets. It watches ExternalSecret resources — an app declares what it needs, ESO fetches it from Infisical and creates the Secret. The app sees a normal Secret with no SDK, no sidecar.
Infisical (192.168.55.204:8080)
└── frank-cluster project / prod environment
└── secrets (KEY = value)
ESO ClusterSecretStore "infisical"
└── authenticates via Machine Identity (Universal Auth)
ExternalSecret (per app, per namespace)
└── references ClusterSecretStore + secret key
└── ESO materializes → K8s SecretDeploying Infisical: Three Apps, Not One
The infisical-standalone chart bundles PostgreSQL and Redis as sub-charts. Deploying it as a single ArgoCD app surfaced three bugs.
Bug 1: Duplicate DB_CONNECTION_URI
The chart has two independent conditions that inject DB_CONNECTION_URI:
postgresql.enabled: true— the bundled PostgreSQL sub-chart injects ituseExistingPostgresSecret.enabled: true— the external secret path also injects it
There is no else branch. Enable the external secret path while disabling bundled PostgreSQL, both conditions fire, and the env var appears twice. Kubernetes accepts duplicate env vars without error — but the second value silently wins.
The fix: split PostgreSQL into a separate ArgoCD app (infisical-postgresql) using the Open Container InitiativeThe body behind the standard image and runtime formats. "OCI registry" means any registry speaking that standard, not a specific vendor's. Bitnami chart. With postgresql.enabled: false in the main infisical app, only the external secret path fires.
Bug 2: Redis Password Hardcoded
The chart builds REDIS_URL using a Helm helper that reads .Values.redis.auth.password — a plain Helm value, not a secret reference. Setting redis.auth.existingSecret has no effect on REDIS_URL construction. The Redis pod uses the password from the secret, but the Infisical pod builds its REDIS_URL using the hardcoded Helm value. Connection refused.
The fix: split Redis into a separate ArgoCD app (infisical-redis). With redis.enabled: false in the main chart, REDIS_URL comes from the infisical-secrets Secret via envFrom.
Bug 3: Bitnami Image Registry
The Bitnami PostgreSQL chart from charts.bitnami.com/bitnami pulls images from docker.io/bitnami/postgresql. Recent tags are unavailable there for architecture reasons. The Infisical chart itself uses mirror.gcr.io/bitnamilegacy/postgresql — a GCR-hosted mirror. Using the OCI chart source (registry-1.docker.io/bitnamicharts) and pinning to the same image mirror resolves pull failures.
A secondary issue: Bitnami chart versions 16.x+ validate non-default image registry overrides. Staying on postgresql 14.1.10 avoids that check.
The Final Shape
Three ArgoCD apps, all in the infisical namespace:
| App | Chart | Purpose |
|---|---|---|
infisical-postgresql | OCI Bitnami postgresql 14.1.10 | PostgreSQL, image from mirror.gcr.io/bitnamilegacy |
infisical-redis | OCI Bitnami redis 18.14.1 | Redis standalone, same image mirror |
infisical | infisical-standalone 1.7.2 | Infisical app only, sub-charts disabled |
Bootstrap secrets — PostgreSQL password, Redis password, Infisical env vars — are SOPS-encrypted and applied out-of-band, matching the Layer 8 pattern.
Connecting ESO to Infisical
ESO authenticates to Infisical using a Machine Identity with Universal Auth. The credentials are SOPS-encrypted in secrets/infisical/eso-credentials.yaml, applied out-of-band.
The ClusterSecretStore ties it together:
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: infisical
spec:
provider:
infisical:
auth:
universalAuthCredentials:
clientId:
name: infisical-credentials
namespace: external-secrets
key: clientId
clientSecret:
name: infisical-credentials
namespace: external-secrets
key: clientSecret
hostAPI: http://192.168.55.204:8080/api
secretsScope:
projectSlug: frank-cluster-iwpg
environmentSlug: prod
secretsPath: /This is deployed as a raw manifest via infisical-extras ArgoCD app, syncing apps/infisical/manifests/.
The Project Slug Surprise
The ClusterSecretStore needs a projectSlug. The intuitive value is the project name — frank-cluster. This returns a 404. Infisical auto-generates a URL-safe slug that differs from the display name. The actual slug is visible at Project Settings → General. The eso-cluster-reader Machine Identity has Viewer access but Viewer cannot call the workspace-list API, so the slug cannot be retrieved programmatically with the same credentials.
ESO v1 Schema Changes
ESO 2.x promoted the API to external-secrets.io/v1. Two schema changes bit the initial manifests:
ClusterSecretStorecredentials: Inv1beta1,clientIdandclientSecretwere wrapped in asecretRef:key. In v1, they are directSecretKeySelectorobjects.ExternalSecretremoteRef: ThemetaData:block underremoteRefis gone; scope is declared once inClusterSecretStore.spec.provider.infisical.secretsScope.
The Smoke Test
With the ClusterSecretStore validated (READY=True):
kubectl get externalsecret cluster-test -n secrets-test
# NAME STORETYPE STORE REFRESH INTERVAL STATUS READY
# cluster-test ClusterSecretStore infisical 30s SecretSynced True
kubectl get secret cluster-test-secret -n secrets-test \
-o jsonpath='{.data.testValue}' | base64 -d
# hello-from-infisicalWhat Changed
Before Layer 9, adding a runtime secret meant: write plaintext YAML, sops encrypt, commit, manually kubectl apply, update every consumer’s deployment. Now: add the secret in the Infisical UI, declare an ExternalSecret in the app’s namespace, ESO syncs within the refreshInterval. Rotation is a UI operation — no pod restart, no git commit.
SOPS stays for bootstrap secrets: the credentials Infisical and ESO themselves need to start. Everything above that layer moves to Infisical.
Missteps
| What Happened | Why It Was Wrong | How We Fixed It | Commit |
|---|---|---|---|
Duplicate DB_CONNECTION_URI env var — the chart has no else branch between bundled PostgreSQL and external secret path, both inject the same env var | Two independent conditions fire regardless of which is enabled; Kubernetes silently accepts duplicates, second value wins | Split PostgreSQL into separate ArgoCD app, disabled bundled sub-chart | 446dab7a, 9a00f46c |
Redis password hardcoded in Helm helper — REDIS_URL is built from .Values.redis.auth.password not the actual secret value | Setting redis.auth.existingSecret has no effect on REDIS_URL construction; the helper ignores it | Split Redis into separate app, set REDIS_URL via envFrom on the actual secret | 95f31f96 |
Wrong Infisical project slug — frank-cluster returns 404; the auto-generated slug is frank-cluster-iwpg | The display name and URL-safe slug differ; Viewer permissions cannot query the workspace-list API to discover the slug | Read the slug from the UI once, committed to cluster-secret-store.yaml | aefa7916 |
ClusterSecretStore schema mismatch for ESO v2.1.0 — v1beta1 credential wrapper syntax used instead of direct SecretKeySelector | Upgrade from v1beta1 to v1 removed the secretRef: wrapper; initial manifests used old syntax | Corrected ClusterSecretStore to v1 schema | 6fe95a3f, b228e2b5 |
References
- Infisical Documentation — self-hosted setup, Machine Identities, Universal Auth
- External Secrets Operator Documentation — ClusterSecretStore, ExternalSecret v1 API
- ESO Infisical Provider — provider-specific config
- Bitnami OCI Charts — postgresql, redis
