Simple Team IAM Role Management With Hybrid ABAC/RBAC - Part 2

You can find the other posts in this series here:

Part 1

Background

The following articles assumes that Active Directory is being used as the source for Identity Providers. In my examples I’ll be using ADFS, but this should apply for other IDPs as well.

In Part 1 of this series, I went over an example of utilizing ABAC to very easily manage IAM Roles for application teams across any number of accounts with minimal overhead; no new AD Groups for new IAM Roles, no need to add AD Users to new AD Groups. Just push the template to a new account with the correct Team Tag, and permissions are already handled while maintaining least privilege.

The solution utilizes generic AD Groups for Roles within a team(Team Lead, Database Admin, etc) to provide levels of access within an account, and utilizes a Tag for the name of the team to determine what accounts someone has access to. Check out the first post for a more in depth explanation.

Team Based Access Isn't Always So Simple

The solution in the first post works exactly as described, but there is at least one glaring flaw: no one can be a member of multiple teams. Real life access breakdowns aren't always so straightforward, and there are plenty of situations where a single person might need to be able to access the accounts for a few teams.

If we stick within the constraints of the model in Post 1, this isn't possible. For anyone that needed access to more than 1 set of team accounts, we'd be looking at reverting back to the normal models of providing access to AWS IAM Roles. This could be either generic Roles that have the potential to be overly permissive(ie: admin to everything), or we're looking at setting up additional IDPs in member accounts and going back to having AD Groups and IAM Roles that are tied 1:1, which doesn't scale well.

While utilizing a mix could still lessen management overhead around IAM Roles and AD Groups, the chances of someone needing access to at least two team accounts would be pretty high, and would be common enough that it would minimize the benefits of the original solution. Like most things, there is a tradeoff between something being simple but limited, or being complex but versatile.

Finding the Balance Between Simplicity and Complexity

In order for us to have a solution that covers more of our needs, we need to slide down the Simplicity/Complexity spectrum. The original solution only needed to consider the possibility of different permissions within a team. Even simpler than that would be having no difference in permissions and just giving all team members the same level of access, but that wouldn't really be that great of a solution.

Now that we're looking at requirements around someone who could be working with 'n' number of teams, the design inherently becomes more complex. I've approached this by basing this solution off the "Team Roles" from the original post. If someone is a member of TeamAlpha and TeamBravo AD Groups, they should be able to assume IAM Roles for both those teams. This is combined with the Team Role based AD Groups, so IAM Roles will still be looking for both the team and the role someone is in.

Unfortunately this isn't as straightforward as the Team Roles. For the Team Roles, we are able to pre-determine the roles, and each IAM Role is tied to a specific Team Role, so we can configure the Trust Policy of that IAM Role to look for a specific Tag:

          Condition:
            StringEquals:
              aws:PrincipalTag/TeamAdmin: "True"

That works great when we're approaching the IAM Roles from this paradigm, but it doesn't carry over to the Team Tag. For Team, we have an undetermined amount of teams that someone may be involved with, each with a different name. Because of this, the CloudFormation Template cannot be configured with aws:PrincipalTag/TeamName hardcoded into the condition. The TeamName Tag will be unique for each account and because of that, must be parametrized similar to the Team Tag used in the original post.

Because of the limitations of IAM Conditions that I ran into during my tests, we can't reuse the same singular Team Tag by adding additional values to it. In my testing, the available comparisons don't like trying to compare against something like a comma delimited list as a Tag Value, for example. So we can't use: Team: TeamAlpha,TeamBravo,TeamCharlie as a tag, and then try and use a condition like StringLike: aws:PrincipalTag/Team: "TeamBravo" to determine if access should be granted. This also makes Claim Rules more complex, since we'd be building that list based on group memberships.

AWS Trust Policy for ABAC

Full CFTs for the Portal Role and Member Roles are in GitHub.

Because of that, I figured I'd just create a unique Tag Key for each Team that we're working with. This approach requires us to change the direction that we approach permissions for AssumeRole. The original approach uses an IAM Policy to only allow the Portal Role to assume other IAM Roles if the destination IAM Role had a Team Tag that matched the Team Tag on the Principal, which was passed as a Session Tag through ADFS.

Since we're now looking at unique Tag Keys for each team, it would make it unmanageable to keep using the Portal IAM Role to dictate what can and cannot be assumed. Shifting this over to the Trust Policy on the Member IAM Roles simplifies the permissions.

I actually ran into significant issues sorting this out. My initial approach was to try and just !Sub a parameter into the aws:PrincipalTag key, but CloudFormation couldn't handle that:

Version: '2012-10-17'
Statement:
- Effect: Allow
  Principal:
    AWS: arn:aws:iam::${PortalAccountId}:root
  Action: sts:AssumeRole
  Condition:
    StringEquals:
      aws:PrincipalTag/TeamAdmin: 'True'
      !Sub aws:PrincipalTag/${TeamName}: 'True'

Interestingly enough, cfn-lint tips over and crashes here due to the last line, and the Console validation says that a map can't be used here and it needs to be a string, even though TeamName is a string. After spending a bunch of time finding ways to bypass the CFN validation with !Subs and !Joins but still erroring out at deployment, I found a solution that works. Let’s look at the Team Admin Role as an example:

  TeamAdminRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref TeamAdminRoleName
      AssumeRolePolicyDocument:
        !Sub |
          {
            "Version": "2012-10-17",
            "Statement": [
              {
                "Effect": "Allow",
                "Principal": {
                  "AWS": "arn:aws:iam::${PortalAccountId}:role/${TeamPortalRoleName}"
                },
                "Action": "sts:AssumeRole",
                "Condition": {
                  "StringEquals": {
                    "aws:PrincipalTag/TeamAdmin": "True",
                    "aws:PrincipalTag/${TeamName}": "True"
                  }
                }
              }
            ]
          }
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess
      Tags:
        - Key: CF-Stack-Name
          Value: !Ref AWS::StackName

By passing in the document as JSON and using !Sub at the top, we are able to get a valid AssumeRolePolicyDocument while allowing us to sub in the parameters we need to.

Changes With AWS IAM Roles

Full CFTs for the Portal Role and Member Roles are in GitHub.

There are a few changes to the AWS IAM Roles from this change. As seen above, we're using JSON Trust Policies, and we're shifting the Team check over to the destination IAM Roles and away from the Portal Role. In this configuration, the Managed Policy on the Team-Portal-Role looks like this:

      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          Effect: Allow
          Action:
          - sts:AssumeRole
          Resource: arn:aws:iam::*:role/Team-*-Role

We're restricting the Resource based on the naming standards that I'm using in my tests, but this could be wide open if you're IAM Roles are configured in a reasonable manner, or based on a different naming standard that you choose.

Shifting the permission checks in this way also allows us to remove the Team Tag from the member IAM Roles if desired. Since the check is in the Trust Policy, this Tag no longer is used for access control.

Changes with ADFS/Active Directory

To facilitate the ability for a member to be in multiple teams, we have to add additional AD Groups that provided access to the IAM Roles/Accounts for those teams. This requires additional overhead compared to using an attribute on AD Users like in the first post, but still streamlines the management of access control over other methods.

For the team memberships, I'm creating a new AD Group for each Team, and adding AD Users to those AD Groups. I created OUs for the different AWS AD Groups that are in play here for easier management. The current breakdown in my sandbox AD Users and Computers looks like this:

To pass these AD Group memberships as Session Tags to facilitate ABAC, this requires additional Claim Rules in ADFS. For right now, I'm using a Claim Rule per Team, like this:

I'm fairly confident that this can be a dynamic Claim Rule like those used for connecting the AD Groups to IAM Roles through ADFS for IAM Role assumption, but I haven't quite sorted that yet. If anyone had experience working with Custom Claim Rules, please reach out if you feel like helping on that.

Beyond that, the Claim Rules around Team Roles are the same as in the first post, and the changes required to facilitate membership to multiple teams is relatively minor.

Change Summary and Functionality

So what do these changes get us? We've still got the Team-Portal-Role that users coming through ADFS can assume. This is still a single IAM Role that ALL people of ALL teams can use as part of this solution. With that, we still don't need to create additional AWS-123456789123-My-Role AD Groups to provide access to specific accounts.

In ADFS, we are adding a new Claim Rule for each team, based on the AD Groups seen earlier. I'm still pretty sure this can be put into a single Custom Claim Rule, and I'm working on figuring out the syntax, but for now:

For controlling access within a team account, we are still using the AD Groups based on standardized roles within a team. Someone can be a member of any or all of these, and it will grant them access to those IAM Roles within their team accounts:

Looking at the SAML Response from our test user logging in, we see that our user is getting claims for TeamAdmin, TeamPowerUser, TeamAlpha, and TeamBravo:

<Attribute Name="https://aws.amazon.com/SAML/Attributes/PrincipalTag:TeamAdmin">
    <AttributeValue>True</AttributeValue>
</Attribute>
<Attribute Name="https://aws.amazon.com/SAML/Attributes/PrincipalTag:TeamPowerUser">
    <AttributeValue>True</AttributeValue>
</Attribute>
<Attribute Name="https://aws.amazon.com/SAML/Attributes/PrincipalTag:TeamAlpha">
    <AttributeValue>True</AttributeValue>
</Attribute>
<Attribute Name="https://aws.amazon.com/SAML/Attributes/PrincipalTag:TeamBravo">
    <AttributeValue>True</AttributeValue>
</Attribute>

So with just these 4 AD Groups that are the basis for these Claims, our user will be able to assume any Team Admin or Team PowerUser IAM Role, as well as any non-privileged IAM Roles(see the view only IAM Role) within any account for TeamAlpha or TeamBravo.

Conclusion

This post expands on the original post to provide an ABAC solution that is more flexible around team membership at the expense of a little bit of extra complexity in configuration and management. Full CloudFormation Templates are available on GitHub to see the configuration of the IAM Roles, and relatively minor changes are required on the AD/ADFS side to facilitate this change.

Like the first post, this should be viewed as a solution to implement alongside other access control methods, not to replace it entirely. There are still situations where someone will need to have broader access, or more specific access, in ways that don't easily fit into this model.