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 locallyAccess & 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

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. Talon’s package is currently private with no imagePullSecret configured — a known gap causing ImagePullBackOff until either the package goes public or option 2 is wired up; see Troubleshooting.

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