External Secrets Operator with Infisical
Synchronize secrets from Infisical to Kubernetes using External Secrets Operator for GitOps-friendly secret management.
When I first configured External Secrets Operator to sync secrets from Infisical into our Kubernetes cluster, I underestimated how many moving parts were involved. The initial setup seemed straightforward — install the operator, point it at Infisical, define your ExternalSecret resources — but the reality was a maze of authentication scopes, environment slugs, and Go template syntax that took me several frustrating evenings to sort out. One particularly painful lesson came when a secret rotation in Infisical silently failed to propagate because I had misconfigured the refreshInterval, and our staging database connections started dropping at 2 AM. That experience taught me to treat the sync pipeline as a first-class concern with proper monitoring and alerting from day one.
Introduction
GitOps requires declarative configuration in Git, but secrets shouldn’t be stored in repositories. External Secrets Operator bridges this gap by synchronizing secrets from Infisical to Kubernetes.
[External Secrets Operator Documentation] — External Secrets Community , 2024-06-01 [Infisical Documentation - Kubernetes Integration] — Infisical , 2024-05-15Architecture Overview
flowchart TB
subgraph Source["🏦 Secret Source"]
Infisical["🔐 Infisical (Source of Truth)"]
Infisical --> Dev["🧪 dev/"]
Infisical --> Staging["🎭 staging/"]
Infisical --> Prod["🚀 prod/"]
end
Infisical -->|"⏰ Poll every 5m"| ESO
subgraph ESO["⚙️ External Secrets Operator"]
Store["📦 ClusterSecretStore"] --> ExtSec["🔗 ExternalSecret (per app)"]
end
ESO -->|"Creates/Updates"| K8s
subgraph K8s["☸️ Kubernetes Secrets"]
ApiSec["🔑 myapp-api-secrets"]
WebSec["🔑 archives-web-secrets"]
end
classDef primary fill:#7c3aed,color:#fff
classDef secondary fill:#06b6d4,color:#fff
classDef db fill:#f43f5e,color:#fff
classDef warning fill:#fbbf24,color:#000
class Source db
class ESO secondary
class K8s secondary
Implementation
Installation
[External Secrets Operator Helm Chart] — External Secrets Community , 2024-04-10HelmRelease
# platform/external-secrets/helmrelease.yaml
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: external-secrets
namespace: external-secrets
spec:
interval: 30m
chart:
spec:
chart: external-secrets
version: ">=0.9.0"
sourceRef:
kind: HelmRepository
name: external-secrets
namespace: flux-system
values:
installCRDs: true
serviceMonitor:
enabled: true
webhook:
create: true
certController:
create: true
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
ClusterSecretStore
Infisical Configuration
# platform/external-secrets/infisical-store.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: infisical-store
spec:
provider:
infisical:
hostAPI: https://app.infisical.com
auth:
universalAuthCredentials:
clientId:
key: clientId
name: infisical-credentials
namespace: external-secrets
clientSecret:
key: clientSecret
name: infisical-credentials
namespace: external-secrets
secretsScope:
projectSlug: my-project
Authentication Secret
# platform/external-secrets/infisical-credentials.yaml
apiVersion: v1
kind: Secret
metadata:
name: infisical-credentials
namespace: external-secrets
type: Opaque
stringData:
clientId: "${INFISICAL_CLIENT_ID}"
clientSecret: "${INFISICAL_CLIENT_SECRET}"
ExternalSecret Resources
API Secrets
# apps/myapp-api/externalsecret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-api-secrets
namespace: myapp-staging
spec:
refreshInterval: 5m
secretStoreRef:
name: infisical-store
kind: ClusterSecretStore
target:
name: myapp-api-secrets
creationPolicy: Owner
template:
type: Opaque
data:
ConnectionStrings__AppDb: |
Host={{ .POSTGRES_HOST }};Port={{ .POSTGRES_PORT }};Database={{ .APP_DB_NAME }};Username={{ .APP_DB_USER }};Password={{ .APP_DB_PASSWORD }};Include Error Detail=true
Nats__Url: "nats://{{ .NATS_USER }}:{{ .NATS_PASSWORD }}@{{ .NATS_HOST }}:{{ .NATS_PORT }}"
Qdrant__ApiKey: "{{ .QDRANT_API_KEY }}"
MinIO__AccessKey: "{{ .MINIO_ACCESS_KEY }}"
MinIO__SecretKey: "{{ .MINIO_SECRET_KEY }}"
Ollama__ApiKey: "{{ .OLLAMA_API_KEY }}"
data:
- secretKey: POSTGRES_HOST
remoteRef:
key: POSTGRES_HOST
property: ""
- secretKey: POSTGRES_PORT
remoteRef:
key: POSTGRES_PORT
- secretKey: APP_DB_NAME
remoteRef:
key: APP_DB_NAME
- secretKey: APP_DB_USER
remoteRef:
key: APP_DB_USER
- secretKey: APP_DB_PASSWORD
remoteRef:
key: APP_DB_PASSWORD
- secretKey: NATS_USER
remoteRef:
key: NATS_USER
- secretKey: NATS_PASSWORD
remoteRef:
key: NATS_PASSWORD
- secretKey: NATS_HOST
remoteRef:
key: NATS_HOST
- secretKey: NATS_PORT
remoteRef:
key: NATS_PORT
- secretKey: QDRANT_API_KEY
remoteRef:
key: QDRANT_API_KEY
- secretKey: MINIO_ACCESS_KEY
remoteRef:
key: MINIO_ACCESS_KEY
- secretKey: MINIO_SECRET_KEY
remoteRef:
key: MINIO_SECRET_KEY
- secretKey: OLLAMA_API_KEY
remoteRef:
key: OLLAMA_API_KEY
Web Application Secrets
# apps/my-web-client/externalsecret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-web-client-secrets
namespace: myapp-staging
spec:
refreshInterval: 5m
secretStoreRef:
name: infisical-store
kind: ClusterSecretStore
target:
name: my-web-client-secrets
creationPolicy: Owner
template:
type: Opaque
data:
OIDC__Authority: "{{ .OIDC_AUTHORITY }}"
OIDC__ClientId: "{{ .OIDC_CLIENT_ID }}"
OIDC__ClientSecret: "{{ .OIDC_CLIENT_SECRET }}"
Api__BaseUrl: "{{ .API_BASE_URL }}"
data:
- secretKey: OIDC_AUTHORITY
remoteRef:
key: OIDC_AUTHORITY
- secretKey: OIDC_CLIENT_ID
remoteRef:
key: OIDC_CLIENT_ID
- secretKey: OIDC_CLIENT_SECRET
remoteRef:
key: OIDC_CLIENT_SECRET
- secretKey: API_BASE_URL
remoteRef:
key: API_BASE_URL
Environment-Specific Configuration
[Managing Secrets in Kubernetes: A Practical Guide] — Kubernetes Authors , 2024-03-20Using Infisical Environments
# apps/myapp-api/externalsecret-staging.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-api-secrets
namespace: myapp-staging
spec:
refreshInterval: 5m
secretStoreRef:
name: infisical-store
kind: ClusterSecretStore
target:
name: myapp-api-secrets
creationPolicy: Owner
dataFrom:
- extract:
key: "" # Empty key = all secrets
rewrite:
- regexp:
source: "(.*)"
target: "$1"
# Scope to staging environment
secretStoreRef:
name: infisical-staging-store
kind: ClusterSecretStore
Per-Environment Stores
# platform/external-secrets/infisical-staging-store.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: infisical-staging-store
spec:
provider:
infisical:
hostAPI: https://app.infisical.com
auth:
universalAuthCredentials:
clientId:
key: clientId
name: infisical-credentials
namespace: external-secrets
clientSecret:
key: clientSecret
name: infisical-credentials
namespace: external-secrets
secretsScope:
projectSlug: my-project
environmentSlug: staging # Environment-specific
secretsPath: / # Root path
Deployment Integration
Using Secrets in Deployments
# apps/myapp-api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-api
namespace: myapp-staging
spec:
template:
spec:
containers:
- name: api
image: registry.example.com/myapp/myapp-api:latest
envFrom:
- secretRef:
name: myapp-api-secrets
env:
- name: Environment
value: "staging"
Monitoring
[Secrets Management Best Practices for Kubernetes] — OWASP Foundation , 2024-01-15Check Sync Status
# View ExternalSecret status
kubectl get externalsecrets -A
# Describe specific ExternalSecret
kubectl describe externalsecret myapp-api-secrets -n myapp-staging
# Check events
kubectl get events -n myapp-staging --field-selector reason=Updated
Prometheus Metrics
# Alerting on sync failures
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: external-secrets-alerts
namespace: external-secrets
spec:
groups:
- name: external-secrets
rules:
- alert: ExternalSecretSyncFailed
expr: externalsecret_status_condition{condition="Ready", status="False"} == 1
for: 10m
labels:
severity: critical
annotations:
summary: "ExternalSecret {{ $labels.name }} sync failed"
Troubleshooting
| Issue | Diagnosis | Solution |
|---|---|---|
| Secret not created | Check ESO logs | Verify ClusterSecretStore credentials |
| Stale values | Check refreshInterval | Force refresh: delete K8s secret |
| Template errors | Describe ExternalSecret | Fix Go template syntax |
| Auth failures | Check Infisical credentials | Rotate machine identity |
Summary
External Secrets Operator enables:
| Feature | Benefit |
|---|---|
| GitOps Compatibility | Secret references in Git, values in Infisical |
| Auto-Sync | Secrets refresh every 5 minutes |
| Templates | Construct connection strings from parts |
| Environment Scoping | Separate dev/staging/prod secrets |
| Audit Trail | All changes tracked in Infisical |
Combined with Flux CD, this enables fully declarative Kubernetes deployments with secure secret management.
Looking back, the time I invested in getting this pipeline right has paid dividends many times over. Before adopting External Secrets Operator with Infisical, rotating a database password meant manually updating secrets across multiple namespaces and hoping I did not miss one. Now, I update the value in Infisical and within five minutes every service picks up the change automatically. The peace of mind alone is worth the initial setup complexity.
[External Secrets Operator] — External Secrets Community , 2024-06-01 [GitOps and Secret Management] — Flux CD Project , 2024-02-10Next Steps
- Set up Prometheus alerts for
externalsecret_status_conditionto catch sync failures before they impact services. - Implement secret versioning in Infisical to enable rollback if a rotation introduces a bad value.
- Explore Infisical’s dynamic secrets for short-lived database credentials that automatically expire.
- Review the Health Checks guide to ensure your services detect stale secrets quickly.