Introduction
This is the second exercise in the "AWS CDK 100 Drill Exercises" series.
For more about AWS CDK 100 Drill Exercises, see this introduction article.
After learning S3 fundamentals in the first exercise, we now dive into AWS Identity and Access Management (IAM). IAM is the foundation of AWS security, controlling who can access your resources and what they can do with them.
Why IAM After S3?
- Security Foundation: IAM is essential for securing all AWS resources
- Real-World Necessity: Every AWS deployment requires proper access management
- CDK Integration: Understanding how CDK generates IAM policies and roles
- Best Practices: Learning secure patterns from the start prevents future vulnerabilities
What You'll Learn
- How CDK creates IAM users, groups, and roles
- Secure password management with AWS Secrets Manager
- The difference between managed policies and inline policies
- Switch role implementation with MFA requirements
- CloudFormation's dynamic secret resolution
- IAM security best practices
📁 Code Repository: All code examples for this exercise are available on GitHub.
Architecture Overview
Here's what we'll build in this exercise:
We'll implement six different patterns across four constructs:
Construct 1: Basic User (CDKDefaultUser)
- Pattern 1: Minimal IAM user configuration
Construct 2: Password Management User (IAMUserWithPassword)
- Pattern 2A: Hardcoded password (⚠️ Not recommended)
- Pattern 2B: Secure password management with Secrets Manager (✅ Recommended)
- Pattern 3A: AWS managed policy attachment
- Pattern 3B: Inline policy attachment
Construct 3: Group Management User (IamUserGroup)
- Pattern 4: Group-based permission management
Construct 4: Switch Role User (SwitchRoleUser)
- Pattern 5: MFA-required role assumption
Prerequisites
To follow along, you'll need:
- AWS CLI v2 installed and configured
- Node.js 20+
- AWS CDK CLI (
npm install -g aws-cdk) - Basic TypeScript knowledge
- AWS Account (Free Tier works for this exercise)
- Understanding of IAM concepts (users, roles, policies)
Project Directory Structure
iam-basics/
├── bin/
│ └── iam-basics.ts # Application entry point
├── lib/
│ ├── stacks/
│ │ └── iam-basics-stack.ts # Main stack definition
│ └── constructs/
│ ├── iam-user-with-password.ts # Patterns 2-3
│ ├── iam-user-with-group.ts # Pattern 4
│ └── iam-user-with-switch-role.ts # Pattern 5
├── test/
│ ├── compliance/
│ │ └── cdk-nag.test.ts # Testing (explained in later exercises)
│ ├── snapshot/
│ │ └── snapshot.test.ts # Testing (explained in later exercises)
│ └── unit/
│ └── iam-basics.test.ts # Testing (explained in later exercises)
├── cdk.json
├── package.json
└── tsconfig.json
Pattern 1: Understanding CDK Default User
Let's start with the simplest IAM user creation. This is all you need to create an IAM user.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';
export class IamBasicsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Minimal IAM user configuration
const cdkDefaultUser = new iam.User(this, 'CDKDefaultUser', {});
}
}
Generated CloudFormation:
{
"Resources": {
"CDKDefaultUserF7AAA71A": {
"Type": "AWS::IAM::User",
"Metadata": {
"aws:cdk:path": "Dev/DrillexercisesIamBasics/CDKDefaultUser/Resource"
}
}
}
}
Default Configuration Details
Let's examine what CDK automatically configures:
- User Name: Auto-generated by AWS
- No Password: Console access disabled by default
- No Policies: Zero permissions (principle of least privilege)
- No Access Keys: Programmatic access disabled
Until you explicitly grant permissions, this user cannot access anything.
Pattern 2A: User with Hardcoded Password (⚠️ Not Recommended)
⚠️ This pattern demonstrates what happens when you use it in production environments.
Note that "PasswordResetRequired": true is set, but the user cannot change the password because they lack permissions.
To allow password changes, you need the IAMUserChangePassword policy shown in [Pattern 2B].
Alternatively, you can configure your AWS account to allow all IAM users to change their own passwords. (See AWS Documentation)
const userWithPassword = new iam.User(this, 'PasswordUser', {
password: cdk.SecretValue.unsafePlainText('InitialPassword123!'),
passwordResetRequired: true,
});
Generated CloudFormation:
{
"UserWithPasswordPasswordUserA5E8EDB8": {
"Type": "AWS::IAM::User",
"Properties": {
"LoginProfile": {
"Password": "InitialPassword123!",
"PasswordResetRequired": true
}
}
}
}
Why This Is Dangerous
- Password in source code: Visible in version control. Even if passed via environment variables, it will be exposed for the following reasons.
- CloudFormation template: Password exposed in console and logs
- No encryption: Stored in plain text
- Audit trail: Difficult to track password changes
Never use this pattern in production.
Pattern 2B: User with Secrets Manager (✅ Recommended)
This is the secure way to manage IAM user passwords.
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
const userName = 'SecretsPasswordUser';
// Create the secret with auto-generated password
const userSecret = new secretsmanager.Secret(this, 'UserSecret', {
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: userName }),
generateStringKey: 'password',
excludePunctuation: true,
passwordLength: 16,
requireEachIncludedType: true,
},
});
// Create user with password from Secrets Manager
const user = new iam.User(this, 'SecretsPasswordUser', {
userName: userName,
password: userSecret.secretValueFromJson('password'),
passwordResetRequired: true,
});
// change password policy
userWithSecretsManager.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('IAMUserChangePassword')
);
// Grant the user permission to read their own password
userSecret.grantRead(user);
// Output the secret ARN for retrieval
new cdk.CfnOutput(this, 'SecretArn', {
value: userSecret.secretArn,
description: 'Retrieve password: aws secretsmanager get-secret-value --secret-id <this-arn>',
});
Generated CloudFormation:
{
"UserWithPasswordSecretsPasswordUserSecret32219BC7": {
"Type": "AWS::SecretsManager::Secret",
"Properties": {
"GenerateSecretString": {
"ExcludePunctuation": true,
"GenerateStringKey": "password",
"SecretStringTemplate": "{\"username\":\"SecretsPasswordUser\"}"
}
}
},
"UserWithPasswordSecretsPasswordUserCFEF7855": {
"Type": "AWS::IAM::User",
"Properties": {
"LoginProfile": {
"Password": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{"Ref": "UserWithPasswordSecretsPasswordUserSecret32219BC7"},
":SecretString:password::}}"
]
]
},
"PasswordResetRequired": true
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/IAMUserChangePassword"
]
]
}
],
"UserName": "SecretsPasswordUser"
}
},
"UserWithPasswordSecretsPasswordUserDefaultPolicy6A5FC9BF": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"secretsmanager:DescribeSecret",
"secretsmanager:GetSecretValue"
],
"Effect": "Allow",
"Resource": {
"Ref": "UserWithPasswordSecretsPasswordUserSecret32219BC7"
}
}
]
},
"Users": [
{"Ref": "UserWithPasswordSecretsPasswordUserCFEF7855"}
]
}
}
}
Key Features of This Pattern
1. CloudFormation Dynamic Reference
The most important part is this:
"Password": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{"Ref": "SecretId"},
":SecretString:password::}}"
]
]
}
CloudFormation uses {{resolve:secretsmanager:...}} to dynamically retrieve the password during stack deployment. The actual password never appears in the CloudFormation template.
2. Auto-Generated Secure Password
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: userName }),
generateStringKey: 'password',
excludePunctuation: true, // Avoid special characters that might cause issues
passwordLength: 16, // Strong password length
requireEachIncludedType: true, // Include uppercase, lowercase, numbers
}
3. Principle of Least Privilege
userSecret.grantRead(user);
This grants only this specific user permission to read their own password secret. The generated policy includes:
secretsmanager:DescribeSecretsecretsmanager:GetSecretValue
Retrieving the Password
After deployment:
# Get the secret ARN from stack outputs
SECRET_ARN=$(aws cloudformation describe-stacks \
--stack-name YourStackName \
--query 'Stacks[0].Outputs[?OutputKey==`SecretArn`].OutputValue' \
--output text)
# Retrieve the password
aws secretsmanager get-secret-value --secret-id $SECRET_ARN \
--query SecretString --output text | jq -r '.password'
Pattern 3: Managed Policies vs Inline Policies
This pattern is implemented within the IAMUserWithPassword construct.
Two types of policies apply to users whose passwords are generated by Secrets Manager.
AWS Managed Policy
userWithPassword.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')
);
Generated CloudFormation:
{
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{"Ref": "AWS::Partition"},
":iam::aws:policy/ReadOnlyAccess"
]
]
}
]
}
Characteristics:
- Maintained by AWS
- Automatically updated with new services
- Can be attached to multiple users/roles/groups
- Reference by ARN
Inline Policy
userWithPassword.addToPolicy(
new iam.PolicyStatement({
actions: ['s3:ListAllMyBuckets'],
resources: ['arn:aws:s3:::*'],
})
);
Generated CloudFormation:
{
"UserDefaultPolicy": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "s3:ListAllMyBuckets",
"Effect": "Allow",
"Resource": "arn:aws:s3:::*"
}
]
},
"Users": [
{"Ref": "User"}
]
}
}
}
Characteristics:
- Custom permissions
- Tightly coupled to the user/role
- Deleted when the user/role is deleted
- Defined directly in the template
When to Use Each
| Use Case | Managed Policy | Inline Policy |
|---|---|---|
| Common AWS permissions | ✅ | ❌ |
| Custom application-specific permissions | ❌ | ✅ |
| Shared across multiple entities | ✅ | ❌ |
| One-time, specific permissions | ❌ | ✅ |
| Frequently changing permissions | ❌ | ✅ |
Pattern 4: Group-Based Permission Management
Groups allow you to grant consistent permissions to multiple users.
This pattern is implemented in iam-user-with-group.ts.
// Create a group
const group = new iam.Group(this, 'IamGroup', {});
// Attach policy to group
group.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'));
// Add user to group
user.addToGroup(group);
Generated CloudFormation:
{
"UserGroupIamGroupAB148728": {
"Type": "AWS::IAM::Group",
"Properties": {
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/ReadOnlyAccess"
]
]
}
]
},
"UserGroupUser5985318E": {
"Type": "AWS::IAM::User",
"Properties": {
"Groups": [
{
"Ref": "UserGroupIamGroupAB148728"
}
],
}
}
Benefits of Group-Based Management
- Centralized Management: Update permissions for all users at once
- Consistency: Ensure all users in a role have identical permissions
- Scalability: Easy to onboard new team members
- Auditability: Clear permission structure
Pattern 5: Switch Role with MFA (Advanced)
💡 Note: Advanced Pattern for Level 100
This pattern is implemented in iam-user-with-switch-role.ts.
This switch role pattern is slightly advanced for Level 100, but we include it here because:
- It's a fundamental IAM best practice
- You'll encounter it frequently in real-world AWS environments
- CDK makes implementation straightforward
This pattern implements a security best practice: requiring MFA for elevated permissions.
const accountId = cdk.Stack.of(this).account;
// Create IAM user
const switchRoleUser = new iam.User(this, 'SwitchRoleUser', {
userName: 'SwitchRoleUser',
password: userSecret.secretValueFromJson('password'),
passwordResetRequired: true,
});
// Create role with MFA requirement
const readOnlyRole = new iam.Role(this, 'ReadOnlyRole', {
assumedBy: new iam.PrincipalWithConditions(
new iam.AccountPrincipal(accountId),
{
Bool: { 'aws:MultiFactorAuthPresent': 'true' },
}
),
maxSessionDuration: cdk.Duration.hours(4),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'),
],
});
// Create policy to allow assuming the role
const assumeRolePolicy = new iam.Policy(this, 'AssumeRolePolicy', {
statements: [
new iam.PolicyStatement({
actions: ['sts:AssumeRole'],
resources: [readOnlyRole.roleArn],
}),
],
});
// Create group and attach policy
const switchRoleGroup = new iam.Group(this, 'SwitchRoleGroup', {});
assumeRolePolicy.attachToGroup(switchRoleGroup);
// Add user to group
switchRoleUser.addToGroup(switchRoleGroup);
Generated CloudFormation:
{
"SwitchRoleUserReadOnlyRole660C7C3B": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
},
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
}
}
]
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{"Ref": "AWS::Partition"},
":iam::aws:policy/ReadOnlyAccess"
]
]
}
],
"MaxSessionDuration": 14400
}
}
}
Understanding the MFA Requirement
The key part is the condition:
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
}
This means:
- Users must authenticate with MFA before assuming the role
- Without MFA, the
AssumeRoleAPI call will fail - Even if the user has the
sts:AssumeRolepermission
How to Use Switch Role
- Enable MFA for the user:
aws iam create-virtual-mfa-device \
--virtual-mfa-device-name SwitchRoleUser-MFA \
--outfile QRCode.png \
--bootstrap-method QRCodePNG
aws iam enable-mfa-device \
--user-name SwitchRoleUser \
--serial-number arn:aws:iam::123456789012:mfa/SwitchRoleUser-MFA \
--authentication-code1 123456 \
--authentication-code2 789012
- Assume the role:
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/ReadOnlyRole \
--role-session-name ReadOnlySession \
--serial-number arn:aws:iam::123456789012:mfa/SwitchRoleUser-MFA \
--token-code 123456
- Use in AWS Console:
- Log in as SwitchRoleUser
- Click on account name → Switch Role
- Enter Account ID and Role Name
- You'll be prompted for MFA code
Benefits of Switch Role Pattern
- Separation of Duties: Regular permissions vs elevated permissions
- Audit Trail: Clear logs of when elevated permissions were used
-
Time-Limited:
maxSessionDurationenforces automatic expiration - MFA Protection: Extra security layer for sensitive operations
Deploy and Verify
Deployment
# Check differences
cdk diff --project=sample --env=dev
# Deploy
cdk deploy "**" --project=sample --env=dev
Verification
- Check IAM Users:
# List all users
aws iam list-users
# Get specific user details
aws iam get-user --user-name SecretsPasswordUser
- Check Attached Policies:
# List user policies
aws iam list-attached-user-policies --user-name PasswordUser
# List inline policies
aws iam list-user-policies --user-name PasswordUser
- Verify Secrets Manager:
# Get secret value
aws secretsmanager get-secret-value \
--secret-id <secret-arn> \
--query SecretString \
--output text
- Test Switch Role:
# Assume role with MFA
aws sts assume-role \
--role-arn <role-arn> \
--role-session-name TestSession \
--serial-number <mfa-device-arn> \
--token-code <mfa-code>
Cleanup
# Delete stack
cdk destroy "**" --project=sample --env=dev
# Force deletion without confirmation
cdk destroy "**" --force --project=sample --env=dev
Important: IAM users and roles are retained by default. If you want to delete them, you need to manually remove them or set appropriate deletion policies.
Best Practices
Security
- Never Hardcode Passwords: Always use Secrets Manager or Parameter Store
- Enable MFA: Especially for privileged accounts
- Use Switch Roles: Separate regular and elevated permissions
- Principle of Least Privilege: Grant only necessary permissions
- Regular Audits: Review IAM policies and access patterns
- Password Policies: Enforce strong password requirements
- Access Key Rotation: Rotate access keys regularly (or avoid them entirely)
Password Management
- Use Secrets Manager: For all password storage
- Auto-Generate: Let AWS create strong passwords
- Require Reset: Force password change on first login
Policy Management
- Prefer Managed Policies: For common permissions
- Use Inline Policies: For specific, one-off permissions
- Group-Based Management: Manage permissions by role, not individual users
Operations
- CloudTrail Logging: Monitor all IAM activities
- Naming Conventions: Use clear, consistent names
- Separate Environments: Different IAM configurations for dev/test/prod
Summary
In this exercise, we learned IAM fundamentals through AWS CDK.
What We Learned
- IAM Basics: Users, groups, roles, and policies
- Secure Passwords: Using Secrets Manager instead of hardcoded values
- CloudFormation Integration: Dynamic secret resolution with
{{resolve:secretsmanager:...}} - Policy Types: Managed vs inline policies and when to use each
- Switch Roles: Implementing role assumption with MFA requirements
- Best Practices: Least privilege, MFA, and group-based management
Key Takeaways
- Security First: IAM is the foundation of AWS security
- Secrets Manager: Essential for password management
- MFA: Critical for elevated permissions
- Groups: Simplify permission management
- Audit: CloudTrail and regular reviews are essential
References
- AWS IAM Official Documentation
- IAM Best Practices
- AWS Secrets Manager
- IAM Policy Reference
- My GitHub Repository
Next up: VPC Basics - Building secure network foundations!
Let's continue learning practical AWS CDK patterns through the 100 drill exercises!
If you found this helpful, please ⭐ the repository!

Top comments (0)