# Proudly found elsewhere and partially copied from: # https://github.com/theserverlessway/aws-baseline AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Sets up Service Control Policies(SCP). (qs-1s3rsr7o0) Parameters: IncludeSecurityHub: AllowedValues: - true - false Type: String IncludeBackup: AllowedValues: - true - false Type: String Conditions: IncludeSecurityHub: !Equals [ !Ref IncludeSecurityHub, true ] IncludeBackup: !Equals [ !Ref IncludeBackup, true ] RolloutSCPs: !And - !Equals [ !Ref IncludeSecurityHub, true ] - !Equals [ !Ref IncludeBackup, true ] Resources: SCPBaseline: Type: AWS::CloudFormation::CustomResource Condition: RolloutSCPs Properties: ServiceToken: !GetAtt SCPCustomResource.Arn Policy: !Sub - | { "Version": "2012-10-17", "Statement": [ ${Statements} ] } - Statements: !Join - ',' - - !If - IncludeSecurityHub - !Sub | { "Condition": { "ArnNotLike": { "aws:PrincipalARN": "arn:${AWS::Partition}:iam::*:role/AWSControlTowerExecution" } }, "Action": [ "securityhub:DeleteInvitations", "securityhub:DisableSecurityHub", "securityhub:DisassociateFromMasterAccount", "securityhub:DeleteMembers", "securityhub:DisassociateMembers" ], "Resource": [ "*" ], "Effect": "Deny", "Sid": "SWProtectSecurityHub" } - !Ref AWS::NoValue - !If - IncludeBackup - !Sub | { "Condition": { "ArnNotLike": { "aws:PrincipalARN": "arn:${AWS::Partition}:iam::*:role/stacksets-exec-*" } }, "Action": [ "iam:AttachRolePolicy", "iam:CreateRole", "iam:DeleteRole", "iam:DeleteRolePermissionsBoundary", "iam:DeleteRolePolicy", "iam:DetachRolePolicy", "iam:PutRolePermissionsBoundary", "iam:PutRolePolicy", "iam:UpdateAssumeRolePolicy", "iam:UpdateRole", "iam:UpdateRoleDescription" ], "Resource": [ "arn:${AWS::Partition}:iam::*:role/service-role/AWSBackupDefaultServiceRole", "arn:${AWS::Partition}:iam::*:role/SuperwerkerBackupTagsEnforcementRemediationRole" ], "Effect": "Deny", "Sid": "SWProtectBackup" } - !Ref AWS::NoValue Attach: true SCPCustomResource: Type: AWS::Serverless::Function Properties: Timeout: 200 Runtime: python3.7 Handler: index.handler Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - organizations:CreatePolicy - organizations:UpdatePolicy - organizations:DeletePolicy - organizations:AttachPolicy - organizations:DetachPolicy - organizations:ListRoots - organizations:ListPolicies - organizations:ListPoliciesForTarget Resource: "*" InlineCode: | import boto3 import cfnresponse import time import random import re o = boto3.client("organizations") CREATE = 'Create' UPDATE = 'Update' DELETE = 'Delete' SCP = "SERVICE_CONTROL_POLICY" def root(): return o.list_roots()['Roots'][0] def root_id(): return root()['Id'] def exception_handling(function): def catch(event, context): try: function(event, context) except Exception as e: print(e) print(event) cfnresponse.send(event, context, cfnresponse.FAILED, {}) return catch def with_retry(function, **kwargs): for i in [0, 3, 9, 15, 30]: # Random sleep to not run into concurrency problems when adding or attaching multiple SCPs # They have to be added/updated/deleted one after the other sleeptime = i + random.randint(0, 5) print('Running {} with Sleep of {}'.format(function.__name__, sleeptime)) time.sleep(sleeptime) try: response = function(**kwargs) print("Response for {}: {}".format(function.__name__, response)) return response except o.exceptions.ConcurrentModificationException as e: print('Exception: {}'.format(e)) raise Exception def handler(event, context): RequestType = event["RequestType"] Properties = event["ResourceProperties"] LogicalResourceId = event["LogicalResourceId"] PhysicalResourceId = event.get("PhysicalResourceId") Policy = Properties["Policy"] Attach = Properties["Attach"] == 'true' print('RequestType: {}'.format(RequestType)) print('PhysicalResourceId: {}'.format(PhysicalResourceId)) print('LogicalResourceId: {}'.format(LogicalResourceId)) print('Attach: {}'.format(Attach)) parameters = dict( Content=Policy, Description="superwerker - {}".format(LogicalResourceId), Name="superwerker", ) policy_id = PhysicalResourceId try: if RequestType == CREATE: print('Creating Policy: {}'.format(LogicalResourceId)) response = with_retry(o.create_policy, **parameters, Type=SCP ) policy_id = response["Policy"]["PolicySummary"]["Id"] if Attach: with_retry(o.attach_policy, PolicyId=policy_id, TargetId=root_id()) elif RequestType == UPDATE: print('Updating Policy: {}'.format(LogicalResourceId)) with_retry(o.update_policy, PolicyId=policy_id, **parameters) elif RequestType == DELETE: print('Deleting Policy: {}'.format(LogicalResourceId)) # Same as above if re.match('p-[0-9a-z]+', policy_id): if policy_attached(policy_id): with_retry(o.detach_policy, PolicyId=policy_id, TargetId=root_id()) with_retry(o.delete_policy, PolicyId=policy_id) else: print('{} is no valid PolicyId'.format(policy_id)) else: raise Exception('Unexpected RequestType: {}'.format(RequestType)) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, policy_id) except Exception as e: print(e) print(event) cfnresponse.send(event, context, cfnresponse.FAILED, {}, policy_id) def policy_attached(policy_id): return [p['Id'] for p in o.list_policies_for_target(TargetId=root_id(), Filter='SERVICE_CONTROL_POLICY')['Policies'] if p['Id'] == policy_id] def policy_attached(policy_id): return [p['Id'] for p in o.list_policies_for_target(TargetId=root_id(), Filter='SERVICE_CONTROL_POLICY')['Policies'] if p['Id'] == policy_id] SCPEnable: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: !GetAtt SCPEnableCustomResource.Arn SCPEnableCustomResource: Type: AWS::Serverless::Function Properties: Timeout: 200 Handler: index.enable_service_control_policies Runtime: python3.7 Policies: - Version: 2012-10-17 Statement: - Effect: Allow Action: - organizations:EnablePolicyType - organizations:DisablePolicyType - organizations:ListRoots Resource: "*" InlineCode: | import boto3 import cfnresponse import time import random import re o = boto3.client("organizations") CREATE = 'Create' UPDATE = 'Update' DELETE = 'Delete' SCP = "SERVICE_CONTROL_POLICY" def root(): return o.list_roots()['Roots'][0] def root_id(): return root()['Id'] def scp_enabled(): enabled_policies = root()['PolicyTypes'] return {"Type": SCP, "Status": "ENABLED"} in enabled_policies def exception_handling(function): def catch(event, context): try: function(event, context) except Exception as e: print(e) print(event) cfnresponse.send(event, context, cfnresponse.FAILED, {}) return catch @exception_handling def enable_service_control_policies(event, context): RequestType = event["RequestType"] if RequestType == CREATE and not scp_enabled(): r_id = root_id() print('Enable SCP for root: {}'.format(r_id)) o.enable_policy_type(RootId=r_id, PolicyType=SCP) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, 'SCP') def with_retry(function, **kwargs): for i in [0, 3, 9, 15, 30]: # Random sleep to not run into concurrency problems when adding or attaching multiple SCPs # They have to be added/updated/deleted one after the other sleeptime = i + random.randint(0, 5) print('Running {} with Sleep of {}'.format(function.__name__, sleeptime)) time.sleep(sleeptime) try: response = function(**kwargs) print("Response for {}: {}".format(function.__name__, response)) return response except o.exceptions.ConcurrentModificationException as e: print('Exception: {}'.format(e)) raise Exception Metadata: SuperwerkerVersion: 0.13.2 cfn-lint: config: ignore_checks: - E9007 - EIAMPolicyWildcardResource