Infisical machine identity authenticating a CLI and injecting a secret into a subprocess at runtime

Every secret has to live somewhere. The only real questions are where, and for how long.

I recently wired up automated pushes to this blog’s repo — a coding agent that commits and pushes on my behalf (more on why I trust an agent with that another time). That meant a GitHub Personal Access Token had to be available to a git push command running on my machine. What it did not mean was scattering that PAT across .env files and shell profiles. I wanted it managed: stored once, fetched at runtime, scoped, and revocable on demand.

Infisical is an open-source secrets management platform — a central vault for API keys, tokens, and credentials, fronted by a CLI, SDKs, and CI/Kubernetes integrations. Think of it as the open-source counterpart to HashiCorp Vault or AWS Secrets Manager: run it on Infisical’s managed cloud (US or EU region) or self-host it. The source is on GitHub and the docs are genuinely good — both worth a look if you want to follow along.

Here’s how I solved it with Infisical and a machine identity: the PAT never on disk, non-interactive auth, short-lived credentials, and every secret scoped and revocable from one place.

What a secrets manager actually buys you

The reflex is to drop the PAT in a .env file and move on. But .env files are where secrets go to leak: they get committed by accident, copied to a second laptop, pasted into a Slack thread “just for a sec,” and they never rotate. Worse, every service that needs the secret keeps its own copy — so there’s no single place to revoke it.

Infisical inverts that. The secret lives in one managed vault and is fetched at runtime, never written into your project:

  • Runtime injectioninfisical run pulls secrets into a single subprocess as environment variables and nowhere else. The value never lands in a file you edit or a repo you commit.
  • Environments and projects — the same key resolves to a different value per --env (dev / staging / prod), so there’s nothing to hand-edit when you promote.
  • Scoped access — machine identities and access policies decide who can read which secrets, so one leaked credential never exposes the whole vault.
  • Rotation and audit — rotate a secret in one place and every consumer picks up the new value on its next run; every read is logged.

I’ve written before about why a real secrets architecture beats scattered plaintext credentials. This is that same principle applied to the smallest possible case — one token, one machine — without giving any of it up.

The rule I wanted to hold: the real secret never touches the repo or a file I edit. One minimal bootstrap credential does end up on disk, as we’ll see — but a scoped, rotatable, revocable key that only fetches secrets is a very different risk from the PAT itself sitting in a .env.

First cut: interactive login

Infisical’s CLI makes the runtime-injection part trivial. You log in once, then wrap any command with infisical run and your secrets arrive as environment variables in that subprocess — and nowhere else:

# Authenticate (opens a browser / stores a session)
infisical login

# Inject GH_PAT into one subprocess, use it, done
infisical run --env=dev -- \
  bash -c 'git push "https://hickepicke:${GH_PAT}@github.com/hickepicke/blog.git" master'

The ${GH_PAT} is expanded by bash inside the Infisical-spawned subprocess. It’s never printed, never written to a file, and the shell history only stores the literal ${GH_PAT}. That’s exactly the property I wanted.

There’s just one problem: interactive login expires. Every few days the session lapses and the next push dies with No valid login session found. Fine when I’m at the keyboard — useless for automation that’s supposed to run without me.

Machine identities: auth without a human

This is what machine identities are for. A machine identity is the secrets-manager equivalent of an IAM role — an entity that represents a workload, not a person, and authenticates non-interactively to get a short-lived access token.

(Worth noting: Infisical’s older Service Tokens do a similar job, but they’re deprecated. Don’t build on them.)

The simplest auth method is Universal Auth: the identity gets a Client ID and a Client Secret, and exchanges them for an access token with a configurable TTL. The flow looks like this:

sequenceDiagram
    participant CLI as Local CLI
    participant Inf as Infisical
    participant GH as GitHub
    CLI->>Inf: login (client-id + client-secret)
    Inf-->>CLI: short-lived access token
    CLI->>Inf: run --env=dev (with token)
    Inf-->>CLI: inject GH_PAT into subprocess
    CLI->>GH: git push (https://user:${GH_PAT}@...)
    GH-->>CLI: refs updated

The Client Secret is long-lived and static — that’s the one credential at rest. But the token it mints is short-lived, the identity is scoped with least privilege, and there’s no human session to expire. For unattended automation, that’s the right trade.

Wiring it up

1. Store the credentials outside the repo. I keep the Client ID and Secret in a 600-mode file in my home directory — never in the working tree, so it can’t be git add-ed by accident. The Infisical CLI reads these exact variable names:

# ~/.infisical/blog-m2m.env  (chmod 600)
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=<client-id>
INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=<client-secret>

Create it without leaking the secret into shell history — make the file first, then paste into an editor:

install -m 600 /dev/null ~/.infisical/blog-m2m.env
$EDITOR ~/.infisical/blog-m2m.env

2. Mint a token and inject the secret. Source the file, log in as the identity, and the access token lands in INFISICAL_TOKEN, which every subsequent CLI command picks up automatically:

set -a; . ~/.infisical/blog-m2m.env; set +a

export INFISICAL_TOKEN=$(infisical login --method=universal-auth \
  --client-id="$INFISICAL_UNIVERSAL_AUTH_CLIENT_ID" \
  --client-secret="$INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET" \
  --silent --plain)

infisical run --projectId="<project-id>" --env=dev -- \
  bash -c 'git push "https://hickepicke:${GH_PAT}@github.com/hickepicke/blog.git" master'

That’s the whole thing. No interactive prompt, no token on disk except the scoped client secret, and the PAT exists only inside the push subprocess.

Things I ran into

Three gotchas turned a five-minute setup into an hour. All of them surface as the same unhelpful 401 Invalid credentials, so here they are explicitly.

Wrong region. Infisical runs separate US and EU instances, and the CLI defaults to the US one (app.infisical.com). My account is on EU, so every login 401’d until I pointed it at the right domain:

infisical login --method=universal-auth ... --domain="https://eu.infisical.com/api"
infisical run --domain="https://eu.infisical.com/api" ...

Machine identities don’t read --projectId from .infisical.json. An interactive user session picks up the project from the repo’s config file; a machine identity does not. Omit it and you get Project ID is required when using machine identity. Pass --projectId explicitly.

Trusted IPs silently reject you. Universal Auth has a Client Secret Trusted IPs setting. If it’s anything narrower than 0.0.0.0/0 and your machine’s egress IP doesn’t match — which, on a home connection with a dynamic IP, it often won’t — the login 401s with no hint that IP is the cause. For a personal box on a dynamic address, 0.0.0.0/0 is the pragmatic setting; the secret-at-rest plus a short token TTL are the real controls, not the IP allowlist.

Rotation and auth hygiene

A machine identity isn’t fire-and-forget. The things I keep an eye on:

  • Client secret rotation. The Client Secret is the one standing credential. Rotate it on a schedule (and immediately if a laptop is lost): generate a new one in the UI, paste it into the 600 file, done. You can have two valid secrets briefly, so rotation is zero-downtime.
  • Access token TTL. Keep it short. The token is minted fresh on every run, so a tight TTL costs you nothing and shrinks the window a leaked token is useful.
  • Least-privilege scope. This identity can read exactly one secret in one environment. If the file leaks, the blast radius is a single GitHub PAT — itself scoped to one repo with a 30-day expiry — not my whole vault.
  • Expiry awareness. If you set a TTL on the client secret itself, write down the date. When it lapses, every login 401s and you’ll waste time blaming the IP allowlist again.

This is the same lifecycle thinking from secrets management at scale, just sized for one credential: scope it tightly, rotate it, keep the live window short, and never let the value rest anywhere you didn’t choose deliberately.

Summary

Pasting the PAT into a .env would have worked too — and left a static, unrotated, broadly-scoped secret sitting in a directory I open every day. Infisical plus a machine identity gives me the same convenience — non-interactive, scriptable auth — with the secret kept somewhere I can actually manage it:

  • The PAT lives in Infisical, fetched just-in-time and injected into a single subprocess.
  • The only credential at rest is a tightly-scoped, rotatable client secret in a 600 file outside the repo.
  • Access tokens are short-lived and minted per run.

It’s Zero Trust thinking applied to the smallest unit of work: don’t trust a credential because it’s convenient — trust it because it’s scoped, short-lived, and accounted for.