The three pillars of AWS security — explained with a building analogy, interactive examples, and zero assumed knowledge.
Before a single line of JSON, let's build a mental model that makes IAM click — permanently.
A permanent identity tied to a human being. One person, one User. Always.
| Credential type | Used for | Example |
|---|---|---|
| Password | Logging into the AWS Console website | You typing in a browser |
| Access Key ID + Secret | CLI commands and SDK calls from code | aws s3 ls in terminal |
An identity that AWS services assume. No password, no access keys — AWS rotates credentials automatically.
Imagine your Lambda function needs to read from S3. You might think: "I'll create a User called 'lambda-user' and hardcode its access keys in the function." This is a serious security mistake:
// WRONG — never do this const s3 = new S3Client({ credentials: { accessKeyId: "AKIA...", secretAccessKey: "..." } }); // CORRECT — attach a Role to your Lambda/EC2, then just: const s3 = new S3Client({}); // SDK auto-picks up Role credentials
Every Role has two separate policies. The Trust Policy answers "who can pick up this badge?" The Permission Policy answers "what rooms can they enter?". You need both.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com" // only Lambda can assume this
},
"Action": "sts:AssumeRole"
}]
}
lambda.amazonaws.com · ec2.amazonaws.com · bedrock.amazonaws.com · ecs-tasks.amazonaws.com| Service | How to attach a Role | Where in console |
|---|---|---|
| Lambda | Set Execution Role when creating, or edit afterwards | Configuration → Permissions |
| EC2 | Attach an IAM Instance Profile to the instance | Actions → Security → Modify IAM role |
| ECS / Fargate | Set Task Role in Task Definition | Task Definition → Task role |
| Bedrock Agent | Assign a service role with Lambda invoke permissions | Agent creation wizard → Role |
| IAM User | IAM Role | |
|---|---|---|
| Who uses it | A human being | An AWS service (Lambda, EC2…) |
| Credentials | Password + access keys | No static credentials — tokens auto-generated |
| Permanence | Permanent until deleted | Credentials expire (15 min–12 hrs) |
| Console login | Yes | No |
| MFA support | Yes — always enable | N/A (no login) |
| Best practice | One per human, least privilege | One per service, least privilege |
The JSON document that defines exactly what actions are allowed or denied, on which resources.
| Field | What it means | Building analogy |
|---|---|---|
| Effect | Allow or Deny — is this permission granted or blocked? | "Access: Allowed" or "Access: Denied" stamped on the door sign |
| Action | Which AWS API calls are allowed (e.g. s3:GetObject, ec2:DescribeInstances) | "Can read files" / "Can print files" — specific verbs, not blanket access |
| Resource | Which specific AWS resource (identified by ARN). Use * for all, but avoid in production | "Applies to Room 302 only" vs "Applies to entire floor 3" |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadMyDocsBucket", // a friendly name for this rule
"Effect": "Allow", // Allow or Deny
"Action": [
"s3:GetObject", // read a file
"s3:ListBucket" // list files in the bucket
],
"Resource": [
"arn:aws:s3:::my-docs-bucket", // the bucket itself
"arn:aws:s3:::my-docs-bucket/*" // all objects inside
]
}
]
}
AmazonS3ReadOnlyAccess, AWSLambdaBasicExecutionRole. Good for getting started — but may include more permissions than you actually need.An ARN is the unique postal address of any AWS resource. You use them in Policy Resource fields to say exactly which resource the policy applies to.
arn : aws : s3 : : : my-bucket arn : aws : lambda : us-east-1 : 123456789012 : function:myFunc arn : aws : iam : : 123456789012 : role/MyRole ^ ^ ^ ^ ^ ^ prefix cloud service region account-id resource
Deny statement overrides any number of Allow statements. Use it to create hard guardrails — e.g. "nobody can ever delete production S3 buckets, regardless of any other policy".Select a service and actions to see the policy JSON update in real time. A starting point for your own policies.
* in Resource with specific ARNs for production use.See exactly how Users, Roles, and Policies combine in common AI and MCP setups.
{
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets", "s3:ListBucket", "s3:GetObject",
"ec2:DescribeInstances", "ec2:DescribeRegions"
],
"Resource": "*" // tighten to specific ARNs in production
}]
}
"ec2.amazonaws.com" as the Principal. The EC2 instance automatically picks up the Role's credentials — no code changes needed.{
"Statement": [
{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:*:*:*" // CloudWatch Logs — always needed
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
},
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
"Resource": "arn:aws:dynamodb:us-east-1:123456:table/AgentMemory"
}
]
}
{
"Statement": [
{
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-*"
},
{
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:123456:function:my-tool-*"
}
]
}
"bedrock.amazonaws.com" as the Principal service.{
"Statement": [
{
"Sid": "DeveloperAccess",
"Effect": "Allow",
"Action": [
"lambda:*",
"logs:*",
"s3:Get*", "s3:List*",
"ec2:Describe*",
"bedrock:InvokeModel"
],
"Resource": "*"
},
{
"Sid": "DenyProduction", // explicit deny overrides everything
"Effect": "Deny",
"Action": ["s3:Delete*", "iam:*"],
"Resource": "*"
}
]
}
Six questions. Click an answer to get instant feedback explaining why it is right or wrong.
Five principles that prevent 90% of security incidents and "Access Denied" headaches.
s3:GetObject — not s3:*, not *. The smaller the blast radius, the safer you are when something goes wrong.Action: "*" on Resource: "*"