Skip to content

Secrets

Values live in the Infisical GUI. Git only stores the three operator CRs that say which project and environment to sync. This page is the process for any app. Not a list of Penvoice or Talon keys. Infra-shaped keys (issuer URL, REDIS_URL, S3 endpoint) are the usual first cells; business keys you add when the app needs them. Full GUI walkthrough: Provision § Infisical.

flowchart LR
  You([you, in a browser]) -->|edit a value| Infisical
  Infisical -->|Machine Identity auth| Op[Secrets Operator]
  Op -->|owns| Sec[(<app>-api-kc)]
  Sec --> Pods[app Rollout, via envFrom]

In Infisical: Project per app, environments staging and prod. Create a key once, fill both cells. Infra examples (not an exhaustive app list): issuer URL for that env’s Keycloak, REDIS_URL=redis://<app>-redis:6379. Never DB_URL / JDBC (CNPG) or the container port.

Project Settings → Machine Identities → Create Identity, Universal Auth, scoped read-only to this project’s environment. Note the Client ID and Client Secret: the secret is shown once.

On the node or locally: Access & consoles → kubectl.

Terminal window
kubectl create secret generic infisical-<app>-identity --namespace <app>-staging \
--dry-run=client -o yaml \
--from-literal=clientId="<staging-client-id>" --from-literal=clientSecret="<staging-client-secret>" \
| kubectl apply -f -
kubectl create secret generic infisical-<app>-identity --namespace <app>-prod \
--dry-run=client -o yaml \
--from-literal=clientId="<prod-client-id>" --from-literal=clientSecret="<prod-client-secret>" \
| kubectl apply -f -

--dry-run=client -o yaml | kubectl apply -f - makes this safe to re-run, e.g. if you mistype a value, just fix it and run the line again instead of deleting the Secret first.

This Secret is the operator’s own credential. Never committed, recreated by hand on rebuild (see Reference).

apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalConnection
metadata: { name: infisical, namespace: <app>-<env> }
spec:
address: http://infisical-infisical-standalone-infisical.infisical.svc.cluster.local:8080 # in-cluster, not the public ingress
---
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalAuth
metadata: { name: <app>-auth, namespace: <app>-<env> }
spec:
method: universal
infisicalConnectionRef: { name: infisical, namespace: <app>-<env> }
universal:
clientIdRef: { name: infisical-<app>-identity, namespace: <app>-<env>, key: clientId }
clientSecretRef: { name: infisical-<app>-identity, namespace: <app>-<env>, key: clientSecret }
---
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata: { name: <app>-synced-secrets, namespace: <app>-<env> }
spec:
infisicalAuthRef: { name: <app>-auth, namespace: <app>-<env> }
sources:
- projectId: "<project-id-from-infisical-ui>"
environmentSlug: staging # overlay sets prod on the other stack
secretPath: "/"
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
creationPolicy: Owner
name: <app>-api-kc
namespace: <app>-<env>

These three files live in workloads/<app>/base/. Overlays patch namespace, environmentSlug, and projectId. Only the values stay out of Git.

envFrom:
- secretRef:
name: <app>-api-kc # Infisical-managed. Edit values in the UI, not here

Skip the cluster entirely for this. The Infisical CLI reads straight from the same Project, no .env file needed.

  1. Add a dev environment to the same Project, alongside staging/prod. The slug must be exactly dev. The CLI fails with a 404 (Folder ... was not found) if the slug is anything else (e.g. development), even if the display name reads “Dev”.
  2. Auth: infisical login (personal, browser-based) for an interactive dev machine. Nothing to store locally. Machine Identities (Universal Auth) are for headless/CI use, like the operator itself; don’t reuse one for a laptop.
  3. Point the repo at the Project: infisical init in the app repo, or hand-write .infisical.json:
    {
    "workspaceId": "<project-id-from-infisical-ui>",
    "defaultEnvironment": "dev",
    "gitBranchToEnvironmentMapping": null
    }
    defaultEnvironment lets every command below skip --env.
  4. Replace .env with a wrapper: infisical run -- <start command> (e.g. infisical run -- go run .) injects secrets straight into the process env. For a reload tool like air, wrap the whole tool (infisical run -- air) rather than sourcing .env inside it. The child process it spawns inherits the injected env for free.

Not every var splits the same way between dev and staging/prod:

  • Dev-only. Anything excluded from Infisical in the cluster for structural reasons (see below) still needs a real value locally. There’s no CNPG or hardcoded manifest to supply it. But only in dev, never copied to staging/prod. Same for anything destructive-by-design (a schema-reset flag, a seed toggle): never let it exist outside dev.
  • Per-environment. Most app config (issuer URLs, CORS origins, log level) needs its own value in every environment, dev included. Don’t assume it’s shared.
  • Independent secrets. A cryptographic secret (session signing key, API key) gets its own generated value per environment. Never reuse a staging/prod secret in dev, or vice versa.

Two exclusions, both deliberate. See Secrets (concept) for why:

env:
- name: DB_URL # CNPG mints/rotates this on its own; a copy in Infisical would go stale
valueFrom:
secretKeyRef: { name: <app>-pg-app, key: uri }
- name: API_PORT # must match containerPort + the Service's targetPort. Keep it in one file
value: "8080"

Two more items don’t route through Infisical at all. They’re bootstrap credentials for the platform itself, not per-app config:

SecretNamespaceWhat it holds
<app>-pg-backup-creds<app>-<env>Oracle Object Storage S3 keys for that env’s Cluster

For that one, the access key is clean hex; the secret key contains +/=: don’t swap them, or a / ends up in the SigV4 credential and breaks request signing.

The cluster has to pull ghcr.io/<owner>/<app>. Two options:

  1. Make the GHCR package public (simplest). The image pulls anonymously. No pull secret needed. Set the package’s visibility to Public in GitHub. Good for images with no embedded secrets (these are static binaries on distroless, so nothing sensitive is baked in).

  2. Keep the package private + add an imagePullSecret. Create a docker-registry secret with a GHCR PAT (scope read:packages) and reference it from the pod spec:

    On the node or locally.

    Terminal window
    kubectl create secret docker-registry ghcr-pull \
    --namespace <app>-<env> \
    --docker-server=ghcr.io \
    --docker-username=<owner> \
    --docker-password=<ghcr-pat-read-packages> \
    --dry-run=client -o yaml \
    | kubectl apply -f -

    Safe to re-run, e.g. after rotating the PAT, run this again instead of deleting the Secret first.

    spec:
    template:
    spec:
    imagePullSecrets:
    - name: ghcr-pull

This platform defaults to option 1 for public-safe images. A brand-new GHCR package inherits its source repo’s visibility, so a private repo’s first push creates a private package by default, not a public one. Make it public right after the first build, or expect ImagePullBackOff; see Troubleshooting for the symptom and fix.

That’s the full playbook. Back to the recipe or the Architecture overview.