Secrets Management (Infisical)¶
The team's single source of truth for secrets is an Infisical
Cloud project. Instead of passing .env files around, you authenticate to Infisical
once and let it inject secrets into your local processes at startup.
This is default-on when you're set up for it. The make run targets auto-detect
Infisical: if the CLI is installed and you have a login session, secrets are injected
automatically — no flag needed. If the CLI is absent or you're not logged in, the targets
fall back transparently to a local .env / the ambient environment exactly as before
(see Configuration Reference). Force the behavior either way with
INFISICAL=1 (always inject) or INFISICAL=0 (always use local .env, e.g. offline).
The app code is unchanged: Config.from_env() in backend/config/base.py reads
REDHOUND_-prefixed environment variables regardless of where they came from.
How it fits together¶
- Source of truth: an Infisical Cloud project with three environments —
dev,staging,prod. Secret names match theREDHOUND_-prefixed keys in.env.exampleone-to-one. - Injection:
infisical run --env=<env> -- <command>fetches the secrets for that environment and exports them into the process environment. Pydantic picks them up with no code change. .envis a local fallback, not the thing you share: stop passing hand-copied.envfiles between teammates — pull from Infisical instead. A local.envstill works as an offline fallback (and Docker Compose still reads it viaenv_file), but it should hold only what Infisical can't own (your Infisical bootstrap credentials) or be regenerated on demand withinfisical export(see CI and production).
One-time setup (per developer)¶
- Install the CLI (macOS):
Other platforms: https://infisical.com/docs/cli/overview.
- Log in and link the repo —
make infisical-loginruns the interactive browser login (stores a token in your OS keychain) andinfisical initif the repo isn't linked yet.initgenerates.infisical.json(project + default-environment mapping, no secrets — safe to commit). The raw commands, if you prefer:
That's it. You never need a hand-copied .env again.
Daily use¶
Once you've run infisical login and infisical init, injection is automatic — just run
the targets as usual:
make dev # backend + frontend, 'dev' secrets injected
make run-backend # backend only
make run-frontend
make run-backend INFISICAL_ENV=staging # pick a different environment
INFISICAL_ENV defaults to dev. Any other target that ends in uv run … can take the
same treatment by prefixing it with infisical run --env=dev --.
Override the auto-detection when you need to:
make dev INFISICAL=0 # offline: ignore Infisical, use local .env
make dev INFISICAL=1 # force injection (errors if not set up)
Note: auto-detection checks that the CLI is installed and that you have a usable auth context (a cheap, local check — no network call). A usable context is either an interactive browser session (
infisical login) or a non-interactive credential in the environment —INFISICAL_TOKEN,INFISICAL_API_KEY, or a universal-auth machine identity (INFISICAL_CLIENT_ID+INFISICAL_CLIENT_SECRET), as used in CI/prod. If your browser session has expired, the CLI will prompt you to re-authenticate on the next inject; runinfisical loginto refresh.
Production (render-at-deploy)¶
Prod is a single-host docker-compose deploy. The model is render-at-deploy: at deploy
time the operator renders the prod environment from Infisical into a dotenv and the host's
/opt/redhound/.env is updated from it. Infisical is touched once, during deploy — the
host stays self-sufficient on reboot / docker compose up even if Infisical is unreachable.
The running stack never talks to Infisical.
make prod-secrets DEPLOY_HOST=deploy@host # render prod secrets + push merged .env
make deploy DEPLOY_HOST=deploy@host # roll out the image (separate step)
What make prod-secrets does:
- Renders Infisical
prod(infisical export --env=prod --format=dotenv) using your own Infisical session — no Infisical credential is stored on the host. - Guards the render: aborts (host
.envuntouched) unless every required key is present and non-empty (PROD_ENV_REQUIRE_KEYS— all the compose${VAR:?}guards except the host-managedIMAGE_TAG). This stops a half-populated env from shipping. - Merges onto the host: Infisical is authoritative for every key it provides;
host-managed keys in
PROD_ENV_PRESERVE_KEYS(defaultIMAGE_TAG, the per-deploy image pin) are preserved from the existing.env. A timestamped backup is kept and the write is atomic (scripts/merge_dotenv.sh).
Create and populate the Infisical
prodenvironment first. There is noprodenvironment yet — until one exists and holds every key compose needs (secrets and stable config likePUBLIC_HOSTNAME/ACME_EMAIL),make prod-secretsaborts by design with the host.envuntouched. The only key kept host-side isIMAGE_TAG; any other host-only key would be dropped by the merge unless added toPROD_ENV_PRESERVE_KEYS.
Auth for non-interactive runners (machine identity) when a human isn't driving:
export INFISICAL_CLIENT_ID=$ID
export INFISICAL_CLIENT_SECRET=$SECRET # auto-detected by the make targets
GitHub Actions / CI: CI does not deploy today, so its secret surface is small and
stays as native GitHub Secrets (CODECOV_TOKEN, …). Syncing those to Infisical buys little
here. If deploy ever moves into CI, authenticate the job to Infisical via GitHub OIDC
(no stored credential) and render there — prefer that over the Infisical→GitHub-Secrets sync,
which duplicates secrets and needs a re-sync on every rotation.
Future: Kubernetes¶
The render-at-deploy script is compose-era glue and is meant to be retired when the stack moves to K8s. The vault decision carries forward unchanged; only the last hop changes:
- Use the External Secrets Operator (Infisical provider) or the Infisical Secrets
Operator to sync Infisical → native K8s
Secretobjects continuously. Pods read normal Secrets — no.env, nomake prod-secrets. - Authenticate via the Kubernetes-native auth method so no bootstrap credential lives on the cluster (the pod's service-account token proves identity).
- Use Terraform to provision the plumbing (operator install, identity bindings,
SecretStore) — never read secret values through Terraform, or they land in TF state.
Because the app reads REDHOUND_-prefixed env vars regardless of source, the migration is
invisible above the delivery layer: same vault, same keys, same app code.
Plan¶
The Infisical Free tier (up to 5 members, 3 projects, 3 environments, machine identities) covers the team-vault use case. Secret versioning, point-in-time recovery, and audit logging are Pro-tier features — adopt them only if/when rotation and audit become requirements.
What is NOT in Infisical¶
The one irreducible bootstrap secret is the credential used to authenticate to
Infisical — your infisical login session, or the machine-identity token in CI/prod.
Infisical centralizes the other ~15 secrets; it cannot centralize the key to itself.