AWS SCP Best Practices

2020.03.25

RSS feed

AWS Service Control Policies (SCPs) are a way of restricting the actions that can be taken in an AWS account so that all IAM users and roles, and even the root user cannot perform them. This feature is part of AWS Organizations, and the SCPs are controlled by the Organization Master account. This article will point out important concepts of SCPs and then provide example SCPs that can be used.

Contents

Understanding SCPs

Example Organization OU layout

Example Organization OU layout

AWS accounts can be organized in AWS Organizations into Organization Units (OUs), which can have child OUs and member accounts. These OUs can have different SCPs applied to them and the accounts can be moved between OUs. This means you can create heavily restrictive SCPs for a production AWS account, and less restrictive (or different) SCPs for a sandbox account. You can also create a nursery OU where you create AWS accounts, set up their baselines, and then move them to their final destination. This may be needed in cases where the initial setup requires you to make AWS calls that you would not otherwise want to allow.

SCPs cannot restrict the Master account of the Organization. This is a primary reason why it is best practice not to use the Organization Master account for anything other than Organization activities. This means you should not put S3 buckets, EC2s, or any other resources in your Organization Master because you cannot use SCPs to create guardrails around that.

SCPs also cannot restrict principals outside of the Organization. A common confusion is people incorrectly assuming that they can somehow block public access to an S3 bucket from users outside of an account by using SCPs. If an S3 bucket is public, an SCP will not be able to stop random Internet users from accessing that S3 bucket (although an SCP can stop that S3 bucket from being made public in the first place, as will be explained later).

SCPs are similar to IAM boundaries, in that they define the maximum set of actions that can be allowed, but do not actually grant any privileges. For example, the default SCP is Allow * on *, but this doesn’t mean that anyone in the accounts can do anything. It only means that the SCP is not further restricting them. Think of the relationship between SCPs and IAM policies as two overlapping circles, with the intersection of them being the allowed actions.

The effective privileges for a principal are the intersection of the SCPs and IAM policies applied to them

The effective privileges for an IAM principal are the intersection of the SCPs applied to their account and IAM policies applied to the principal

When SCPs were announced at re:Invent 2016 as part of the release of Organizations, they were limited to only restricting Actions (they could not use Conditions or Resources), so the use cases were limited to things like denying CloudTrail from being stopped. They now have nearly all the abilities of normal IAM policies so they can be complex. The most important case where they do not have all the abilities of normal IAM policies is that SCPs still cannot use Condition or Resource restrictions when the Effect is “Allow”, so you should not use complex “Allow” statements.

The member accounts of an AWS Organization are unable to see the SCPs that have been applied to them. Further, when actions are denied, there is no way to know whether that is due to an IAM policy, an SCP, or something else (ex. session policy, IAM boundary, resource policy). This means there will be no indication in the error message from an API call or in the CloudTrail log to show what denied the call. This can make debugging issues difficult.

Creating SCPs without breaking things

There is no “audit” mode for SCPs or other way to test whether an SCP is going to break things. There is an Organization level view of IAM Access Advisor that can be used to identify unused services. This can show whether services are used in an account, OU, or within an Organization. This information is accessible in the web console of the Organization Master account under the IAM service page under “Organization activity”, or via the API iam:GetOrganizationsAccessReport.

Organization Access Advisor view

Organization Access Advisor view

For more fine-grained data, you’ll have to review CloudTrail logs, being aware that CloudTrail logs do not record all actions, such as the data level activities including S3 object get and put actions (by default), cloudwatch:PutMetricData, and more.

Two person rule concept

SCPs can be changed, so this opens a concept worth considering, which is having a two-person rule enforcement. As an example, imagine you have an AWS account that has your backups in it, and you have one set of employees that can access that account and another set of employees that can adjust the SCPs for that account. You can apply an SCP that denies the ability to delete S3 buckets in that account and other actions, which would protect the backups as no person acting alone could delete the backed up data. The person that can set the SCPs has no ability to grant privileges to themself, and the person with access to the account is restricted by the SCP.

If one day, your company decided it wanted to adjust how it does back-ups, you could have one person remove the SCP, then the other person perform the adjustment, and then the original person re-apply the SCP. This doesn’t ensure that the second person doesn’t destroy the backups, but it does ensure that they are allowed to do that for only a limited time window. Also, for protecting S3 backups, you should be aware of S3 Object Lock as an alternative option.

Example SCPs

In this section I’ll mention some example SCPs, most found in various places throughout the AWS docs. Although I show these as policies, you can copy and paste the statements from these to create a single large policy to avoid the SCP limits.

Allow only approved services

The default SCP is FullAWSAccess which grants Allow of * on *. The only time your SCPs should include an Allow statement is either in that policy or by using a custom policy that lists allowed services. The allowed services you choose may be those that meet some compliance (ex. HIPAA), or those that the security team has otherwise approved. AWS does not have any baseline standards when it releases new services, so new services, even when GA (Generally Available), often do not have CloudTrail support to provide an audit log of their use, and may lack other features that are critical to you.

Having only a single statement granting Allow, and everything else Deny, simplifies your policies. Also Allow statements in an SCP cannot use conditional or resource restrictions (as mentioned here), so complicated Allow statements will not work as intended.

There are currently 213 IAM privilege service names, so in creating a policy to allow the current set of services, you need to be mindful of the SCP policy limits. One odd difference between IAM policies and SCPs is that white-space counts against the size limits in SCPs, so removing spaces and newlines can help circumvent the policy limitations if you run into them. You can use this policy and remove the services from it you don’t need.

{    
  "Version": "2012-10-17",
  "Statement": {
    "Sid": "AllowList",
    "Effect": "Allow",
    "Action": ["a4b:*","access-analyzer:*","account:*","acm:*","acm-pca:*","amplify:*","apigateway:*","application-autoscaling:*","applicationinsights:*","appmesh:*","appmesh-preview:*","appstream:*","appsync:*","arsenal:*","artifact:*","athena:*","autoscaling:*","autoscaling-plans:*","aws-marketplace:*","aws-marketplace-management:*","aws-portal:*","backup:*","backup-storage:*","batch:*","budgets:*","cassandra:*","ce:*","chatbot:*","chime:*","cloud9:*","clouddirectory:*","cloudformation:*","cloudfront:*","cloudhsm:*","cloudsearch:*","cloudtrail:*","cloudwatch:*","codebuild:*","codecommit:*","codedeploy:*","codeguru-profiler:*","codeguru-reviewer:*","codepipeline:*","codestar:*","codestar-notifications:*","cognito-identity:*","cognito-idp:*","cognito-sync:*","comprehend:*","comprehendmedical:*","compute-optimizer:*","config:*","connect:*","cur:*","dataexchange:*","datapipeline:*","datasync:*","dax:*","dbqms:*","deeplens:*","deepracer:*","detective:*","devicefarm:*","directconnect:*","discovery:*","dlm:*","dms:*","ds:*","dynamodb:*","ebs:*","ec2:*","ec2-instance-connect:*","ec2messages:*","ecr:*","ecs:*","eks:*","elastic-inference:*","elasticache:*","elasticbeanstalk:*","elasticfilesystem:*","elasticloadbalancing:*","elasticmapreduce:*","elastictranscoder:*","es:*","events:*","execute-api:*","firehose:*","fms:*","forecast:*","frauddetector:*","freertos:*","fsx:*","gamelift:*","glacier:*","globalaccelerator:*","glue:*","greengrass:*","groundstation:*","groundtruthlabeling:*","guardduty:*","health:*","iam:*","imagebuilder:*","importexport:*","inspector:*","iot:*","iot-device-tester:*","iot1click:*","iotanalytics:*","iotevents:*","iotsitewise:*","iotthingsgraph:*","kafka:*","kendra:*","kinesis:*","kinesisanalytics:*","kinesisvideo:*","kms:*","lakeformation:*","lambda:*","launchwizard:*","lex:*","license-manager:*","lightsail:*","logs:*","machinelearning:*","macie:*","managedblockchain:*","mechanicalturk:*","mediaconnect:*","mediaconvert:*","medialive:*","mediapackage:*","mediapackage-vod:*","mediastore:*","mediatailor:*","mgh:*","mobileanalytics:*","mobilehub:*","mobiletargeting:*","mq:*","neptune-db:*","networkmanager:*","opsworks:*","opsworks-cm:*","organizations:*","outposts:*","personalize:*","pi:*","polly:*","pricing:*","qldb:*","quicksight:*","ram:*","rds:*","rds-data:*","rds-db:*","redshift:*","rekognition:*","resource-groups:*","robomaker:*","route53:*","route53domains:*","route53resolver:*","s3:*","sagemaker:*","savingsplans:*","schemas:*","sdb:*","secretsmanager:*","securityhub:*","serverlessrepo:*","servicecatalog:*","servicediscovery:*","servicequotas:*","ses:*","shield:*","signer:*","sms:*","sms-voice:*","snowball:*","sns:*","sqs:*","ssm:*","ssmmessages:*","sso:*","sso-directory:*","states:*","storagegateway:*","sts:*","sumerian:*","support:*","swf:*","synthetics:*","tag:*","textract:*","transcribe:*","transfer:*","translate:*","trustedadvisor:*","waf:*","waf-regional:*","wafv2:*","wam:*","wellarchitected:*","workdocs:*","worklink:*","workmail:*","workmailmessageflow:*","workspaces:*","xray:*"],
    "Resource": "*"
  }
}

Deny root user access

The AWS Control Tower service recommends an SCP for denying the root user. This is great because it mitigates the concerns on AWS around password recovery (ie. account take-over) that can happen with Root users. It also means that if the root user cannot be used, then there isn’t a need to set up a multi-factor device for the user.

{
  "Version": "2012-10-17",
  "Statement": {
    "Sid": "DenyRootUser",
    "Effect": "Deny",
    "Action": "*",
    "Resource": "*",
    "Condition": {
      "StringLike": { "aws:PrincipalArn": "arn:aws:iam::*:root" }
    }
  }
}

Require the use of IMDSv2

These policies comes from the AWS docs on EC2 here. They are mentioned as IAM policies, but can be applied as SCPs. IMDSv2 is a more secure version of metadata service, that makes it harder to steal the IAM role from an EC2 unless you have full RCE (remote code execution) on the EC2. This new version largely came about as a result of the Capital One breach, although the problems with the original metadata service had been known about for years prior to that incident.

By default, all EC2s still allow access to the original metadata service, which means that if an attacker finds an EC2 running a proxy or WAF, or finds and SSRF vulnerability, they likely can steal the IAM role of the EC2. By enforcing IMDSv2, you can mitigate that risk. Be aware that this potentially could break some applications that have not yet been updated to work with the new IMDSv2.

This policy will require role credentials for an EC2 to have been retrieved using the IMDSv2. This policy can be applied generally to entire account as it only impacts the principals when they have the ec2:RoleDelivery variable associated with them, which will only happen on EC2s.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "RequireAllEc2RolesToUseV2",
            "Effect": "Deny",
            "Action": "*",
            "Resource": "*",
            "Condition": {
                "NumericLessThan": {
                    "ec2:RoleDelivery": "2.0"
                }
            }
        }
    ]
}

This next policy ensures that EC2s can only be created if you enforce that they use IMDSv2. Existing EC2s will not be impacted.

{
    "Version": "2012-10-17",
    "Statement": {
        "Sid": "RequireImdsV2",
        "Effect": "Deny",
        "Action": "ec2:RunInstances",
        "Resource": "arn:aws:ec2:*:*:instance/*",
        "Condition": {
            "StringNotEquals": {
                "ec2:MetadataHttpTokens": "required"
            }
        }
    }
}

Someone could create an EC2 that enforces IMDSv2 and then remove that enforcement, so to deny that you’ll additionally want this policy:

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Deny",
        "Action": "ec2:ModifyInstanceMetadataOptions",
        "Resource": "*"
    }
}

In order to get the maximum security benefit out of IMDSv2, you’ll also want to ensure the hop count that can be set is restricted:

{
    "Version": "2012-10-17",
    "Statement": {
        "Sid": "MaxImdsHopLimit",
        "Effect": "Deny",
        "Action": "ec2:RunInstances",
        "Resource": "arn:aws:ec2:*:*:instance/*",
        "Condition": {
            "NumericGreaterThan": {"ec2:MetadataHttpPutResponseHopLimit": "1"}
        }
    }
}

Deny ability to create IAM access keys

In order to avoid having long lived credentials that never expire which can end up being exposed, some companies ban access keys entirely. This policy denies the ability to create IAM users and access keys.

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Deny",
        "Action": ["iam:CreateAccessKey", "iam:CreateUser"],
        "Resource": "*"
    }
}

Region enforcement

Your company likely only works out of a few AWS regions, and for various reasons you probably want to avoid other regions from being used. AWS provides an example policy here, but it is incomplete. There are a number of global services on AWS not mentioned in that policy which will not be usable if you apply that policy. Obtaining the complete list is described by me here.

The following policy enforces that only ap-southeast-2 can be used, along with the global services. Be aware that if you use Lambda@Edge, you might need to adjust your code, or this policy, as you’ll likely end up making calls in other regions as the Lambdas that are run will default to making calls in the closest region to the distribution’s point of presence.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "RestrictRegion",
            "Effect": "Deny",
            "NotAction": [
                "a4b:*",
                "budgets:*",
                "ce:*",
                "chime:*",
                "cloudfront:*",
                "cur:*",
                "globalaccelerator:*",
                "health:*",
                "iam:*",
                "importexport:*",
                "mobileanalytics:*",
                "organizations:*",
                "route53:*",
                "route53domains:*",
                "shield:*",
                "support:*",
                "trustedadvisor:*",
                "waf:*",
                "wellarchitected:*"
            ],
            "Resource": "*",
            "Condition": {
                "StringNotEquals": {
                    "aws:RequestedRegion": [
                        "ap-southeast-2"
                    ]
                }
            }
        }
    ]
}

Deny ability to leave Organization

Once you’ve done all this hard work of setting guardrails in your account, you want to avoid having the accounts simply leave your organization where they would no longer be restricted by your SCPs, so this protects against that.

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Deny",
        "Action": "organizations:LeaveOrganization",
        "Resource": "*"
    }
}

Deny ability to make a VPC accessible from the Internet that isn’t already

A good strategy used by some companies is to setup the networking resources for an account as part of the initial account setup and then not allow that to be changed. As part of this, companies will provide sandbox or dev accounts that are not allowed to have publicly accessible network resources. This SCP, from the AWS docs here, enforces that.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": [
        "ec2:AttachInternetGateway",
        "ec2:CreateInternetGateway",
        "ec2:CreateEgressOnlyInternetGateway",
        "ec2:CreateVpcPeeringConnection",
        "ec2:AcceptVpcPeeringConnection",
        "globalaccelerator:Create*",
        "globalaccelerator:Update*"
      ],
      "Resource": "*"
    }
  ]
}

Protect security baseline

Once you’ve configured an AWS account to meet a security baseline, you’ll want to ensure your configuration cannot be modified by a user accidentally changing things, or an attacker attempting to avoid detection and response processes. The canoncial example of how to use SCPs was focused on this concept and was to deny the ability to turn of CloudTrail logs. However, you should be using Organization Trails which cannot be turned off by member accounts in the first place, so there is no longer a need for that policy.

There are a lot of security configurations you’ll likely set up, and you should think through what you need to do to protect these. For example, if your logging and alerting pipeline relies on CloudWatch Alarms or SNS, you may want to create SCPs that protects those. The following policies should work for most accounts without causing usability issues.

Deny ability to disrupt GuardDuty

GuardDuty is a great service from AWS for detecting compromises and more. This policy ensures it isn’t turned off, or that findings are filtered.

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Deny",
        "Action": [
            "guardduty:DeleteDetector",
            "guardduty:DisassociateFromMasterAccount",
            "guardduty:UpdateDetector",
            "guardduty:CreateFilter",
            "guardduty:CreateIPSet"
        ],
        "Resource": "*"
    }
}

Deny ability to disrupt CloudWatch Event collection

My preferred way of aggregating GuardDuty alerts other real-time information from accounts is to use EventBridge CloudWatch Rules. I discuss how to do this in my blog post GuardDuty Event Collection via CloudWatch Events.

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Deny",
        "Action": [
            "events:DeleteRule",
            "events:DisableRule",
            "events:RemoveTargets"
        ],
        "Resource": "arn:aws:events:*:*:rule/default/CHANGEME"
    }
}

Deny ability to modify an important IAM role

This policy (from the AWS docs here) can be used to deny modifications of an incident response or other security auditing role.

{    
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyAccessToASpecificRole",
      "Effect": "Deny",
      "Action": [
        "iam:AttachRolePolicy",
        "iam:DeleteRole",
        "iam:DeleteRolePermissionsBoundary",
        "iam:DeleteRolePolicy",
        "iam:DetachRolePolicy",
        "iam:PutRolePermissionsBoundary",
        "iam:PutRolePolicy",
        "iam:UpdateAssumeRolePolicy",
        "iam:UpdateRole",
        "iam:UpdateRoleDescription"
      ],
      "Resource": [
        "arn:aws:iam::*:role/CHANGEME"
      ]
    }
  ]
}

Protect security settings

The following SCP protects some important security settings from being turned off. None of these features are enabled by default and should be enabled as part of your initial account baseline. These features are:

{    
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": [
        "access-analyzer:DeleteAnalyzer",
        "ec2:DisableEbsEncryptionByDefault",
        "s3:PutAccountPublicAccessBlock"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

AWS Wishlist

To make SCPs easier to use, I would recommend AWS do the following:

  • Allow member account to query what SCPs are applied to them.
  • Provide a way of identifying why an action was denied. One possible solution would be to have an API call that you could pass the requestID from a CloudTrail log event that would identify what denied the action (ex. SCP? IAM Policy? IAM Boundary? Session policy? Resource Policy?) and which statement did that. Open-sourcing the IAM policy engine would also be very helpful here, along with helping a lot of other areas AWS security.
  • Have an audit mode for SCPs that would record in CloudTrail whether API calls would have been denied had the SCP not been in an audit mode.