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.

If you don’t care about any of my ramblings and want to skip to the design, click here.

Unfortunately for you I’m incapable of being brief with anything I do, so this is going to be a long post. Anyone who cares enough to write about IAM probably cares way too much about details and explaining those details, and should go outside on occasion. But if I went outside ever I wouldn’t have time to be writing about IAM, so here we are.

I’ve been mulling over simple IAM strategies to make management easier for the last few weeks. Everyone knows that least privilege should be mandatory, and that access should be as limited as possible. The downside is that as the permissions get more specific, the complexity of creating and managing these permissions and who has access to them gets more cumbersome and complex.

One common strategy I’ve seen in environments I’ve worked in is workloads split by account, where application/dev teams have access to their specific accounts but shouldn’t be accessing other accounts. This provides a nice compartmentalization and allows for simpler management of the scope of individual IAM Roles because the blast radius for mistakes caused by specific teams is a lot smaller. It also limits teams access to information that they don’t have a need to access.

Managing Roles Is A Pain

While those individual accounts are great for providing separation by default, having a large number of accounts increases the overhead with access management. Teams need to be able to enter their environments, but not those for other teams. I usually see two broad approaches to handling that access with IAM Roles:

The first is to have the Identity Provider configured in each account, and use account specific AD Groups to provide access to team members. The administration of this can get very tedious, there is a 1:1 ratio between AWS IAM Roles and AD Security Groups. If you need additional IAM Roles, you have to go through the process to add additional AD Groups in Active Directory, and add the members into those AD Groups. For an admin that needs access to 25, 50, or more accounts, they could be in as many AD Groups.

The second approach is to use a portal account. This account has more general IAM Roles, and members are able to assume IAM Roles in other accounts from here. This can simplify management some and allows for more general Role Based Access Control(RBAC), but is prone to cause issues with people having more access than they need because it’s way easier to just give someone access to a broader IAM Role than to create a new set of IAM Roles in the Portal and Member accounts. To lock down access in a way that restricts team members to their specific accounts, you’ll have to create IAM Roles in in both the Portal and the relevant Member accounts, and you still have the issue of needing AD Groups specific to the specialized IAM Roles.

Neither of these options is great, and both can get increasingly annoying to create, deploy, and manage.

Enter ABAC

Attribute Based Access Control(ABAC) provides an opportunity to simplify some of this overhead, although it’s rarely used from what I’ve seen. A very minimal version of ABAC is often used, where a single IAM Role will only have permissions to access resources based on something like the name of the resource, or a specific tag. This works great for service roles and similar situations, but for some reason as soon as anything more broad is being designed, ABAC seems to go out the window.

It doesn’t help that AWS themselves provide less than useful articles around using ABAC. Nearly everything they’ve published on the subject seems to assume that all the resources exist in one account. The examples they provide require additional controls around creation of resources with specific tag values(matching the principal), and controls to block the use of tags more broadly so teams cannot bypass the controls. The normal examples are around things like Secrets Manager, where a team can only access the secrets in the shared account that all teams seem to use, based on tags corresponding with their team, project, application, or similar attribute.

Worse than that, it assumes that everything in an environment is tagged correctly with the appropriate tags for controlling access, which is never the case. The overhead of applying appropriate tags to thousands of resources across dozens or hundreds of accounts to appropriately facilitate the traditional examples of ABAC is a massive undertaking, and generally so daunting and likely to have complications that it wouldn’t be reliable or safe to use that as the foundation for controlling access.

The reality is that neither roles or attributes should be used alone for controlling access. A team may have a variety of roles, including a team lead who can authorize things like IAM changes, a database administrator who needs specialized access, etc. At the same time, we don’t want to give the database admin for one team permissions to databases from another team. At this point, we’re looking at both roles(database admin) and attributes(team).

If the environment is similar to the ones described previously where teams are siloed within their own accounts, this provides a great opportunity to simplify management of IAM using a hybrid of ABAC/RBAC.

Hybrid ABAC/RBAC As A Solution

The solution that I’ve come up with to address these issues is to utilize a single Team-Portal-Role that ALL members of all teams can use as an entry point into the AWS environment. When users assume the IAM Role through ADFS, additional Session Tags are added to the Principal containing additional information about what team they belong to, and what permissions they should have. Having those attributes allows us to deploy a set of IAM Roles into each account that are identical except for a single tag value, while still allowing us to only allow teams to access accounts that they should be accessing.

Those IAM Roles are based on the team members roles within their team, and allows a standard set of IAM Roles to be easily managed and a much more streamlined process of providing access to staff. This scales infinitely to an unlimited number of accounts without any additional AD Groups that need to be created. Beyond that, if something like SSM Parameters are utilized appropriately, these IAM Roles can be managed centrally with updates pushed to all accounts automagically since the CloudFormation Stacks in member accounts can use a local parameter to configure the relevant tag values.

On the backend, specific AD attributes are passed to indicate the team the person is on, and AD Users are added to specific AD Groups that are relevant to their role within the team. These AD Groups and attributes are used by Claim Rules in ADFS to pass the appropriate Session Tags for this solution to work.

Active Directory Config

I’ve setup a mock AD environment and connected it to AWS using ADFS. In there, we have John Admin. Some may say he was destined to be an administrator. John is a member a few AD Groups. The first provides the ability to assume the Team-Portal-Role through ADFS, and the next two are about his roles within the team:

We’re also using the Department parameter in AD to provide the Team that someone belongs to, but this could easily be a different attribute including a custom one.

To send these to AWS, there are several Claim Rules setup in ADFS. The Team tag is a simple ‘LDAP Attributes as Claims’ Rule, and the Claims for the role within a team are provided by those other AD Groups using the ‘Send Group Membership as a Claim’ template:

If we peak at the AssumeRoleWithSAML event in AWS, we’ll see these attributes passed in the requestParameters section:

    "requestParameters": {
        "sAMLAssertionID": "_f5e4cbae-645e-4789-ac03-d3e8fb7f7899",
        "roleSessionName": "johnadmin@shawntorsitano.com",
        "principalTags": {
            "TeamPowerUser": "True",
            "TeamAdmin": "True",
            "Team": "Team1"
        },

Portal Configuration

The CFT used to create the Portal Role can be found on GitHub here.

The Portal account needs an Identity Provider setup, but since those articles have been created a hundred times over I’m not going to rehash that here. AWS has an example for ADFS here.

The Portal account contains a single IAM Role that can be assumed by all members of various teams. It’s worth noting here that you don’t need to use the portal account method for access for all IAM Roles to utilize this. Any general account can be used as a portal account, even if the normal configuration in your environment is to have the IDP configured in every account. There is no reason why this can’t be used in conjunction with other methods.

The Team-Portal-Role configuration looks like this:

  AllowAssumeRoleManagedPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Ref AssumeRoleManagedPolicyName
      Description: !Ref AssumeRoleManagedPolicyDescription
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          Effect: Allow
          Action:
          - sts:AssumeRole
          Resource: arn:aws:iam::*:role/*
          Condition:
            StringEquals:
              iam:ResourceTag/Team: "${aws:PrincipalTag/Team}"
          
  PortalRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref PortalRoleName
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Federated: !Sub arn:aws:iam::${AWS::AccountId}:saml-provider/${IdpName}
          Action:
          - sts:AssumeRoleWithSAML
          - sts:TagSession
          Condition:
            StringEquals:
              SAML:aud: https://signin.aws.amazon.com/saml
      ManagedPolicyArns:
        - !Ref AllowAssumeRoleManagedPolicy
      Tags:
        - Key: CF-Stack-Name
          Value: !Ref AWS::StackName

Here we’re using a Managed Policy and adding it to an IAM Role. The important parts here are the AssumeRolePolicyDocument and the Managed Policy. The Trust Policy created by AssumeRolePolicyDocument has an additional permission that is not normal for SAML Roles: sts:TagSession. This is required for the Session Tags to be passed appropriately.

The Managed Policy attached to the IAM Role has a very important condition:

          Condition:
            StringEquals:
              iam:ResourceTag/Team: "${aws:PrincipalTag/Team}"

${aws:PrincipalTag/Team} is a variable that is referring to the Team tag passed through ADFS earlier. This condition will prevent the user who has assumed this IAM Role from assuming any role that does not have a resource tag that matches their team tag. This is the primary component that restricts team members to only access accounts they should be accessing.

Member Account Configuration

The CFT used to create the Member Roles can be found on GitHub here.

On the member account side, things get a little more interesting. The IAM Roles created in Member accounts need to have a relevant Team tag to identify who should be able to access this account. But as we all know, there are various roles within a team, and we need to be able to restrict access further. Anyone on a team will probably be allowed to view all resources within an account and should be able to use a view only IAM Role. We might want to restrict who can work with the databases to a specific database administrator. For thing that are very sensitive like IAM, we may want to only allow a team lead access to those permissions.

Luckily since we are already filtering broad access by the Team attribute, we are now able to introduce fine-grained access based on role within a team(which is actually still an attribute, but that’s just semantics). If we tried to do this on the Portal end, we’d end up with several Portal IAM Roles, which would then be tied 1:1 to member IAM Roles. As an example, if the condition on the Portal end also looked for a TeamAdmin Tag, that IAM Role could only assume IAM Roles intended for Team admins.

By pushing these conditions to the Trust Policy on the member IAM Roles, we are able to minimize the total number of IAM Roles while still restricting access as intended.

Let’s look at the Team-Admin-Role:

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

Here we see that assumption is restricted to the Portal account, which is defined as a parameter. The condition here is the important part. In it, we’re looking for aws:PrincipalTag/TeamAdmin: True. This is a Session Tag that was passed through ADFS based on the AD User being in the AWS-Team-Admin-Role AD Group shown in an earlier screenshot.

Not to sound too dramatic, but this is where the magic happens. We’ve got a general, non-specific AD Group named AWS-Team-Admin-Role. This AD Group can include every single person in every single team that we want to be an admin for their team. And because of the Team condition for AssumeRole earlier, they will only be able to admin the accounts they are supposed to, regardless of who is in this AD Group. To reiterate, they will not be able to access other teams accounts and this group will still provide them admin over their teams accounts, due to the attributes being passed.

This is where the simplification really shines. To give someone Admin access to a team’s accounts, no new AD Group needs to be created, no new IAM Role needs to be made, no connections need to be made between the IDP and IAM Role. Simply add someone to a generic AD Group and boom – they have the access they need for their role within their team while still being restricted from other accounts.

What About IAM Roles Open To Multiple Team Members/Roles?

Glad you asked. We’ve got an Admin, and since they already have Admin, they can probably assume any IAM Role in the account. Maybe we’ve got a developer performing both database administration along with another job function. There are two approaches to this:

The first is to put that AD User in the relevant AD Groups for those IAM Roles. If we want Admin to also be able to assume PowerUser, Database Admin, etc, we can add them to those role based AD Groups that are used to provide access to those IAM Roles. As seen above, John Admin was also a member of the PowerUsers AD Group. For the PowerUsers IAM Role, it’ll see that Session Tag, and allow access.

The other is to add additional permissions to the Trust Policy on the IAM Role in the Member account. In the example above, we saw that the Team-Admin-Role only allows assumption if the TeamAdmin tag was present and equaled true. As a side note, the Boolean true is used here mostly as a placeholder because ADFS won’t allow a blank value in my testing. The important part is the Tag Key, and if an AD User isn’t part of that AD Group, no Tag with that Key is sent, so false won’t come up in this situation.

Allowing multiple different team roles to assume an IAM Role looks like this:

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

There’s two distinct sections based on the Principal Tags provided. Additional sections can be added as appropriate depending on how this access is determined in your company.

Both options are perfectly reasonable, and there may be situations where both are used. If you define 10-15+ Team Roles, you may want to allow all of them to be assumed by someone in a higher ‘tier’ of permissions. If you only have a few, it may make more sense to just add people to each AD Group. This is flexible that way, and it’s up to you on what you’d prefer.

What About Blocking Modification of Tags?

With the way this is designed, you don’t even need to worry about people modifying tags to try and bypass controls. If I have admin permissions within my team’s accounts, and I modify the Team tags, the worst I can do is prevent my team from accessing the IAM Roles they need to access. Since I don’t have access to any IAM Roles in any other accounts, there is no way for me to modify something to give myself access to something I don’t already have access to.

You could still block these actions with an SCP if you feel so inclined, which will prevent an over-curious team lead from locking themselves out of their account. But that would be a learning moment for them and also be kind of funny, so your call.

Conclusion

The approach I took here is incredibly simple, but strangely enough not one I found elsewhere while researching ABAC. I think this premise offers huge benefits for administering team IAM Roles across large environments in a very simple manner when applications/teams are broken up into individual accounts.

The current configuration relies on deployment of the CFT to member accounts with the parameter for the Team tag changed appropriately, but future posts will iterate on this to provide more streamlined approaches to deployments across accounts.

If you have any questions, please feel free to reach out to me on LinkedIn.