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]
1. Create the Project and environment
Section titled “1. Create the Project and environment”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.
2. Create a Machine Identity
Section titled “2. Create a Machine Identity”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.
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).
3. The three CRs
Section titled “3. The three CRs”apiVersion: secrets.infisical.com/v1beta1kind: InfisicalConnectionmetadata: { 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/v1beta1kind: InfisicalAuthmetadata: { 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/v1beta1kind: InfisicalStaticSecretmetadata: { 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.
4. Wire the Rollout
Section titled “4. Wire the Rollout”envFrom: - secretRef: name: <app>-api-kc # Infisical-managed. Edit values in the UI, not here5. (optional) Wire local dev
Section titled “5. (optional) Wire local dev”Skip the cluster entirely for this. The Infisical CLI reads straight from
the same Project, no .env file needed.
- Add a
devenvironment to the same Project, alongsidestaging/prod. The slug must be exactlydev. 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”. - 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. - Point the repo at the Project:
infisical initin the app repo, or hand-write.infisical.json:{"workspaceId": "<project-id-from-infisical-ui>","defaultEnvironment": "dev","gitBranchToEnvironmentMapping": null}defaultEnvironmentlets every command below skip--env. - Replace
.envwith a wrapper:infisical run -- <start command>(e.g.infisical run -- go run .) injects secrets straight into the process env. For a reload tool likeair, wrap the whole tool (infisical run -- air) rather than sourcing.envinside 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 outsidedev. - Per-environment. Most app config (issuer URLs, CORS origins, log level) needs its own value in every environment,
devincluded. 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.
What stays out of Infisical
Section titled “What stays out of Infisical”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"Other out-of-band secrets
Section titled “Other out-of-band secrets”Two more items don’t route through Infisical at all. They’re bootstrap credentials for the platform itself, not per-app config:
| Secret | Namespace | What 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.
GHCR: public package vs imagePullSecret
Section titled “GHCR: public package vs imagePullSecret”The cluster has to pull ghcr.io/<owner>/<app>. Two options:
-
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).
-
Keep the package private + add an
imagePullSecret. Create a docker-registry secret with a GHCR PAT (scoperead: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.