Replace IAM user access keys with roles and short-lived credentials
For developers and platform engineers migrating workloads off long-lived IAM user keys. This walks you through finding active key usage, switching local dev, EC2/ECS/EKS, and CI/CD to role-based short-lived credentials, then disabling and deleting the old keys without breaking deployments.
TL;DR — Replace hard-coded or long-lived IAM user access keys by attaching roles to compute and by having humans and CI assume roles with STS-issued temporary credentials. The most common successful path is: create a least-privilege role, update the workload or profile to use that role, verify with
aws sts get-caller-identity, then deactivate the old access key before deleting it. Reading time: ~5 min
Goal
When you finish, your app, local development shell, and CI/CD jobs will authenticate to AWS with short-lived credentials from IAM roles or STS assume-role flows, and the old IAM user access keys will be disabled and removed without breaking the workload.
Prerequisites
- An AWS account and permission to manage IAM roles, policies, and users; at minimum:
iam:CreateRole,iam:AttachRolePolicy,iam:PassRole,iam:UpdateAccessKey,iam:DeleteAccessKey,sts:AssumeRole - AWS CLI v2 installed — check with:
aws --version
- A current admin or break-glass session to make IAM changes
- The IAM username or access key IDs you are replacing
- The workload location: local dev, EC2, ECS task, EKS pod, Lambda, or CI runner
- If used in CI/CD: access to your CI system’s secret settings and OIDC/workload identity settings
jqinstalled for a few verification commands — check with:
jq --version
Steps
Step 1: Find which IAM user keys are still in use
List the user’s keys and last-used timestamps.
USER_NAME=deploy-bot
aws iam list-access-keys --user-name "$USER_NAME"
aws iam get-access-key-last-used --access-key-id AKIAEXAMPLE123456789
Success looks like JSON showing the key IDs and a LastUsedDate/ServiceName pair for the key you are replacing.
If you do not know which user owns a key:
aws iam get-access-key-last-used --access-key-id AKIAEXAMPLE123456789
Success looks like output containing "UserName": "deploy-bot".
Step 2: Create a replacement role with the same minimum permissions
Create a trust policy for the principal that will assume the role. For a human or CI principal in the same account, start with this trust policy and replace the account ID and principal ARN.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/deploy-bot"
},
"Action": "sts:AssumeRole"
}
]
}
Save that as trust-policy.json, then create the role:
aws iam create-role \
--role-name deploy-bot-role \
--assume-role-policy-document file://trust-policy.json
Attach the same policy the user had, or attach a managed policy. Example with an inline least-privilege policy for S3 deploys:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-deploy-bucket",
"arn:aws:s3:::my-deploy-bucket/*"
]
}
]
}
aws iam put-role-policy \
--role-name deploy-bot-role \
--policy-name deploy-bot-inline \
--policy-document file://deploy-policy.json
Success looks like exit code 0 and no stderr output.
Step 3: Switch local development from static keys to assume-role
If developers currently use ~/.aws/credentials with long-lived keys, move the keys into a source profile only temporarily, and create a role profile that assumes the new role.
[profile legacy-user]
aws_access_key_id = AKIAEXAMPLE123456789
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
[profile dev-admin]
role_arn = arn:aws:iam::123456789012:role/deploy-bot-role
source_profile = legacy-user
region = us-east-1
Test it:
aws sts get-caller-identity --profile dev-admin
Success looks like an ARN ending in assumed-role/deploy-bot-role/..., not user/deploy-bot.
If your org uses AWS IAM Identity Center or another federated login already, use that as the source profile instead of a static key profile.
Step 4: Switch workloads on AWS compute to attached roles
For EC2, attach an instance profile role to the instance. In your cloud provider dashboard, go to your instance settings (for example, EC2 console: Instances → select instance → Actions → Security → Modify IAM role) and attach deploy-bot-role. Then on the instance, remove exported static credentials and test the metadata-backed role:
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
aws sts get-caller-identity
Success looks like an ARN with assumed-role/deploy-bot-role/i-....
For ECS, set the task role to deploy-bot-role in the task definition, deploy the new revision, then test in the container:
aws sts get-caller-identity
For EKS, use IAM Roles for Service Accounts or your cluster’s workload identity feature. Bind the Kubernetes service account used by the pod to deploy-bot-role, redeploy the pod, then test inside the pod:
kubectl exec deploy/my-app -- aws sts get-caller-identity
Success in all cases is an assumed-role/... ARN and no static credentials present in environment variables.
Step 5: Switch CI/CD from stored AWS keys to role assumption
Best option: use your CI platform’s OIDC/workload identity integration to assume the role without storing secrets. In your CI provider’s dashboard, enable OIDC/workload identity for the repository/project, then update the role trust policy to trust that OIDC provider and the specific repository/project subject. Because provider claim formats vary, use your CI provider’s exact issuer URL and subject claim values.
If you need an immediate bridge, have CI assume the role using the existing user key, then remove the key after OIDC is in place:
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/deploy-bot-role \
--role-session-name ci-build-$(date +%s) > /tmp/sts.json
export AWS_ACCESS_KEY_ID=$(jq -r '.Credentials.AccessKeyId' /tmp/sts.json)
export AWS_SECRET_ACCESS_KEY=$(jq -r '.Credentials.SecretAccessKey' /tmp/sts.json)
export AWS_SESSION_TOKEN=$(jq -r '.Credentials.SessionToken' /tmp/sts.json)
aws sts get-caller-identity
Success looks like an assumed-role ARN and an expiration timestamp in /tmp/sts.json.
Step 6: Deactivate the old access key before deleting it
⚠️ Deactivating a key can break running jobs, cron tasks, or old laptops immediately. Do this during a change window and watch logs/alerts for at least one credential refresh cycle.
Deactivate the key:
aws iam update-access-key \
--user-name "$USER_NAME" \
--access-key-id AKIAEXAMPLE123456789 \
--status Inactive
Success looks like exit code 0. Existing sessions may continue until their temporary credentials expire; new uses of the old key will fail.
Watch for failures in the app, CI, or CloudTrail. If something breaks, re-enable once, fix the remaining caller, and retry:
aws iam update-access-key \
--user-name "$USER_NAME" \
--access-key-id AKIAEXAMPLE123456789 \
--status Active
Step 7: Delete the old access key and remove it from config
After a full work cycle with no failures, delete the key:
aws iam delete-access-key \
--user-name "$USER_NAME" \
--access-key-id AKIAEXAMPLE123456789
Remove the old profile or environment variables from developer machines, CI secrets, .env files, and deployment manifests.
Success looks like aws iam list-access-keys --user-name "$USER_NAME" no longer returning that key ID.
Verify it works
Run these checks from each place that used the old key.
Local dev or CI:
aws sts get-caller-identity
aws configure list
Expected shape:
{
"UserId": "AROAXXXXXXXXXXXXX:ci-build-1754380000",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/deploy-bot-role/ci-build-1754380000"
}
aws configure list should show credentials coming from assume-role or environment/session values, not a long-lived shared credentials file entry for the deleted key.
Confirm the old key is unusable:
AWS_ACCESS_KEY_ID=AKIAEXAMPLE123456789 AWS_SECRET_ACCESS_KEY=bad-or-old aws sts get-caller-identity
Expected failure shape:
An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity operation: The security token included in the request is invalid.
Confirm the key is gone:
aws iam list-access-keys --user-name "$USER_NAME"
Expected result: the deleted key ID is absent.
Common pitfalls
Trust policy points at the wrong principal
Mistake: the role trust policy names the wrong user, account, OIDC provider, or subject claim.
Symptom: aws sts assume-role fails with:
An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::123456789012:user/deploy-bot is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::123456789012:role/deploy-bot-role
Fix: edit trust-policy.json so Principal and any conditions exactly match the caller, then run aws iam update-assume-role-policy --role-name deploy-bot-role --policy-document file://trust-policy.json.
Static environment variables override the role
Mistake: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are still exported in the shell, container, or CI job.
Symptom: aws sts get-caller-identity still shows arn:aws:iam::123456789012:user/deploy-bot or fails with an old key.
Fix: run unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN and remove those variables from the job or manifest.
EC2/ECS/EKS role attached, but app still uses hard-coded keys
Mistake: the runtime has a role, but the app config explicitly sets credentials.
Symptom: role attachment looks correct, but requests still fail after key deactivation.
Fix: delete credential fields from app config and let the AWS SDK default credential chain use instance/task/pod credentials.
Deleting the key before deactivating it first
Mistake: the key is deleted immediately, leaving no rollback path.
Symptom: production job starts failing and you cannot restore the same key.
Fix: create a temporary replacement path with role assumption, then cut over again using Inactive first and only delete after a clean observation window.
Session duration too short for the job
Mistake: the role’s max session duration or CI-requested session length is shorter than the deployment or batch job.
Symptom: jobs fail mid-run with expired token errors such as:
ExpiredToken: The security token included in the request is expired
Fix: increase the role’s max session duration and request a matching duration in the assume-role call, or split the job so it refreshes credentials between stages.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI