Skip to main content
RunBook Academy

TerraformVI · Providers and the Provider EcosystemProduction Terraform

Provider Authentication in Production

Intermediate⏱ ~14 minbash

What you'll learn

  • Describe the credential hierarchy the AWS provider uses to authenticate
  • Configure the `assume_role` block for cross-account production access
  • Configure OIDC for CI/CD pipelines so long-lived credentials stay out of the system
  • Apply per-environment credential discipline so dev, staging, and prod do not share an identity
  • Recognise the production cost of a leaked long-lived credential

Prerequisites

None — start here.

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

Authentication is the boundary between the configuration and the cloud. A misconfigured credential is a security incident waiting to happen. A hard-coded credential in Git is a security incident that has already happened. The lesson teaches the credential hierarchy the AWS provider uses, the production patterns for cross-account and CI/CD access, and the operational cost of a leak.

The credential hierarchy

The AWS provider tries credentials in order, first match wins:

  1. Static credentials in the provider block. The least secure option. Credentials in the configuration, the configuration in Git, the Git history forever.
  2. Environment variables. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN. Set by the shell, by a CI runner, or by an OIDC exchange.
  3. Shared credentials file. ~/.aws/credentials. Long-lived access keys on a developer workstation.
  4. Shared config file. ~/.aws/config. Profiles with role_arn for sts:AssumeRole.
  5. Container or instance profile. EC2, ECS, EKS, Lambda metadata service. The platform hands the workload an identity.
  6. Web identity token. A JWT exchanged via sts:AssumeRoleWithWebIdentity. The mechanism behind OIDC for CI/CD.

Two production defaults follow:

  • In production, the platform hands Terraform its identity. Instance profile, OIDC token, or an explicit sts:AssumeRole. Long-lived access keys are the exception, not the default.
  • Per-environment identities. Dev, staging, and production each have a distinct IAM principal. A Terraform run against staging cannot affect production because the staging role cannot reach production resources.

The simplest case: workload identity

On an EC2 instance, ECS task, or EKS pod with an attached role:

provider "aws" {
  region = "eu-west-2"
  # No credentials block. The provider uses the instance profile.
}

The provider calls the EC2 metadata service, retrieves short-lived credentials, and uses them for the duration of the run. No access key exists. The role is rotated by the platform.

This is the production default for any Terraform that runs inside AWS: build agents, bastion hosts, control-plane nodes. The Terraform developer working from a laptop does not get this benefit by default — they need a profile or an OIDC exchange.

Cross-account access with assume_role

Most production estates have more than one AWS account: shared services, logging, networking, plus per-environment accounts. The pattern is sts:AssumeRole:

provider "aws" {
  region = "eu-west-2"

  assume_role {
    role_arn     = "arn:aws:iam::222222222222:role/TerraformApply"
    session_name = "terraform-ci-${var.build_id}"
    external_id  = "acme-prod-tf-2026"
    duration     = "1h"
  }
}

Five attributes matter:

  • role_arn. The role Terraform assumes. The role’s trust policy must allow the calling principal.
  • session_name. Appears in CloudTrail as AssumedRole/<session_name>. Pick a name that identifies the run; do not pick the same name for every run.
  • external_id. Required when the role’s trust policy declares one. A shared secret that prevents the “confused deputy” problem.
  • duration. Session length. 1 hour is the production default; longer sessions are a larger blast radius.
  • tags (optional). Tags applied to the assumed role session, useful for billing allocation.

The role’s trust policy is where the security boundary lives. A role that allows any AWS principal to assume it is not a boundary. Confirm the Principal is the specific identity (or OIDC token issuer) you intend.

OIDC for CI/CD

Long-lived access keys in a CI runner are a known antipattern. The replacement is OIDC. The CI runner presents a JWT signed by the CI provider; AWS exchanges it for short-lived credentials.

# CONFIGURATION: GitHub Actions with OIDC.
permissions:
  id-token: write
  contents: read

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111111111111:role/github-actions-terraform
          aws-region: eu-west-2
          duration: 3600  # 1 hour

      - run: terraform init
      - run: terraform plan
      - run: terraform apply

The OIDC trust on the AWS role has a Condition block that restricts which repositories, branches, or pull requests can exchange a token for the role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::111111111111:oidc-provider/token.actions.githubusercontent.com" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:sub": "repo:acme/infra:ref:refs/heads/main"
      }
    }
  }]
}

The sub claim is the trust boundary. Restrict it to the branches and repos that should be able to deploy. A pull request from a fork should not match the trust condition; if it does, you have given the world a way to assume your production role.

Per-environment discipline

Production Terraform should not run with the same identity as dev. Three patterns enforce the separation:

1. Per-environment roles. Production, staging, and dev each have a distinct IAM role. The CI pipeline selects the role based on the environment it is targeting. A staging apply cannot affect production because the staging role has no production permissions.

2. Per-environment OIDC trusts. Each role’s trust policy references a specific branch or environment. The role for production allows repo:acme/infra:ref:refs/heads/main. The role for staging allows repo:acme/infra:environment:staging.

3. Per-environment state and backend. A separate state file per environment is mandatory. The credential and the state should both be partitioned by environment, with no shared backend or shared role.

The configuration can parameterise the role:

variable "environment" {
  type = string
}

provider "aws" {
  region = "eu-west-2"

  assume_role {
    role_arn = "arn:aws:iam::111111111111:role/terraform-${var.environment}"
  }
}

A change to environment = "production" in a developer workspace cannot reach production because the developer does not have permission to assume the production role.

The audit trail

Every production Terraform run should leave an audit trail in CloudTrail:

  • Who. The session name set by assume_role.session_name or by the OIDC exchange. The session appears as AssumedRole/<session_name>.
  • What. Every API call Terraform makes against AWS. The full call surface is recorded.
  • When. A timestamp. Useful for correlating with incidents.
  • From where. The source IP. The CI runner or workstation that initiated the call.

For a production estate:

  • Centralise CloudTrail. All accounts, all regions, one S3 bucket with object lock.
  • Alert on unusual Terraform activity. A run from a new source IP, a run at an unusual hour, a run that calls Delete* against production. The alert should page.
  • Review session names. The session name should be unique per run. A repeated session name is suspicious.

The cost of a leaked credential

A leaked credential is a security incident. The operational cost:

Immediate. Rotate the credential. For a long-lived access key, delete the IAM access key. For an OIDC trust, narrow the trust condition. For a session token, expire it. The rotation should happen in minutes, not hours.

Investigation. Pull CloudTrail for every API call the credential made between the leak and the rotation. Identify which resources were read, which were modified, which were created. The blast radius is the union of those actions.

Containment. Anything the credential created that should not exist must be deleted. Anything the credential modified must be reverted. The state file is the audit trail of what Terraform believes is true; reality may have moved beyond it.

Remediation. Replace the leaked credential type with a less-leakable identity. Long-lived access keys become OIDC. Shared profiles become per-run session names. Broad OIDC trusts become narrow sub conditions.

Documentation. The runbook for credential rotation is a required artefact. The team should rehearse the rotation at least once per quarter.

Operational guidance

For a production estate:

  • No long-lived access keys in production. Replace them with workload identity (EC2 instance profile, ECS task role, EKS service account) or with OIDC for CI/CD.
  • Per-environment roles. One IAM role per environment per account, with a narrow trust policy.
  • OIDC trusts with sub conditions. Restrict by repository and branch.
  • Session names that identify the run. Avoid the default of empty or terraform.
  • CloudTrail alerts. Alert on new source IPs, unusual hours, and destructive operations.
  • Rehearse credential rotation. A quarterly drill catches gaps before an incident does.

What comes next

The next lesson is on provider failure modes — what to do when a provider call fails, from rate limits to schema drift.

Verification

Knowledge check · 6 questions

  1. Q1. What is the AWS provider's preferred credential mechanism in production?

  2. Q2. What does the `assume_role.session_name` argument do?

  3. Q3. An OIDC trust policy that allows any GitHub Actions job in the organisation to assume the role is an acceptable production default.

  4. Q4. What is the first action when a long-lived AWS access key is committed to Git?

  5. Q5. Which of the following are production credential discipline controls? (Select all that apply.)

  6. Q6. A CI job accidentally logs `AWS_SECRET_ACCESS_KEY` to a public artifact. What is the operational sequence?

Passing score: 75%. Answers are checked in this browser.