← Holocron Logs

One Secrets Manager, Three Injection Patterns

Standing up Infisical across four services meant three completely different ways to get a secret from vault to process, and a fleet sweep that found a live credential nobody knew was sitting on disk.

Why this matters beyond the homelab: Every service on the fleet had its own answer to “where does this credential live,” and most of those answers were “in a plaintext .env file, hopefully backed up.” Consolidating that into one secrets manager is straightforward in theory. In practice, three services needed three different ways to actually get a secret out of vault and into a running process, and none of the three could reuse another’s approach.


The setup

Infisical went up self-hosted at secrets.tima.dev, running on an LXC on Node-B. Four projects: k2so, jocasta, outline, and infra, each with a single production environment. That environment slug matters more than it should: it has to be lowercase production, not Production, and deleting the default Dev/Staging environments in the UI also deletes Production along with them. Recreating it explicitly was a five-minute detour the first time and a non-issue every time after.

Four machine identities, one per host, each scoped to only the secrets that host needs.

Three patterns, three services

K-2SO was the simplest case. It’s a long-running Node.js process under PM2, so infisical run -- node index.js wraps the start command and injects all 15 secrets into the process environment at launch. No code changes, no file on disk.

Jocasta couldn’t use the same trick because the Hermes framework reads from a .env file directly rather than process.env. The fix was a systemd ExecStartPre hook that regenerates /root/.hermes/.env fresh on every service start, pulling all 26 secrets from Infisical each time. The one thing worth flagging: the Infisical login command echoes its own token to stdout by default, which would otherwise land straight in the systemd journal. Redirecting that specific line to /dev/null closed the leak before it opened.

Outline runs as a Docker Compose stack, which can’t do runtime injection the way PM2 or a systemd hook can. The pattern here is export, bring the stack up, then shred: write a temporary .env, run docker compose up -d, and delete the file within seconds of the containers reading it. Confirmed the file was gone before the containers had even finished their health checks.

What the sweep found

Closing out the migration meant a fleet-wide grep for any remaining .env files outside Infisical’s scope. Most of it was expected cleanup. One result wasn’t: a three-week-old Jocasta state snapshot at /root/.hermes/state-snapshots/20260805-010042-pre-update/.env, holding a full set of live credentials from before the migration even started. It was a pre-update backup doing exactly what backups do, just with the wrong contents. Shredded on discovery.

The CLI gotcha

One authentication detail cost more time than it should have: relying on exported environment variables alone (INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET) causes the CLI to intermittently fall through to an interactive browser login instead of authenticating as the machine identity. Passing --client-id and --client-secret explicitly on every call fixed it for good. If a script that authenticated fine yesterday suddenly prompts for a browser, that’s the first thing to check.

The lesson

There’s no single right way to get a secret into a process. The right way is whatever the runtime already gives you a hook for, and finding that hook for three services meant three different answers. The snapshot leak is the more useful lesson, though: migrating secrets in place doesn’t retroactively clean up the copies your own tooling made of the old ones. A secrets migration isn’t done until you’ve swept for the backups of what you were migrating away from.

← Back to Holocron Logs