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 Questions8
Q1. A Lambda function needs to read items from a single DynamoDB table. Following AWS best practices, how should the developer grant the function this access?
Select one answer before revealing.
Q2. What is an IAM policy?
Select one answer before revealing.
Q3. What is the recommended way to give an application running on an EC2 instance permission to access an S3 bucket?
Select one answer before revealing.
Q4. 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.
Q5. 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.
Q6. 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.
Q7. 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.
Q8. 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.