The problem roles solve
Your app on a cloud server needs to read files from S3. How does the program prove who it is? The tempting answer is to paste an IAM user's access keys into the code or an environment file. Now a long-lived secret sits on disk, gets copied into Git, leaks into logs, and works forever for whoever steals it.
You saw this pattern with Docker: baking secrets into an image is a known mistake, you inject configuration at runtime instead. AWS takes that idea further with roles.
A role is an identity with policies but no password and no permanent keys. Instead, a trusted thing (a server, a Lambda function, a person from another account) assumes the role and receives temporary credentials that expire automatically, usually within hours.
How it looks in practice
You attach a role to a server when you launch it. Code on that server just asks the AWS SDK for credentials, and the SDK fetches short-lived keys belonging to the role. Nothing to paste, nothing to leak, and a stolen credential dies on its own within hours.
You can always ask AWS who you currently are:
aws sts get-caller-identity
{
"Arn": "arn:aws:sts::123456789012:assumed-role/photo-app-role/i-0abc123"
}assumed-role in the ARN is the tell: this program is acting as the role photo-app-role, with temporary credentials. Rule of thumb: people get IAM users, programs get roles.
Why temporary credentials expire on their own
Temporary credentials carry a built-in expiry timestamp, and AWS runs this exact check on every request signed with them. Here credentials were issued at time 9000 seconds with a one-hour lifetime, and a request arrives at time 13000.
issued_at=9000 lifetime_s=3600 now=13000 expires_at=$((issued_at + lifetime_s)) if [ $now -lt $expires_at ]; then echo "credentials valid ($((expires_at - now))s left)" else echo "credentials expired $((now - expires_at))s ago - the SDK fetches fresh ones from the role" fi
Output
credentials expired 400s ago - the SDK fetches fresh ones from the roleThe credentials expired at 12600, so the request at 13000 arrives 400 seconds too late and is rejected. Nothing breaks, because the SDK notices the expiry and fetches a fresh set from the role before signing.
That automatic refresh is why a stolen role credential is worth so much less than a stolen permanent key. An attacker who copies it has at most a few hours of access, and no way to renew it, because renewal requires being the server the role is attached to.
The security advantage of a role over access keys
For code running on a server, the advantage is that role credentials are temporary and delivered automatically, so there is no long-lived secret to store, commit, or leak.
Policies work identically on both. A role and an IAM user can carry the exact same permissions, and IAM evaluates requests from either one by the same three rules. Nothing about what an identity can do changes.
The difference is credential lifetime. Access keys live until someone remembers to rotate them, which in practice can mean years. Role credentials expire in hours and never sit in your codebase at all.
That single difference kills the most common leak paths: Git history, config files, container images, and log output. None of them can capture a credential that was never written to disk.
Replacing a leaked access key on a server
After revoking the leaked key, the server should use an IAM role for S3 access.
Attach a role with least-privilege S3 permissions to the server, and the SDK picks up temporary credentials automatically. The config file entry disappears entirely, so there is nothing left to leak on the next accidental push.
Two principles are doing the work here. Programs should not hold permanent credentials at all, which rules out generating a fresh access key and pasting that in instead. And a role is assumed by the server itself, so the credential delivery path involves no human copying anything.
Two operational notes worth keeping. Revoke fast, because the window between a push and a scan by an attacker is measured in minutes rather than days. And assume anything ever pushed to a public repo is compromised permanently, since deleting the commit does not remove it from clones or caches.