Skip to content

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 the REDHOUND_-prefixed keys in .env.example one-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.
  • .env is a local fallback, not the thing you share: stop passing hand-copied .env files between teammates — pull from Infisical instead. A local .env still works as an offline fallback (and Docker Compose still reads it via env_file), but it should hold only what Infisical can't own (your Infisical bootstrap credentials) or be regenerated on demand with infisical export (see CI and production).

One-time setup (per developer)

  1. Install the CLI (macOS):
brew install infisical/get-cli/infisical

Other platforms: https://infisical.com/docs/cli/overview.

  1. Log in and link the repomake infisical-login runs the interactive browser login (stores a token in your OS keychain) and infisical init if the repo isn't linked yet. init generates .infisical.json (project + default-environment mapping, no secrets — safe to commit). The raw commands, if you prefer:
infisical login    # browser login
infisical init     # link repo (once)

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; run infisical login to 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:

  1. Renders Infisical prod (infisical export --env=prod --format=dotenv) using your own Infisical session — no Infisical credential is stored on the host.
  2. Guards the render: aborts (host .env untouched) unless every required key is present and non-empty (PROD_ENV_REQUIRE_KEYS — all the compose ${VAR:?} guards except the host-managed IMAGE_TAG). This stops a half-populated env from shipping.
  3. Merges onto the host: Infisical is authoritative for every key it provides; host-managed keys in PROD_ENV_PRESERVE_KEYS (default IMAGE_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 prod environment first. There is no prod environment yet — until one exists and holds every key compose needs (secrets and stable config like PUBLIC_HOSTNAME / ACME_EMAIL), make prod-secrets aborts by design with the host .env untouched. The only key kept host-side is IMAGE_TAG; any other host-only key would be dropped by the merge unless added to PROD_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 Secret objects continuously. Pods read normal Secrets — no .env, no make 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.