On this page
  1. Why does CI concentrate non-human identities?
  2. Where do pipeline credentials leak?
  3. What should replace stored credentials?
  4. How do you detect a leak that already happened?
  5. Where AI coding assistants get this wrong
  6. Checklist
  7. FAQ
    1. Are GitHub Actions secrets encrypted?
    2. Can a masked secret still leak?
    3. Should the deploy job run on a self-hosted runner?
    4. How do I rotate a credential that several workflows use?
  8. Related topics
  9. Sources
concept

Non-Human Identities in CI/CD: Where They Leak

CI concentrates production credentials: repository secrets, deploy keys, cloud keys, and tokens in logs. Most leaks happen in the pipeline, not the app.

Quick answer

  • A CI pipeline usually holds more privileged credentials than the application it deploys, because it needs to build, push, and deploy.
  • The four common leak paths are repository secrets copied into forks or logs, static cloud keys, deploy keys with write access, and tokens printed during debugging.
  • Prefer platform-issued short-lived credentials over stored secrets: OIDC federation for cloud access, and scoped tokens for the registry.
  • Masking a secret in logs reduces accidental disclosure; it does not remove the secret from the environment or the artifact.
  • Treat a pull request from a fork as untrusted code and never expose production credentials to that job.

Why does CI concentrate non-human identities?

Because the pipeline sits at the intersection of source, artifacts, and production. It must read the repository, push an image to a registry, and deploy to an environment, so its credentials are broad by nature. A single workflow file can hold access to every environment the team operates.

That concentration is invisible in architecture diagrams. The application’s runtime identity is reviewed; the deploy job’s identity often is not. An attacker who can modify a workflow or read its secrets inherits the deploy path without touching the app.

Where do pipeline credentials leak?

Logs are the most common. A command that echoes an environment variable, a failing CLI that prints its configuration, or a test that dumps request headers writes the credential into a log that may be readable by everyone with repository access and retained for months. Masking helps only when the secret is registered with the masker and printed verbatim.

Fork pull requests are the second path. A workflow triggered by a fork can run attacker-controlled code with the base repository’s secrets if the configuration is careless. Third is artefacts and caches: a build that writes a credential into a tarball, a Docker layer, or a test report publishes it to everyone who can download the artefact.

# Separate untrusted pull-request work from privileged deploys.
on:
  pull_request:        # no secrets, no id-token
  push:
    branches: [main]   # deploy job only from main
permissions:
  contents: read
  id-token: write      # granted only to the deploy job
jobs:
  test:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
  deploy:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production

What should replace stored credentials?

For cloud access, OIDC federation: the job requests a short-lived token and exchanges it for a role scoped to the environment. For registry access, a scoped token with push rights only to the specific repository. For deployment targets, a platform identity or a short-lived certificate rather than a static SSH key with root access.

Where a static secret is unavoidable, put it in an environment-scoped secret store with an approval requirement, so it exists only for the job that needs it. A repository-wide secret is available to every workflow, including ones added later by someone who did not read the file.

How do you detect a leak that already happened?

Scan the full history, not the working tree, and scan build logs where the platform allows export. Look for provider-specific token formats and for high-entropy strings in artefacts. When you find one, revoke it before cleaning history: the credential is live until the provider invalidates it, regardless of what the repository now contains.

Then fix the path that produced it. A secret removed from a log line but still available to the same job will leak again. The durable fix is to remove the credential from the job’s environment, not to add another filter.

Where this bites vibecoders

A generated workflow often prints environment variables while debugging a deploy and leaves the debug step in place. It also tends to grant write permissions globally because a step failed without them. Both are identity problems: the first copies secrets into logs, the second gives every job more access than its task needs.

Where AI coding assistants get this wrong

  • Printing environment variables in a failing deploy step and committing the debug line.
  • Using repository-wide secrets instead of environment-scoped secrets with approvals.
  • Running privileged deploy jobs on pull requests from forks.
  • Granting contents: write to every job because one step needed it.
  • Baking credentials into a Docker image layer or build artefact.
  • Removing a leaked token from the repository without revoking it at the provider.

Checklist

  • Replace static cloud keys with OIDC federation.
  • Grant write permissions per job, not per workflow.
  • Scope deployment credentials to an environment with an approval gate.
  • Never run privileged jobs on untrusted pull requests.
  • Scan history and build logs for leaked tokens.
  • Revoke any found credential before cleaning up the repository.

FAQ

Are GitHub Actions secrets encrypted?

Yes, at rest and in transit, but they are decrypted into the job environment, where any step can read them and print them. Encryption protects the store, not the running job. That is why masking, scoping, and short-lived credentials matter more than the storage mechanism.

Can a masked secret still leak?

Yes. Masking matches exact registered values, so a transformed version — base64 encoded, split across lines, embedded in JSON, or partially printed — may not be masked. A secret can also leak through an artefact without passing through the log at all.

Should the deploy job run on a self-hosted runner?

Only if it cannot run on a hosted runner for a named reason, and only with ephemeral isolation. A persistent self-hosted runner retains workspace files, caches, and credentials between jobs, so one compromised job can affect the next. The deploy job is the worst place to accept that risk.

How do I rotate a credential that several workflows use?

Give each workflow its own credential first, then rotate them separately. A shared credential cannot be rotated without coordinating every consumer, which is why it is rarely rotated. Splitting it is the prerequisite for routine rotation, not an optimization.

Sources

Share: