Least Privilege & Roles Over Keys
Grant the minimum permissions each identity needs, and reach for short-lived roles instead of long-lived access keys wherever you possibly can.
If there is one habit that separates a tidy AWS account from a scary one, it is this: every identity gets exactly the permissions it needs, and not one action more. It sounds obvious, yet the most common way accounts get compromised is not some exotic hack — it is an over-broad policy plus a leaked key.
Start from zero, add on demand
Resist the urge to attach AdministratorAccess "just to get unblocked". Start with nothing and add specific actions as real errors tell you what is missing. When you are unsure what an app actually uses, turn on IAM Access Analyzer and let it generate a policy from observed CloudTrail activity — it writes a tighter policy than most humans would.
Roles beat keys, every time
Long-lived access keys are a liability: they sit in ~/.aws/credentials, in CI secrets, sometimes (sadly) in Git history. A role hands out temporary credentials that expire on their own, so there is nothing durable to steal.
- EC2, ECS and Lambda — attach an IAM role, never bake in keys.
- Humans — sign in through IAM Identity Center (SSO) and assume roles.
- CI/CD — use OIDC federation so GitHub Actions assumes a role with no stored secret at all.
Guardrails that scale
In an AWS Organization, wrap accounts in Service Control Policies (SCPs). They set the outer boundary — a member account literally cannot exceed it, no matter what an individual IAM policy says. Least privilege at the org level.
Example
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadWriteOwnBucketOnly",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::acme-reports",
"arn:aws:s3:::acme-reports/*"
]
},
{
"Sid": "DenyIfNotTLS",
"Effect": "Deny",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
}
}
]
}When to use it
- A Lambda function is given only the s3:GetObject permission on a specific bucket rather than AmazonS3FullAccess, limiting damage if the function is exploited.
- An EC2 instance profile grants only SSM permissions so developers can connect via Session Manager without needing an SSH key at all.
- A CI/CD pipeline uses an IAM role with time-limited STS credentials instead of a static access key, so leaked credentials expire automatically.
More examples
Scope Policy to a Single Resource
Restricts read access to a single key prefix within one bucket, applying least-privilege at the resource level.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::reports-bucket/2024/*"
}]
}Use Instance Profile Instead of Keys
Attaches an IAM role to an EC2 instance so the application authenticates via the metadata service with no keys on disk.
# Attach a role to an EC2 instance (no keys in the OS)
aws ec2 associate-iam-instance-profile \
--instance-id i-0abc123 \
--iam-instance-profile Name=AppServerRole
# The instance can now call AWS APIs without any keys stored on disk
aws sts get-caller-identity # run from inside the instanceSimulate Policy Before Applying
Simulates dangerous actions against a draft policy before attaching it, confirming they are denied by the least-privilege design.
aws iam simulate-custom-policy \
--policy-input-list file://my-policy.json \
--action-names s3:DeleteBucket ec2:TerminateInstances \
--query 'EvaluationResults[].{Action:EvalActionName,Decision:EvalDecision}' \
--output table
Discussion