DEV Community

Cover image for AWS CDK 100 Drill Exercises #002: IAM Basics —— Users, Roles, and Secure Password Management

AWS CDK 100 Drill Exercises #002: IAM Basics —— Users, Roles, and Secure Password Management

Level 100

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?

  1. Security Foundation: IAM is essential for securing all AWS resources
  2. Real-World Necessity: Every AWS deployment requires proper access management
  3. CDK Integration: Understanding how CDK generates IAM policies and roles
  4. 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:

Architecture Overview

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
Enter fullscreen mode Exit fullscreen mode

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', {});
  }
}
Enter fullscreen mode Exit fullscreen mode

Generated CloudFormation:

{
  "Resources": {
    "CDKDefaultUserF7AAA71A": {
      "Type": "AWS::IAM::User",
      "Metadata": {
        "aws:cdk:path": "Dev/DrillexercisesIamBasics/CDKDefaultUser/Resource"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

Generated CloudFormation:

{
  "UserWithPasswordPasswordUserA5E8EDB8": {
    "Type": "AWS::IAM::User",
    "Properties": {
      "LoginProfile": {
        "Password": "InitialPassword123!",
        "PasswordResetRequired": true
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why This Is Dangerous

  1. Password in source code: Visible in version control. Even if passed via environment variables, it will be exposed for the following reasons.
  2. CloudFormation template: Password exposed in console and logs
  3. No encryption: Stored in plain text
  4. 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>',
});
Enter fullscreen mode Exit fullscreen mode

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"}
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Features of This Pattern

1. CloudFormation Dynamic Reference

The most important part is this:

"Password": {
  "Fn::Join": [
    "",
    [
      "{{resolve:secretsmanager:",
      {"Ref": "SecretId"},
      ":SecretString:password::}}"
    ]
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

3. Principle of Least Privilege

userSecret.grantRead(user);
Enter fullscreen mode Exit fullscreen mode

This grants only this specific user permission to read their own password secret. The generated policy includes:

  • secretsmanager:DescribeSecret
  • secretsmanager: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'
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

Generated CloudFormation:

{
  "ManagedPolicyArns": [
    {
      "Fn::Join": [
        "",
        [
          "arn:",
          {"Ref": "AWS::Partition"},
          ":iam::aws:policy/ReadOnlyAccess"
        ]
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:::*'],
  })
);
Enter fullscreen mode Exit fullscreen mode

Generated CloudFormation:

{
  "UserDefaultPolicy": {
    "Type": "AWS::IAM::Policy",
    "Properties": {
      "PolicyDocument": {
        "Statement": [
          {
            "Action": "s3:ListAllMyBuckets",
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::*"
          }
        ]
      },
      "Users": [
        {"Ref": "User"}
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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"
     }
    ],
   }
}
Enter fullscreen mode Exit fullscreen mode

Benefits of Group-Based Management

  1. Centralized Management: Update permissions for all users at once
  2. Consistency: Ensure all users in a role have identical permissions
  3. Scalability: Easy to onboard new team members
  4. 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);
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the MFA Requirement

The key part is the condition:

"Condition": {
  "Bool": {
    "aws:MultiFactorAuthPresent": "true"
  }
}
Enter fullscreen mode Exit fullscreen mode

This means:

  • Users must authenticate with MFA before assuming the role
  • Without MFA, the AssumeRole API call will fail
  • Even if the user has the sts:AssumeRole permission

How to Use Switch Role

  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. 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

  1. Separation of Duties: Regular permissions vs elevated permissions
  2. Audit Trail: Clear logs of when elevated permissions were used
  3. Time-Limited: maxSessionDuration enforces automatic expiration
  4. 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
Enter fullscreen mode Exit fullscreen mode

Verification

  1. Check IAM Users:
   # List all users
   aws iam list-users

   # Get specific user details
   aws iam get-user --user-name SecretsPasswordUser
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. Verify Secrets Manager:
   # Get secret value
   aws secretsmanager get-secret-value \
     --secret-id <secret-arn> \
     --query SecretString \
     --output text
Enter fullscreen mode Exit fullscreen mode
  1. 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>
Enter fullscreen mode Exit fullscreen mode

Cleanup

# Delete stack
cdk destroy "**" --project=sample --env=dev

# Force deletion without confirmation
cdk destroy "**" --force --project=sample --env=dev
Enter fullscreen mode Exit fullscreen mode

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

  1. Never Hardcode Passwords: Always use Secrets Manager or Parameter Store
  2. Enable MFA: Especially for privileged accounts
  3. Use Switch Roles: Separate regular and elevated permissions
  4. Principle of Least Privilege: Grant only necessary permissions
  5. Regular Audits: Review IAM policies and access patterns
  6. Password Policies: Enforce strong password requirements
  7. Access Key Rotation: Rotate access keys regularly (or avoid them entirely)

Password Management

  1. Use Secrets Manager: For all password storage
  2. Auto-Generate: Let AWS create strong passwords
  3. Require Reset: Force password change on first login

Policy Management

  1. Prefer Managed Policies: For common permissions
  2. Use Inline Policies: For specific, one-off permissions
  3. Group-Based Management: Manage permissions by role, not individual users

Operations

  1. CloudTrail Logging: Monitor all IAM activities
  2. Naming Conventions: Use clear, consistent names
  3. Separate Environments: Different IAM configurations for dev/test/prod

Summary

In this exercise, we learned IAM fundamentals through AWS CDK.

What We Learned

  1. IAM Basics: Users, groups, roles, and policies
  2. Secure Passwords: Using Secrets Manager instead of hardcoded values
  3. CloudFormation Integration: Dynamic secret resolution with {{resolve:secretsmanager:...}}
  4. Policy Types: Managed vs inline policies and when to use each
  5. Switch Roles: Implementing role assumption with MFA requirements
  6. 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


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)