Skip to content
QSWEQB
hardDesignScenarioMidSeniorStaff

Your pipeline deploys using a long-lived cloud access key stored as a CI secret. What would you change and why?

Replace the stored key with OIDC federation so each job exchanges a short-lived signed token for temporary credentials, then make the trust policy specific to a repository, branch or environment. Also narrow the pipeline's own token, pin third-party steps to commit SHAs, and attest what you built.

4 min readUpdated 2026-07-26

What the interviewer is scoring

  • Does the candidate name what specifically is wrong with the stored key rather than saying secrets are risky
  • Whether they can describe the token exchange, including who verifies the signature and against what
  • That the trust policy is treated as the real security boundary, with a wildcard subject called out as the failure
  • Whether the pipeline's own token and third-party steps are recognised as a second credential path
  • Can they say what they would be able to prove after an incident, not only what they prevented

Answer

Name the actual defect

"Long-lived credential" is not a slogan; it describes four concrete properties, and being able to list them is what separates a security answer from a compliance answer. The key works from anywhere, so possession is sufficient and location proves nothing. It works indefinitely, so a copy taken today is still useful next year. It is visible to every job in the pipeline that can read that secret, which usually includes jobs that only needed to run tests. And rotating it is a coordinated change across the pipeline and the cloud account, which is why it never happens on the schedule the policy claims.

The blast radius is the sum of those. Anyone who can persuade the pipeline to print an environment variable, or who compromises a build step, walks away with production access that outlives the job by months.

Federated identity, issued per job

The replacement is to stop storing anything and let the job prove who it is at run time. The CI provider acts as an OIDC issuer: the job requests a short-lived JWT describing itself, and the cloud provider is configured to trust that issuer and to accept tokens whose claims match a specific identity. The cloud returns temporary credentials scoped to a role.

sequenceDiagram
    participant Job as CI job
    participant Issuer as OIDC issuer
    participant STS as Cloud token service
    participant Cloud as Cloud API
    Job->>Issuer: request token for this audience
    Issuer-->>Job: JWT with repo, ref and workflow claims
    Job->>STS: exchange the JWT for credentials
    STS->>Issuer: fetch signing keys, verify claims
    STS-->>Job: credentials valid for minutes
    Job->>Cloud: deploy

What to notice is that nothing durable ever crosses the boundary. The only persistent artefact is the trust policy sitting in the cloud account, and a token captured from a job log expires before it is useful.

In GitHub Actions the job opts in with permissions: id-token: write, and the platform exposes a request URL and bearer token in the environment; the standard cloud login actions do the exchange for you. AWS accepts the token through sts:AssumeRoleWithWebIdentity, Azure through workload identity federation on an app registration, GCP through Workload Identity Federation. The pattern is the same on GitLab and other providers because it is the same protocol.

permissions:
  contents: read
  id-token: write        # required for the token request; nothing else needs it
jobs:
  deploy:
    environment: production
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/deploy-app
          aws-region: eu-west-1

The trust policy is where this gets undone

Federation moves the security decision into the condition on the token's claims, so a sloppy condition gives away everything the stored key did and looks modern while doing it. The sub claim identifies the workflow context precisely — repo:org/app:ref:refs/heads/main, or repo:org/app:environment:production. A condition matching repo:org/app:* accepts any ref in that repository, which includes a branch anyone with write access can push and a tag they can create. If a job in that repository can be triggered on an arbitrary branch, the deploy role is reachable from an arbitrary branch.

Bind to a branch or, better, to a deployment environment, because an environment can additionally require reviewers and restrict which branches may use it. Always constrain the audience as well as the subject. And check for the same mistake on the other side: a role that any repository in the organisation can assume is a lateral-movement path from whichever repository has the weakest review.

The second credential path is the pipeline itself

Removing the cloud key leaves the token the CI platform issues for the repository. On GitHub that is GITHUB_TOKEN, and if the default is write it can push commits, alter workflows and publish packages. Set the organisation default to read-only and declare permissions per job, so the test job cannot do what the release job can.

Two specifics are worth knowing by name because they come up constantly. pull_request_target runs in the context of the base repository with access to its secrets, which is safe only as long as it does not check out and execute the contributor's code — and the reason the trigger exists at all is to do something with that pull request, so the temptation is built in. And actions/checkout writes its token into the local git config by default, meaning any later step, including a build script from a dependency, can read it; persist-credentials: false removes it when you do not need it.

Third-party steps run as you

Every action or template you reference executes inside your job with your permissions and your token. Referencing it by tag is trusting a mutable pointer: the tj-actions/changed-files compromise in March 2025 worked by repointing existing tags at malicious code, which then dumped secrets from CI logs across a very large number of repositories. Pin to a full commit SHA so the reference is immutable, and accept the cost, which is real — SHAs do not tell you what version you are on and need tooling to update. Vendoring the handful of steps you depend on most is a defensible alternative.

Reusable third parties are also the argument for ephemeral runners. A self-hosted runner reused between jobs carries whatever the previous job left behind, including credentials in the filesystem and processes still running.

What you can prove afterwards

The last piece is evidentiary rather than preventive, and it is the part senior candidates remember. Generate provenance for what you build — actions/attest-build-provenance produces a signed SLSA statement recording the source commit, the workflow and the builder, and gh attestation verify checks an artefact against it. Require that verification at the admission point, so an image nobody can trace to a pipeline run cannot be deployed. During an incident that turns "we think this came from main" into a signed statement, and it is what makes the difference between a scoped rollback and a full rebuild of every environment.

Likely follow-ups

  • Your trust policy matches repo:org/app:* and someone opens a pull request from a fork. What can they reach?
  • Why is pull_request_target dangerous, and what makes it different from pull_request?
  • You pin every action to a commit SHA. What have you given up, and how do you keep them current?
  • How would you give a deployment job write access to production while keeping the test job unable to reach it?

Related questions

Further reading

ci-cdoidcsupply-chain-securitysecretsleast-privilege