Conditional IAM Roles
Giving Consuming Teams A Choice
Generally IAM roles are all-or-nothing deals, you either accept or reject the policies in their entirety. Here we look at leveraging CloudFormation conditions to build a configurable IAM role, that results in a more configurable and testable method for controlling role scope.
Where Might This Be Useful
When controlling IAM roles within our sphere of control we follow the principle of least privilege, deploying exactly what is required to perform our tasks, which is relatively simple. When we tread over into our sphere of influence it becomes far harder.
Take the example of wanting to enforce compliance standards across a large estate, as you build up trust with the team they may allow you to selectively enable automatic controls to remediate non-compliant resources. These can be things like automatically shutting down public S3 buckets, ensuring VPC flow logs are on, or deleting default VPCs. But as a delivery team with existing resources, I need to be able to slow bleed such actions into my environments, but how can I do that autonomously and with control?
Potentially the disparate permissions could be split into many roles deployed individually, but that starts to confer an ever increasing maintenance burden, and significantly complicates matters for the compliance team. From their perspective I want to deploy one standard resource into all environments, and let the delivery team autonomously decide what is enabled.
Onwards To A Solution
CloudFormation Conditions
One of the lesser known CloudFormation options, Conditions allow us to selectively deploy resources. Let’s look at a simple example:
Parameters:
MasterAccount:
Type: String
BlockPublicRDP:
Type: String
Conditions:
ShouldBlockPublicRDP: !Equals [!Ref BlockPublicRDP, "Enabled"]
Resources:
CrossAccountRole:
Type: AWS::IAM::Role
Properties:
RoleName: CrossAccountRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
AWS:
- !Sub "arn:aws:iam::${MasterAccount}:root"
Action:
- "sts:AssumeRole"
BlockPublicRDPPolicy:
Condition: ShouldBlockPublicRDP
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: BlockPublicRDP
Roles:
- !Ref CrossAccountRole
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ec2:RevokeSecurityGroupEgress"
- "ec2:RevokeSecurityGroupIngress"
Resource: "*"
- We have a parameter
BlockPublicRDP
which gets compared against “Enabled” - If true, we deploy the
BlockPublicRDPPolicy
resource
Tuning Up The Parameters
The above code although simple, is somewhat naive. We can improve it already by applying some constraints to the parameters.
Parameters:
MasterAccount:
AllowedPattern: ^\d{12}$
Type: String
BlockPublicRDP:
AllowedValues: ["Enabled", "Disabled"]
ConstraintDescription: Must be Enabled or Disabled
Default: Disabled
Type: String
- The regex on master account locks it down to valid AWS account Ids
- By adding a Disabled default we make our policies opt-in for safety
- By setting allowed values we will explicitly only handle Enabled or Disabled
Scalably Passing Parameters
Anyone that’s passed parameters over the CLI will understand how unwieldly it becomes. The scalable way to pass parameters is with a json
file committed into source control. To that end let’s look at the files are formatted:
[
{
"ParameterKey": "MasterAccount",
"ParameterValue": "111122223333"
},
{
"ParameterKey": "BlockPublicRDP",
"ParameterValue": "Enabled"
}
]
Now we can deploy the template with commands of the form:
aws cloudformation create/update-stack --stack-name ConditionalRole --template-body file://role.yaml --parameters file://parameters.json --capabilities CAPABILITY_NAMED_IAM
CloudFormation Conditions for IAM Conditions
One of the lesser used be more interesting parts of IAM policies are the condition statements that can be applied. Taking our above example a step further, what if we wanted to lock down what security groups we could edit by tag. In the policy we need to extend the statement to include a condition stanza so it looks like:
BlockPublicRDPPolicy:
Condition: ShouldBlockPublicRDP
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: BlockPublicRDP
Roles:
- !Ref CrossAccountRole
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ec2:RevokeSecurityGroupEgress"
- "ec2:RevokeSecurityGroupIngress"
Resource: "*"
Condition:
StringEquals:
"ec2:ResourceTag/Owner": !Ref SecurityGroupTagValue
Now we can pass in an extra parameter to lock down the policy, but we need to maintain the ability to open it up to acting on all security groups. So let’s add another condition to control out condition…
[
{
"ParameterKey": "MasterAccount",
"ParameterValue": "111122223333"
},
{
"ParameterKey": "BlockPublicRDP",
"ParameterValue": "Enabled"
},
{
"ParameterKey": "SecurityTagRestriction",
"ParameterValue": "Enabled"
},
{
"ParameterKey": "SecurityGroupTagValue",
"ParameterValue": "JoshArmi"
}
]
Parameters:
MasterAccount:
AllowedPattern: ^\d{12}$
Type: String
BlockPublicRDP:
AllowedValues: ["Enabled", "Disabled"]
ConstraintDescription: Must be Enabled or Disabled
Default: Disabled
Type: String
SecurityTagRestriction:
AllowedValues: ["Enabled", "Disabled"]
ConstraintDescription: Must be Enabled or Disabled
Default: Disabled
Type: String
SecurityGroupTagValue:
Type: String
Default: Disabled
Conditions:
ShouldBlockPublicRDP: !Equals [!Ref BlockPublicRDP, "Enabled"]
RestrictedByTags: !Equals [!Ref SecurityTagRestriction, "Enabled"]
Resources:
CrossAccountRole:
Type: AWS::IAM::Role
Properties:
RoleName: CrossAccountRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
AWS:
- !Sub "arn:aws:iam::${MasterAccount}:root"
Action:
- "sts:AssumeRole"
BlockPublicRDPPolicy:
Condition: ShouldBlockPublicRDP
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: BlockPublicRDP
Roles:
- !Ref CrossAccountRole
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ec2:RevokeSecurityGroupEgress"
- "ec2:RevokeSecurityGroupIngress"
Resource: "*"
Condition: !If
- RestrictedByTags
- StringEquals:
ec2:ResourceTag/Owner: !Ref SecurityGroupTagValue
- !Ref AWS::NoValue
Now if we set SecurityTagRestriction
to Enabled
then the condition is applied. If it is set to Disabled
then by the magic of AWS::NoValue
the Condition property disappears and it’s like it never existed at all.
Making The Configuration Consumable
So we’ve given the delivery teams a capbility to select known IAM configurations by only touching the parameter json file, which is a significant win, rather than hand editing the yaml with potentially disastrous and unforseen consequences, we’ve locked down the possible number of states of the role and given something which is inherently more testable.
But once again donning the hat of the compliance team how do we determine what configuration is in flight in a given account, there is one missing permission from the role which enables us to understand the configuration:
ListPolicies:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: ListRolePolicies
Roles:
- !Ref CrossAccountRole
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "iam:ListAttachedRolePolicies"
Resource: !GetAtt CrossAccountRole.Arn
With that permission in place we can list the policies attached to the role, allowing us to determine the state of play in any given account.
Assuming the role and running aws iam list-attached-role-policies --role-name CrossAccountRole
returns:
{
"AttachedPolicies": [
{
"PolicyName": "BlockPublicRDP",
"PolicyArn": "arn:aws:iam::285525127666:policy/BlockPublicRDP"
},
{
"PolicyName": "ListRolePolicies",
"PolicyArn": "arn:aws:iam::285525127666:policy/ListRolePolicies"
}
]
}
Click Here For Full Source
Conclusion
This feels like a good, low investment solution for managing IAM at scale that is consumed by other delivery teams, they can opt-in to IAM on their terms, which also gives them the option of opt-out in the case of an issue.
By making the configuration done via parameters in the json file, we can provide an interface that reduces the burden on the consuming teams whilst also giving the compliance team a more manageable surface to test compared to hand-editing templates.
Although CloudFormation being CloudFormation, I did revisit and old bug-bear which still seems to be an issue as explained below:
Configuring The Tag Key
The eagle eyed amongst you will see that potentially you might want to configure the key of the tag as well as the value, unfortunately it does not appear that computed keys are allowed in CloudFormation. So consider this another addition to my #awswishlist