← Back to Projects

Keyless CI/CD with OIDC for Terraform

Short-lived federated credentials instead of stored secrets, and the root cause of token expiry on long applies

CI: GitHub Actions + Azure DevOps
Auth: Workload identity federation
IaC: Terraform (azurerm)
Role: Platform engineer

Overview

Every new repository gets Azure access through Entra ID workload identity federation, so each pipeline run receives a short-lived token instead of a stored client secret. I built this into the developer platform's onboarding. I also tracked down why long Terraform applies sometimes died partway through and left orphaned resources behind.

How It Works

CI jobrequests ID tokenGitHub OIDCissuerEntra IDfederated credentialAzure tokenshort-livedTerraformazurerm
No secret is stored anywhere in the chain

The federated credential's subject is scoped to repo:<org>/<repo>:environment:<env>, so only that environment's jobs can get that environment's access.

Implementation

  1. Onboarding as code: creating a repository dispatches a workflow that adds one federated credential per environment.
  2. Scoped RBAC for each CI identity, granted through governance pull requests.
  3. Environment scoping, so production credentials are only issued to production-environment jobs, which can sit behind approval gates.
workflow (illustrative)
permissions:
  id-token: write
  contents: read
env:
  ARM_USE_OIDC: "true"
  ARM_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
  ARM_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
  ARM_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
# Let the provider fetch fresh assertions itself via
# ACTIONS_ID_TOKEN_REQUEST_URL / _TOKEN. Don't export a one-off ARM_OIDC_TOKEN.

Root-Causing AADSTS700024

Symptom

Applies longer than about 40 minutes (for example, managed database builds) failed with AADSTS700024: Client assertion is not within its valid time range. The next run then failed with "already exists, needs to be imported".

  1. A shared workflow fetched one ID token at the start, exported it as a static ARM_OIDC_TOKEN, and unset the request URL and token.
  2. So the provider had no way to request a new assertion, and the original one expired during long applies.
  3. Any resource being created at that moment ended up in Azure but not in state.
  4. Fix: let the provider refresh its own token. Recovery: import the orphan, or delete it if that's safe, then re-apply.

Key Outcomes

Lessons Learned

Conclusion

Keyless authentication removes a whole class of secret-management risk. Making it reliable for long-running infrastructure means understanding token lifetimes all the way down.

Technologies Used

GitHub ActionsReusable WorkflowsAzure DevOpsTerraformazurerm ProviderEntra IDWorkload Identity FederationOIDC

About this case study: this write-up is drawn from my hands-on platform engineering experience. Organisation names, system names, figures and timelines have been changed or generalised, and all code, configuration and queries are illustrative examples written for this page.

← FinOps: Turning Off What Nobody UsesCertificate and Secret Lifecycle at Scale →
← Back to Projects Get in Touch