Building a Self-Hosted Secrets Platform With OIDC, Not Long-Lived Tokens

The problem with GitHub Secrets at scale

For a long time, secrets across my projects lived exactly where most people's do: in GitHub Secrets, one repository at a time. It works fine when you have one project. It starts to feel wrong once you have several. Every repository ends up with its own scattered set of credentials, there's no central place to see what has access to what, and revoking or rotating anything means hunting through each repository individually.

A conversation about how a larger platform team handled this properly stuck with me. They used HashiCorp Vault, with short-lived, tightly scoped credentials issued on demand rather than long-lived tokens sitting in CI configuration. I didn't have Vault, but I had a weekend, and I wanted to actually understand that model by building an equivalent myself, not just read about it.

Why Infisical

I chose Infisical as a self-hosted, open-source secrets manager. Conceptually, it maps closely onto Vault: projects instead of namespaces, machine identities instead of auth methods, scoped roles instead of policies. The vocabulary differs, but the underlying ideas, centralised secrets, scoped access, short-lived credentials, are the same discipline either way.

Solving the bootstrap problem

The first real design problem is one that's easy to overlook: Infisical can't hand out the credentials required to create itself. Something has to bootstrap the bootstrapper.

I solved this with a deliberately minimal encrypted Ansible Vault, committed to a private repository, containing only what's genuinely needed to stand Infisical up in the first place: its own database credentials, its encryption key, and the handful of infrastructure secrets (like DNS API access) required to deploy it. Everything else moved to Infisical itself once it existed. I also kept an independent disaster-recovery copy of that bootstrap material somewhere entirely separate from the repository, because a root of trust that only exists in one place isn't really a root of trust.

Moving from static tokens to OIDC

The part I'm most pleased with is how GitHub Actions authenticates to this whole system. Instead of a long-lived Infisical token sitting in GitHub Secrets (which would have just moved the original problem somewhere else), workflows authenticate using OpenID Connect. GitHub issues a short-lived, cryptographically signed identity token for the workflow run itself, and that token is exchanged with Infisical for scoped access, without any static secret ever needing to exist in GitHub at all.

I went a step further than a single shared identity per project. Each application gets three separate machine identities: one for pull request checks, one for builds, and one for deployments, each bound via OIDC claims to a specific event type, branch, and even the specific workflow file that's allowed to use it. A pull request identity simply cannot be used to trigger a production deployment, not because of a policy someone has to remember to enforce, but because the token exchange itself will reject the mismatch.

Least privilege, actually enforced

Every identity starts with no access at all. Access is then granted explicitly, project by project: a build identity might get read access to the application's own secrets and the shared container registry project, while only the deploy identity gets access to genuinely privileged things like DNS provider credentials. None of this is enforced by convention or documentation. It's enforced by Terraform, applied consistently, every time.

What this actually taught me

Building this wasn't really about Infisical specifically. It was about understanding, hands-on, why short-lived, narrowly scoped, cryptographically verified credentials are worth the extra setup over a static token that works everywhere forever. That's a lesson that transfers directly to any secrets platform, Infisical, Vault, or otherwise, and it's a much deeper understanding than I'd have gotten from reading a comparison article between the two.