Identity and Access Management (IAM)
IAM misconfigurations are behind some of the biggest cloud breaches in recent history. In March 2021, MobiKwik suffered a 100 million user data breach traced back to a single leaked AWS access key. Getting IAM right isn’t optional: it’s the foundation of everything you build in the cloud. In this post, I’ll cover what AWS IAM is, walk through creating and assuming an IAM role with a scoped S3 policy, and show how to use CloudFormation to enforce an MFA-gated IAM baseline across an entire account.
What Is AWS IAM?
IAM has existed as a concept long before AWS. At its core, it’s an access control framework: defining who can access what resources, under what conditions. AWS IAM puts this into practice by letting you:
- Create users, groups, and roles
- Attach policies that grant or deny specific API actions on specific resources
- Enforce conditions like MFA presence, IP address restrictions, and time-of-day controls
The distinction between authentication (proving who you are) and authorization (controlling what you can do) is central to understanding IAM.
Lab: Create and Assume an IAM Role with S3 Scoping
This lab creates a custom policy that grants full S3 access except to two protected buckets, then attaches it to a role and user.
Step 1: Create the Policy
The policy below grants broad S3 access while explicitly scoping resource access to avoid the two pre-created sensitive buckets. It uses the visual policy editor output:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:ListStorageLensConfigurations",
"s3:ListAccessPointsForObjectLambda",
"s3:GetAccessPoint",
"s3:PutAccountPublicAccessBlock",
"s3:GetAccountPublicAccessBlock",
"s3:ListAllMyBuckets",
"s3:ListAccessPoints",
"s3:PutAccessPointPublicAccessBlock",
"s3:ListJobs",
"s3:PutStorageLensConfiguration",
"s3:ListMultiRegionAccessPoints",
"s3:CreateJob"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3::851592760101:accesspoint/*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:*:851592760101:storage-lens/*",
"arn:aws:s3:*:851592760101:accesspoint/*",
"arn:aws:s3:::cfst-3352-0420dac77a81904f3474ca99-appconfigprod1-8x31vcbk3f3w",
"arn:aws:s3:::cfst-3352-0420dac77a81904f3474ca99-appconfigprod2-19s6selwyo5x3",
"arn:aws:s3:*:851592760101:job/*",
"arn:aws:s3:::*/*",
"arn:aws:s3:us-west-2:851592760101:async-request/mrap/*/*",
"arn:aws:s3-object-lambda:*:851592760101:accesspoint/*"
]
}
]
}
Step 2: Attach the Policy to a Role
Create an IAM role and attach the policy from Step 1 to it. Then attach the role to the lab user.
Step 3: Verify Access
With the policy in place, the user can access the appconfigprod1 and appconfigprod2 buckets but is blocked from the customer-data buckets — exactly as intended. Developers get what they need; sensitive data stays protected.
CloudFormation: IAM Baseline with MFA Enforcement
You can also provision groups, policies, and roles as code using CloudFormation. The template below creates a complete IAM baseline — including MFA-enforced admin roles, a read-only role, and CloudFormation deployment roles:
Parameters:
AllowRegion:
Type: String
Description: 'A single region that resources can be created in'
Default: 'ap-southeast-2'
BaselineNamePrefix:
Type: String
Description: 'The prefix for roles, groups and policies created by this stack'
Default: 'Baseline'
BaselineExportName:
Type: String
Description: 'The CloudFormation export name prefix'
Default: 'Baseline'
MinLength: '3'
MaxLength: '32'
IdentityManagementAccount:
Type: String
Description: AccountId trusted to assume all roles (blank for no cross-account trust)
Default: ''
ToolingManagementAccount:
Type: String
Description: AccountId trusted to assume ReadOnly and StackSet roles
Default: ''
OrganizationsRootAccount:
Type: String
Description: AccountId trusted to assume Organizations role
Default: ''
Conditions:
LinkToIdentityManagementAccount: !Not
- !Equals
- !Ref IdentityManagementAccount
- ''
LinkToToolingManagementAccount: !Not
- !Equals
- !Ref ToolingManagementAccount
- ''
LinkToOrganizationsRootAccount: !Not
- !Equals
- !Ref OrganizationsRootAccount
- ''
Resources:
ManageSelfIAMUserGroupPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
ManagedPolicyName: !Sub '${BaselineNamePrefix}-ManageSelfIAMUserGroupPolicy'
Description: !Sub 'Policy for ${BaselineNamePrefix} managing own IAM user'
Path: /
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: 'iam:GetAccountPasswordPolicy'
Resource: '*'
- Sid: AllowUsersToListMFADevicesandUsersForConsole
Effect: Allow
Action:
- 'iam:ListMFADevices'
- 'iam:ListVirtualMFADevices'
- 'iam:ListUsers'
Resource: '*'
- Effect: Allow
Action:
- 'iam:ChangePassword'
Resource:
- !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username}'
- Sid: AllowUsersToDeactivateTheirOwnVirtualMFADevice
Effect: Allow
Action:
- 'iam:DeactivateMFADevice'
- 'iam:*LoginProfile'
- 'iam:*AccessKey*'
- 'iam:*SSHPublicKey*'
Resource:
- !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username}'
- !Sub 'arn:aws:iam::${AWS::AccountId}:mfa/${!aws:username}'
Condition:
Bool:
'aws:MultiFactorAuthPresent': true
- Sid: AllowUsersToCreateEnableResyncDeleteTheirOwnVirtualMFADevice
Effect: Allow
Action:
- 'iam:CreateVirtualMFADevice'
- 'iam:EnableMFADevice'
- 'iam:ResyncMFADevice'
- 'iam:DeleteVirtualMFADevice'
Resource:
- !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username}'
- !Sub 'arn:aws:iam::${AWS::AccountId}:mfa/${!aws:username}'
PrivilegedAdminRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: !Sub '${BaselineNamePrefix}-PrivilegedAdmin'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS:
- !Sub 'arn:aws:iam::${AWS::AccountId}:root'
Action: 'sts:AssumeRole'
Condition:
Bool:
'aws:MultiFactorAuthPresent': 'true'
- !If
- LinkToIdentityManagementAccount
- Effect: Allow
Principal:
AWS:
!Sub 'arn:aws:iam::${IdentityManagementAccount}:root'
Action: 'sts:AssumeRole'
Condition:
Bool:
'aws:MultiFactorAuthPresent': 'true'
- !Ref 'AWS::NoValue'
AccountWideReadOnlyRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: !Sub '${BaselineNamePrefix}-AccountWideReadOnly'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/ReadOnlyAccess'
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS:
- !Sub 'arn:aws:iam::${AWS::AccountId}:root'
Action: 'sts:AssumeRole'
Condition:
Bool:
'aws:MultiFactorAuthPresent': 'true'
CloudFormationRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: !Sub '${BaselineNamePrefix}-CloudFormation'
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: root
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: '*'
Resource: '*'
- Effect: Deny
Action:
- 'cloudformation:CreateStack'
- 'cloudformation:UpdateStack'
Resource: '*'
Condition:
'Null':
'cloudformation:TemplateURL': 'true'
StackSetRole:
Type: AWS::IAM::Role
Properties:
RoleName: AWSCloudFormationStackSetExecutionRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS:
- !Sub 'arn:aws:iam::${AWS::AccountId}:root'
Action: 'sts:AssumeRole'
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
Outputs:
PrivilegedAdminRole:
Description: Privileged Admin Role
Value: !GetAtt PrivilegedAdminRole.Arn
Export:
Name: !Sub '${BaselineExportName}-PrivilegedAdminRole'
AccountWideReadOnlyRole:
Description: Account Wide Read Only Role
Value: !GetAtt AccountWideReadOnlyRole.Arn
Export:
Name: !Sub '${BaselineExportName}-AccountWideReadOnlyRole'
CloudFormationRole:
Description: Baseline CloudFormation Role
Value: !GetAtt CloudFormationRole.Arn
Export:
Name: !Sub '${BaselineExportName}-CloudFormationRole'
The template enforces MFA as a condition on all privileged role assumptions — meaning even if credentials are compromised, an attacker can’t assume the admin role without a valid MFA token.
Wrapping Up
IAM is not a set-it-and-forget-it service. The key principles to live by:
- Least privilege — grant only the permissions needed for the job, and no more
- MFA everywhere — especially for privileged roles
- Infrastructure as Code — define IAM in CloudFormation or Terraform so changes are reviewed and versioned
- Audit regularly — use IAM Access Analyzer and AWS Config rules to catch drift
Hope this was helpful and informative. Feel free to message me on LinkedIn with any questions — Artist out!