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 =
stagingorprod
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>.
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
Project → Access Control → Machine Identities → Add 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, port3306
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
| Stack | Keys to add (staging shown; production uses prod-<slug>) | passwordField for step 5 |
|---|---|---|
| Prisma / NestJS / SvelteKit | DATABASE_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 CMS | CRAFT_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-simple | DB_HOST=persona-mysql-haproxy.percona-pxc-operator.svc.cluster.local, DB_NAME=stg-<slug>, DB_USER=stg-<slug>, DB_PASSWORD=<pw> | DB_PASSWORD |
| Symfony | DATABASE_PASSWORD=<pw> and DATABASE_URL=mysql://stg-<slug>:<pw>@persona-mysql-haproxy.percona-pxc-operator.svc.cluster.local:3306/stg-<slug>?serverVersion=8.0 | DATABASE_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
stagingandprodexist - Machine identity
CI/CDhas access (Viewer) - Every key from
.env.exampleexists instagingandprod; DB password key name noted for step 5 -
.infisical.jsoncommitted,.envignored,ddev startproduces a working.env