OpenID Connect for Cloud Deployments

Most pipelines that deploy to a cloud provider used to store a long-lived access key or service account key as a repository secret. That key worked forever, in every workflow run, until someone remembered to rotate it. OpenID Connect (OIDC) removes that liability: GitHub Actions requests a short-lived, cryptographically signed identity token for the running job, your cloud provider verifies that token against a trust policy, and hands back credentials that expire in about an hour. No cloud secret ever sits in your repository.

This lesson assumes you already know how to write a basic workflow and use secrets. It focuses on how OIDC federation works, how to configure the workflow and the cloud side correctly, and the mistakes that turn a well-intentioned OIDC setup into an unscoped credential-issuing machine.

Overview / How it works

OIDC federation for deployments involves three parties: GitHub’s OIDC provider, your cloud provider’s identity service, and your workflow. The flow is always the same shape, regardless of cloud:

  1. Your job requests an ID token from GitHub’s OIDC token endpoint. This requires the id-token: write permission — it is not granted by default.
  2. GitHub issues a signed JSON Web Token (JWT). The token’s claims describe the run: repository, ref, workflow, environment, actor, and a sub (subject) claim that combines several of these, for example repo:example-org/payments-service:ref:refs/heads/main.
  3. A cloud-specific action (aws-actions/configure-aws-credentials, azure/login, or google-github-actions/auth) sends that JWT to the cloud provider’s token exchange endpoint, along with the identifier of an IAM role, service principal, or service account you want to assume.
  4. The cloud provider validates the JWT’s signature against GitHub’s published OIDC discovery document, then checks the JWT’s claims against a trust policy you configured ahead of time. If the claims match, it issues short-lived cloud credentials scoped to that one role.

Nothing about this requires a stored secret on the GitHub side. The trust relationship lives entirely in cloud-side configuration: an IAM identity provider plus a role trust policy in AWS, a federated credential on an app registration in Azure, or a workload identity pool and provider in GCP. Because the credentials are minted per run and expire quickly, a leaked build log or a compromised runner has a much smaller blast radius than a static access key.

Syntax or workflow structure

Every OIDC-based deploy job needs three things: the permission to request a token, a pinned action that performs the exchange, and a role or identity reference that matches what you configured on the cloud side.

The permissions block is the part most tutorials skip and most real workflows get wrong:

permissions:
  id-token: write   # required to request the OIDC token
  contents: read    # only what the job actually needs to read the repo

Set this at the workflow level if every job needs it, or scope it to the single job that deploys, leaving other jobs (linting, unit tests) with no elevated permissions at all. Pin the cloud action to a release tag or, for anything touching production credentials, to a commit SHA. A tag can be moved by anyone with write access to that action’s repository; a SHA cannot. The trade-off is that SHA-pinned actions do not receive automatic security patches, so you take on the job of watching for and manually bumping updates.

Examples

Example 1 — AWS deploy via an assumed role. The job authenticates once, then every subsequent AWS CLI or SDK call in that job uses the resulting session credentials automatically.

name: Deploy to AWS

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-payments-deploy
          aws-region: us-east-1

      - name: Sync build output to S3
        run: aws s3 sync ./dist s3://example-app-static-assets --delete

Expected behavior: the configure-aws-credentials step exchanges the run’s OIDC token for a session that expires in roughly one hour, exports it as environment variables for later steps, and the deploy runs without any AWS secret ever appearing in the workflow file.

Example 2 — Azure login with a federated credential. Azure uses the same JWT exchange, but the identity is a federated credential attached to an app registration rather than an IAM role.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Azure login via OIDC
        uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebafae4a9 # v2.1.1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Azure Web App
        run: az webapp deploy --name example-app --resource-group example-rg --src-path ./dist

Note that the client, tenant, and subscription IDs are not secrets in the credential-leak sense — they identify which app registration to use, not a password — but storing them as repository secrets keeps them out of a public workflow file and easy to rotate per environment.

Example 3 — GCP Workload Identity Federation. GCP calls its version workload identity federation; a pool and provider replace the IAM identity provider you configure in AWS.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.6
        with:
          workload_identity_provider: projects/111111111111/locations/global/workloadIdentityPools/gha-pool/providers/gha-provider
          service_account: gha-deployer@example-project.iam.gserviceaccount.com

      - name: Deploy to Cloud Run
        run: gcloud run deploy example-app --image us-docker.pkg.dev/example-project/app/image:latest --region us-central1

Expected behavior for all three: the deploy step only succeeds if the run’s claims (repository, branch or environment, and in Azure/GCP’s case the workflow path) match what was configured in the trust policy or federated credential. Any other repository or branch attempting to use the same role or service account is rejected before any cloud credential is ever issued.

Step by step

  1. In the cloud console, register GitHub’s OIDC provider once per account or project. For AWS this is an IAM identity provider with issuer URL https://token.actions.githubusercontent.com and audience sts.amazonaws.com.
  2. Create the role, service principal, or service account the workflow will assume, and attach the minimum IAM policy it needs — write access to one S3 bucket or one Cloud Run service, not account-wide admin.
  3. Write a trust policy or federated credential that restricts the sub claim to your exact repository and branch or environment, and, for AWS, also checks the aud claim.
  4. In the workflow, add permissions: id-token: write at the workflow or job level.
  5. Add the cloud login action pinned to a tag or SHA, referencing the role or identity created in step 2.
  6. Gate the deploy job behind a GitHub Environment with required reviewers for production, so credential issuance for the production role also requires human approval.
  7. Run the workflow from the exact branch or environment the trust policy allows, and confirm the deploy step succeeds; then try running it from a different branch and confirm the cloud provider rejects the exchange.

Common Mistakes

Mistake 1: forgetting the id-token: write permission. Without it, GitHub never issues a token to request, and the cloud action fails immediately with no credentials to exchange.

Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers

Correction: add the permission explicitly. Default workflow permissions do not include id-token: write even when the repository’s default token permissions are set to “read and write,” because it is treated as a separate, higher-sensitivity grant.

permissions:
  id-token: write
  contents: read

Mistake 2: a trust policy scoped too broadly. GitHub’s OIDC issuer is shared by every public and private repository on GitHub. If your condition on the sub claim uses a wildcard that matches more than your own repository, or you omit the audience check entirely, any workflow anywhere that knows your role’s ARN can attempt to assume it.

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:*"
    }
  }
}

Correction: pin both the audience and a specific repository and ref (or environment) in the subject claim.

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
    },
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:example-org/payments-service:ref:refs/heads/main"
    }
  }
}

Best Practices

  • Scope every trust policy to one repository and one branch or GitHub Environment name; never accept a wildcard across an entire organization.
  • Create a separate role or service account per environment (staging, production) with its own least-privilege policy, so a compromised staging deploy cannot touch production resources.
  • Put production deploy jobs behind a protected GitHub Environment with required reviewers, so credential issuance also requires human sign-off, not just a matching claim.
  • Never trigger an OIDC-authenticated deploy job from pull_request_target on untrusted fork input, and never run privileged deploy jobs on self-hosted runners that also build code from forks — both let attacker-controlled code run with access to real cloud credentials.
  • Prefer OIDC over storing any cloud access key or service account key as a repository secret; if a provider or workflow still needs one, treat it as a stopgap and migrate it.
  • Audit trust policies and federated credentials periodically; a role’s trust policy is a credential in itself and deserves the same review as an IAM policy.

Practice Exercises

  1. Register your cloud provider’s OIDC trust with GitHub’s issuer, create a role scoped to a single test repository and branch, and write a workflow that assumes it. Confirm the deploy step succeeds only from that exact branch.
  2. Deliberately broaden the trust policy’s subject condition to a wildcard, rerun the workflow from a different branch, and confirm the exchange now succeeds. Then revert the policy and confirm it is rejected again, to see the risk directly.
  3. Move your deploy job behind a GitHub Environment with a required reviewer, and verify the OIDC token exchange only happens after the approval is granted.

Summary

OIDC replaces stored cloud secrets with a per-run identity exchange: GitHub issues a short-lived signed token, your cloud provider checks it against a trust policy you control, and hands back credentials that expire in about an hour. The workflow side needs the id-token: write permission and a pinned login action; the cloud side needs a trust policy scoped tightly to one repository and branch or environment, with an audience check where the provider supports one. The two failure modes to watch for are forgetting the permission, which simply breaks the deploy, and scoping the trust policy too broadly, which quietly lets other repositories assume your role. Pair OIDC with protected environments and least-privilege IAM policies, and a stolen build log stops being a path to your cloud account.