GitHub·8 min read·

OpenID Connect Instead of Static Secrets in GitHub Actions

Replace long-lived cloud credentials in GitHub Actions with OpenID Connect — short-lived minted tokens bound to your repository by a trust policy.

NB

Netbay Developer Relations

Netbay Engineering

On this page

The classic way to give a workflow cloud access is to drop a full set of long-lived credentials into a secret. It works, right up until those credentials leak, rotate on a schedule, or grow so many that nobody can track them. OpenID Connect (OIDC) in GitHub Actions fixes this fundamentally: instead of a static key, the workflow requests a short-lived token minted on the fly, and the cloud provider trusts that the token really came from your repository. This post explains the flow and wires a minimal example.

Why static secrets are a risk

A static credential in a secret is a standing key to your cloud account. Anyone with read access to a workflow that uses it, or anyone who triggers a malicious pull-request-triggered run that can read it, may be able to exfiltrate it. Rotation is manual, secrets sprawl, and a single leaked key compromises everything it can reach. Even though GitHub masks secrets in logs, the risk of a supply-chain step reading them is real.

OIDC removes the standing credential entirely. The workflow presents proof of its identity — repository, workflow, branch, environment — and the provider issues a token valid for a short window, typically a few minutes. There is nothing long-lived to leak.

How the OIDC handshake works

The flow has four moving parts:

  1. The workflow sets permissions id-token: write, granting it the right to request a token.
  2. GitHub's OIDC authority issues a signed token containing claims about the run (repository, ref, environment, actor).
  3. The cloud provider verifies the signature and checks that the claims match a configured trust policy.
  4. The provider exchanges the verified GitHub token for its own short-lived cloud credentials inside your workflow.

The security hinges on the trust policy: the provider must accept tokens only from specific repositories, refs, and environments. Get that policy wrong and any repository could impersonate yours. Under the hood, requesting the short-lived token is just an HTTP call to the GitHub OIDC endpoint, using a unique request ID and an audience claim you choose:

bash
# GitHub has already populated ACTIONS_ID_TOKEN_REQUEST_URL and REQUEST_TOKEN in the runner
curl -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN"      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://myapp"   | jq -r .value > /tmp/gh_token.jwt
# decode the claims to inspect repository / ref before trusting it downstream
echo "$ACTIONS_ID_TOKEN_REQUEST_TOKEN" > /dev/null
jq -R 'split(".")[1] | @base64d | fromjson' /tmp/gh_token.jwt

Most setups never call this directly — a vendor action wraps it — but understanding the raw request makes the trust model concrete.

A workflow that requests a token

On GitHub's side, requesting the token is delegated to the setup action for your cloud, or you can call the token endpoint directly. Here is the shape for a cloud that exposes an action:

yaml
name: Deploy via OIDC
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-22.04
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Configure cloud credentials
        uses: acme-org/configure-cloud@v2
        with:
          audience: ${{ github.repository }}
      - name: Deploy
        run: ./deploy.sh

The important pieces are the permissions block granting id-token: write and the environment reference. The configure action exchanges GitHub's token for cloud credentials that are injected into the environment of this job only, then expire. No cloud secret lives in the repository.

Writing the trust policy

The provider side is where you enforce that only your repo can assume the role. A typical trust policy on the cloud side matches the subject claim. The subject format is roughly repo:octo-org/octo-repo:ref:refs/heads/main plus optional environment. A minimal policy allows the workflow to assume the deploy role only when the subject matches your repository and the main branch. You also bind the audience claim to pin the token to a specific provider audience.

This is the single most important control. If the policy is too broad (for example, matching any branch), a pull request branch could trigger a token exchange with deploy permissions. Prefer to restrict to a protected environment and release-ready refs.

Comparing OIDC and static secrets

Here is the honest trade-off. OIDC wins on security: no long-lived key, automatic expiry, and per-run scoping. It also removes the pain of rotating cloud keys. The cost is configuration complexity — you must set up the trust policy and the token exchange correctly, which is more moving parts than pasting a key. For anything touching production, the security win usually justifies the setup. For low-value, isolated jobs a scoped static secret with strict permissions can still be acceptable, but OIDC is the direction everything is moving.

Least privilege with OIDC

Even with OIDC, combine it with least privilege on the software side. Give the assumed role the minimum permissions the deploy actually needs, keep id-token write scoped to the jobs that need it, and require the environment gate for anything sensitive. OIDC replaces the static key, but it does not replace the discipline of granting only what a job uses.

gh runner OIDC authority cloud provider signed token with claims verify signature + policy short-lived creds deploy job no static key stored; creds expire after run

Takeaway

OpenID Connect swaps standing cloud keys for minted, expiring tokens that a trust policy binds to your exact repository and branch. The added setup is worth it for anything that touches production credentials. To keep using those protected deploys, you need reliable infrastructure — an Ubuntu VPS on Netbay, up in under 60 seconds at netbayhosts.in, is a solid deployment target.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance