Skip to main content

Step 2 — Infisical: where the secrets live

The cluster never gets secrets from CI or from git. Every pod gets its environment from a Kubernetes Secret called app-secrets, which the External Secrets Operator fills from Infisical using two coordinates:

  • Infisical project slug = your project slug
  • Infisical environment slug = staging or prod

If either coordinate is wrong, or the cluster is not allowed into the project, nothing deploys. This page is short but it is where most first deploys fail.

2.1 Create the project

Infisical (https://eu.infisical.com) → Projects → Add project → name it exactly <slug>.

Fix the slug immediately

Infisical generates its own slug for a new project, which is usually the name plus a random suffix (my-project-a1b2). The cluster looks your secrets up by slug, not by name.

Open the new project → Project Settings → find Project slug → set it to exactly <slug> → save.

Symptom if you forget: the ExternalSecret in your namespace reports SecretSyncedError and the deploy sits in Progressing forever.

2.2 Environments

A new project comes with dev, staging and prod. The cluster uses the environment slugs staging and prod (not production). Check them under Project Settings → Environments and don't rename them.

dev is for your laptop.

2.3 Give the cluster access

Add the machine identity

Project → Access ControlMachine IdentitiesAdd identity → pick CI/CD → role Viewer (read is all it needs) → add.

CI/CD is the one identity the cluster authenticates with, for every project. Without it the cluster gets a 401/403 from Infisical and, again, SecretSyncedError.

While you're there: Members → add the developers who need to edit secrets.

2.4 Add the secrets

Use .env.example from step 1 as the checklist. Every variable the app reads must exist in both staging and prod, with real values. Extra keys are harmless (the cluster copies everything in the environment); missing keys break the app at runtime.

Database keys (only if the app has a MySQL database)

The cluster creates the MySQL user for you, and it reads the password from Infisical to do so. That means:

  • the password must be its own key, whose name you will declare in step 5 as passwordField
  • user name and database name are fixed by convention: stg-<slug> on staging, prod-<slug> on production
  • host is always persona-mysql-haproxy.percona-pxc-operator.svc.cluster.local, port 3306

Generate a password per environment (URL-safe, so it can also live inside a connection string):

openssl rand -base64 32 | tr -d '/+=' | cut -c1-32
StackKeys to add (staging shown; production uses prod-<slug>)passwordField for step 5
Prisma / NestJS / SvelteKitDATABASE_PASSWORD=<pw> and DATABASE_URL=mysql://stg-<slug>:<pw>@persona-mysql-haproxy.percona-pxc-operator.svc.cluster.local:3306/stg-<slug>DATABASE_PASSWORD (default)
Craft CMSCRAFT_DB_SERVER=persona-mysql-haproxy.percona-pxc-operator.svc.cluster.local, CRAFT_DB_PORT=3306, CRAFT_DB_DATABASE=stg-<slug>, CRAFT_DB_USER=stg-<slug>, CRAFT_DB_PASSWORD=<pw>CRAFT_DB_PASSWORD
api-simpleDB_HOST=persona-mysql-haproxy.percona-pxc-operator.svc.cluster.local, DB_NAME=stg-<slug>, DB_USER=stg-<slug>, DB_PASSWORD=<pw>DB_PASSWORD
SymfonyDATABASE_PASSWORD=<pw> and DATABASE_URL=mysql://stg-<slug>:<pw>@persona-mysql-haproxy.percona-pxc-operator.svc.cluster.local:3306/stg-<slug>?serverVersion=8.0DATABASE_PASSWORD

If your slug is long, prod-<slug> must still be at most 32 characters. If it isn't, pick a shorter user name and use it here instead of <slug>; you will pass the same name as parameters.user in step 5.

Redis (only if you enable it in step 5)

redis-service:6379 inside the namespace, no password. Craft: REDIS_SERVER=redis-service, REDIS_PORT=6379. Node: REDIS_URL=redis://redis-service:6379.

Things you do not need

PORT, HOST, NODE_ENV: the image and chart already handle these. Don't add them unless the app insists.

2.5 Wire up local development

Log in once on your laptop (EU region):

infisical login --domain https://eu.infisical.com

In the project repo, link it to the Infisical project and commit the result:

infisical init # pick the org, then the project you just created
git add .infisical.json

.infisical.json looks like this (one per repo, workspaceId differs):

{
"workspaceId": "aa0b7239-8066-4094-9ce6-b062bd681b1d",
"defaultEnvironment": "dev",
"gitBranchToEnvironmentMapping": null
}

Then make ddev pull a fresh .env on every start. In .ddev/config.yaml:

hooks:
post-start:
- exec-host: 'infisical export --env=dev > .env'

exec-host runs on your Mac with your own Infisical login, so nobody has to share credentials. Fill the dev environment with local values (ddev's database is db/db/db on host db).

Let Claude Code do it

Phase 2 of the project prompt from the prompt generator produces the secrets table and the infisical secrets set commands, and wires up ddev. Create the Infisical project and add CI/CD first; the prompt can't do that for you.

Done when

  • Infisical project exists and its slug is exactly <slug>
  • Environments staging and prod exist
  • Machine identity CI/CD has access (Viewer)
  • Every key from .env.example exists in staging and prod; DB password key name noted for step 5
  • .infisical.json committed, .env ignored, ddev start produces a working .env