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.
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: deployWhat 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
- How would you design a deployment pipeline that can be rolled back safely?hardAlso on ci-cd6 min
- What does least privilege look like in practice, and what is separation of duties there to prevent?mediumAlso on least-privilege6 min
- Which tests should run on every commit and which should run nightly, and how do you decide?mediumAlso on ci-cd6 min
- The monthly bill run dies two thirds of the way through and the cycle closes tomorrow. What do you do?hardSame kind of round: scenario5 min
- A corporate action is confirmed with an effective date in the past, after you have already struck positions and sent statements. How does your system cope?hardSame kind of round: design5 min
- A trade was booked with the wrong quantity and you find out after it has been confirmed, reported and sent for clearing. What has to happen?hardSame kind of round: scenario6 min
- A family plan shares 100 GB across five SIMs with each SIM capped at 30 GB. How does charging enforce both limits at once?hardSame kind of round: design6 min
- A fill arrives for an order your system has already marked cancelled. What do you do?hardSame kind of round: scenario5 min