Authentication and Credentials
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.
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.
# 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 "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.
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 "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.
- Hardcoding
access_keyandsecret_keyin 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
.tfvarsfile that then gets committed, putting secrets in git history under the guise of configuration. - Forgetting that a stray
AWS_ACCESS_KEY_IDin the shell wins over the named profile in the chain, silently applying against the wrong account.
- 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_rolecredentials 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.
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_IDin 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.hclfile 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