Skip to content
(214) 444-7884
ITECS
DevOpsNovember 8, 202612 min read

Replace Long-Lived Cloud Keys in CI With OIDC

Swap stored cloud keys in CI for OIDC tokens that expire with the job. Scope the trust policy to repo and environment, test allow and deny, then delete the key.

Sequence diagram: a CI job requests a token and GitHub returns a signed JWT. The cloud role’s trust policy checks the sub claim, denies a subject that does not match with no credential, and otherwise returns a short-lived credential. A stored cloud key beside the flow is struck through.
How a CI job reaches the cloud without a stored key. The job requests a token, GitHub signs it, and the cloud role’s trust policy checks the sub claim before a short-lived credential returns. A subject that does not match gets no credential. A conceptual drawing of the flow GitHub documents for its OIDC provider.

Replace every cloud key stored in CI with OpenID Connect (OIDC) federation. The job asks the CI platform for a signed token, and the cloud trades it for a credential that expires with the job. No cloud secret sits in a CI variable for anyone to copy.

The switch moves the risk to a new place. A stored key was the thing to guard. After the move, the trust policy on the cloud role decides which jobs get in. Write it loosely and any branch can deploy to production. So the work is in writing that condition and proving it refuses the jobs it should.

The pattern applies wherever a pipeline signs in: the cloud account, the container registry, the Terraform state backend and AI provider APIs. The examples use GitHub Actions and AWS, with Azure Pipelines alongside. For AI-provider calls, the Claude Platform on AWS access guide is the worked example.

Why stored cloud keys are the first target in a CI breach

OWASP’s Top 10 CI/CD Security Risks lists insufficient credential hygiene as CICD-SEC-6. Its risk page calls credentials “the most sought-after object by adversaries.” A cloud key in a CI variable is that object, kept where many people and tools can reach it. The same page recommends temporary credentials over static ones.

GitHub’s secure-use reference sets the size of the exposure. Any user with write access to a repository has read access to all of its secrets. A compromised third-party action in the job gets them as well. In GitHub’s words, it “would have access to all secrets configured on your repository.”

CICD-SEC-2, inadequate identity and access management, covers the other half. OWASP warns that shared identities widen a credential’s footprint and make accountability hard during an investigation. One cloud key copied into ten repositories is a shared identity. OIDC gives each job a short-lived identity of its own.

How a job gets a token that lives for one job

The trust is set up once per cloud account. The account registers GitHub’s OIDC issuer, and a role states which workflows may assume it. After that, every run that needs the cloud makes four hops.

  1. The job requests a token.

    The job needs the id-token: write permission. GitHub says it only allows fetching the token and grants no write access to other resources.

  2. GitHub signs a JSON Web Token (JWT) for that job.

    Its claims name the repository, the ref, the environment when the job uses one, the workflow and whether the runner is GitHub-hosted or self-hosted.

  3. The job presents the token, and the cloud checks its claims against the role’s trust policy.

    On AWS the trust policy checks two claims: the audience, aud, which says who the token is for, and the subject, sub. A mismatch ends the request with no credential.

  4. The cloud returns a short-lived credential.

    GitHub describes it as “only valid for a single job, and then automatically expires.”

The sub claim carries the decision. GitHub builds it from the repository plus one qualifier, in this order of precedence. A job that references an environment gets repo:ORG/REPO:environment:NAME. A pull request run without an environment gets repo:ORG/REPO:pull_request. Any other run gets the branch or tag, as in repo:ORG/REPO:ref:refs/heads/main.

Check the subject format before writing any condition. GitHub’s OIDC reference says repositories created after July 15, 2026 use an immutable subject with owner and repository IDs, such as repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main. Older repositories keep the name-only format unless an admin opts in. A rename or transfer after that date moves a repository to the new format. GitHub Enterprise Server does not get it.

Scope the trust condition to one repository and one environment

GitHub’s AWS setup guide includes a trust policy that matches repo:octo-org/octo-repo:* with StringLike. GitHub explains that it allows “any branch, pull request merge branch, or environment” in the repository. That example is fine for proving the plumbing in a sandbox account.

Put the same wildcard on a production role and the role belongs to anyone who can push a branch. That is the same group that could read the old secret. A wildcard subject is a stored key with extra steps.

Write every other condition with StringEquals and the exact subject, and keep the audience check. For the official AWS action the audience is sts.amazonaws.com. The wildcard is the one StringLike case, and it stays in a sandbox.

"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud":
      "sts.amazonaws.com",
    "token.actions.githubusercontent.com:sub":
      "repo:ORG/REPO:environment:production"
  }
}
The condition of an AWS role trust policy for GitHub’s OIDC provider, following GitHub’s AWS guide, scoped to one repository’s production environment.

Then pick the subject by what the job does.

repo:ORG/REPO:*StringLike, sandbox only

MatchesAny branch, pull request merge branch or environment in the repository.

Use it forA sandbox account while you test the setup. Never a role that reaches production.

repo:ORG/REPO:ref:refs/heads/main

MatchesJobs running on main that name no environment.

Use it forBuild and publish steps after a merge, such as pushing an image to the registry.

repo:ORG/REPO:environment:production

MatchesJobs that reference the production environment.

Use it forDeploys, once the environment has protection rules.

repo:ORG/REPO:pull_request

MatchesPull request runs that name no environment.

Use it forA plan role that reads state and writes only the lock file, for a Terraform plan.

Subjects in GitHub’s name-only format. Repositories created after July 15, 2026 carry owner and repository IDs in the repo segment, as in repo:ORG@ID/REPO@ID:environment:production. Only the first row uses StringLike.

An environment subject is only as strong as the environment’s rules. GitHub’s environment guide says a workflow that references a missing environment creates it, with no protection rules. Anyone who can edit workflows can do that. Create and protect the environment before the trust policy names it.

Then set its deployment protection rules. Limit deployment branches to main or to release tags. Add required reviewers, turn on prevent self-review, and clear the option that lets administrators bypass the rules. A job that references the environment waits until those rules pass.

Check the GitHub plan before you rely on reviewers. On Free, Pro and Team plans, GitHub limits required reviewers and wait timers to public repositories. Private repositories on those plans still get deployment branch rules. Required reviewers on a private repository need GitHub Enterprise.

Adding environment: production to a deploy job breaks a working pipeline. It changes the job’s subject from the branch form to the environment form, so a trust policy written for the branch denies the job. Change the policy and the workflow together.

A team with many repositories may want one shared deploy workflow. GitHub can add job_workflow_ref to the subject through a customization template, so a role can require one approved reusable workflow. Create the matching cloud condition first. GitHub warns that tokens may be rejected if the condition is missing when the next job runs.

Azure Pipelines: federate the service connection

GitHub Actions jobs that deploy to Azure use the same subject strings, which GitHub’s reference lists for Azure beside AWS. Azure Pipelines works differently. Its cloud identity lives in an Azure Resource Manager service connection, and Microsoft recommends workload identity federation for it. Microsoft says federation “eliminates the need for secrets and secret management.”

Microsoft’s conversion tool moves an existing connection to federation in place when Azure DevOps created it and only one project uses it. A converted connection can be reverted within seven days.

In Azure Pipelines the scoping decision sits on the connection. Microsoft advises against granting every pipeline access to a connection and says to authorize each pipeline individually. Then add approvals and checks. Checks live outside the YAML, so a user who edits the pipeline file cannot change them.

A branch control check on the service connection limits it to named branches, written in full, such as refs/heads/main. It can also require that those branches have protection enabled. A manual approval on the production environment adds the human gate.

Microsoft retires the vstoken.dev.azure.com issuer on July 1, 2027, for federated connections in Azure public cloud. The change covers single-tenant app registrations and managed identities. New connections already use the Microsoft Entra issuer, and Azure DevOps flags the existing connections that need updating.

Terraform state: give the backend its own roles

Terraform signs in to its state backend separately from the deploy steps, so the backend needs the same move. HashiCorp’s s3 backend and azurerm backend pages give the settings and roles below.

s3

SettingLeave access_key out of the backend block. The backend reads the credentials the job’s OIDC role already set in the environment, or a token file through assume_role_with_web_identity.

RolesA plan role that reads state and writes only the lock file: list and get on the state, and get, put and delete on the .tflock file. The apply role adds put on the state file.

azurerm

SettingSet use_oidc and use_azuread_auth to true. On GitHub the backend finds the ID token itself. On Azure Pipelines, add oidc_azure_service_connection_id.

RolesStorage Blob Data Contributor on the state container, HashiCorp’s least-privilege recommendation. Access keys and SAS tokens are “Not recommended for new workloads.”

From HashiCorp’s s3 and azurerm backend pages. The lock-file permissions apply when use_lockfile is set to true.

Pass credentials through environment variables, as both pages recommend. Values passed with -backend-config or written into the configuration end up in the .terraform directory and in plan files. Keep write access to the state file for the apply job behind the protected environment.

Prove allow and deny before you delete the old key

Leave the old key in place but unused while you test. Write the expected result for each case first. Then change one thing at a time: the branch, the environment, the repository or the audience.

A test plan for GitHub Actions and AWS, run before the old key is deleted. Under each case is what a correct run shows.
Deploy job on main, production environment, rules passed
Expect Allow
A role session tied to this run. It is the control for every denial below.
The same workflow pushed from a feature branch
Expect Deny
Rejected at role assumption, with no credential issued.
The deploy job with its environment line removed
Expect Deny
The subject falls back to the branch form and no longer matches.
Another repository in the organization, same workflow file
Expect Deny
The repository segment of the subject differs.
A pull request run against the deploy role
Expect Deny
Pull request runs reach only the plan role, which reads state and writes only the lock file.
A token requested with a different audience
Expect Deny
The audience condition does not match.
An allowed job calling an action outside the role’s policy
Expect Deny
An authorization error from the service after a successful sign-in.
The old key deactivated, then one full release
Expect Pass
Every stage green with no stored cloud key in reach.

Count a denial only when an allow case passed in the same test run. Otherwise a typo in the role ARN can look like a working control. GitHub also advises reading the run logs after testing valid and invalid inputs, to confirm nothing sensitive was printed.

Deactivate the old key before you delete it. Delete it once a full release has run green without it, then remove the CI variable that held it.

What OIDC leaves in place

Over-scoped roles. The trust policy decides who may assume the role. The permission policy decides what the role can do. An administrator role behind a perfect subject condition is still an administrator in every job that matches.

Third-party actions. Every action in the deploy job runs with the credential in reach. GitHub’s strongest recommendation is a workflow dependency lockfile, which records the exact commit of every action, including transitive ones. Where a repository cannot adopt one, pin each action to a full-length commit SHA.

Dependabot creates no alerts for actions pinned to a SHA, so pair the pins with Dependabot version updates. GitHub also recommends setting the GITHUB_TOKEN default to read access on repository contents and raising it per job.

Self-hosted runners. GitHub-hosted runners run each job in a clean, isolated virtual machine. GitHub says self-hosted runners can be persistently compromised by untrusted code and “should almost never be used for public repositories.”

GitHub also asks whether a runner machine can reach sensitive services, such as cloud metadata services. If it can, the machine’s own cloud identity is open to every job it runs, whatever the trust policy says. Prefer just-in-time runners, which take at most one job, and keep production deploys on runners that pull requests cannot reach.

The token’s runner_environment claim records github-hosted or self-hosted. GitHub notes that custom claims are unavailable in AWS, so an AWS trust policy checks the audience and subject. Add runner_environment to the subject through GitHub’s subject customization template, and a production role can refuse self-hosted runners.

Build integrity. A valid token identifies the job that ran. Whether the artifact matches its source is a separate question, and SLSA’s build track addresses it. Build L3 requires a platform that stops runs from influencing one another and keeps provenance signing secrets away from user-defined build steps.

An illustrative rollout for twenty repositories

Illustrative scenario: a platform lead owns twenty repositories. They deploy to two AWS accounts through GitHub Actions and to one Azure subscription through Azure Pipelines, and Terraform state sits in S3. The order below is a planning example written from the steps above. It describes no client engagement and no measured result.

  1. Inventory every stored credential.

    List the cloud keys in repository, organization and environment secrets, Azure DevOps service connections and variable groups. Record which job uses each one and what it can reach.

  2. Register the issuer and settle the subject format.

    Add GitHub’s OIDC provider once per AWS account. Note which repositories already use the immutable subject, and decide whether older ones opt in.

  3. Move one low-risk service first.

    Pick a repository with a staging environment. Write its roles, run the allow and deny matrix, deactivate the old key and ship one full release.

  4. Move the Terraform state roles.

    Split the plan and apply roles for the infrastructure repositories. Every later deploy reads that state, so prove it before batching.

  5. Convert the Azure service connections.

    Use the conversion tool where a connection qualifies and recreate the rest. Test each connection’s branch control check and approval before the seven-day revert window closes.

  6. Batch the remaining repositories.

    Group them by deploy target. Each gets its own role per environment from the first repository’s template, and each batch re-runs the matrix.

  7. Delete the old keys and keep them gone.

    Delete the deactivated keys and the CI variables that held them. Secret scanning finds keys already in the Git history, and push protection blocks a push that contains a supported secret type. On private repositories both need GitHub Secret Protection.

The first repository produces the trust policy template and the test matrix. The other nineteen reuse both with a new subject.

ITECS runs this migration as part of CI/CD pipeline design and hardening, one of its DevOps services for software teams.

FAQ

OIDC in CI questions

Does OIDC remove the need for a secrets manager?

No. OIDC removes the stored cloud key from CI. Database passwords, third-party API keys and signing keys still need a home. HashiCorp Vault accepts the same GitHub token through its JWT auth method. GitHub’s Vault guide binds each Vault role to at least one claim, such as the repository. Jobs then fetch those secrets without a stored credential.

Can one role serve every repository?

It can, but every matching repository then holds the permissions of the most demanding one. Start with one role per repository and environment. For a shared deploy role, add job_workflow_ref to the subject and require one approved reusable workflow. GitHub notes that custom claims are unavailable in AWS, so an AWS role can check only what the subject and audience carry.

What breaks first when we switch?

The subject match. Repositories created after July 15, 2026, and older ones after a rename or transfer, use the immutable subject with owner and repository IDs, so a name-only condition denies them. A job without id-token: write cannot request the token at all. Run the allow and deny tests before you delete the old key.

If the keys span many repositories, start with a DevOps assessment, which ends in a written plan that sets the order of work. ITECS then runs the move under its CI/CD pipeline service. Learn about our DevOps service or start the no-cost intake.

Ready to see where AI moves your business forward?

1Send intake
2Scope the need
3Choose the next step

Share This Article

Send this guide to a colleague or save it for planning.

Sources And Trust Signals

This guide draws on the vendor and standards documentation below, each opened on November 8, 2026. The rollout section is an illustrative planning example.

The trust flow, the example token and the single-job credential lifetime. Opened November 8, 2026.

Claims, subject precedence, the immutable subject for repositories created after July 15, 2026, id-token: write and the subject customization template. Opened November 8, 2026.

The trust policy condition, the sts.amazonaws.com audience, the wildcard example, and the note that custom claims are unavailable in AWS. Opened November 8, 2026.

Vault’s JWT auth method and roles bound to at least one token claim. Opened November 8, 2026.

Protection rules, their availability by plan for private repositories, and environments created on first reference. Opened November 8, 2026.

Secret exposure to write access and third-party actions, dependency lockfiles, SHA pinning, Dependabot alerts, the read-only GITHUB_TOKEN default and self-hosted runners. Opened November 8, 2026.

Scanning of the full Git history for committed credentials, and availability on private repositories. Opened November 8, 2026.

Blocking a push that contains a supported secret before it reaches the repository. Opened November 8, 2026.

The ten CI/CD risk identifiers, CICD-SEC-1 to CICD-SEC-10. Opened November 8, 2026.

Credentials as the most sought-after object, and the recommendation to prefer temporary credentials. Opened November 8, 2026.

Shared identities, their footprint and accountability. Opened November 8, 2026.

Workload identity federation, conversion and its seven-day revert window, per-pipeline authorization and the July 1, 2027 issuer retirement. Opened November 8, 2026.

Checks kept outside YAML and owned by the resource, and branch control with fully qualified branch names. Opened November 8, 2026.

Credentials from the environment, web identity tokens, state and lock-file permissions and the -backend-config warning. Opened November 8, 2026.

OIDC settings for GitHub and Azure DevOps, the recommended data role and the access-key status. Opened November 8, 2026.

What Build L2 and Build L3 require of a build platform. Opened November 8, 2026.

The worked example for AI-provider calls from CI, with its full allow and deny matrix.

About The Author

The ITECS Team

ITECS' AI consulting, security, training, and DevOps team helps Dallas businesses adopt practical AI safely, backed by more than 24 years of IT operations experience.