IAM Users, Access Keys & When to Avoid Them
An IAM user is a long-lived identity, and its access keys are static secrets your SDK can sign requests with.
Search across all documentation pages
An IAM user is a long-lived identity, and its access keys are static secrets your SDK can sign requests with.
They work, but they are the riskiest way to authenticate - this page shows how they function and why a role is usually the better call.
An IAM user represents a single person or application with permanent credentials attached directly to it.
Its access key is a pair - an access key ID plus a secret access key - that never expires on its own.
That permanence is the problem. A key committed to Git, baked into an image, or pasted in Slack stays valid until a human notices and deactivates it.
Roles solve this by issuing temporary credentials through STS that expire in minutes to hours and rotate automatically.
The practical rule: use access keys only when nothing can assume a role, and even then rotate them on a schedule.
Quick-reference recipe card - copy-paste ready.
# --- Python (boto3) ---
import boto3
iam = boto3.client("iam")
# Create a key for an existing user (returns the secret ONCE).
key = iam.create_access_key(UserName="ci-deployer")["AccessKey"]
print(key["AccessKeyId"], key["SecretAccessKey"])
# List a user's keys to audit age and status.
for k in iam.list_access_keys(UserName="ci-deployer")["AccessKeyMetadata"]:
print(k["AccessKeyId"], k["Status"], k["CreateDate"])// --- TypeScript (AWS SDK v3) ---
import {
IAMClient, CreateAccessKeyCommand, ListAccessKeysCommand,
} from "@aws-sdk/client-iam";
const iam = new IAMClient({});
const { AccessKey } = await iam.send(
new CreateAccessKeyCommand({ UserName: "ci-deployer" }));
console.log(AccessKey?.AccessKeyId, AccessKey?.SecretAccessKey);
const { AccessKeyMetadata } = await iam.send(
new ListAccessKeysCommand({ UserName: "ci-deployer" }));
AccessKeyMetadata?.forEach((k) =>
console.log(k.AccessKeyId, k.Status, k.CreateDate));When to reach for this:
A safe two-key rotation: create a new key, verify it, then deactivate and delete the old one with zero downtime.
# --- Python (boto3) ---
import boto3
from botocore.exceptions import ClientError
USER = "ci-deployer"
OLD_KEY_ID = "AKIAOLDEXAMPLE12345"
iam = boto3.client("iam")
# 1. Create the replacement key.
new_key = iam.create_access_key(UserName=USER)["AccessKey"]
print("Deploy this new key to the app:", new_key["AccessKeyId"])
# 2. After the app is running on the new key, disable the old one first.
iam.update_access_key(UserName=USER, AccessKeyId=OLD_KEY_ID, Status="Inactive")
# 3. If nothing breaks, delete the old key permanently.
try:
iam.delete_access_key(UserName=USER, AccessKeyId=OLD_KEY_ID)
print("Old key deleted.")
except ClientError as e:
print("Delete failed, re-activate if needed:", e.response["Error"]["Code"])// --- TypeScript (AWS SDK v3) ---
import {
IAMClient, CreateAccessKeyCommand,
UpdateAccessKeyCommand, DeleteAccessKeyCommand,
} from "@aws-sdk/client-iam";
const USER = "ci-deployer";
const OLD_KEY_ID = "AKIAOLDEXAMPLE12345";
const iam = new IAMClient({});
// 1. Create the replacement key.
const { AccessKey } = await iam.send(
new CreateAccessKeyCommand({ UserName: USER }));
console.log("Deploy this new key to the app:", AccessKey?.AccessKeyId);
// 2. After the app runs on the new key, disable the old one first.
await iam.send(new UpdateAccessKeyCommand({
UserName: USER, AccessKeyId: OLD_KEY_ID, Status: "Inactive",
}));
// 3. If nothing breaks, delete the old key permanently.
try {
await iam.send(new DeleteAccessKeyCommand({
UserName: USER, AccessKeyId: OLD_KEY_ID,
}));
console.log("Old key deleted.");
} catch (e: any) {
console.log("Delete failed, re-activate if needed:", e.name);
}What this demonstrates:
Inactive) before deleting gives you a safe rollback window.AKIA for a user's long-lived key and ASIA for temporary STS credentials.Status is Active or Inactive until a human or automation changes it.| Aspect | IAM user access key | Role (STS) credentials |
|---|---|---|
| Lifetime | Permanent until deleted | Minutes to hours |
| Rotation | Manual | Automatic |
| Secret stored in app | Yes | No (on AWS compute) |
| Blast radius if leaked | Large, indefinite | Small, time-boxed |
| Best for | Off-AWS legacy systems | Almost everything else |
# --- Python (boto3) ---
# Explicit keys are an anti-pattern; prefer a named profile so the SDK
# can refresh role credentials automatically.
import boto3
session = boto3.Session(profile_name="ci-deployer-role")
s3 = session.client("s3")// --- TypeScript (AWS SDK v3) ---
// Same idea: let a profile-backed provider handle refresh for you.
import { S3Client } from "@aws-sdk/client-s3";
import { fromIni } from "@aws-sdk/credential-providers";
const s3 = new S3Client({ credentials: fromIni({ profile: "ci-deployer-role" }) });list_access_keys metadata.| Alternative | Use When | Don't Use When |
|---|---|---|
IAM role + STS assume_role | Any code that can call STS or run on AWS | The caller genuinely cannot assume a role |
| Instance/task/Lambda role | Code runs on EC2, ECS, or Lambda | Running entirely outside AWS |
| IAM Identity Center (SSO) | Human developers needing CLI/console access | Machine-to-machine automation |
| IAM user access keys | Off-AWS legacy systems with no role path | A role-based option exists (it usually does) |
They never expire on their own. If one leaks, it stays valid until a human deactivates it, so the exposure window can be indefinite. Temporary role credentials expire automatically and shrink that window to minutes or hours.
No. The secret is returned only once, at creation. If you lose it, create a new key and delete the old one - there is no recovery path.
Two. This limit exists specifically so you can rotate with overlap: create the second, migrate to it, then remove the first.
Inactive.Look at the access key ID prefix: AKIA is a user's long-lived key, ASIA is a temporary credential issued by STS.
No. UpdateAccessKey with Status="Inactive" disables the key but keeps it, so you can re-activate it as a rollback. DeleteAccessKey removes it permanently.
Prefer not to. On AWS compute, use the attached role so there is no stored secret at all. If you must store one, keep it in a secrets manager, never in code or an image layer.
A role assumed via STS, or the instance/task/Lambda role for code running on AWS. Both deliver temporary, auto-rotating credentials with no secret in your codebase.
Yes, narrowly - for systems outside AWS that cannot assume a role, or legacy tooling during migration. Even then, scope permissions tightly and rotate on a schedule.
Call list_access_keys and read the CreateDate and Status for each key. Alert on any key older than your rotation policy allows.
Its requests immediately fail authentication. That is why you deactivate first and verify nothing breaks before deleting - deletion is irreversible.
Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Reviewed by Chris St. John·Last updated Jul 24, 2026