AWS CLI: InvalidClientTokenId — the security token in the request is invalid
Hi, it's BlueByte. When the AWS CLI answers a command you know should work with An error occurred (InvalidClientTokenId) ... The security token included in the request is invalid, nothing in your account is broken — AWS looked at the credentials the request carried and refused them. We'll walk through what that message means, the handful of reasons AWS rejects a token, how to see exactly which credentials the CLI picked up, how to fix it per cause, and how to keep it from coming back.
What "the security token included in the request is invalid" means
InvalidClientTokenId is a credential-authentication failure, returned before AWS ever checks whether you're allowed to do anything. It can appear on any service call — the AWS docs show it against aws s3 ls (the ListBuckets operation) — so the operation named in the message isn't the culprit:
$ aws s3 ls
An error occurred (InvalidClientTokenId) when calling the ListBuckets operation:
The security token included in the request is invalid.The phrase "security token" covers the access key ID and, for temporary credentials, the session token that travels with it. AWS is saying it can't map what you sent to a live credential. You never reached the permission check, so this is not an access-denied.
The handful of reasons AWS rejects the token
Most cases are one of these:
- The access key was deleted or deactivated in IAM after you configured it.
- You're using temporary credentials that expired — an STS session from an assumed role,
aws sso login, or a manually pasted session token is short-lived, and once it lapses the token is invalid. - A stale environment variable (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, orAWS_SESSION_TOKEN) lingers in your shell and, because environment variables outrank the credentials file in AWS's precedence order, overrides the good profile you meant to use. - The key belongs to a different partition than the endpoint — a standard-partition key against a GovCloud (
aws-us-gov) or China (aws-cn) endpoint is not recognized. - The access key ID was copied wrong — truncated or transposed.
First, see which credentials the CLI is actually using
Don't guess which credential went out — ask the CLI. aws configure list prints the resolved values and, crucially, where each came from:
aws configure list Name Value Type Location
---- ----- ---- --------
profile <not set> None None
access_key ****************ABCD env AWS_ACCESS_KEY_ID
secret_key ****************wXyZ env AWS_SECRET_ACCESS_KEY
region us-east-1 env AWS_DEFAULT_REGIONThe Location column answers most of these failures. If it reads env when you expected ~/.aws/credentials, an environment variable is winning. Confirm the identity end to end:
aws sts get-caller-identityIf that returns the same InvalidClientTokenId, the credentials themselves are the problem, not one command. For the full trace of what was loaded, re-run with --debug and read the Looking for credentials via: lines.
Fix it by cause: stale env vars, expired session, dead key
- Stale environment variables — clear them and let the profile take over:
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
aws sts get-caller-identity-
Expired temporary credentials — re-authenticate. For IAM Identity Center / SSO:
aws sso login --profile my-sso. For an assumed role, runaws sts assume-roleagain and replace the exported values. -
Deleted or deactivated key — check its state, then rotate:
aws iam list-access-keys --user-name my-userIf the key is Inactive or missing, create a new one and store it with aws configure.
- Wrong partition — target the matching partition, e.g.
--region us-gov-west-1, with a key issued there.
A real case: a leftover AWS_SESSION_TOKEN
A deploy script that ran fine yesterday started failing with InvalidClientTokenId on aws s3 cp. aws configure list showed access_key ... Location env AWS_ACCESS_KEY_ID — but the project uses a named profile. Someone had exported temporary credentials in that shell for an unrelated test; the AWS_SESSION_TOKEN among them had since expired, and because env vars outrank the profile, every command carried the dead token. Running unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN and then aws sts get-caller-identity returned the profile's expected ARN, and the deploy went through.
Verify the fix and keep it from recurring
Verify with aws sts get-caller-identity — a JSON block with your Account, UserId, and Arn means the credentials are live. To keep it away: prefer short-lived credentials (SSO or assumed roles) so an old key can't linger; don't export credentials into your shell for one-off tasks — pass --profile instead; and make aws sts get-caller-identity the first step in CI, so a bad credential fails loudly at the top of the job instead of deep inside a deploy.
How it differs from InvalidAccessKeyId and SignatureDoesNotMatch
InvalidClientTokenId means the key ID or session token isn't a live credential AWS recognizes. InvalidAccessKeyId is close but distinct — "The AWS Access Key Id you provided does not exist in our records" — AWS has no record of that key at all, usually a typo or a key from another account. SignatureDoesNotMatch is different again: AWS recognizes the key but the request signature didn't verify, most often a clock off by more than a few minutes (date to check) or a secret key mangled by special characters. And none of these is AccessDenied, where the credentials are valid but lack permission for the action.
Related questions
My key worked an hour ago — why is it invalid now?
If you're using temporary credentials (SSO, an assumed role, or a pasted session token), they expire on a fixed lifetime. Re-authenticate with aws sso login or assume the role again. Long-term keys don't expire, so if one suddenly fails, check whether it was deactivated in IAM.
aws configure list shows my key under 'env' but I never set it — where's it from?
Environment variables outrank the credentials file in AWS's precedence order. Something in your shell profile, a .env, or a previous export set AWS_ACCESS_KEY_ID. Unset it and the profile takes over.
Does InvalidClientTokenId mean my IAM policy is wrong?
No. It fails at authentication, before any permission check. A policy problem shows up as AccessDenied with a message naming the action and resource.
I pasted temporary credentials but forgot the session token — is that the cause?
Yes. Temporary credentials are only valid together with their AWS_SESSION_TOKEN. An access key and secret from an STS session without the matching session token are rejected as invalid.
Could a clock problem cause this?
A wrong clock usually produces SignatureDoesNotMatch, not InvalidClientTokenId. Still, run date; if it's off by more than a few minutes, sync it before ruling it out.
References
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
More in this category
Git: fatal: detected dubious ownership in repository
Since the CVE-2022-24765 fix in Git 2.35.2, Git refuses to read a repository whose working tree or .git directory is owned by a different user than the one running the command. It shows up in containers, CI jobs, sudo sessions and shared drives. Fix the ownership if the repo should be yours, or add the exact path to safe.directory in your global config — never in the repo's own config, which Git ignores for this.
Kubernetes: Internal error occurred: failed calling webhook
An admission webhook sits in front of your write, the API server could not get an answer out of it, and failurePolicy: Fail turned that silence into a rejection. The tail of the message is the whole diagnosis: context deadline exceeded means the call went nowhere, no endpoints available means nothing is running, and an x509 line means the API server does not trust the webhook's certificate. Each has a different fix, and none of them is your manifest.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
A statement waited the full innodb_lock_wait_timeout for a row lock another transaction is still holding, and gave up. sys.innodb_lock_waits names the blocking session and hands you the KILL statement, and a blocking_query of NULL means the blocker is idle on an open transaction. The detail most retry loops get wrong: by default only the timed-out statement is rolled back, so your transaction is still open and still holds every lock it took earlier.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
Reads keep working and every write is rejected, because the last background save failed and stop-writes-on-bgsave-error defaults to yes. The log names the real cause — no space, a dir the redis user can't write, a read-only mount at rename time, or fork failing with Cannot allocate memory. Fix the cause, run one BGSAVE, and writes come back on their own with no restart: rdb_last_bgsave_status flips from err to ok. Setting stop-writes-on-bgsave-error no restores writes instantly but leaves the snapshot broken, so treat it as a deliberate trade, not the fix.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (exit 134)
The V8 heap has its own ceiling, derived from system memory and the Node release, and it is often far below the RAM you have; when a build or server reaches it, V8 aborts with FATAL ERROR: Reached heap limit and exit code 134. Read the real limit with v8.getHeapStatistics().heap_size_limit, then raise it with --max-old-space-size (in MiB) or NODE_OPTIONS for a large workload, size it below the cgroup limit inside containers, and use --heapsnapshot-near-heap-limit to catch a leak in a long-running process. Exit 137 with no FATAL ERROR line is a container kill, not this.
Docker: "exec format error" when the container starts — wrong-platform image, no emulator, or a script with no shebang
The container exits on its first instruction with exec format error — the kernel's ENOEXEC, meaning the file exists but cannot be executed here. In practice that is an image built on one CPU architecture (an Apple-silicon Mac produces linux/arm64) and run on another (an x86_64 server) with no QEMU handler registered in binfmt_misc, or an entrypoint script whose first line is not a shebang. uname -m, docker image inspect and ls /proc/sys/fs/binfmt_misc tell the causes apart; the fix is an explicit docker buildx build --platform (or a manifest list for both), QEMU registration or --platform when you mean to emulate, and a #!/bin/sh line for the script.