Chapter 7: Providers in Depth
Topic 44

Authentication and Credentials

AuthSecurity

How Terraform authenticates to AWS decides two things at once: whether an apply works, and how much damage a leaked secret can do. The AWS provider uses the standard credential chain — environment variables, shared profiles, instance roles, and in CI an OIDC-assumed role — so you rarely configure credentials in Terraform at all; you arrange for the chain to find them.

There is one rule that overrides everything else on this page: never put long-lived access keys in your Terraform code, your tfvars, or anything that reaches version control. A committed key pair is a permanent credential in a public history, and rotating it is the easy part — finding everywhere it leaked to is not.

The AWS Credential Chain

The provider resolves credentials in a fixed order and uses the first source it finds. Static keys set directly in the provider block come first of all — which is exactly why you leave them out — so in practice the chain runs: environment variables (AWS_ACCESS_KEY_ID, AWS_PROFILE), then a named profile in ~/.aws/credentials, then a container (ECS) or EC2 instance role attached to the compute Terraform runs on. Because the chain is ordered, a stray AWS_ACCESS_KEY_ID in your shell quietly wins over the profile you meant to use — the most common "why is this applying to the wrong account?" surprise.

The AWS credential chain — first match wins
Environment variables
Shared profile (~/.aws)
Instance / ECS role
In CI, skip all three: an OIDC-assumed IAM role mints short-lived credentials per run, with no static keys stored.

Local Development Auth

For local work, authenticate with a named profile rather than keys pasted into a block. The best version of this is an SSO-backed profile that mints short-lived credentials, so even a machine that gets compromised holds nothing permanent. You select it with AWS_PROFILE and Terraform's provider picks it up through the chain — no access_key anywhere in the config.

~/.aws/config — a short-lived SSO profile
# Run `aws sso login --profile dev` to refresh credentials
[profile dev]
sso_session    = corp
sso_account_id = 111122223333
sso_role_name  = Developer
region         = us-east-1

With that profile present, the provider needs nothing but a region. The credentials come from the chain, scoped and short-lived, and rotate themselves each login.

provider.tf — no keys, credentials come from the chain
provider "aws" {
  region = "us-east-1"
  # export AWS_PROFILE=dev before running terraform
}

CI/CD Auth With OIDC

In CI, the right pattern is OIDC, not stored keys. GitHub Actions or GitLab presents a signed identity token to AWS STS, which exchanges it for short-lived credentials of an IAM role — no access key is ever stored as a CI secret. The pipeline configures AWS once, and Terraform's provider reads the resulting environment credentials through the chain. This removes the single highest-value secret a CI system would otherwise hold.

.github/workflows/apply.yml — OIDC role assumption, no stored keys
permissions:
  id-token: write   # required for the OIDC token
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::111122223333:role/GhaTerraform
      aws-region:     us-east-1
  - run: terraform apply -auto-approve

assume_role and MFA

Role assumption is the same mechanism whether it backs a cross-account provider or an elevated local session. An assume_role block on the provider trades the chain's base credentials for a role's temporary credentials, and it can require MFA before granting them — the way a human reaches a privileged production role for a one-off apply. The base identity stays low-privilege; the elevated access is short-lived and gated.

provider.tf — assuming an elevated role with MFA
provider "aws" {
  region = "us-east-1"

  assume_role {
    role_arn     = "arn:aws:iam::111122223333:role/ProdDeployer"
    session_name = "prod-deploy"
  }
}

What Never to Do

The anti-patterns are all variations of one thing: a long-lived secret somewhere it can leak. Static keys hardcoded in a provider block. Keys in a .tfvars file that later gets committed. Keys in a committed .env. A secret in an output, printed into a CI log that is retained and searchable. And the subtle one — credentials marked sensitive that still land in state in plaintext. None of these are protected by the chain; the chain only helps when you let it supply the credentials instead of embedding them.

Common Mistakes
  • Hardcoding access_key and secret_key in the provider block, leaking permanent credentials into version control where rotation does not undo the exposure.
  • Using a long-lived IAM user's static keys in CI instead of OIDC role assumption, leaving a high-value secret to store, protect, and rotate forever.
  • Running everyday applies with a developer's broad admin profile rather than a scoped role, so any mistake or compromise has admin-level reach.
  • Storing credentials in a .tfvars file that then gets committed, putting secrets in git history under the guise of configuration.
  • Forgetting that a stray AWS_ACCESS_KEY_ID in the shell wins over the named profile in the chain, silently applying against the wrong account.
Best Practices
  • Use OIDC-assumed IAM roles in CI/CD so there are no static keys to store or rotate, only short-lived credentials minted per run.
  • Authenticate local development with short-lived SSO or assume_role credentials instead of static user keys, so a stolen laptop holds nothing permanent.
  • Scope the role Terraform runs as to least privilege for exactly what it manages, not a blanket admin policy.
  • Keep every credential out of code, tfvars, and state, and let the provider's credential chain supply them at run time.
  • Gate elevated production roles behind MFA on assume_role, keeping the base identity low-privilege and the elevation short-lived.
Comparable tools CloudFormation runs with the caller's IAM context, no separate credential config Pulumi uses the same AWS credential chain Ansible reads AWS credentials from the same environment and profiles

Knowledge Check

Why is OIDC role assumption preferred over static IAM user keys in CI?

  • It mints short-lived credentials per run, so there is no stored key to protect or rotate
  • It is the only mechanism Terraform's provider can authenticate with inside a CI pipeline
  • It grants the run broader IAM permissions than a scoped static-key user would hold
  • It encrypts the long-lived access keys with KMS before storing them as CI secrets

A named profile is set but applies hit the wrong account. What is a likely cause?

  • A stray AWS_ACCESS_KEY_ID in the shell wins, because environment vars precede profiles in the chain
  • Profiles are ignored entirely unless their access keys are copied into the provider block directly by hand
  • Terraform always falls back to the last profile defined in the credentials file
  • The .terraform.lock.hcl file is missing the target account's credentials

Where must AWS credentials never live?

  • In code, tfvars, or state — anything that can reach version control
  • In environment variables, which the credential chain cannot read
  • In a short-lived SSO session, which is too volatile to trust
  • In an EC2 instance role, which leaks keys to every process

What does an assume_role block on the provider do?

  • Trades the chain's base credentials for a role's short-lived credentials, optionally gated by MFA
  • Embeds a permanent access key pair for the target role directly in the provider block
  • Disables the credential chain entirely so only the named role's static keys are read
  • Stores the assumed role's temporary session credentials in the state file so the next run can reuse them

You got correct