Code Agency
3 min read

Secrets that never touch git: Infisical, ESO and the flow we trust

Secrets in .env files end up in repos; sealed secrets end up unrotatable. The Infisical → External Secrets Operator → pod pipeline that keeps credentials out of both git and CI logs.

Every GitOps setup eventually hits the same wall: the cluster state lives in git, but a database password cannot live in git. Teams solve this two ways, and we've seen both fail.

The two failure modes

Plain .env in the repo. Even a private repo. Even with .gitignore "handling it" — until someone git add -f's it at 11 PM to unblock a deploy, and now it's in history forever, readable by anyone with clone access, present in every fork and every laptop that ever pulled.

Sealed Secrets. Better — the encrypted blob is safe to commit, decryptable only by the controller holding the cluster's private key. But rotation means re-encrypting with kubeseal and opening a PR, so "rotate this leaked credential now" turns into "rotate this leaked credential after CI runs and someone approves it." And the audit trail is a git blame on a base64 blob — it tells you a secret changed, never who touched it in the secrets manager or why.

We wanted secret material that never enters a git diff, and rotation that never needs a commit.

Infisical → External Secrets Operator → pod

Infisical holds the actual values — organized by project, environment and path, with its own access log for every read and write. The cluster never stores secrets as YAML; it stores a reference to where Infisical keeps them.

External Secrets Operator (ESO) is the bridge. A SecretStore points at an Infisical project using a machine-identity client ID/secret — itself the one credential we create by hand, once, with kubectl create secret and never commit:

infisical-secret-store.yaml
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: infisical-backend
spec:
  provider:
    infisical:
      hostAPI: https://app.infisical.com/api
      auth:
        universalAuthCredentials:
          clientId:
            name: infisical-universal-auth
            key: clientId
          clientSecret:
            name: infisical-universal-auth
            key: clientSecret
      secretsScope:
        projectSlug: odoo-client-prod
        environmentSlug: prod
        secretsPath: "/database"

An ExternalSecret then declares which keys a workload needs and what native Secret to materialize:

external-secret.yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: odoo-db-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: infisical-backend
    kind: SecretStore
  target:
    name: odoo-db-credentials
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: DB_PASSWORD
    - secretKey: ADMIN_PASSWD
      remoteRef:
        key: ADMIN_PASSWD

Everything committed to git — the SecretStore, the ExternalSecret, the ArgoCD Application syncing both — contains zero secret material. Pods still consume a bog-standard Secret via envFrom, so nothing in the application changes. Only where that Secret's contents come from changes.

Rotation without a commit

Rotate the value in Infisical and ESO picks it up on its next refreshInterval — no PR, no kubeseal, no waiting on CI. For workloads that only read environment variables at startup, we pair this with Reloader: annotate the Deployment once, and a changed Secret triggers an automatic rolling restart. Leak a credential at 2 AM, rotate it in Infisical, and the fix is live before the incident channel finishes filling up.

What never reaches CI logs

Because the GitOps pipeline only ever applies SecretStore and ExternalSecret manifests — references, not values — Woodpecker never has a secret to accidentally echo, mask, or leave in a build artifact. The only place the actual value is fetched is the ESO controller talking directly to the Infisical API, inside the cluster, over TLS.

This is the same discipline behind ConfigMaps and Secrets for Odoo taken one step further: native Secret objects are still where pods read credentials from, but nothing upstream of ESO ever holds the plaintext. It's standard on every GitOps pipeline we wire up as part of managed cloud hosting — the git history stays clean no matter how many times a credential rotates.

Want us to publish something specific?

Tell us what you'd like to read and we'll add it to our writing queue.

Get the next one in your inbox

New articles, videos and the occasional engineering note — a short mail when there’s something worth reading, nothing else.