Cutting the credential chain in cloud deployments

Application security tends to get the attention, but the hosting environment deserves equal scrutiny. Deploying to a cloud provider typically means authenticating with a service principal, proving authorization through role-based access control, and supplying resource metadata like subscription IDs or project names. Scale that across tens or hundreds of service principals — each ideally scoped by least privilege — and you have a sprawling inventory of passwords and certificates to manage.

The risk is real. Cryptographic failures rank #2 on the OWASP 2021 Top 10, and IBM's Cost of a Data Breach 2022 report found stolen or lost credentials were both the most common cause of breaches and the slowest to identify. Even with tools like GitHub Advanced Security secret scanning and push protection — which now covers custom patterns — detecting exposed secrets after the fact doesn't solve the underlying problem of maintaining and rotating credentials across a CI/CD pipeline.

From an SRE perspective, this maintenance burden qualifies as toil. Tools like Hashicorp Vault can organize and automate secret management, but the more elegant solution is eliminating the need for long-lived secrets altogether. GitHub Actions has supported OpenID Connect (OIDC) for cloud deployments since 2021, offering a path to passwordless authentication.

How OIDC replaces credentials

OIDC is an authentication protocol built on OAuth 2.0. Instead of exchanging a password, the workflow requests an ID token from GitHub — digitally signed and formatted as a JSON Web Token (JWT) — containing claims about the workflow run: repository, run number, actor, and similar context.

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "example-thumbprint",
  "kid": "example-key-id"
}
{
  "jti": "example-id",
  "sub": "repo:octo-org/octo-repo:environment:prod",
  "environment": "prod",
  "aud": "https://github.com/octo-org",
  "ref": "refs/heads/main",
  "sha": "example-sha",
  "repository": "octo-org/octo-repo",
  "repository_owner": "octo-org",
  "actor_id": "12",
  "repository_visibility": private,
  "repository_id": "74",
  "repository_owner_id": "65",
  "run_id": "example-run-id",
  "run_number": "10",
  "run_attempt": "2",
  "actor": "octocat",
  "workflow": "example-workflow",
  "head_ref": "",
  "base_ref": "",
  "event_name": "workflow_dispatch",
  "ref_type": "branch",
  "job_workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main",
  "iss": "https://token.actions.githubusercontent.com",
  "nbf": 1632492967,
  "exp": 1632493867,
  "iat": 1632493567
}

The cloud provider validates this token and trades it for a short-lived access token scoped to the workflow's needs. This establishes a trust relationship between GitHub and a service principal in your cloud provider. AWS calls this an OIDC provider in IAM, Azure uses a federated identity credential, and GCP refers to it as workload identity federation.

Tip: this means that your cloud provider needs to support OpenID Connect as an authentication mechanism. There are several examples available in the GitHub docs.

Requesting an ID token from your workflow

Every workflow run already generates a GITHUB_TOKEN, commonly referenced via ${{ secrets.GITHUB_TOKEN }} for tasks like publishing packages or commenting on issues. To enable OIDC, that token needs explicit permission to request an ID token — set the id-token permission to write.

permissions:
  id-token: write # This is required for requesting the JWT

This permission can be declared at the workflow level or scoped to an individual job. If only one job needs the token, grant it there — least privilege applies to CI/CD tooling just as it does to cloud resources.

Authenticating with the cloud provider's action

Each major cloud provider offers a GitHub Action that handles the OIDC exchange: AWS's configure-aws-credentials, Azure's login, Google Cloud's auth, and Hashicorp's vault-action.

Note: while several cloud providers have GitHub Actions that support OIDC authentication, it’s possible to create a custom action for those providers which do not have an official GitHub action that supports this approach. You can find out more about the process in the GitHub docs.

When configured for OIDC, the action requests the GitHub ID token, forwards it to the cloud provider along with the necessary service principal identifiers, and receives a short-lived access token in return. Subsequent workflow steps use that access token for authenticated operations.

An Azure login example illustrates the pattern:

name: Login to Azure and execute the Azure CLI
on: [push]

permissions:
  id-token: write

jobs: 
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 'Login to Azure using OIDC'
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: 'List the Azure Resource Groups'
        run: |
          az group list

Notable details in that configuration:

  • The id-token permission is set to write at the workflow level; omit it and the login step fails because the workflow can't retrieve the GitHub ID token.
  • The Azure login action takes a Client ID, Tenant ID, and Subscription ID — no password or certificate in sight. Those IDs are passed via GitHub Secrets, which masks them in workflow logs.
  • The action sends the GitHub ID token plus service principal details to Azure. Azure validates whether this specific workflow is allowed access and returns an access token or fails the step.
Note: the configurable properties for each GitHub Action are set by the owner of the action. While client-id, tenant-id and subscription-id are used for the Azure/login step, these are not the same for actions from the other cloud providers. Make sure to familiarize yourself with the appropriate action for your cloud provider, and the recommended configuration.

The result is a deployment pipeline that no longer depends on static credentials. Certificates and passwords don't need rotation because they don't exist. The trust between GitHub and the cloud provider, combined with the short-lived nature of the exchanged access token, removes a significant operational burden — one step closer to a passwordless cloud deployment.