Deep Dive: Securing CI/CD Pipelines with Runtime Secrets Injection
Run env in one of your pipeline jobs and look at what comes back. Then check what's baked into your base images, and what's sitting in your CI variable store. Odds are a database password, a cloud key, or a registry token is staring back at you from all three places at once.
That's not a hygiene problem you can lint your way out of. It's an architecture problem. Every one of those secrets is a standing credential: long-lived, broadly scoped, copied into places you no longer fully control, and rotated roughly never. It exists before any job needs it and long after. That's the same disease that makes standing admin accounts so dangerous for humans — pointed at your build and deploy path instead.
This is a deep dive on the alternative: stop storing secrets with your workloads, and deliver them at runtime, only to a workload that has cryptographically proven who it is. Runtime injection, workload identity, and short-lived credentials — the pieces that make real CI/CD secrets management work.
Why the usual hiding places are broken
Environment variables. The moment a secret becomes an env var, you've lost track of where it goes. It's inherited by every child process. It's readable at /proc/<pid>/environ. It lands in crash dumps and gets shipped to your error tracker. One well-meaning debug step that echoes the environment, one verbose log line, and the secret is now in a log aggregator that half the org can query. Env vars are convenient precisely because everything can read them — which is exactly the problem.
Baked into images. A secret in an image layer is there forever. Layers are immutable and cached; docker history and a registry pull are all it takes to recover one. Anyone with pull access to that image has the secret, including anyone who compromises your registry or your layer cache. And rotation means rebuilding and redeploying every image that embedded it — so in practice, it doesn't get rotated.
Pipeline config and CI variables. These sprawl. Org-level variables end up visible to every repo and job that shouldn't need them. Anyone who can edit a pipeline can read or exfiltrate them, which turns your CI config into an attack surface: a malicious pull request adds one step that curls your secrets to an external endpoint, and on providers with pull_request_target-style triggers or fork builds, that step can run with full access before a human ever reviews it.
The through-line: all three store a secret at rest, next to the workload, hoping nobody reads it from the many places it now lives. Encryption at rest doesn't fix this, because the workload — and everything adjacent to it — needs the plaintext to actually use the secret.
Flip the model: deliver at runtime, not at rest
The principle behind runtime injection is simple to state and load-bearing to implement: the workload never holds a long-lived secret. It proves its identity, and in exchange receives exactly the credential it needs, for exactly as long as it needs it, delivered into memory at the moment of use.
Concretely, that means the secret is fetched over an authenticated API call, mounted as a short-lived projected file, or injected by a sidecar/agent — and it is never written into the image, the build environment, or the pipeline config. There's nothing to grep out of a log, nothing baked into a layer, nothing sitting in a variable store waiting to leak.
Which raises the one question every runtime injection design has to answer: how does the broker know this workload is allowed to receive this secret — without a pre-shared secret to bootstrap that trust? Hand the workload a bootstrap token and you've just recreated the standing-secret problem one level up. This is the classic secret-zero problem, and workload identity is how you solve it.
Workload identity and attestation (the JWT/OIDC part)
The insight is that the platform your workload already runs on can vouch for it — cryptographically, without you distributing anything.
In CI/CD: GitHub Actions, GitLab, and similar providers can mint a signed OIDC token (a JWT) for a running job on demand. That token carries verifiable claims about what is running: an issuer (iss), an audience (aud) you specify, and a subject (sub) like repo:your-org/your-app:ref:refs/heads/main, plus context such as the workflow, environment, and ref. Nothing pre-shared — the runner requests the token at runtime and the provider signs it.
In Kubernetes: projected service account tokens do the same job. They're audience-scoped, time-bound, bound to the pod, and signed by the cluster's identity provider — so a pod can present proof of its service account identity without you mounting a static credential.
Stronger still: SPIFFE/SPIRE performs node and workload attestation, verifying the actual process against a node agent before issuing an SVID (an X.509 or JWT identity document). That's the rigorous end of the spectrum when platform-level claims aren't enough.
The broker's job on the receiving end is where attestation happens. It fetches the issuer's public keys from its JWKS endpoint (published under the provider's /.well-known/openid-configuration), verifies the JWT signature, checks aud, iss, and expiry, and then matches the claims against policy: only the main branch of your-org/your-app, running in the prod environment, may mint production database credentials. You're binding the credential to a verified identity and context — not to a shared string that anyone who finds it can replay.
Rotation and short-lived credentials: shrink the blast radius
Once identity is attested, the credential you hand back should be dynamic and short-lived — an STS token, a per-session database credential, a scoped registry token — with a TTL measured in minutes.
This changes the economics of a leak. A leaked static key is an incident: you scramble to rotate it across every consumer, hoping you found them all before someone else did. A leaked ten-minute token is usually a non-event — it's expired before it's useful, and there's nothing standing to rotate because nothing long-lived was ever issued. Rotation stops being a quarterly project and becomes a property of the system.
You also get real DevSecOps telemetry as a side effect. Every credential mint is an auditable event tied to a specific, attested workload identity and a specific policy decision — who (which workload), what (which credential and scope), and why (which policy allowed it). That's a materially better audit trail than a spreadsheet of who-has-which-static-key, and it's the kind of signal a risk engine can actually reason about.
Reference architecture
Here's how the pieces fit together end to end.

The workload proves identity to the platform, presents that proof to the broker, and receives a short-lived credential minted on demand.
- Request identity token. The workload asks its platform (CI provider or Kubernetes) for a signed identity token.
- Signed JWT. The platform returns a short-lived JWT with verifiable claims — iss, aud, sub, and workload context.
- Present JWT. The workload presents the token to the RankEZ broker instead of any pre-shared secret.
- Short-lived credential. The broker validates the signature against the provider's JWKS, checks the claims against policy, risk-scores the request, and mints a scoped credential with a short TTL.
- Use and expire. The workload uses the credential against the target — database, cloud API, or registry — and it expires automatically. Nothing standing is left behind.
The critical detail lives in the footer of that diagram: no secret ever exists in the image, the build environment, or the pipeline config. Identity is proven; credentials are minted on demand and expire on their own.
Applying this without re-plumbing every pipeline
At RankEZ, this is the same principle we apply to human privileged access — zero standing privilege — pushed down to workloads. Your images, build environments, and CI config hold no standing secrets. Workloads prove identity using the platform you already run on (CI OIDC, Kubernetes service accounts, SPIFFE). Credentials are minted on demand, scoped to the task, short-lived, and every request is policy-checked and risk-scored before anything is issued.
For Kubernetes specifically, that means runtime injection into attested pods, dynamic secrets, and automatic expiry — so the credential a workload uses at 3 a.m. is as tightly scoped and short-lived as the one it uses at 3 p.m., and neither one is sitting in a manifest waiting to be stolen.
If you'd like to see what your pipelines look like with zero standing secrets, start a RankEZ trial for DevOps and Kubernetes secret management — and grep your next build for password to see the difference.
