Cloud Fundamentals: AWS Core Services

IAM Fundamentals

18 min Lesson 2 of 30

IAM Fundamentals

AWS Identity and Access Management (IAM) is the security backbone of every AWS account. Before a single API call reaches any AWS service — S3, EC2, RDS, anything — IAM intercepts it and answers one question: is this caller allowed to perform this action on this resource? Getting IAM wrong is the single most common root cause of AWS security breaches. Getting it right means you can give every team, every service, and every automated pipeline exactly the permissions they need and nothing more.

This lesson covers the four building blocks — Users, Roles, Policies, and Groups — and then walks through the exact evaluation logic AWS uses when it decides to allow or deny an API call. Understanding that logic is what separates engineers who cargo-cult IAM from engineers who design it.

Users

An IAM User is a long-term identity tied to a person or a legacy application. Each user gets permanent credentials: a password for the AWS Console and optionally an Access Key ID + Secret Access Key pair for API/CLI access. The word "long-term" is the risk: static credentials can be leaked, phished, or committed to a git repository. They do not expire on their own.

Big-tech standard for human access: use IAM Identity Center (SSO) instead of IAM Users. Identity Center federates your corporate IdP (Okta, Azure AD, Google Workspace) into temporary, short-lived AWS credentials. Engineers log in with their corporate account, get a time-boxed session, and there are no static access keys to leak. IAM Users remain relevant for legacy tooling that cannot do federation, and for the one root-account break-glass scenario.

Production pitfall — root account: The AWS root account (the email+password you used to create the account) has unconditional access to everything in the account — including cancelling it, changing billing, and removing all IAM policies. It bypasses every IAM restriction. At every company above startup scale, the root account password is stored in a vault, MFA is mandatory, and the access keys are deleted entirely. Never use root for day-to-day work. Create a dedicated admin IAM User or Identity Center user for that.

Groups

An IAM Group is a collection of users that share the same attached policies. You attach policies to the group, not to each individual user. When an engineer joins the platform team, you add them to the PlatformEngineers group and they instantly inherit the right permissions. When they move teams, you remove them from the group and the permissions are gone. This is operationally far cleaner than managing per-user policy attachments at scale.

Groups cannot be nested (a group cannot belong to another group), and they cannot be assumed by services or federated identities — that is what Roles are for.

Roles

An IAM Role is a temporary identity that any trusted principal can assume. Unlike a User, a Role has no password and no permanent credentials. When a principal assumes a role, AWS STS (Security Token Service) issues short-lived credentials — an Access Key, a Secret Key, and a Session Token — that expire in 15 minutes to 12 hours. The caller uses those temporary credentials for that session and discards them.

Roles are the right answer for virtually every non-human use case:

  • EC2 instance role: An EC2 instance needs to read objects from S3. You attach a role with s3:GetObject permission to the instance profile. The application running on the instance calls the EC2 metadata endpoint (169.254.169.254) to get auto-rotating temporary credentials. No static keys anywhere in the code or environment.
  • Lambda execution role: Every Lambda function has an execution role that defines what AWS services the function can call. The function gets temporary credentials injected automatically by the Lambda runtime.
  • Cross-account role: The deployment pipeline in Account A needs to deploy resources in Account B. Account B creates a role with a trust policy allowing Account A to assume it. The pipeline assumes the role, gets temporary credentials scoped to Account B, and deploys. No credentials ever leave Account B.
  • Service-to-service via IRSA: In EKS (Kubernetes on AWS), pods use IAM Roles for Service Accounts (IRSA). A Kubernetes Service Account is annotated with a role ARN; the pod gets OIDC-federated temporary credentials bound to that role. No access keys on the node, no shared secrets.
Pro practice: Default to Roles over Users for every automated workload. If you find yourself generating access keys for a script, Lambda, or CI/CD pipeline, stop and ask whether a role would work. It almost always does. Roles eliminate entire classes of credential-leak incidents. At Amazon, all services run under roles — static access keys in application code are a red flag in code review.

Policies

A Policy is a JSON document that defines permissions. It is the core unit of authorization in IAM. A policy contains one or more statements, each specifying: an Effect (Allow or Deny), a list of Actions (which API calls), a list of Resources (which ARNs), and optionally a Condition block (context constraints like MFA required, source IP range, time of day).

{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowS3ReadOnlyOnSpecificBucket", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::my-app-assets", "arn:aws:s3:::my-app-assets/*" ] }, { "Sid": "DenyDeleteUnlessMFA", "Effect": "Deny", "Action": [ "s3:DeleteObject", "s3:DeleteBucket" ], "Resource": "*", "Condition": { "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" } } } ] }

AWS ships hundreds of AWS Managed Policies (like AmazonS3ReadOnlyAccess). They are convenient but almost always too broad for production use — AmazonS3ReadOnlyAccess grants read on every bucket in the account. Production workloads should use Customer Managed Policies with the minimum required actions scoped to specific resource ARNs. This is the principle of least privilege, and it is non-negotiable at big-tech scale.

There are two attachment patterns:

  • Identity-based policies — attached to a User, Group, or Role. They say what that identity can do.
  • Resource-based policies — attached to a resource itself (an S3 bucket policy, an SQS queue policy, a KMS key policy). They say who can do what to this specific resource. Resource-based policies also enable cross-account access without role assumption.

Policy Evaluation Logic

When an API call arrives at AWS, IAM runs through a deterministic evaluation chain. Knowing this chain lets you debug unexpected denials and design policies correctly without trial and error.

IAM Policy Evaluation Logic Flow API Call 1. Explicit Deny? (SCP, identity or resource policy) YES DENY NO 2. SCP Allows? (AWS Orgs Service Control Policy) NO DENY YES 3. Resource Policy Allows? (bucket policy, KMS key policy…) YES + same acct ALLOW NO/other acct 4. Identity Policy Allows? (user, group, or role policy) YES ALLOW NO IMPLICIT DENY (default — nothing explicitly allowed)
IAM policy evaluation order: explicit Deny always wins first, then SCPs gate the action, then resource-based and identity-based policies are checked, and the default is always Deny.

The evaluation rules in plain English:

  1. Explicit Deny wins unconditionally. If any policy — identity-based, resource-based, SCP, or permissions boundary — contains a Deny statement that matches the request, the request is denied immediately. No other policy can override an explicit Deny.
  2. SCPs act as an account-level guardrail. If you are using AWS Organizations, a Service Control Policy (SCP) on the account or OU must allow the action. SCPs do not grant permissions; they only restrict the maximum permissions available. If an SCP does not explicitly allow an action, the action is denied even if an IAM policy does allow it.
  3. Resource-based policies can grant access on their own (same account). If a resource-based policy (e.g., an S3 bucket policy) grants the action to the requesting principal, and that principal is in the same AWS account, access is allowed — even without a matching identity-based policy. For cross-account access, both the resource-based policy on the target resource AND an identity-based policy on the calling principal must allow the action.
  4. Identity-based policies must allow the action if there is no resource-based policy grant.
  5. Implicit Deny is the default. If none of the above evaluation steps produces an explicit Allow, the request is denied. IAM does not grant permissions by default — you must be explicit.
Permissions boundaries add a sixth layer: they define the maximum permissions an identity-based policy can grant to a User or Role. Even if an identity policy allows s3:*, a permissions boundary that only permits s3:GetObject means the effective permission is s3:GetObject. Boundaries are used to delegate role creation to teams without giving them unlimited privilege escalation.

Practical IAM: Creating a Least-Privilege Role for a Lambda

The following is a production-grade pattern: a Lambda function that reads from one specific DynamoDB table and writes logs to CloudWatch. Nothing more. We create the role, the policy, and attach them — all via the AWS CLI.

# 1. Create the trust policy (who can assume this role — Lambda service) cat > /tmp/trust-policy.json <<'EOF' { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" }] } EOF # 2. Create the role aws iam create-role \ --role-name lambda-inventory-reader \ --assume-role-policy-document file:///tmp/trust-policy.json \ --description "Lambda role for inventory-reader function" # 3. Create a least-privilege inline policy cat > /tmp/lambda-policy.json <<'EOF' { "Version": "2012-10-17", "Statement": [ { "Sid": "DynamoDBReadOnly", "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/inventory" }, { "Sid": "CloudWatchLogs", "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/inventory-reader:*" } ] } EOF # 4. Attach the policy to the role aws iam put-role-policy \ --role-name lambda-inventory-reader \ --policy-name inventory-reader-policy \ --policy-document file:///tmp/lambda-policy.json # 5. Verify what the role can do — simulate an API call aws iam simulate-principal-policy \ --policy-source-arn arn:aws:iam::123456789012:role/lambda-inventory-reader \ --action-names dynamodb:GetItem \ --resource-arns arn:aws:dynamodb:us-east-1:123456789012:table/inventory

The simulate-principal-policy command is invaluable during debugging. It tells you exactly whether a given principal can perform a given action on a given resource — and which policy statement produced the decision — without making a real API call to the target service.

Common IAM Mistakes in Production

  • Wildcard actions on wildcard resources: "Action": "*", "Resource": "*" effectively makes a User or Role an account admin. Any policy containing this in a non-admin role is a critical finding in a security audit.
  • Access key rotation neglect: Long-term access keys must be rotated. AWS IAM Access Analyzer flags keys older than 90 days. Use aws iam list-access-keys to audit. Better: switch to roles and eliminate the problem entirely.
  • Confusing the trust policy with the permission policy: The trust policy on a role controls who can assume it. The permission policy controls what the role can do. Both must be correct. A role with no trust policy cannot be assumed by anyone.
  • Not scoping resource ARNs: "Resource": "arn:aws:s3:::*" grants the specified actions on every bucket in the account. Always specify the exact ARN — arn:aws:s3:::my-specific-bucket and arn:aws:s3:::my-specific-bucket/*.
IAM is eventually consistent: After you attach or detach a policy, the change propagates globally across all AWS data centers. In practice this takes seconds, but in rare cases it can take up to a minute. If your automation grants a role a new permission and immediately calls an API using that role, you may get an unexpected AccessDenied error. Add a brief wait or retry with exponential backoff in your deployment scripts after IAM changes.

What Comes Next

Lesson 3 moves to EC2 — the compute layer. Every EC2 instance you launch will have an IAM instance profile (a role container). The patterns from this lesson — trust policies, least-privilege permission policies, resource ARN scoping — apply directly to how you grant EC2 instances access to S3, Systems Manager, CloudWatch, and any other AWS service they need to call.