Nightmare infrastructure – episode 4. Just when you thought it couldn’t get any worse...
29 October 2025 - 1 min. read
Damiano Giorgi
DevOps Engineer
Day by day the number of companies that look with interest at the cloud computing world increases and many of them wonder how the cloud can help their business and which security flaws it can introduce. One of the most complicated tasks of the security team in fact is to understand how to exploit novel cloud technologies, with their unprecedented flexibility, without introducing security vulnerabilities. One of the most useful tools to enforce compliance with security best practices is certainly Infrastructure as Code (IaC), which allows us to handle our resources inside the cloud environment through templating systems. This makes it possible to deploy the infrastructure in a repeatable and reproducible way.
More farsighted people know that to deploy this infrastructure the CloudFormation template needs to create an IAM Role for the EC2. Thus we need to make sure that the developer who will run the template will not be able to modify the policies of this role in order to give the EC2 (and thus himself) wider permission (e.g. admin access).---
AWSTemplateFormatVersion: '2010-09-09'
Description: |
CloudFormation and IAM Permission Boundaries Demo
################################################################################
# Metadata #
################################################################################
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label: {default: 'Required parameters'}
Parameters:
- VpcId
- AmiId
- KeyName
- SubnetId
- Label: {default: 'Optional parameters'}
Parameters:
- NameSpace
- ProjectName
- Environment
################################################################################
# Parameters #
################################################################################
Parameters:
NameSpace:
Type: String
Default: 'besharp'
ProjectName:
Type: String
Default: 'permission-boundaries-demo'
Environment:
Type: String
Default: 'dev'
VpcId:
Type: AWS::EC2::VPC::Id
SubnetId:
Type: AWS::EC2::Subnet::Id
AmiId:
Type: AWS::EC2::Image::Id
KeyName:
Type: AWS::EC2::KeyPair::KeyName
################################################################################
# Conditions #
################################################################################
Conditions: {}
################################################################################
# Mappings #
################################################################################
Mappings: {}
################################################################################
# Resources #
################################################################################
Resources:
#################################### S3 ####################################
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'com.${NameSpace}.${ProjectName}'
Tags:
- Key: Name
Value: !Sub '${NameSpace}-${ProjectName}'
- Key: Owner
Value: 'name.surname@besharp.it'
################################### EC2 ####################################
EC2Instance:
Type: AWS::EC2::Instance
Properties:
IamInstanceProfile: !Ref IAMInstanceProfile
ImageId: !Ref AmiId
InstanceType: t3a.micro
KeyName: !Ref KeyName
NetworkInterfaces:
- AssociatePublicIpAddress: 'true'
DeviceIndex: '0'
GroupSet:
- !Ref EC2SecurityGroup
SubnetId: !Ref SubnetId
Tags:
- Key: Name
Value: !Sub '${NameSpace}-${ProjectName}'
- Key: Owner
Value: 'name.surname@besharp.it'
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub '${NameSpace}-${ProjectName}-ec2'
GroupDescription: !Sub 'Security Group for ${NameSpace}-${ProjectName}-ec2'
VpcId: !Ref VpcId
Tags:
- Key: Name
Value: !Sub '${NameSpace}-${ProjectName}-ec2'
- Key: Owner
Value: 'name.surname@besharp.it'
################################### IAM ####################################
IAMInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref IAMRole
InstanceProfileName: !Sub '${NameSpace}-${ProjectName}'
IAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${NameSpace}-${ProjectName}'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Path: '/'
Policies:
- PolicyName: 'EC2Access'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- 's3:GetObject'
Resource: !Sub '${S3Bucket.Arn}/*'
Tags:
- Key: Name
Value: !Sub '${NameSpace}-${ProjectName}'
- Key: Owner
Value: 'name.surname@besharp.it'
################################################################################
# Outputs #
################################################################################
Outputs:
StackName:
Description: 'Stack name.'
Value: !Sub '${AWS::StackName}'
Here we can find the information to create:{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudFormationReadAccess",
"Effect": "Allow",
"Action": [
"cloudformation:DescribeStacks",
"cloudformation:ListChangeSets",
"cloudformation:ListExports",
"cloudformation:ListImports",
"cloudformation:ListStacks"
],
"Resource": "*"
},
{
"Sid": "CloudFormationWriteAccess",
"Effect": "Allow",
"Action": [
"cloudformation:CreateStack",
"cloudformation:DeleteStack",
"cloudformation:TagResource",
"cloudformation:UpdateStack",
"cloudformation:ValidateTemplate"
],
"Resource": "*"
},
{
"Sid": "EC2ReadAccess",
"Effect": "Allow",
"Action": [
"ec2:DescribeImages",
"ec2:DescribeInstances",
"ec2:DescribeKeyPairs",
"ec2:DescribeSecurityGroupReferences",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeVpcs"
],
"Resource": "*"
},
{
"Sid": "EC2WriteAccess",
"Effect": "Allow",
"Action": [
"ec2:AssociateIamInstanceProfile",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CreateSecurityGroup",
"ec2:CreateTags",
"ec2:DeleteSecurityGroup",
"ec2:DeleteTags",
"ec2:RevokeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress",
"ec2:RunInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:TerminateInstances"
],
"Resource": "*"
},
{
"Sid": "IAMReadAccess",
"Effect": "Allow",
"Action": [
"iam:GetInstanceProfile",
"iam:GetRole",
"iam:GetRolePolicy"
],
"Resource": "*"
},
{
"Sid": "IAMWriteAccess",
"Effect": "Allow",
"Action": [
"iam:AddRoleToInstanceProfile",
"iam:CreateInstanceProfile",
"iam:CreateRole",
"iam:DeleteInstanceProfile",
"iam:DeleteRole",
"iam:DeleteRolePolicy",
"iam:PassRole",
"iam:PutRolePolicy",
"iam:RemoveRoleFromInstanceProfile",
"iam:TagRole",
"iam:UntagRole"
],
"Resource": "*"
},
{
"Sid": "S3WriteAccess",
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:PutBucketTagging"
],
"Resource": "*"
}
]
}
If we connect to the newly created EC2 instance, we can verify that it can download objects from the S3 Bucket:ubuntu@ec2-demo:~$ aws s3 cp s3://com.besharp.permission-boundaries-demo/if-you-download-me-you-are-fine . download: s3://com.besharp.permission-boundaries-demo/if-you-download-me-you-are-fine to ./if-you-download-me-you-are-fine ubuntu@ec2-demo:~$ cat if-you-download-me-you-are-fine test-okAnd that it can’t, for example, create other buckets:
ubuntu@ec2-demo:~$ aws s3api create-bucket --bucket can-i-create-it An error occurred (AccessDenied) when calling the CreateBucket operation: Access DeniedNow, the developer (regardless of the good or bad intention) might want to modify the set of permissions attached to the role to execute actions that normally he is not authorized to do. The IAM Role resource can be modified like this:
IAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${NameSpace}-${ProjectName}'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Path: '/'
Policies:
- PolicyName: 'AdministratorAccess'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: 'Allow'
Action:
- '*'
Resource: '*'
Tags:
- Key: Name
Value: !Sub '${NameSpace}-${ProjectName}'
In this way, for example, the instance can download objects from the bucket, but also create new buckets (not to mention that, modifying the policy roughly like that, in fact it has administrator permissions inside the account):ubuntu@ec2-demo:~$ aws s3 cp s3://com.besharp.permission-boundaries-demo/if-you-download-me-you-are-fine .
download: s3://com.besharp.permission-boundaries-demo/if-you-download-me-you-are-fine to ./if-you-download-me-you-are-fine
ubuntu@ec2-demo:~$ cat if-you-download-me-you-are-fine
test-ok
ubuntu@ec2-demo:~$ aws s3api create-bucket --bucket can-i-create-it
{
"Location": "/can-i-create-it"
}
Clearly the principal concern is not only the fact that a developer can gain unintended permissions, but also the increased attack surface exploitable from malicious entities: the old style linux privileges escalation ported to the cloud!{
"Sid": "IAMPermissionBoundaryWriteAccess",
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:PutRolePolicy",
"iam:UpdateRole",
"iam:UpdateRoleDescription"
],
"Resource": "arn:aws:iam::111122223333:role/dev-namespace/*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/besharp-permission-boundary-demo"
}
}
},
{
"Sid": "IAMPassRoleAccess",
"Effect": "Allow",
"Action": [
"iam:PassRole"
],
"Resource": "arn:aws:iam::111122223333:role/dev-namespace/*"
},
{
"Sid": "IAMWriteAccess",
"Effect": "Allow",
"Action": [
"iam:AddRoleToInstanceProfile",
"iam:CreateInstanceProfile",
"iam:DeleteInstanceProfile",
"iam:DeleteRole",
"iam:DeleteRolePolicy",
"iam:RemoveRoleFromInstanceProfile",
"iam:TagRole",
"iam:UntagRole"
],
"Resource": "*"
},
If during the creation phase of the IAM role through CloudFormation is not present the permission boundary attachment, the creation will fail.
To attach the permission boundary to the instance IAM role, the resource will be modified as following: IAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${NameSpace}-${ProjectName}'
PermissionsBoundary: !Ref PermissionBoundaryArn
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Path: '/dev-namespace/'
Policies:
- PolicyName: 'AdministratorAccess'
PolicyDocument:
Version: '2012-10-17'
Statement:
Effect: 'Allow'
Action:
- '*'
Resource: '*'
Tags:
- Key: Name
Value: !Sub '${NameSpace}-${ProjectName}'
- Key: Owner
Value: 'name.surname@besharp.it'
Note: to make the solution safer and to limit the dangerous action iam:PassRole, we added the Path attribute, which in fact creates a namespace for developers inside IAM.Attaching the permission boundary, the creation phase will succeed and the instance permissions (and the ones of any IAM role that the developer wants to create) will be limited.ubuntu@ec2-demo:~$ aws s3 cp s3://com.besharp.permission-boundaries-demo/if-you-download-me-you-are-fine . download: s3://com.besharp.permission-boundaries-demo/if-you-download-me-you-are-fine to ./if-you-download-me-you-are-fine ubuntu@ec2-demo:~$ cat if-you-download-me-you-are-fine test-ok ubuntu@ec2-demo:~$ aws s3api create-bucket --bucket can-i-create-it An error occurred (AccessDenied) when calling the CreateBucket operation: Access DeniedIn this configuration permission boundaries are the IAM policies under Security team control. The team can agree on which is the maximum set of permissions that a resource can have and AWS IAM will assure that all resources with the restricted IAM role associated with them will not be able to perform unintended actions or to create another resource which can do so.