Why Long-Lived AWS Access Keys Are Dangerous (and What to Use Instead)
A long-lived AWS access key is a permanent credential (an access key ID and secret access key) that belongs to an IAM user and never expires. It works from anywhere on the internet, inherits every permission the user has, and stays valid until someone manually deletes it. These properties make long-lived keys one of the most common causes of AWS account compromise.
If your workload runs inside AWS, use an IAM role instead of an access key. If it runs outside AWS, use STS temporary credentials, OIDC federation, or IAM Identity Center. Long-lived access keys should be a last resort, not a default.
Simple explanation
When you create an IAM user and generate an access key, AWS gives you two strings: an access key ID (starts with AKIA) and a secret access key. Together, they work like a username and password for the AWS API. Scripts, CLI tools, and applications use them to authenticate.
The problem is that these credentials are permanent. Unlike a browser session that expires when you log out, an access key stays valid for months or years until someone remembers to delete it. If that key ends up somewhere it should not be (a Git commit, a log file, a Slack message), anyone who finds it gets the same access as the IAM user who owns it.
Temporary credentials, issued through AWS STS, solve this by expiring automatically. A leaked temporary credential is useless after a few hours. A leaked access key is usable forever.
Analogy
Think of it like a hotel key card vs a house key. The hotel card stops working at checkout. If someone finds it on the floor, they have a narrow window before it is useless. A house key works forever. If someone copies it, they can walk in any time, any day, until you physically change the locks.
Why long-lived access keys are dangerous
Four properties make long-lived access keys uniquely risky:
- No automatic expiry. A password session ends. An STS credential expires. An access key stays valid until someone manually deactivates or deletes it. Keys created years ago and forgotten are still fully functional.
- Usable from anywhere. There is no IP restriction by default. An attacker with your key can make API calls from any network, any country, at any time. AWS cannot distinguish their request from yours.
- Full IAM user permissions. The key inherits every policy attached to the IAM user. If the user has
AdministratorAccess, the attacker has administrator access to your entire account. - Easy to leak. Access keys are short strings that end up in code, config files, Docker images, environment variables, CI/CD logs, and third-party tools. Every copy is another place the key can escape.
Most leaked access keys are not stolen by sophisticated attackers. They are accidentally published by the people who created them. A single git push with a .env file is all it takes.
How access keys get leaked
Key leaks follow predictable patterns. Understanding them is how you prevent them.
Committed to Git repositories
This is the most common leak. A developer puts AWS_ACCESS_KEY_ID in a .env file for local testing. The file gets committed and pushed to GitHub. Automated scanners, both legitimate (GitHub Secret Scanning, AWS) and malicious (credential harvesting bots), find the key within minutes.
Git history is permanent. Even if you force-push a cleaned branch, anyone who cloned or forked the repo before the cleanup still has the key. Any repository that ever contained a key in its history should be treated as compromised. Rotate the key immediately.
Hardcoded in application source code
Instead of using environment variables or Secrets Manager, a developer writes client = boto3.client(‘s3’, aws_access_key_id=‘AKIA…’) directly in the source. This ends up in build artifacts, Docker images pushed to public registries, or log output.
Exposed in CI/CD logs
Access keys passed as environment variables to CI/CD jobs appear in build logs when a step prints all environment variables on failure, or when the key is embedded in a URL or command argument. See managing secrets in CI/CD pipelines for how to handle this correctly.
The safest CI/CD setup stores zero AWS secrets. Use OIDC federation (for GitHub Actions, GitLab, etc.) so that temporary credentials are issued per job. No key ever touches the pipeline configuration. See secure CI/CD pipelines for setup details.
Stored in S3 buckets or public snapshots
Credential files copied to S3 buckets with overly permissive access policies, or EC2 AMIs and EBS snapshots shared publicly while containing ~/.aws/credentials files, are found regularly by automated scanners.
Shared with third-party services
Access keys given to third-party SaaS tools (monitoring, analytics, deployment services) give those tools ongoing access to your account. If the third party is breached, your key is exposed. Use cross-account role assumption instead when the tool supports it.
Passed through chat and documentation
Keys shared in Slack messages, Confluence pages, support tickets, or email are stored in systems you do not control and cannot force-expire. Treat any key shared through a communication channel as compromised.
Analogy
Every time you paste an access key into a new system, you are photocopying a master key and handing it to someone. You cannot un-photocopy a key. You cannot track every copy. And every copy works just as well as the original.
What attackers do with leaked keys
The gap between a key being leaked and being exploited is often measured in minutes. Automated bots continuously scan public repositories, paste sites, and other sources for the AKIA prefix pattern. When a key is found, it is tested within seconds.
Analogy
A leaked access key is like dropping your house key on a busy street along with a note listing your address, alarm code, and which drawer has the valuables. The attackers do not need to be skilled. They just need to pick it up.
Common attacker actions:
- Crypto mining. Spin up large EC2 instances in every available region. Bills reach tens of thousands of dollars within hours.
- Data exfiltration. Download data from S3 buckets, DynamoDB tables, RDS databases, and Secrets Manager.
- Backdoor creation. Create new IAM users or roles with
AdministratorAccessfor persistent access, even after the original key is rotated. This is why post-compromise cleanup requires checking for suspicious activity beyond the key itself. - Ransomware and destruction. Delete backups, snapshots, and databases, then demand payment.
- Cross-account movement. If the compromised user has cross-account role trust, the attacker can pivot into other AWS accounts.
Researchers have tested this by committing dummy AWS keys to public GitHub repos. The median time to first unauthorized API call was under 4 minutes. Bots are watching every public commit in real time.
(1) Deactivate the key with aws iam update-access-key —status Inactive. (2) Review CloudTrail logs for API calls made with that key. (3) Check CloudTrail log types for new IAM users, roles, or access keys the attacker may have created. (4) Delete the compromised key once confirmed no legitimate workloads depend on it.
How it works: why IAM roles and STS are safer
The fundamental difference is credential lifetime.
IAM users vs IAM roles
An IAM user has permanent credentials: a password for console access and optionally one or two access keys for API access. These credentials persist until manually changed.
An IAM role has no permanent credentials at all. When a service (EC2, Lambda, ECS) or external identity (GitHub Actions, an on-premises server) assumes a role, AWS STS issues a set of temporary credentials: an access key ID starting with ASIA, a secret access key, and a session token. These expire automatically, typically within 1 to 12 hours.
Long-lived user keys start with AKIA. Temporary STS keys start with ASIA. If you see AKIA in a config file or log, that is a permanent key and a potential risk.
Why expiry matters
A leaked long-lived key gives an attacker unlimited time. A leaked temporary credential gives them hours at most, and often less because the credential may already be near expiry when found. This dramatically reduces the blast radius of any leak.
Why roles reduce operational burden
With access keys, you need to store the secret securely, distribute it to the right systems, rotate it periodically, and revoke it when no longer needed. Each step is a potential failure point.
With roles, AWS handles credential issuance and rotation automatically. There is no secret to store, no rotation schedule to manage, and no key to revoke. The workload assumes the role, and STS handles the rest. This is why least privilege is easier to maintain with roles: you scope the role’s permissions and let AWS manage the credentials.
Analogy
An IAM role is like a building that issues visitor badges at the front desk. Each badge works for a few hours and then deactivates. An access key is like giving someone a permanent employee ID card. If they lose it, it works indefinitely until someone in HR remembers to cancel it.
When to use long-lived access keys
Long-lived access keys are not the default choice. They are a fallback for situations where temporary credentials are not available.
Narrow cases where long-lived keys may be appropriate
- Local development when IAM Identity Center (SSO) is not set up and you need CLI access from your laptop.
- On-premises workloads that cannot use IAM Roles Anywhere because of certificate infrastructure constraints.
- Third-party integrations that require static credentials and do not support role assumption or OIDC.
In all three cases, scope the key to the minimum permissions needed, set a rotation policy, and monitor usage through CloudTrail.
When you should not use long-lived keys
- EC2 instances - use an instance profile (IAM role).
- Lambda functions - use an execution role.
- ECS and EKS workloads - use task roles or IAM Roles for Service Accounts (IRSA).
- GitHub Actions - use OIDC federation. See GitHub Actions for AWS.
- Other CI/CD systems - use OIDC where supported, or STS AssumeRole with short-lived credentials. See secure CI/CD pipelines.
- Any workload running inside AWS - a role is always available.
If the workload can assume a role, it should. The only reason to use a long-lived key is when you have genuinely exhausted the alternatives.
Long-lived access keys vs safer alternatives
| Credential type | Where it fits | Expires automatically | Secrets to store | Operational overhead | Recommendation |
|---|---|---|---|---|---|
| Long-lived IAM user access key | Fallback for local dev, legacy on-prem, some third-party tools | No | Yes (key ID + secret) | High: manual rotation, secure storage, revocation | Avoid unless no alternative exists |
| IAM role on AWS workloads | EC2, Lambda, ECS, EKS, CodeBuild | Yes (STS credentials, typically 1-12 hours) | None | Low: AWS manages credential lifecycle | Recommended for all AWS workloads |
| STS temporary credentials (AssumeRole) | Cross-account access, privilege elevation, external systems calling AWS | Yes (configurable, up to 12 hours) | Caller needs initial credentials to call AssumeRole | Medium: requires trust policy setup | Recommended for cross-account and external access |
| GitHub OIDC federation | GitHub Actions workflows deploying to AWS | Yes (per-job STS credentials) | None. No AWS secrets in GitHub | Low: one-time OIDC provider setup | Recommended for all GitHub Actions |
| IAM Identity Center (SSO) | Developer CLI/console access across multiple accounts | Yes (session-based) | None. Authenticates via identity provider | Medium: requires AWS Organizations and IdP setup | Recommended for human access |
| IAM Roles Anywhere | On-premises or non-AWS workloads with PKI | Yes (X.509 certificate-based, STS credentials issued) | X.509 certificate (managed by your PKI) | Medium: requires certificate authority trust | Recommended for on-prem workloads with PKI |
If your workloads run on EC2, Lambda, or ECS, start with IAM roles. They require the least setup and eliminate access keys entirely. If you deploy from GitHub Actions, add OIDC next. These two changes alone remove the majority of long-lived key risk in most accounts.
Example: replacing access keys in GitHub Actions with OIDC
Instead of storing AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as GitHub Secrets, you configure GitHub to present an OIDC token that AWS exchanges for temporary credentials. No secrets to store, nothing to rotate.
# .github/workflows/deploy.yml
jobs:
deploy:
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
aws-region: us-east-1The IAM role’s trust policy grants GitHub’s OIDC provider permission to assume it:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:*"
}
}
}
]
}With this setup, no access key exists. There is nothing to leak, nothing to rotate, and nothing stored in GitHub Secrets. For a deeper walkthrough, see GitHub Actions for AWS deployments.
How to audit and remove existing access keys
If your account already uses long-lived access keys, follow this sequence to migrate away safely.
1. Inventory all keys
aws iam generate-credential-report
aws iam get-credential-report \
--query 'Content' --output text | base64 --decode > credential-report.csvThe CSV shows every IAM user, whether they have access keys, when each key was created, and when it was last used. Sort by access_key_1_last_used_date and look for keys older than 90 days, keys never used, and keys not used in 30+ days. These are your highest-priority cleanup targets.
2. Find unused or old keys
# List all access keys for all users
for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do
echo "=== $user ==="
aws iam list-access-keys --user-name "$user" \
--query 'AccessKeyMetadata[*].[AccessKeyId,Status,CreateDate]' \
--output table
doneFocus on keys with a CreateDate older than 90 days, and any key with Status: Active that has not been used recently.
3. Check what each key is used for
aws iam get-access-key-last-used \
--access-key-id AKIAIOSFODNN7EXAMPLEThe LastUsedDate and ServiceName fields tell you which AWS service the key accessed most recently. Use this plus CloudTrail logs to identify the workload that depends on the key.
4. Replace with roles or temporary credentials
For each workload using a key, pick the right replacement:
EC2? Attach an instance profile. Lambda? Assign an execution role. ECS/EKS? Use task roles or IRSA. GitHub Actions? Set up OIDC. On-prem? Use IAM Roles Anywhere or short-lived STS credentials. See the comparison table above to match each workload to its replacement.
5. Deactivate, verify, delete
# Deactivate first, do not delete immediately
aws iam update-access-key \
--user-name deploy-bot \
--access-key-id AKIAIOSFODNN7EXAMPLE \
--status Inactive
# Wait and verify no workloads break (1-2 weeks recommended)
# Once confirmed safe, delete
aws iam delete-access-key \
--user-name deploy-bot \
--access-key-id AKIAIOSFODNN7EXAMPLEDeactivating gives you a rollback path. If something breaks unexpectedly, you can reactivate the key while you fix the remaining dependency. Once everything works without the key for 1-2 weeks, delete it permanently.
Common mistakes
- Adding .env to .gitignore after the first commit. Git tracks full history. If a file with credentials was committed even once, the credentials are in the repository forever. You must rotate the key immediately. If the repository was ever public, treat the key as fully compromised.
- Thinking private repos are safe for credentials. Private repositories can be made public accidentally, forked by a contractor, or accessed by a compromised developer account. Credentials should never be stored in any repository, private or public. Use Secrets Manager or environment-injected secrets instead.
- Rotating a key without updating all consumers. Creating a new key, updating one script, and forgetting another means the old key stays active to avoid breaking the forgotten system. Before deleting the old key, list every system that uses it and confirm each one is migrated.
- Rotating without eliminating the design problem. If you rotate a long-lived key and replace it with another long-lived key, you have not reduced risk. You have just restarted the clock. Rotation is a stopgap. The goal is to replace the key with a role or temporary credentials so there is nothing to rotate.
- Using one access key across all environments. Dev, staging, and prod should have separate credentials and separate IAM users. A leak in dev should not give an attacker access to production data.
- Not enabling GuardDuty. AWS GuardDuty detects anomalous access key usage: calls from unusual IPs, unusual regions, or unusual hours. Without it, you have no real-time alerting on compromised keys.
The majority of five-figure AWS bills from compromised accounts trace back to a single access key that was committed to a public repository. If you do nothing else after reading this page, run git log —all -p | grep AKIA on every repository your team maintains. If you find a match, rotate that key today.
Summary
- Long-lived access keys never expire and work from anywhere. Once leaked, they stay valid until manually revoked.
- Common leak paths: Git commits, hardcoded source code, CI/CD logs, S3 buckets, third-party integrations, and chat messages.
- Attackers exploit leaked keys for crypto mining, data theft, backdoor creation, and ransomware, often within minutes.
- For workloads inside AWS, use IAM roles. They issue short-lived credentials automatically with no key to manage.
- For CI/CD, use OIDC federation to eliminate stored secrets entirely.
- For developer access, use IAM Identity Center instead of personal access keys.
- Audit existing keys with the IAM credential report. Deactivate before deleting to allow safe rollback.
Frequently asked questions
Are long-lived AWS access keys ever acceptable?
In narrow cases, yes. Local development machines where IAM Identity Center is not available, specific on-premises workloads that cannot use IAM Roles Anywhere, and certain third-party integrations that require static credentials. Even then, scope the key to the minimum permissions needed, rotate it regularly, and monitor usage through CloudTrail.
What should I use instead of access keys on EC2, Lambda, or ECS?
IAM roles. EC2 uses instance profiles, Lambda uses execution roles, and ECS uses task roles. All of them receive temporary credentials from STS automatically. There is no key to store, rotate, or leak.
How do I replace AWS access keys in GitHub Actions?
Configure OIDC federation between GitHub and your AWS account. Create an IAM role with a trust policy that accepts tokens from GitHub OIDC provider, then use the aws-actions/configure-aws-credentials action with role-to-assume. No secrets are stored in GitHub at all.
What should I do if an AWS access key was exposed publicly?
Act immediately. Deactivate the key with aws iam update-access-key --status Inactive. Review CloudTrail logs for any API calls made with that key. Check for new IAM users, roles, or access keys the attacker may have created as backdoors. Delete the compromised key. If the key had broad permissions, assume the account is compromised and investigate thoroughly.
What is the difference between an access key and STS temporary credentials?
An access key belongs to an IAM user and never expires until manually deleted. STS temporary credentials are issued by assuming an IAM role and expire automatically, typically within 1 to 12 hours. Temporary credentials also include a session token in addition to the key ID and secret. If temporary credentials leak, the damage window is bounded by their expiry time.