Adding AWS SSO and controlling permissions
Tags: aws, opensource, security, tutorial
Published: 2022-03-23 12:30:11


## Introduction

This is the second in a series of posts looking at some of the core services, building blocks and approaches that will help you build out a multi-account best practice landing zone:

* Part 1: [Initial setup up of a multi-account AWS environment](https://dev.to/aws-heroes/setting-up-a-multi-account-aws-environment-1h67)
* Part 2: Adding AWS SSO and controlling permissions
* Part 3: [Centralising audit, compliance and incident detection](https://dev.to/aws-heroes/centralising-audit-compliance-and-incident-detection-11fi)

## Setting up AWS Single Sign-On (SSO)

AWS SSO is the service to centrally manage access across your AWS organization. Your identity source in AWS SSO defines where your users and groups are managed. Although most enterprises will choose to manage users through Active Directory or an external identity provider, AWS SSO comes with an inbuilt identity source that allows you to create your users and groups directly in SSO, which is what we will use here.

AWS SSO prevents the need to store credentials on disk. Adding and removing users from a group dynamically changes their permissions, and you can customise the maximum session durations. There are some shortcomings with the service, and if you want to get more involved, it's worth checking out [aws-sso-util](https://github.com/benkehoe/aws-sso-util) by [Ben Kehoe](https://twitter.com/ben11kehoe).

Setting up AWS SSO is something that must be done in the console in the management account, clicking on the 'Enable AWS SSO' button.


![Enable AWS SSO](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t7pr09ibb2230a97j588.png)

Once enabled, you will see a dashboard where you can customise the User Portal URL to something more memorable.

Click on Settings and make a note of the AWS SSO ARN as well as the User Portal URL. We then manually create the four groups we are going to use, which are "Developer", "Admin", "SecurityAdmin" and "IncidentResponse". For each one, make a note of the Group ID.

![AWS SSO Groups](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fh5pp0le0t6zpn0ml70s.png)

Next we want to create a user. Set the username you want, fill in the name and unique email address, and finally select the groups you want to add the user too. For simplicity, I add the user to all groups for testing purposes.

Now this has been set up, we are in a position to start exploring how to make our environments secure and put in place more fine grained permissions.

## Permission Sets and Assignments

Having defined four groups, we have to apply policies to give them permission to access AWS resources. All users within a group will inherit these permissions. We do this by creating a permission set, which is a template you create and maintain that defines a collection of one or more IAM policies. For this blog, we will just focus on setting up the Developer permission set.

```yaml
  DevPermissionSet:
    Type: AWS::SSO::PermissionSet
    Properties:
      Name: 'Developer'
      Description: 'Developer access to AWS organization'
      InstanceArn: {AWS SSO instance ARN}
      ManagedPolicies: 
      - arn:aws:iam::aws:policy/AdministratorAccess
      SessionDuration: 'PT1H'
```

This creates a permission set with admin access that we can assign to an AWS account using an Assignment as shown below:

```yaml
  DevAssignmentSandbox:
    Type: AWS::SSO::Assignment
    Properties:
      InstanceArn: {AWS SSO instance ARN}
      PermissionSetArn: !GetAtt DevPermissionSet.PermissionSetArn
      PrincipalId: {GroupID for Developer Group in SSO}
      PrincipalType: GROUP
      TargetType: AWS_ACCOUNT
      TargetId: {Account ID for target account}
```
We create an Assignment for each account in the Sandbox, Dev and Prod OUs. Unfortunately there is no current way in CloudFormation to natively reference OUs as targets. There is a custom resource provider in org-formation you can use to iterate over OUs, but for this post we use CloudFormation to make it clear how it works under the covers.

After running the pipeline, we can log in as our user via the User Portal URL, and are given an option to access the Sandbox, Dev and Prod accounts as a developer. The admin access may be acceptable for the sandbox account, but is overly permissive for other environments. Our next goal is to look at a way of retaining a single developer permission set but putting more control on the permissions.

## Fine tune developer permission set

Luckily [Ben Kehoe](https://twitter.com/ben11kehoe) has written a great article on how to ['Use aws:PrincipalAccount to fine-tune your AWS SSO permission sets'](https://ben11kehoe.medium.com/use-aws-principalaccount-to-fine-tune-your-aws-sso-permission-sets-228d968631f6), and this is the approach we will use.

We extend the permission set to grant read only and support access, and then add an inline policy with admin access, but allow this only where the account is one of those in the Sandbox OU.

```yml
  DevPermissionSet:
    Type: AWS::SSO::PermissionSet
    Properties:
      Name: !Ref permissionSetName
      Description: !Sub '${permissionSetName} access to AWS organization'
      InstanceArn: !Ref instanceArn
      ManagedPolicies: 
      - arn:aws:iam::aws:policy/ReadOnlyAccess
      - arn:aws:iam::aws:policy/AWSSupportAccess
      SessionDuration: !Ref sessionDuration
      InlinePolicy:
        Version: 2012-10-17
        Statement:
          - Sid: 'AdminAccessToResources'
            Effect: Allow
            Action: 
              - "*"
            Resource: 
              - "*"
            Condition:
              StringEquals:
                aws:PrincipalAccount: Fn::EnumTargetAccounts SandboxBinding ${account}
```
This now allows users in the developer group to inherit admin permissions in a Sandbox account. We go on to add a new statement in the developer policy for accounts in the Dev OU as shown below. Note that this a very cut down example of what it may look like for your organisation.

```yml
- Sid: DeveloperPermissionsPolicy
  Effect: Allow
  Action:
    - account:ListRegions
    - cloudformation:*
    - cloudwatch:*
    - dynamodb:*
    - iam:Get*
    - iam:List*
    - lambda:*
    - logs:*
    - organizations:DescribeOrganization
    - s3:*
    - sts:*
    - secretsmanager:*
  Resource: "*"
  Condition:
    StringEquals:
      aws:PrincipalAccount: Fn::EnumTargetAccounts DevBinding ${account}
- Sid: DeleteDetachTagRole
  Effect: Allow
  Action:
    - iam:DeleteRole
    - iam:DeleteRolePolicy
    - iam:DetachRolePolicy
    - iam:TagRole
    - iam:UntagRole
  Resource:
    - arn:aws:iam::*:role/dev-*  
  Condition:
    StringEquals:
      aws:PrincipalAccount: Fn::EnumTargetAccounts DevBinding ${account}
- Sid: CreateRole
  Effect: Allow
  Action:
    - iam:AttachRolePolicy
    - iam:CreateRole
    - iam:PutRolePolicy
    - iam:PutRolePermissionsBoundary
  Resource:
    - arn:aws:iam::*:role/dev-*
  Condition:
    StringEquals:
      aws:PrincipalAccount: Fn::EnumTargetAccounts DevBinding ${account}
```
We can now log into a Development account and carry out actions such as creating an S3 bucket. We can try and launch an EC2 instance, and this will fail as we are not authorised to do so. However, there is a big security flaw in the policy above. In the Development accounts, we want to give developers the capability to create roles to support the services they are building. The policy above allows the developer to create a new IAM role (prefixed with 'dev-') with admin access, assume the new role, and then carry out actions with these privileges. We can test this by assuming a new role with admin access, and successfully launching a new EC2 instance.

Our next goal is to look at constraining the permissions that can be granted when creating these roles using a permission boundary.

## Constrain permissions using a Permission Boundary

A permissions boundary is an advanced feature for using a managed policy to set the maximum permissions that an identity-based policy can grant to an IAM entity. An entity's permissions boundary allows it to perform only the actions that are allowed by both its identity-based policies and its permissions boundaries.

We start by deploying an example managed policy to all Development accounts

```yml
  ExamplePermissionBoundaryPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: dev-permission-boundary-policy
      Description: Permission Boundary example
      Path: /dev/
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action: ...
```
Next we update the CreateRole statement to enforce that a role can only be created if it includes the managed policy above as a permission boundary. This will now limit the permissions that can be assigned, as the allowable permissions for that role are those allowed by both the identity policy attached to the role, and those allowed in the permission boundary

```yml
- Sid: CreateRoleWithPermissionBoundary
  Effect: Allow
  Action:
    - iam:AttachRolePolicy
    - iam:CreateRole
    - iam:PutRolePolicy
    - iam:PutRolePermissionsBoundary
  Resource:
    - arn:aws:iam::*:role/dev-*
  Condition:
    StringLike:
      iam:PermissionsBoundary: !Sub arn:aws:iam::*:policy/dev/dev-permission-boundary-policy
    StringEquals:
      aws:PrincipalAccount: Fn::EnumTargetAccounts DevBinding ${account}
```
This covers off the Sandbox and Dev OUs, but what about Prod? Currently developers have read only access to Production, so what happens if there is an issue or incident that needs a higher level of permission to investigate. This is where we look at cross account roles.

## Elevating privileges with Cross Account Role

In support of security incidents and exceptional cases, you should have a process by which authorised administrators can temporarily gain access to your accounts. We will support this break glass process using cross-account roles. In this scenario, we want our users in the IncidentResponse group to be able to assume an Administrator role in a Production account.

The template below could be run as and when required. The `AssumeRoleBinding` targets accounts in the Security OU and the `RoleBinding` targets accounts in the Prod OU.

The template creates an IAM role in Production accounts with admin access that can be assumed by accounts in the Security OU.

```yml
OrganizationBindings:

  AssumeRoleBinding:
    OrganizationalUnit: !Ref SecurityOU
    Region: 'eu-west-2'

  RoleBinding:
    OrganizationalUnit: !Ref ProdOU
    Region: 'eu-west-2'

Resources:

  Role:
    Type: AWS::IAM::Role
    OrganizationBinding: !Ref RoleBinding
    Properties:
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess
      RoleName: "elevated-security-role"
      AssumeRolePolicyDocument:
       Version: 2012-10-17
       Statement:
         - Effect: Allow
           Action: sts:AssumeRole
           Principal:
            AWS: Fn::EnumTargetAccounts AssumeRoleBinding '${account}' # role can only be assumed from SecurityOU
```
In the accounts in the Security OU, we only want users in the IncidentResponse group to be able to assume the role. We can do that using an inline policy on the permission set.

```yml
  IncidentResponsePermissionSet:
    Type: AWS::SSO::PermissionSet
    Properties:
      Name: !Ref permissionSetName
      Description: !Sub '${permissionSetName} access to Production accounts'
      InstanceArn: !Ref instanceArn
      SessionDuration: !Ref sessionDuration
      InlinePolicy:
        Version: 2012-10-17
        Statement:
          - Sid: 'AccessToElevatedRole'
            Effect: Allow
            Action: sts:AssumeRole
            Resource: Fn::EnumTargetAccounts RoleBinding 'arn:aws:iam::${account}:role/elevated-security-role'
```

We prevent all Security Admins from being able to assume the role by placing an explicit deny on their permission set

```yml
  SecurityPermissionSet:
    Type: AWS::SSO::PermissionSet
    Properties:
      Name: !Ref permissionSetName
      Description: !Sub '${permissionSetName} access to AWS organization'
      InstanceArn: !Ref instanceArn
      ManagedPolicies: 
      - arn:aws:iam::aws:policy/AdministratorAccess
      SessionDuration: !Ref sessionDuration
      InlinePolicy:
        Version: 2012-10-17
        Statement:
          - Sid: 'DenyAccessToElevatedRole'
            Effect: Deny
            Action: sts:AssumeRole
            Resource: Fn::EnumTargetAccounts RoleBinding 'arn:aws:iam::${account}:role/elevated-security-role'
```

This now means that users in the IncidentResponse group can access their Security account, and from their assume the elevated role in a Production account. This could be further improved by raising an alert whenever this role is assumed. This can be achieved by ensuring that CloudTrail logs to a CloudWatch Log Group, and then setting up a Metric Filter when as `assumeRole` event takes place for the elevated role. This can then trigger a CloudWatch Alarm and appropriate action take place. We will look more at CloudTrail in our next blog post in this series.

Finally, adopting a multi-account setup using AWS Organizations allows us to take advantage of Service Control Policies, which we will look at now.

## Applying Service Control Policies to your Organization

A Service Control Policy (SCP) is a special type of organization policy used to manage permissions in an AWS organization. An SCP alone is not sufficient to grant permission. Instead, it defines a guardrail on the actions that can be delegated to IAM user and roles in an account. The administrator must still attach identity-based or resource-based policies to actually grant permission.

An SCP restricts permissions for IAM users and roles in member accounts, including the member account's root user. They have no effect on users or roles in the management account. SCPs do not affect any service-linked role. Service-linked roles enable other AWS services to integrate with AWS Organizations and can't be restricted by SCPs.

Root is the parent OU for all accounts and other OU's in your organization. When you apply a policy to the root, it applies to every OU and account in the organization. An OU can contain other OU's up to five levels deep. You can apply an SCP at root, to an OU or to a specific account within an OU.

So let's take a look at some typical SCPs that we can apply.

**Restrict use of Root User**
The root user is the owner of the AWS account. It is not possible to use an IAM policy to explicitly deny the root user access to resource. The root user also has permissions to carry out other destructive activities such as closing an account. AWS strongly recommend not to use root, and so we can apply an SCP to block access for the root user.

```yml
RestrictAccessForRootSCP:
  Type: OC::ORG::ServiceControlPolicy
  Properties:
    PolicyName: RestrictAccessForRoot
    Description: Deny root user from accessing any resources
    PolicyDocument:
      Version: '2012-10-17'
      Statement:
        - Sid: RestrictAccessForRoot
          Effect: Deny
          Action:
            - '*'
          Resource:
            - '*'
          Condition:
            StringLike:
              'aws:PrincipalArn':
                - 'arn:aws:iam::*:root'
```

**Enforce use of tags**

Another best practice within AWS is to create an effective tagging strategy for your resources. This can be used to allocate costs, for operational support, or even to enforce access control in combination with tag-based conditions.

An effective resource tagging strategy will combine both [Tag Policies](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_tag-policies.html) and SCPs. Tag Policies only enforce the accepted value of a tag, and not its presence. You can then use an SCP to ensure that a resource cannot be created with the relevant tags present. The example below is for EC2 instances, but a similar concept applies across other types of resources.

```yml
- Sid: DenyNonTaggedResourceCreation
  Effect: Deny
  Action: 'ec2:RunInstances'
  Resource:
   - 'arn:aws:ec2:*:*:instance/*'
   - 'arn:aws:ec2:*:*:volume/*'
  Condition:
    'Null':
      'aws:RequestTag/ServiceName': 'true'
```

**Prevent member accounts from leaving the organization**

Now we have set up our organization with some of the guardrails in place, we want to prevent a member account from leaving. This can be carried out with the following:

```yml
## Prevent leaving organization
- Sid: PreventLeavingOrganization
  Effect: Deny
  Action:
   - 'organizations:LeaveOrganization'
  Resource: '*'
```

**Deny access to AWS based on the requested AWS Region**

Many organisations have requirements around data sovereignty and impose restrictions on the regions in which services can be deployed. This policy uses the Deny effect to deny access to all requests for operations that don't target the London region (eu-west-2). The `NotAction` element enables you to list services whose operations are exempted from this restriction, typically as global services have endpoints physically hosted in the us-east-1 region, so they must be exempted in this way.

```yml
- Sid: RestrictAccessOnlyToLondonRegion
  Effect: Deny
  NotAction:
   - 'a4b:*'
   - 'acm:*'
   - 'access-analyzer:*'
   - 'aws-marketplace-management:*'
   - 'aws-marketplace:*'
   - 'aws-portal:*'
   - 'budgets:*'
   - 'ce:*'
   - 'chime:*'
   - 'cloudfront:*'
   - 'config:*'
   - 'cur:*'
   - 'directconnect:*'
   - 'ec2:DescribeRegions'
   - 'ec2:DescribeTransitGateways'
   - 'ec2:DescribeVpnGateways'
   - 'fms:*'
   - 'globalaccelerator:*'
   - 'health:*'
   - 'iam:*'
   - 'importexport:*'
   - 'kms:*'
   - 'mobileanalytics:*'
   - 'networkmanager:*'
   - 'organizations:*'
   - 'pricing:*'
   - 'route53:*'
   - 'route53domains:*'
   - 's3:GetAccountPublic*'
   - 's3:ListAllMyBuckets'
   - 's3:PutAccountPublic*'
   - 'shield:*'
   - 'sts:*'
   - 'support:*'
   - 'trustedadvisor:*'
   - 'waf-regional:*'
   - 'waf:*'
   - 'wafv2:*'
   - 'wellarchitected:*'
  Resource: '*'
  Condition:
    StringNotEquals:
      'aws:RequestedRegion':
        - eu-west-2
    ArnNotLike:
      'aws:PrincipalARN':
        - 'arn:aws:iam::*:role/ByPassThisSCPRole'
```

**Prevent internet access**

There are many new approaches around networking setup within your AWS environment, with this recent blog post talking about [deployment models for AWS Network Firewall](https://aws.amazon.com/blogs/networking-and-content-delivery/deployment-models-for-aws-network-firewall/).

Some companies may want to ensure all egress is routed via AWS Network Firewall in combination with AWS Transit Gateway creating the notion of an Inspection VPC, with this controlling an allow list for outbound traffic. In this case, we can enforce that accounts are not able to create their own Internet Gateway using the following SCP: 

```yml
- Sid: DenyCreateInternetGateway
  Effect: Deny
  Action:
    - 'ec2:AttachInternetGateway'
    - 'ec2:CreateInternetGateway'
  Resource: '*'
- Sid: DenyCreateEgressOnlyInternetGateway
  Effect: Deny
  Action:
    - 'ec2:AttachEgressOnlyInternetGateway'
  Resource: '*'
```

**Prevent large instance types**

In our setup we provide a Sandbox account to allow developers to innovate. We can allow them to spin up EC2 instances, but can prevent any large instances being launched:

```yml
- Sid: DenyLargerThan2XLarge
  Effect: Deny
  Action:
    - "ec2:RunInstances"
  Resource: "arn:aws:ec2:*:*:instance/*"
  Condition:
    ForAnyValue:StringNotLike:
      "ec2:InstanceType":
        - "*.nano"
        - "*.small"
        - "*.micro"
        - "*.medium"
        - "*.large"
        - "*.xlarge"
        - "*.2xlarge"
```

There are a maximum of 5 SCPs that can be attached to either the root, an OU or an account. This means we will create an SCP combining a number of statements.

One great aspect about managing the AWS environment through configuration is that the `organization.yml` file clearly shows what SCPs are applied, which can help when tracking down issues. An example is shown below:

```yml
  OrganizationRoot:
    Type: OC::ORG::OrganizationRoot
    Properties:
      DefaultOrganizationAccessRoleName: OrganizationAccountAccessRole
      DefaultBuildAccessRoleName: OrganizationFormationBuildAccessRole
      ServiceControlPolicies:
        - !Ref OrgWideManagementControlsSCP
        - !Ref OrgWideServiceControlsSCP

  <<: !Include ./250-scps/deny-root-access.yml
  <<: !Include ./250-scps/deny-large-ec2.yml
  <<: !Include ./250-scps/org-wide-management-controls.yml
  <<: !Include ./250-scps/org-wide-service-controls.yml
```

If you log into the Management Account, access AWS Organizations, select the Root account and select Policies, you will see these new SCPs applied:

![Applied SCPs to Root](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tp6u9ew7nhnzfgyo648i.png)

This post has touched on a number of the different policy types that exist, and how powerful the use of conditions and permission boundaries can be. In the next post, we will start to look at compliance and incident detection with services such as AWS Config, AWS CloudTrail and Amazon GuardDuty.