IAM, STS & Security for Developers
A comprehensive deep dive into AWS IAM and STS — policies, roles, permission boundaries, credential chain, cross-account access, STS API calls, confused deputy, and security best practices for the DVA-C02 exam.
What is AWS IAM?
AWS Identity and Access Management (IAM) is the global authorization system for AWS. Every API call to any AWS service is evaluated by IAM — it determines whether the caller is authenticated (who are you?) and authorized (are you allowed to do this?).
Core mental model: IAM is a bouncer for every AWS service. Before any action executes, IAM checks: does the identity making this request have a policy that explicitly allows this action on this resource?
IAM Core Components
| Component | Description |
|---|---|
| User | A person or application with long-term credentials (access key + secret). Avoid for applications — use roles instead. |
| Group | Collection of users. Policies attached to groups apply to all members. |
| Role | An identity with temporary credentials. Assumed by services, applications, users, or other accounts. |
| Policy | JSON document defining permissions. Attached to users, groups, or roles. |
IAM Policy Structure
Every IAM policy is a JSON document with one or more statements:
1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Sid": "AllowS3ReadOnMyBucket",
6 "Effect": "Allow",
7 "Action": [
8 "s3:GetObject",
9 "s3:ListBucket"
10 ],
11 "Resource": [
12 "arn:aws:s3:::my-bucket",
13 "arn:aws:s3:::my-bucket/*"
14 ],
15 "Condition": {
16 "StringEquals": {
17 "s3:prefix": ["uploads/"]
18 }
19 }
20 },
21 {
22 "Sid": "DenyDeleteEverywhere",
23 "Effect": "Deny",
24 "Action": "s3:DeleteObject",
25 "Resource": "*"
26 }
27 ]
28}Policy evaluation logic:
- Default is implicit Deny — nothing is allowed unless explicitly permitted
- An explicit Allow grants access
- An explicit Deny always wins — overrides any Allow, including from other policies
Policy Types
| Type | Attached to | Scope | Notes |
|---|---|---|---|
| Identity-based | User / Group / Role | What the identity can do | Most common |
| Resource-based | S3 bucket, SQS queue, Lambda, KMS key, etc. | Who can access this resource | Enables cross-account without role assumption |
| Permission Boundary | User or Role | Max permissions ceiling | Cannot grant more than boundary allows |
| SCP (Service Control Policy) | AWS Organization OU / Account | Account-level guardrail | Applied before identity policies |
| Session Policy | AssumeRole call | Further restricts session | Cannot expand role permissions |
| ACL | S3, VPC | Legacy resource control | Deprecated in favor of bucket policies |
Identity-Based vs Resource-Based Policies
Same-account access: Either identity policy OR resource policy is sufficient (logical OR).
Cross-account access: Both identity policy AND resource policy must allow (logical AND) — OR use role assumption.
1// S3 bucket policy (resource-based) — allow cross-account read
2{
3 "Version": "2012-10-17",
4 "Statement": [{
5 "Effect": "Allow",
6 "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
7 "Action": ["s3:GetObject", "s3:ListBucket"],
8 "Resource": [
9 "arn:aws:s3:::shared-bucket",
10 "arn:aws:s3:::shared-bucket/*"
11 ]
12 }]
13}IAM Roles
A role is an IAM identity with no permanent credentials — temporary credentials are issued each time the role is assumed. Roles are the recommended approach for all non-human access.
Trust Policy vs Permission Policy
Every role has two policies:
1// Trust Policy — WHO can assume this role
2{
3 "Version": "2012-10-17",
4 "Statement": [{
5 "Effect": "Allow",
6 "Principal": {
7 "Service": "lambda.amazonaws.com" // Lambda service can assume this role
8 },
9 "Action": "sts:AssumeRole"
10 }]
11}1// Permission Policy — WHAT the role can do
2{
3 "Version": "2012-10-17",
4 "Statement": [{
5 "Effect": "Allow",
6 "Action": [
7 "dynamodb:GetItem",
8 "dynamodb:PutItem",
9 "logs:CreateLogGroup",
10 "logs:PutLogEvents"
11 ],
12 "Resource": "*"
13 }]
14}Common Role Use Cases
| Service | Role type | Purpose |
|---|---|---|
| Lambda | Execution role | Call DynamoDB, S3, SQS on behalf of your function |
| EC2 | Instance profile | Application on EC2 accesses AWS without hardcoded keys |
| ECS / EKS | Task role | Container-level permissions (not instance-level) |
| CodePipeline | Service role | Deploy to S3, ECS, Lambda |
| Cross-account | Role with trust policy | Account A assumes role in Account B |
Permission Boundaries
A permission boundary sets the maximum permissions an identity can have. Even if an identity policy grants more, the boundary caps it.
Effective permissions = Identity Policy ∩ Permission Boundary ∩ SCP
1// Permission boundary — dev role can only use S3 and DynamoDB
2{
3 "Version": "2012-10-17",
4 "Statement": [{
5 "Effect": "Allow",
6 "Action": ["s3:*", "dynamodb:*"],
7 "Resource": "*"
8 }]
9}Use case: Allow developers to create IAM roles for their Lambda functions, but prevent them from creating roles with Admin access.
AWS STS — Security Token Service
STS issues temporary security credentials (Access Key ID + Secret + Session Token) with a configurable expiry. All role assumptions go through STS.
STS API Calls
| API | Who calls it | Use case |
|---|---|---|
AssumeRole | IAM user, role, or service | Cross-account, service federation, temporary escalation |
AssumeRoleWithWebIdentity | App with OIDC token | Mobile/web app with Google, Apple, Cognito Identity Pool |
AssumeRoleWithSAML | Enterprise SSO user | SAML 2.0 corporate federation (Active Directory) |
GetSessionToken | IAM user | MFA-protected API calls |
GetFederationToken | Broker application | Legacy federation; prefer AssumeRole |
AssumeRole in Practice
1import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts';
2import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
3
4const sts = new STSClient({ region: 'us-east-1' });
5
6// Assume a role in another account
7const { Credentials } = await sts.send(new AssumeRoleCommand({
8 RoleArn: 'arn:aws:iam::999988887777:role/DataAccessRole',
9 RoleSessionName: 'MyAppSession', // appears in CloudTrail logs
10 DurationSeconds: 3600, // 15 min (900s) to 12 hours (43200s)
11 ExternalId: 'unique-external-id', // prevents confused deputy
12 // Policy: JSON.stringify(sessionPolicy) // optional session policy to restrict further
13}));
14
15// Use temporary credentials
16const s3 = new S3Client({
17 region: 'us-east-1',
18 credentials: {
19 accessKeyId: Credentials.AccessKeyId,
20 secretAccessKey: Credentials.SecretAccessKey,
21 sessionToken: Credentials.SessionToken,
22 },
23});
24
25const objects = await s3.send(new ListObjectsV2Command({ Bucket: 'cross-account-bucket' }));Credential Duration
| API | Min | Max |
|---|---|---|
AssumeRole | 15 minutes | 12 hours (role's MaxSessionDuration) |
AssumeRoleWithWebIdentity | 15 minutes | 12 hours |
AssumeRoleWithSAML | 15 minutes | 12 hours |
GetSessionToken | 15 minutes | 36 hours |
AWS SDK Credential Chain
When your application uses the AWS SDK, credentials are resolved in this exact order — the first match wins:
| Priority | Source | How it works |
|---|---|---|
| 1 | Explicit in code | new S3Client({ credentials: { accessKeyId, secretAccessKey } }) |
| 2 | Environment variables | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN |
| 3 | AWS SSO | ~/.aws/sso/cache via aws configure sso |
| 4 | Shared credentials file | ~/.aws/credentials → [default] or named profile |
| 5 | AWS config file | ~/.aws/config |
| 6 | Container credentials | ECS task role via metadata endpoint |
| 7 | EC2 instance profile | Instance metadata service (IMDS): http://169.254.169.254/... |
Best practices: Never hardcode credentials (priority 1). In production, always use roles (priority 6 for ECS, priority 7 for EC2, automatic for Lambda).
1# Lambda, ECS, EC2 automatically get credentials via the role — no code needed
2# The SDK picks them up from the environment automatically (priority 6 or 7)
3
4# For local development, use named profiles
5export AWS_PROFILE=my-dev-profile
6# Or assume a role via CLI
7aws sts assume-role --role-arn arn:aws:iam::123:role/DevRole --role-session-name localCross-Account Access Patterns
Pattern 1: Role Assumption (Most Common)
Setup:
- In Account B: create role with trust policy allowing Account A
- In Account A: attach policy allowing
sts:AssumeRoleon that role ARN - Application assumes the role → gets temporary credentials → accesses Account B resources
Pattern 2: Resource-Based Policy (No Role Assumption)
1// Account B's S3 bucket policy grants Account A directly
2{
3 "Principal": { "AWS": "arn:aws:iam::111122223333:role/AppRole" },
4 "Action": "s3:GetObject",
5 "Effect": "Allow",
6 "Resource": "arn:aws:s3:::account-b-bucket/*"
7}Key difference: With resource policy, Account A's identity retains its original permissions alongside the cross-account access. With role assumption, the caller gives up its original permissions and takes on only the assumed role's permissions.
Confused Deputy Attack & ExternalId
When a third-party service (Vendor) assumes your role, any of their customers could trick them into using their AWS privileges to access your account. The ExternalId prevents this.
1// Trust policy with ExternalId condition
2{
3 "Effect": "Allow",
4 "Principal": { "AWS": "arn:aws:iam::VENDOR_ACCOUNT:root" },
5 "Action": "sts:AssumeRole",
6 "Condition": {
7 "StringEquals": {
8 "sts:ExternalId": "your-unique-external-id-abc123"
9 }
10 }
11}The ExternalId should be unique per customer at the vendor — a UUID or your account ID works well.
IAM Policy Conditions
Conditions add fine-grained control to policies:
1{
2 "Effect": "Allow",
3 "Action": "s3:*",
4 "Resource": "*",
5 "Condition": {
6 "Bool": { "aws:MultiFactorAuthPresent": "true" },
7 "IpAddress": { "aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"] },
8 "StringEquals": { "aws:RequestedRegion": "us-east-1" },
9 "DateGreaterThan": { "aws:CurrentTime": "2024-01-01T00:00:00Z" },
10 "StringLike": { "s3:prefix": "home/${aws:username}/*" }
11 }
12}Common condition keys:
| Key | Purpose |
|---|---|
aws:MultiFactorAuthPresent | Require MFA for sensitive operations |
aws:SourceIp / aws:VpcSourceIp | Restrict to IP range or VPC |
aws:RequestedRegion | Restrict to specific regions |
aws:PrincipalTag/key | Attribute-based access control (ABAC) |
aws:ResourceTag/key | Restrict to tagged resources |
aws:CalledVia | Allow action only when called by a specific service |
sts:ExternalId | Confused deputy prevention |
IAM Best Practices for Developers
- Never use root account for day-to-day operations — enable MFA and lock it away
- Never hardcode credentials — use roles, environment variables, or secrets manager
- Least privilege — start with minimal permissions, add as needed
- Use roles for EC2/Lambda/ECS — not access keys in environment variables
- Rotate access keys — if you must use them, rotate every 90 days
- Enable MFA for all human users, especially for console access
- Use permission boundaries when delegating role creation to developers
- Audit with IAM Access Analyzer — finds unused permissions and external access
DVA-C02 Quick Reference
| Topic | Key Fact |
|---|---|
| Explicit Deny | Always overrides any Allow |
| Default permission | Implicit Deny (nothing allowed unless explicitly permitted) |
| Same-account: identity OR resource policy | Either is sufficient |
| Cross-account: identity AND resource policy | Both required (or use role assumption) |
| Role assumption vs resource policy | Role: give up original permissions; Resource policy: keep original permissions |
| STS AssumeRole duration | 15 min – 12 hours |
| STS GetSessionToken duration | 15 min – 36 hours |
| Confused deputy fix | ExternalId in trust policy condition |
| Permission boundary effect | Caps maximum permissions — cannot grant more |
| Session policy effect | Further restricts — cannot expand role permissions |
| SDK credential chain first | Explicit in code (avoid!) |
| SDK credential chain last | EC2 Instance Profile / ECS Task Role |
| AssumeRoleWithWebIdentity | OIDC providers: Google, Apple, Cognito Identity Pool |
| AssumeRoleWithSAML | Enterprise Active Directory / SAML 2.0 federation |
| CloudTrail identity | RoleSessionName appears as the principal in logs |
Practice Questions5
Q1. A Lambda function needs to read from an S3 bucket. The developer attaches an IAM user's access keys to the function. A security reviewer flags this as incorrect. What is the recommended approach?
Select one answer before revealing.
Q2. A developer wants to grant an external AWS account temporary access to upload objects to an S3 bucket. The access should expire after 2 hours. Which approach should be used?
Select one answer before revealing.
Q3. A Lambda function uses STS AssumeRole to assume a role in another account. The function fails with "AccessDenied: User is not authorized to perform sts:AssumeRole." What must be configured?
Select one answer before revealing.
Q4. A developer is implementing attribute-based access control (ABAC) in AWS. They want to allow engineers to start/stop only EC2 instances tagged with the same "team" tag as the engineer's IAM user. Which IAM feature enables this?
Select one answer before revealing.
Q5. A developer needs to generate temporary credentials for a web application that authenticates users via Facebook. Users should get AWS credentials scoped to their own data. Which AWS service and API should be used?
Select one answer before revealing.