Terraform "No Valid Credential Sources Found" and "IAM Role Cannot Be Assumed": Every AWS Provider Credential Error, Decoded and Fixed (Profiles, Env Vars, assume_role, SSO, CI)

Logeshwaran
—

Terraform's AWS provider fails before it touches a single resource when it can't find or can't use credentials, and it says so in one of five sentences: No valid credential sources found, failed to get shared config profile, NAME, validating provider credentials: ... InvalidClientTokenId, Cannot assume IAM Role (the one that lists "a number of possible causes"), and invalid AWS Region. Older provider versions wrapped all of them in error configuring Terraform AWS Provider, which is still what most people search for. Each sentence points at a different layer of the same chain, and the fix is different for each, so the first job is to read which one you have. The surprise for anyone coming from the AWS CLI: Terraform's assume_role block ignores your environment variables for the role settings, and the provider block and the S3 backend authenticate separately, so a profile that works for terraform init can still fail at plan. This page decodes the five messages, shows the credential order the provider actually uses, and gives the fix for profiles, environment variables, SSO, assume_role, CI runners and EC2.

Ethan, a developer, manages the AWS account behind Jake's phone repair shop with Terraform. It worked for months from his laptop. The week he moved it to a CI runner it failed with the first message; the week after, when he added a deploy role, it failed with the fourth. Jake's observation was that "the computer keeps finding new ways to say no." Ethan's, by the end, was that it had said exactly which lock was closed every time.

⚡ Quick Answer

• No valid credential sources found: nothing in the chain had keys. Set AWS_PROFILE or profile = "...", or export AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, in the shell that runs Terraform. Details.

• failed to get shared config profile, NAME: that profile isn't in ~/.aws/config or ~/.aws/credentials of the user running Terraform. Fix.

• InvalidClientTokenId / ExpiredToken on GetCallerIdentity: keys were found and AWS rejected them. Rotate, re-login to SSO, or unset the stale environment variables. Fix.

• Cannot assume IAM Role: the base credentials worked but sts:AssumeRole failed; read the Error: line under the three bullets. Fix.

Test the exact chain Terraform will use: aws sts get-caller-identity in the same shell, with the same AWS_PROFILE. If that fails, Terraform will too.

The five messages, decoded

Every one of these comes from the provider's own startup, before any resource is read, and each names a different failure. The texts below are from Terraform 1.16 with AWS provider 6.69; providers from the 4.x and 5.x era prefix them with error configuring Terraform AWS Provider: and otherwise read the same.

MessageWhat happenedFix lives in
Error: No valid credential sources foundThe provider walked the whole credential chain and found nothing: no provider arguments, no environment variables, no usable profile, no container or instance roleYour shell, your profile files, or the runner's role
Error: failed to get shared config profile, NAMEYou named a profile (profile = or AWS_PROFILE) that doesn't exist in the files the provider is reading~/.aws/config and ~/.aws/credentials of the user running Terraform
Error: Retrieving AWS account details: validating provider credentials: retrieving caller identity from STS: ... api error InvalidClientTokenId (or ExpiredToken, SignatureDoesNotMatch)Credentials were found and AWS refused themThe keys themselves: rotated, expired session, wrong secret, or a stale variable overriding the right profile
Error: Cannot assume IAM Role, then IAM Role (arn) cannot be assumed. There are a number of possible causes...Base credentials worked; the sts:AssumeRole call for the role in your assume_role block or profile failedThe Error: line after the bullets: the role's trust policy, your permission to assume, the ARN, or the base credentials
Error: invalid AWS Region: (with nothing after the colon)No Region anywhere: not in the provider block, not in AWS_REGION, not in the profileregion = in the provider, or AWS_REGION, or region= in the profile

One more that looks like a provider error and isn't: an error on terraform init mentioning the S3 bucket or DynamoDB table is the backend, which authenticates on its own and ignores the provider block; our S3 backend guide is the page for that. And an AccessDenied on a specific resource during plan means credentials are fine and permissions aren't; that's IAM policy work, not this page.

Jake: "Five different sentences for 'can't log in'?"

Ethan: "Five different doors. Nothing in the pockets, the wrong name on the key ring, a key that doesn't turn, a second door behind the first, and no address on the envelope. It's helpful once you can tell them apart."

Before you change anything, give yourself one minute with the order below. Most of what you'll fix on this page comes down to something earlier in this list quietly winning over the thing you configured, and once you can see the order, you can usually spot your culprit without touching a file.

The order the provider looks in

The AWS provider resolves credentials in a fixed order, the same one the AWS CLI and the AWS SDK for Go use, and the first source that produces keys wins:

  1. Arguments in the provider "aws" block: access_key, secret_key, token, or profile.
  2. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, and AWS_PROFILE to pick a profile.
  3. The shared credentials file, ~/.aws/credentials.
  4. The shared config file, ~/.aws/config, including SSO profiles, role_arn profiles and credential_process.
  5. Container credentials: an ECS task role, CodeBuild's role, or EKS IRSA.
  6. The EC2 instance profile, through the instance metadata service.

Two consequences explain most mysteries. First, an AWS_ACCESS_KEY_ID left in your shell from an old project beats the profile you carefully configured, because step 2 comes before step 3; stale environment variables are the single most common cause of the third message. Second, step 6 is why a laptop with no credentials at all pauses for a few seconds before failing: the provider tries to reach the EC2 metadata service and waits for the timeout. Setting AWS_EC2_METADATA_DISABLED=true on machines that aren't EC2 makes the failure immediate, and the first message's detail line shows exactly that attempt.

The Region is resolved the same way, from region in the provider block, then AWS_REGION or AWS_DEFAULT_REGION, then the profile's region= line, then the instance's own Region on EC2. There is no default; the fifth message is the provider refusing to guess.

"No valid credential sources found"

Error: No valid credential sources found

  with provider["registry.terraform.io/hashicorp/aws"],
  on main.tf line 6, in provider "aws":
   6: provider "aws" {

Please see https://registry.terraform.io/providers/hashicorp/aws
for more information about providing credentials.

Error: failed to refresh cached credentials, no EC2 IMDS role found,
operation error ec2imds: GetMetadata, access disabled to EC2 IMDS via client
option, or "AWS_EC2_METADATA_DISABLED" environment variable

The second Error: line is the last thing the chain tried, which is useful: here it got all the way to the instance metadata service and found nothing, so every earlier source was empty too. The fix is to put credentials somewhere in the chain, in the shell that runs Terraform, and the cleanest options in order of preference:

  • A named profile, so no secret sits in the shell or the code: export AWS_PROFILE=shop or profile = "shop" in the provider block, with the profile defined in the files below.
  • SSO, if your organization uses IAM Identity Center: aws sso login --profile shop, then the same AWS_PROFILE=shop. The provider reads the SSO token cache the CLI writes.
  • Environment variables for one-off runs and CI: export AWS_ACCESS_KEY_ID=..., AWS_SECRET_ACCESS_KEY=..., and AWS_SESSION_TOKEN=... when the keys are temporary.
  • A role on the machine itself for CI and servers: an EC2 instance profile, an ECS task role, or OIDC federation in GitHub Actions, which puts temporary keys in the environment for you.

What you want to skip is pasting access_key and secret_key into the provider block: hard-coded credentials leak the moment the file is committed, and a repository with Jake's shop's keys in it is a weekend you don't want.

A second cause of the same message hides in the detail line: a profile that exists but has no usable keys. A [profile deploy] with role_arn and source_profile = staging, where staging's keys are invalid, produces "No valid credential sources found" with operation error STS: AssumeRole ... InvalidClientTokenId as the detail, because the role assumption happened inside the profile resolution rather than in the provider's assume_role block. Read the detail line before you reach for the first bullet above.

"failed to get shared config profile, NAME"

Error: failed to get shared config profile, nope

The provider was told to use a profile and couldn't find it. Check four things, because the profile usually exists somewhere, just not where this process looks:

  1. The name, exactly as typed, in AWS_PROFILE or profile = . Profiles are case-sensitive.
  2. The file format. In ~/.aws/config the header is [profile shop]; in ~/.aws/credentials it is [shop]. Writing [shop] in the config file defines a profile the provider cannot see. Our profile rules guide has every variant.
  3. The user. Terraform run with sudo, by a CI service account, or inside a container reads that user's home directory, not yours. echo $HOME in the same context Terraform runs in.
  4. Custom file paths. If AWS_CONFIG_FILE, AWS_SHARED_CREDENTIALS_FILE, shared_config_files or shared_credentials_files are set, the default files are not read at all.
aws configure list-profiles          # what the CLI sees in this shell
aws configure list --profile shop     # where each value came from
echo "$AWS_PROFILE $AWS_CONFIG_FILE $AWS_SHARED_CREDENTIALS_FILE"

"validating provider credentials ... InvalidClientTokenId" and friends

Error: Retrieving AWS account details: validating provider credentials: retrieving caller identity from STS:
operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: ...,
api error InvalidClientTokenId: The security token included in the request is invalid.

Credentials were found and sent, and AWS said no. The provider validates credentials at startup by calling sts:GetCallerIdentity, which is why the error names that operation; the same call from the CLI reproduces the problem outside Terraform. The API error code after api error tells you which kind of no:

API errorMeaningFix
InvalidClientTokenIdThe access key ID doesn't exist, was deleted, or is from another partition or accountFind which source supplied it (aws configure list), replace it; unset stale AWS_ACCESS_KEY_ID variables
ExpiredTokenTemporary credentials whose session ended: SSO, assumed role, CI tokenaws sso login again, or re-run the step that assumed the role
SignatureDoesNotMatchThe secret key doesn't match the key ID, or has a trailing space or newlineRe-copy the secret; check the variable with printf '%q'
AccessDenied on GetCallerIdentityRare: a policy or SCP denies even identity lookupsTalk to whoever owns the account's guardrails

Because environment variables outrank profiles, the usual story is a correct profile shadowed by an old variable. The diagnosis is one command in the same shell:

aws configure list
#       Name                    Value             Type    Location
#    profile                     shop              env    AWS_PROFILE
# access_key     ****************OLD1              env
# secret_key     ****************OLD2              env
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
aws sts get-caller-identity --profile shop

If the Type column says env for the keys while you expected a profile, you've found it. On Windows PowerShell the equivalent is Remove-Item Env:AWS_ACCESS_KEY_ID and the two siblings.

skip_credentials_validation = true in the provider block turns off the startup check. It doesn't make bad credentials work; it moves the same 403 to the first resource Terraform reads, where it appears as Error: reading STS Caller Identity or an error on whatever resource comes first. It exists for local emulators and air-gapped setups, not as a fix.

If you're here with the role error, take heart: you've already passed the first door. Your base credentials worked, which means you can test the next step by hand with the same keys, and the Error line below the bullets will tell you exactly which of the three suspects is yours.

"Cannot assume IAM Role": the second door

Error: Cannot assume IAM Role

  with provider["registry.terraform.io/hashicorp/aws"],
  on main.tf line 4, in provider "aws":
   4: provider "aws" {

IAM Role (arn:aws:iam::123456789012:role/terraform-deploy) cannot be assumed.

There are a number of possible causes of this - the most common are:
  * The credentials used in order to assume the role are invalid
  * The credentials do not have appropriate permission to assume the role
  * The role ARN is not valid

Error: operation error STS: AssumeRole, https response error StatusCode: 403,
RequestID: ..., api error InvalidClientTokenId: The security token included in the request is invalid.

This appears when the provider block has an assume_role section. The provider found base credentials, then called sts:AssumeRole for the ARN you gave, and that call failed. The three bullets are generic; the Error: line under them is specific, and it is the line to read:

The Error line saysWhich bullet it isFix
InvalidClientTokenId or ExpiredTokenBase credentials are badFix the base credentials first (previous section); the role was never the problem
AccessDenied: User: arn:aws:iam::111122223333:user/ethan is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::123456789012:role/terraform-deployNo permission to assumeTwo places: the role's trust policy must allow your principal (or your account), and your identity policy must allow sts:AssumeRole on that ARN. Cross-account needs both. Our AssumeRole AccessDenied guide walks the trust policy side.
AccessDenied with the role's external_id conditionPermission, with a twistAdd external_id = "..." in the assume_role block to match the trust policy's condition
ValidationException or MalformedPolicyDocumentThe ARN is not validCheck the ARN: arn:aws:iam::ACCOUNT:role/NAME, the right account, no path typo; copy it from aws iam get-role
The requested DurationSeconds exceeds the MaxSessionDuration set for this roleRole settingsLower duration in the block, or raise the role's maximum session duration

The block itself, with the two optional arguments that most often need to match the role's trust policy:

provider "aws" {
  region = "us-east-1"
  assume_role {
    role_arn     = "arn:aws:iam::123456789012:role/terraform-deploy"
    session_name = "terraform"
    external_id  = "shop-deploy"      # only if the trust policy demands one
    # duration   = "1h"
  }
}

Two things that catch CLI users. The provider's assume_role settings come only from this block or from a profile's role_arn line; there is no environment variable for them, so AWS_ROLE_ARN is read only for web-identity federation, not for plain role assumption. And role chaining is written as two assume_role blocks in order, the first role assumed with your base credentials and the second with the first's session. If you'd rather keep role assumption in the profile, the config-file form is [profile deploy] with role_arn = ... and source_profile = staging, and then the error, when it happens, is the first message with the AssumeRole detail rather than this one.

Jake: "So the role error can mean the role is fine and my own key is dead?"

Ethan: "That's the first bullet, and it's the one I hit. The bullets are a list of suspects. The Error line is the confession."

"invalid AWS Region:" with nothing after the colon

Error: invalid AWS Region:

   4: provider "aws" {}

The empty value after the colon is the whole story: nothing set a Region. Add region = "us-east-1" to the provider block, or export AWS_REGION, or put region = us-east-1 in the profile. A profile in ~/.aws/credentials can hold keys but not a Region, which is a common way to have working keys and this error together; the Region line belongs in ~/.aws/config. A misspelled Region produces the same message with the bad value after the colon.

SSO profiles: the login that expires

With IAM Identity Center, the profile holds no keys; it holds a sso_session reference, and the CLI writes a cached token when you log in. Terraform reads that cache. So the sequence is: aws sso login --profile shop, then AWS_PROFILE=shop terraform plan. When the session expires, usually after hours, the provider reports the third message with ExpiredToken, or, on older caches, the first message with a detail about the SSO token; either way, logging in again fixes it. Two gotchas: the CLI and Terraform must agree on which config file holds the profile, and a profile written in the legacy sso_start_url form and one in the sso-session form behave the same for Terraform as long as the CLI can log in with it.

[profile shop]
sso_session  = company
sso_account_id = 123456789012
sso_role_name  = TerraformDeploy
region         = us-east-1

[sso-session company]
sso_start_url = https://company.awsapps.com/start
sso_region    = us-east-1

If your failure is in CI, you have the same chain in a stricter place: nobody is there to export a variable by hand, so whatever your runner gives the provider is all it gets. The good news is that CI usually offers you a role, and a role is the one credential source you can't leave in a shell by mistake.

CI runners: GitHub Actions, CodeBuild, containers

In CI the chain is shorter and the first message is the usual outcome of a missing step. The patterns that work:

  • GitHub Actions: aws-actions/configure-aws-credentials with OIDC puts temporary keys and the Region into the job's environment; Terraform picks them up through steps 2 and the Region variable. If the Terraform step runs in a different job, the credentials aren't there; configure them in the same job.
  • CodeBuild and ECS: the task or project role is found through container credentials (step 5) with nothing to configure. If you set AWS_PROFILE in the buildspec by habit, you get the second message, because no profile files exist on the runner.
  • EC2 self-hosted runners: the instance profile is step 6; an instance without one gives the first message with the IMDS detail, and an instance with IMDSv2 required works unchanged, since the provider supports both versions.
  • Docker on your laptop: the container has no ~/.aws unless you mount it (-v ~/.aws:/root/.aws:ro) or pass the variables with -e. The $HOME inside the container is the one that counts.

Your deploy role in CI is best expressed as the runner's own identity assuming the account's deploy role through the assume_role block, so the base credentials are short-lived and the role's trust policy names the runner's role, not a person.

The backend is not the provider

Terraform's S3 backend reads its own credentials, from the backend "s3" block or the same chain, during init, and the provider reads credentials during plan. They can disagree. A profile in the backend block doesn't apply to the provider, and a provider-level assume_role doesn't apply to the backend; if the state bucket lives in a different account from the resources, each side needs its own assume_role (the backend's is assume_role { role_arn = ... } inside the backend block too). When init succeeds and plan fails with one of the five messages, that gap is the first thing to check.

The worked example: Ethan's two failures

The CI failure was the first message. The runner, a CodeBuild project, had a service role, but Ethan's buildspec exported AWS_PROFILE=shop out of habit, which turned the first message into the second; removing the line let container credentials through, and plan ran. The deploy-role failure was the fourth message with InvalidClientTokenId in the Error line: his laptop had an old AWS_ACCESS_KEY_ID in ~/.zshrc from a deleted key, which outranked the SSO profile and was what the provider used as base credentials for AssumeRole. aws configure list showed env in the Type column; two unset lines and one aws sso login later, the role assumed and the plan matched the CI run.

Jake: "Both times it was something extra, not something missing."

Ethan: "A habit in the buildspec and a leftover in my shell. The provider reads in a strict order, and anything earlier in the order wins, even when it's wrong."

The checklist, in order

  1. Read the first Error: line; match it to one of the five.
  2. In the same shell, run aws configure list and aws sts get-caller-identity. If the CLI fails the same way, fix the CLI first.
  3. Look for stale AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN and AWS_PROFILE; unset what you didn't mean.
  4. Profile errors: check the [profile x] vs [x] header rule, and $HOME for the user running Terraform.
  5. Role errors: read the Error line under the three bullets; fix base credentials before touching the role.
  6. Region: set it in the provider block in every repository, so no shell has to remember it.
  7. Separate the backend from the provider; they authenticate on their own.

Frequently asked questions about Terraform AWS provider credential errors

What does "No valid credential sources found" mean in Terraform?

The AWS provider checked the provider block, environment variables, profile files, container credentials and the instance metadata service, and none produced credentials. Set a profile, export keys, or run on a machine with a role.

What does "error configuring Terraform AWS Provider" mean?

It is the prefix older provider versions put in front of every startup credential error. The sentence after it, such as no valid credential sources, failed to get shared config profile, or IAM Role cannot be assumed, is the one that matters.

How do I make Terraform read the AWS credentials file?

Put the profile in ~/.aws/credentials as [name] or in ~/.aws/config as [profile name], then set profile = "name" in the provider block or export AWS_PROFILE=name, in the shell that runs Terraform.

Why does Terraform say "failed to get shared config profile"?

The named profile is not in the files the provider reads: wrong name, wrong header format ([profile x] in config vs [x] in credentials), a different user's home directory, or a custom file path set by AWS_CONFIG_FILE.

Why does Terraform ignore my profile and use old keys?

Environment variables outrank profile files. An old AWS_ACCESS_KEY_ID in the shell wins; aws configure list shows env in the Type column. Unset the variables.

What does "IAM Role cannot be assumed" mean in Terraform?

The provider found base credentials but the sts:AssumeRole call for the assume_role ARN failed. The Error line under the three bullets says why: invalid base credentials, no permission or trust, or a bad ARN.

How do I use assume_role in the Terraform AWS provider?

Add an assume_role block with role_arn, optionally session_name, external_id and duration. Role chaining is two assume_role blocks in order. Environment variables cannot set these.

Does Terraform support AWS SSO profiles?

Yes. Run aws sso login --profile NAME, then use that profile; the provider reads the CLI's token cache. An expired session shows up as ExpiredToken, fixed by logging in again.

Why does Terraform fail with InvalidClientTokenId?

The credentials it found are not valid: a deleted or rotated key, a key from another account, or temporary credentials missing their session token. Find the source with aws configure list and replace it.

What does skip_credentials_validation do?

It turns off the startup sts:GetCallerIdentity check. Bad credentials then fail at the first resource instead; it is for emulators, not a fix.

Why does Terraform say "invalid AWS Region:" with nothing after it?

No Region was set anywhere. Add region to the provider block, export AWS_REGION, or put region in the profile in ~/.aws/config.

Why does terraform plan hang before failing on my laptop?

The provider tries the EC2 instance metadata service last and waits for its timeout. Set AWS_EC2_METADATA_DISABLED=true on machines that are not EC2.

Why does terraform init work but plan fails with a credential error?

The S3 backend and the provider authenticate separately. A profile or assume_role in the backend block does not apply to the provider, and the reverse.

How do I give Terraform credentials in GitHub Actions?

Use aws-actions/configure-aws-credentials with OIDC in the same job as the Terraform step; it exports temporary keys and the Region that the provider picks up.

Why does Terraform in CodeBuild fail with a profile error?

The runner has no profile files. Remove AWS_PROFILE from the buildspec and let the project's service role be found through container credentials.

Should I put access_key and secret_key in the provider block?

No. Hard-coded credentials leak the moment the file is committed to a repository. Use a profile, SSO, environment variables, or a role.

Ethan's buildspec lost one line and his shell profile lost two, and both the runner and the laptop now produce the same plan. Jake's verdict: "So the fix was deleting things." Ethan said that with credentials, it usually is.

📌 If you keep one line from this page

Match the first Error line to one of the five messages, run aws configure list and aws sts get-caller-identity in the same shell, unset stale AWS_ variables, and remember that environment beats profile, the Error line beats the bullets, and the backend is not the provider.

No valid credential sources · failed to get shared config profile · InvalidClientTokenId / ExpiredToken · Cannot assume IAM Role · invalid AWS Region.

Revision note. Written October 15, 2026, with Terraform 1.16.6 and AWS provider 6.69.0, every message reproduced on a machine with no AWS access at all. "First things first," says the old proverb; with credentials, the first thing is which of the five doors is closed.

Related