TerraformVI · Providers and the Provider EcosystemProduction Terraform
Provider Authentication in Production
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
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:
- Static credentials in the
providerblock. The least secure option. Credentials in the configuration, the configuration in Git, the Git history forever. - 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. - Shared credentials file.
~/.aws/credentials. Long-lived access keys on a developer workstation. - Shared config file.
~/.aws/config. Profiles withrole_arnforsts:AssumeRole. - Container or instance profile. EC2, ECS, EKS, Lambda metadata service. The platform hands the workload an identity.
- 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 asAssumedRole/<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_nameor by the OIDC exchange. The session appears asAssumedRole/<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
subconditions. 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
Q1. What is the AWS provider's preferred credential mechanism in production?
Q2. What does the `assume_role.session_name` argument do?
Q3. An OIDC trust policy that allows any GitHub Actions job in the organisation to assume the role is an acceptable production default.
Q4. What is the first action when a long-lived AWS access key is committed to Git?
Q5. Which of the following are production credential discipline controls? (Select all that apply.)
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.