From 725182178f6d29877fbbbea5cfddecb9e8d63a30 Mon Sep 17 00:00:00 2001 From: superwerker-bot Date: Thu, 2 Feb 2023 10:39:11 +0000 Subject: [PATCH 1/3] release: 0.15.0 --- templates/backup.yaml | 1040 ----------------------- templates/budget.yaml | 172 ---- templates/control-tower.yaml | 226 ----- templates/guardduty.yaml | 308 ------- templates/living-documentation.yaml | 135 --- templates/notifications.yaml | 85 -- templates/rootmail.yaml | 784 ----------------- templates/security-hub.yaml | 435 ---------- templates/service-control-policies.yaml | 321 ------- templates/superwerker.template.yaml | 705 +++++++++------ 10 files changed, 434 insertions(+), 3777 deletions(-) delete mode 100644 templates/backup.yaml delete mode 100755 templates/budget.yaml delete mode 100644 templates/control-tower.yaml delete mode 100644 templates/guardduty.yaml delete mode 100755 templates/living-documentation.yaml delete mode 100644 templates/notifications.yaml delete mode 100644 templates/rootmail.yaml delete mode 100644 templates/security-hub.yaml delete mode 100644 templates/service-control-policies.yaml diff --git a/templates/backup.yaml b/templates/backup.yaml deleted file mode 100644 index 4d2d6bf..0000000 --- a/templates/backup.yaml +++ /dev/null @@ -1,1040 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Metadata: - SuperwerkerVersion: 0.13.2 - cfn-lint: - config: - ignore_checks: - - E9007 - - EPolicyWildcardPrincipal - - E1029 - -Transform: AWS::Serverless-2016-10-31 -Description: Sets up backups. (qs-1s3rsr7la) - -Resources: - - OrganizationsLookup: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt OrganizationsLookupCustomResource.Arn - - OrganizationsLookupCustomResource: - Type: AWS::Serverless::Function - Properties: - Handler: index.handler - Runtime: python3.7 - Policies: - - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - organizations:ListRoots - - organizations:DescribeOrganization - Resource: "*" - InlineCode: | - import boto3 - import cfnresponse - - org = boto3.client("organizations") - - CREATE = 'Create' - DELETE = 'Delete' - UPDATE = 'Update' - - - 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 handler(event, context): - RequestType = event["RequestType"] - LogicalResourceId = event["LogicalResourceId"] - PhysicalResourceId = event.get("PhysicalResourceId") - - print('RequestType: {}'.format(RequestType)) - print('PhysicalResourceId: {}'.format(PhysicalResourceId)) - print('LogicalResourceId: {}'.format(LogicalResourceId)) - - id = PhysicalResourceId - - data = {} - - organization = org.describe_organization()['Organization'] - data['OrgId'] = organization['Id'] - - roots = org.list_roots()['Roots'] - data['RootId'] = roots[0]['Id'] - - cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) - - OrganizationConformancePackBucket: - Type: AWS::S3::Bucket - Properties: - BucketName: !Sub awsconfigconforms-${AWS::AccountId} - - OrganizationConformancePackBucketPolicy: - Type: AWS::S3::BucketPolicy - Properties: - Bucket: !Ref OrganizationConformancePackBucket - PolicyDocument: - Version: '2012-10-17' - Statement: - - Sid: AllowGetPutObject - Effect: Allow - Principal: "*" - Action: - - s3:GetObject - - s3:PutObject - Resource: !Sub ${OrganizationConformancePackBucket.Arn}/* - Condition: - StringEquals: - aws:PrincipalOrgID: !GetAtt OrganizationsLookup.OrgId - ArnLike: - aws:PrincipalArn: !Sub arn:${AWS::Partition}:iam::*:role/aws-service-role/config-conforms.amazonaws.com/AWSServiceRoleForConfigConforms - - Sid: AllowGetBucketAcl - Effect: Allow - Principal: "*" - Action: s3:GetBucketAcl - Resource: !Sub ${OrganizationConformancePackBucket.Arn} - Condition: - StringEquals: - aws:PrincipalOrgID: !GetAtt OrganizationsLookup.OrgId - ArnLike: - aws:PrincipalArn: !Sub arn:${AWS::Partition}:iam::*:role/aws-service-role/config-conforms.amazonaws.com/AWSServiceRoleForConfigConforms - - BackupResources: - Type: AWS::CloudFormation::StackSet - DependsOn: EnableCloudFormationStacksetsOrgAccessCustomResource - Properties: - StackSetName: superwerker-backup - PermissionModel: SERVICE_MANAGED - OperationPreferences: - MaxConcurrentPercentage: 50 - Capabilities: - - CAPABILITY_IAM - - CAPABILITY_NAMED_IAM - AutoDeployment: - Enabled: true - RetainStacksOnAccountRemoval: false - StackInstancesGroup: - - Regions: - - !Ref AWS::Region - DeploymentTargets: - OrganizationalUnitIds: - - !GetAtt OrganizationsLookup.RootId - TemplateBody: !Sub | - Resources: - AWSBackupDefaultServiceRole: - Type: AWS::IAM::Role - Properties: - RoleName: AWSBackupDefaultServiceRole - Path: /service-role/ - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: backup.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:${AWS::Partition}:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup - - arn:${AWS::Partition}:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores - - ConfigRemediationRole: - Type: AWS::IAM::Role - Properties: - RoleName: SuperwerkerBackupTagsEnforcementRemediationRole - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: ssm.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: AllowTagging - PolicyDocument: - Statement: - - Effect: Allow - Action: - - dynamodb:TagResource - - ec2:CreateTags - - rds:AddTagsToResource - - rds:DescribeDBInstances - Resource: '*' - - BackupTagsEnforcement: - DependsOn: BackupResources - Type: AWS::Config::OrganizationConformancePack - Properties: - ExcludedAccounts: - - !Ref AWS::AccountId # exclude management account since it has no config recorder set up - DeliveryS3Bucket: !Ref OrganizationConformancePackBucket - OrganizationConformancePackName: superwerker-backup-enforce - TemplateBody: !Sub | - Resources: - ConfigRuleDynamoDBTable: - Type: AWS::Config::ConfigRule - Properties: - ConfigRuleName: superwerker-backup-enforce-dynamodb-table - Scope: - ComplianceResourceTypes: - - AWS::DynamoDB::Table - InputParameters: - tag1Key: superwerker:backup - tag1Value: daily,none - Source: - Owner: AWS - SourceIdentifier: REQUIRED_TAGS - - ConfigRemediationDynamoDBTable: - DependsOn: ConfigRuleDynamoDBTable - Type: AWS::Config::RemediationConfiguration - Properties: - ConfigRuleName: superwerker-backup-enforce-dynamodb-table - Automatic: true - MaximumAutomaticAttempts: 10 - RetryAttemptSeconds: 60 - TargetId: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${BackupTagRemediation} - TargetType: SSM_DOCUMENT - Parameters: - ResourceValue: - ResourceValue: - Value: "RESOURCE_ID" - AutomationAssumeRole: - StaticValue: - Values: - - arn:${AWS::Partition}:iam::${AWS::AccountId}:role/SuperwerkerBackupTagsEnforcementRemediationRole # ${AWS::AccountId} is magically replaced with the actual sub-account id (magic by Conformance Pack) - ResourceType: - StaticValue: - Values: - - AWS::DynamoDB::Table - - ConfigRuleEbsVolume: - Type: AWS::Config::ConfigRule - Properties: - ConfigRuleName: superwerker-backup-enforce-ebs-volume - Scope: - ComplianceResourceTypes: - - AWS::EC2::Volume - InputParameters: - tag1Key: superwerker:backup - tag1Value: daily,none - Source: - Owner: AWS - SourceIdentifier: REQUIRED_TAGS - - ConfigRemediationEbsVolume: - DependsOn: ConfigRuleEbsVolume - Type: AWS::Config::RemediationConfiguration - Properties: - ConfigRuleName: superwerker-backup-enforce-ebs-volume - Automatic: true - MaximumAutomaticAttempts: 10 - RetryAttemptSeconds: 60 - TargetId: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${BackupTagRemediation} - TargetType: SSM_DOCUMENT - Parameters: - ResourceValue: - ResourceValue: - Value: "RESOURCE_ID" - AutomationAssumeRole: - StaticValue: - Values: - - arn:${AWS::Partition}:iam::${AWS::AccountId}:role/SuperwerkerBackupTagsEnforcementRemediationRole # ${AWS::AccountId} is magically replaced with the actual sub-account id (magic by Conformance Pack) - ResourceType: - StaticValue: - Values: - - AWS::EC2::Volume - - ConfigRuleRdsDbInstance: - Type: AWS::Config::ConfigRule - Properties: - ConfigRuleName: superwerker-backup-enforce-rds-instance - Scope: - ComplianceResourceTypes: - - AWS::RDS::DBInstance - InputParameters: - tag1Key: superwerker:backup - tag1Value: daily,none - Source: - Owner: AWS - SourceIdentifier: REQUIRED_TAGS - - ConfigRemediationRdsDbInstance: - DependsOn: ConfigRuleRdsDbInstance - Type: AWS::Config::RemediationConfiguration - Properties: - ConfigRuleName: superwerker-backup-enforce-rds-instance - Automatic: true - MaximumAutomaticAttempts: 10 - RetryAttemptSeconds: 60 - TargetId: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${BackupTagRemediation} - TargetType: SSM_DOCUMENT - Parameters: - ResourceValue: - ResourceValue: - Value: "RESOURCE_ID" - AutomationAssumeRole: - StaticValue: - Values: - - arn:${AWS::Partition}:iam::${AWS::AccountId}:role/SuperwerkerBackupTagsEnforcementRemediationRole # ${AWS::AccountId} is magically replaced with the actual sub-account id (magic by Conformance Pack) - ResourceType: - StaticValue: - Values: - - AWS::RDS::DBInstance - - BackupTagRemediation: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - assumeRole: '{{ AutomationAssumeRole }}' - parameters: - ResourceValue: - type: String - AutomationAssumeRole: - type: String - default: '' - ResourceType: - type: String - mainSteps: - - name: synthArn - action: aws:branch - inputs: - Choices: - - NextStep: tagDynamoDbTable - Variable: '{{ ResourceType }}' - StringEquals: AWS::DynamoDB::Table - - NextStep: tagEbsVolume - Variable: '{{ ResourceType }}' - StringEquals: AWS::EC2::Volume - - NextStep: getRdsDBInstanceArnByDbInstanceResourceIdentifier - Variable: '{{ ResourceType }}' - StringEquals: AWS::RDS::DBInstance - - name: tagDynamoDbTable - action: 'aws:executeAwsApi' - inputs: - Service: dynamodb - Api: TagResource - Tags: - - Key: 'superwerker:backup' - Value: daily - ResourceArn: !Sub 'arn:${AWS::Partition}:dynamodb:{{ global:REGION }}:{{ global:ACCOUNT_ID }}:table/{{ ResourceValue }}' - isEnd: true - - name: tagEbsVolume - action: 'aws:executeAwsApi' - inputs: - Service: ec2 - Api: CreateTags - Tags: - - Key: 'superwerker:backup' - Value: daily - Resources: - - '{{ ResourceValue }}' - isEnd: true - - name: getRdsDBInstanceArnByDbInstanceResourceIdentifier - action: aws:executeAwsApi - inputs: - Service: rds - Api: DescribeDBInstances - Filters: - - Name: dbi-resource-id - Values: - - '{{ ResourceValue }}' - outputs: - - Name: DBInstanceArn - Selector: $.DBInstances[0].DBInstanceArn - - name: tagRdsInstance - action: 'aws:executeAwsApi' - inputs: - Service: rds - Api: AddTagsToResource - Tags: - - Key: 'superwerker:backup' - Value: daily - ResourceName: '{{ getRdsDBInstanceArnByDbInstanceResourceIdentifier.DBInstanceArn }}' - isEnd: true - - BackupTagRemediationPublic: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt BackupTagRemediationPublicCustomResource.Arn - DocumentName: !Ref BackupTagRemediation - - BackupTagRemediationPublicCustomResource: - Type: AWS::Serverless::Function - Properties: - Handler: index.handler - Runtime: python3.7 - Policies: - - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: ssm:ModifyDocumentPermission - Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/* - InlineCode: | - import boto3 - import cfnresponse - import os - - ssm = boto3.client("ssm") - - CREATE = 'Create' - DELETE = 'Delete' - UPDATE = 'Update' - - - 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 handler(event, context): - RequestType = event["RequestType"] - print('RequestType: {}'.format(RequestType)) - - PhysicalResourceId = event.get("PhysicalResourceId") - Properties = event["ResourceProperties"] - DocumentName = Properties["DocumentName"] - - id = "{}-{}".format(PhysicalResourceId, DocumentName) - - data = {} - - if RequestType == CREATE or RequestType == UPDATE: - ssm.modify_document_permission( - Name=DocumentName, - PermissionType='Share', - AccountIdsToAdd=['All'] - ) - elif RequestType == DELETE: - ssm.modify_document_permission( - Name=DocumentName, - PermissionType='Share', - AccountIdsToRemove=['All'] - ) - - cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) - - EnableCloudFormationStacksetsOrgAccessCustomResource: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt EnableCloudFormationStacksetsOrgAccessCustomResourceFunction.Arn - - EnableCloudFormationStacksetsOrgAccessCustomResourceFunction: - Type: AWS::Serverless::Function - Properties: - Handler: index.handler - Runtime: python3.7 - Timeout: 900 # give it more time since it installs dependencies on the fly - Role: !GetAtt EnableCloudFormationStacksetsOrgAccessCustomResourceRole.Arn # provide explicit role to avoid circular dependency with AwsApiLibRole - Policies: - - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: sts:AssumeRole - Resource: !GetAtt AwsApiLibRole.Arn - Environment: - Variables: - AWSAPILIB_ROLE_ARN: !GetAtt AwsApiLibRole.Arn - InlineCode: | - import boto3 - import os - import cfnresponse - import sys - import subprocess - - # load awsapilib in-process as long as we have no strategy for bundling assets - sys.path.insert(1, '/tmp/packages') - subprocess.check_call([sys.executable, "-m", "pip", "install", '--target', '/tmp/packages', 'awsapilib==0.10.1']) - import awsapilib - from awsapilib import Cloudformation - - CREATE = 'Create' - DELETE = 'Delete' - UPDATE = 'Update' - - 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 handler(event, context): - RequestType = event["RequestType"] - Properties = event["ResourceProperties"] - LogicalResourceId = event["LogicalResourceId"] - PhysicalResourceId = event.get("PhysicalResourceId") - - print('RequestType: {}'.format(RequestType)) - print('PhysicalResourceId: {}'.format(PhysicalResourceId)) - print('LogicalResourceId: {}'.format(LogicalResourceId)) - - id = PhysicalResourceId - - data = {} - - cf = Cloudformation(os.environ['AWSAPILIB_ROLE_ARN']) - - if RequestType == CREATE: - cf.stacksets.enable_organizations_trusted_access() - - cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) - - EnableCloudFormationStacksetsOrgAccessCustomResourceRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - - EnableCloudFormationStacksetsOrgAccessCustomResourceRolePolicy: - Type: AWS::IAM::Policy - Properties: - PolicyDocument: - Statement: - - Effect: Allow - Action: sts:AssumeRole - Resource: !GetAtt AwsApiLibRole.Arn - Version: 2012-10-17 - PolicyName: !Sub ${EnableCloudFormationStacksetsOrgAccessCustomResourceRole}Policy - Roles: - - !Ref EnableCloudFormationStacksetsOrgAccessCustomResourceRole - - AwsApiLibRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - AWS: !GetAtt EnableCloudFormationStacksetsOrgAccessCustomResourceRole.Arn - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/AdministratorAccess - - # Proudly found elsewhere and partially copied from: - # https://github.com/theserverlessway/aws-baseline - TagPolicy: - DependsOn: TagPolicyEnable - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt TagPolicyCustomResource.Arn - Policy: | - { - "tags": { - "superwerker:backup": { - "tag_value": { - "@@assign": [ - "none", - "daily" - ] - }, - "enforced_for": { - "@@assign": [ - "dynamodb:table", - "ec2:volume" - ] - } - } - } - } - Attach: true - - TagPolicyCustomResource: - Type: AWS::Serverless::Function - Metadata: - cfn-lint: - config: - ignore_checks: - - EIAMPolicyWildcardResource - 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' - TAG_POLICY = "TAG_POLICY" - - - def root(): - return o.list_roots()['Roots'][0] - - - def root_id(): - return root()['Id'] - - 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 TAG_POLICYs - # 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=LogicalResourceId, - ) - - policy_id = PhysicalResourceId - - try: - if RequestType == CREATE: - print('Creating Policy: {}'.format(LogicalResourceId)) - response = with_retry(o.create_policy, - **parameters, Type=TAG_POLICY - ) - 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='TAG_POLICY')['Policies'] if - p['Id'] == policy_id] - - TagPolicyEnable: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt TagPolicyEnableCustomResource.Arn - - TagPolicyEnableCustomResource: - Type: AWS::Serverless::Function - Properties: - Timeout: 200 - Handler: index.enable_tag_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' - TAG_POLICY = "TAG_POLICY" - - - def root(): - return o.list_roots()['Roots'][0] - - - def root_id(): - return root()['Id'] - - - def tag_policy_enabled(): - enabled_policies = root()['PolicyTypes'] - return {"Type": TAG_POLICY, "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_tag_policies(event, context): - RequestType = event["RequestType"] - if RequestType == CREATE and not tag_policy_enabled(): - r_id = root_id() - print('Enable TAG_POLICY for root: {}'.format(r_id)) - o.enable_policy_type(RootId=r_id, PolicyType=TAG_POLICY) - cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, 'TAG_POLICY') - - - 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 TAG_POLICYs - # 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 policy_attached(policy_id): - return [p['Id'] for p in - o.list_policies_for_target(TargetId=root_id(), Filter='TAG_POLICY')['Policies'] if - p['Id'] == policy_id] - - BackupPolicy: - DependsOn: BackupPolicyEnable - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt BackupPolicyCustomResource.Arn - Policy: !Sub | - { - "plans": { - "superwerker-backup": { - "regions": { - "@@assign": [ - "${AWS::Region}" - ] - }, - "rules": { - "backup-daily": { - "lifecycle": { - "delete_after_days": { - "@@assign": "30" - } - }, - "target_backup_vault_name": { - "@@assign": "Default" - } - } - }, - "selections": { - "tags": { - "backup-daily": { - "iam_role_arn": { - "@@assign": "arn:${AWS::Partition}:iam::$account:role/service-role/AWSBackupDefaultServiceRole" - }, - "tag_key": { - "@@assign": "superwerker:backup" - }, - "tag_value": { - "@@assign": [ - "daily" - ] - } - } - } - } - } - } - } - Attach: true - - BackupPolicyCustomResource: - 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' - BACKUP_POLICY = "BACKUP_POLICY" - - - def root(): - return o.list_roots()['Roots'][0] - - - def root_id(): - return root()['Id'] - - - 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 BACKUP_POLICYs - # 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=LogicalResourceId, - ) - - policy_id = PhysicalResourceId - - try: - - if RequestType == CREATE: - print('Creating Policy: {}'.format(LogicalResourceId)) - response = with_retry(o.create_policy, - **parameters, Type=BACKUP_POLICY - ) - 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='BACKUP_POLICY')['Policies'] if - p['Id'] == policy_id] - - BackupPolicyEnable: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt BackupPolicyEnableCustomResource.Arn - - BackupPolicyEnableCustomResource: - Type: AWS::Serverless::Function - Properties: - Timeout: 200 - Handler: index.enable_tag_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' - BACKUP_POLICY = "BACKUP_POLICY" - - - def root(): - return o.list_roots()['Roots'][0] - - - def root_id(): - return root()['Id'] - - - def backup_policy_enabled(): - enabled_policies = root()['PolicyTypes'] - return {"Type": BACKUP_POLICY, "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_tag_policies(event, context): - RequestType = event["RequestType"] - if RequestType == CREATE and not backup_policy_enabled(): - r_id = root_id() - print('Enable BACKUP_POLICY for root: {}'.format(r_id)) - o.enable_policy_type(RootId=r_id, PolicyType=BACKUP_POLICY) - cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, 'BACKUP_POLICY') - - - 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 BACKUP_POLICYs - # 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 policy_attached(policy_id): - return [p['Id'] for p in - o.list_policies_for_target(TargetId=root_id(), Filter='BACKUP_POLICY')['Policies'] if - p['Id'] == policy_id] diff --git a/templates/budget.yaml b/templates/budget.yaml deleted file mode 100755 index d3259ac..0000000 --- a/templates/budget.yaml +++ /dev/null @@ -1,172 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 -Description: Sets up Budget pieces for account management. (qs-1s3rsr7io) - -Metadata: - SuperwerkerVersion: 0.13.2 - cfn-lint: - config: - ignore_checks: - - E9007 - -Parameters: - BudgetLimitInUSD: - Default: 100 - Description: Initial value. Will be overwritten by the scheduled lambda function. - Type: String - -Resources: - BudgetAlarm: - Type: AWS::CloudWatch::Alarm - Properties: - AlarmActions: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:opsitem:3#CATEGORY=Cost # severity 3 (~medium) should suffice - AlarmDescription: Superwerker default budget forecast exceed previous three months - ComparisonOperator: GreaterThanThreshold - Dimensions: - - Name: TopicName - Value: !GetAtt BudgetNotification.TopicName - EvaluationPeriods: 1 - MetricName: NumberOfMessagesPublished - Namespace: AWS/SNS - Period: 300 # publishing window of SNS metrics to CW - Statistic: Sum - Threshold: 0 - TreatMissingData: missing # currently CW does not auto close ops items where it's alert transitions back to OK - but still transition back after one (1) initial SNS notification from budgets to "Ok" would communicate wrong semantics; - - BudgetLambda: - Type: AWS::Serverless::Function - Properties: - Environment: - Variables: - StackName: !Sub ${AWS::StackName} - Events: - Schedule: - Type: Schedule - Properties: - Schedule: cron(0 0 L * ? *) - Handler: index.handler - InlineCode: | - from dateutil.relativedelta import * - import boto3 - import datetime - import json - import os - - def handler(event, context): - - ce = boto3.client('ce') - - end = datetime.date.today().replace(day=1) - start = end + relativedelta(months=-3) - - start = start.strftime("%Y-%m-%d") - end = end.strftime("%Y-%m-%d") - - response = ce.get_cost_and_usage( - Granularity='MONTHLY', - Metrics=[ - 'UnblendedCost', - ], - TimePeriod={ - 'Start': start, - 'End': end, - }, - ) - - avg = 0 - - for result in response['ResultsByTime']: - total = result['Total'] - cost = total['UnblendedCost'] - amount = int(float(cost['Amount'])) - avg = avg + amount - - avg = int(avg/3) - budget = str(avg) - - stack_name = os.environ['StackName'] - - log({ - 'average': avg, - 'budget': budget, - 'end': end, - 'event': event, - 'level': 'debug', - 'stack': stack_name, - 'start': start, - }) - - cf = boto3.client('cloudformation') - - cf.update_stack( - Capabilities=[ - 'CAPABILITY_IAM', - ], - Parameters=[ - { - 'ParameterKey': 'BudgetLimitInUSD', - 'ParameterValue': budget, - } - ], - StackName=stack_name, - UsePreviousTemplate=True, - ) - - def log(msg): - print(json.dumps(msg), flush=True) - - Runtime: python3.7 - Policies: - - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - ce:GetCostAndUsage - Resource: '*' - - Effect: Allow - Action: - - budgets:ModifyBudget - Resource: !Sub arn:${AWS::Partition}:budgets::${AWS::AccountId}:budget/${BudgetReport} - - Effect: Allow - Action: - - cloudformation:UpdateStack - Resource: !Sub ${AWS::StackId} - Timeout: 10 - - BudgetNotification: - Type: AWS::SNS::Topic - - BudgetNotificationPolicy: - Type: AWS::SNS::TopicPolicy - Properties: - PolicyDocument: - Statement: - Action: SNS:Publish - Effect: Allow - Principal: - Service: budgets.amazonaws.com - Resource: !Ref BudgetNotification - Topics: - - !Ref BudgetNotification - - BudgetReport: - Type: AWS::Budgets::Budget - Properties: - Budget: - BudgetLimit: - Amount: !Ref BudgetLimitInUSD - Unit: USD - BudgetType: COST - CostTypes: - IncludeCredit: false - IncludeRefund: false - TimeUnit: MONTHLY - NotificationsWithSubscribers: - - Notification: - ComparisonOperator: GREATER_THAN - NotificationType: FORECASTED - Threshold: 100 - Subscribers: - - SubscriptionType: SNS - Address: !Ref BudgetNotification diff --git a/templates/control-tower.yaml b/templates/control-tower.yaml deleted file mode 100644 index b7f80d1..0000000 --- a/templates/control-tower.yaml +++ /dev/null @@ -1,226 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 -Description: Sets up AWS ControlTower. (qs-1s3rsr7lh) - -Parameters: - AuditAWSAccountEmail: - Type: String - LogArchiveAWSAccountEmail: - Type: String - -Resources: - - SetupControlTower: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt SetupControlTowerCustomResource.Arn - - SetupControlTowerCustomResource: - Type: AWS::Serverless::Function - Properties: - Handler: index.handler - Runtime: python3.7 - MemorySize: 2048 - Timeout: 900 # give it more time since it installs awsapilib and tries to deploy control tower with retries - Role: !GetAtt SetupControlTowerCustomResourceRole.Arn # provide explicit role to avoid circular dependency with AwsApiLibRole - Policies: - - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: sts:AssumeRole - Resource: !GetAtt AwsApiLibRole.Arn - Environment: - Variables: - AWSAPILIB_CONTROL_TOWER_ROLE_ARN: !GetAtt AwsApiLibRole.Arn - LOG_ARCHIVE_AWS_ACCOUNT_EMAIL: !Ref LogArchiveAWSAccountEmail - AUDIT_AWS_ACCOUNT_EMAIL: !Ref AuditAWSAccountEmail - InlineCode: | - import boto3 - import os - import cfnresponse - import sys - import subprocess - - # load awsapilib in-process as long as we have no strategy for bundling assets - sys.path.insert(1, '/tmp/packages') - subprocess.check_call([sys.executable, "-m", "pip", "install", '--target', '/tmp/packages', 'https://github.com/superwerker/awsapilib/archive/198a3269e324455dc3cc499b61bf61e5ec095779.zip']) - - # workaround for install awsapilib via zip (remove me once back to official awsapilib version) - with open('/tmp/packages/awsapilib/.VERSION', 'w') as version_file: - version_file.write("2.3.1-ctapifix\n") - - import awsapilib - from awsapilib import ControlTower - - CREATE = 'Create' - DELETE = 'Delete' - UPDATE = 'Update' - - 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 handler(event, context): - RequestType = event["RequestType"] - Properties = event["ResourceProperties"] - LogicalResourceId = event["LogicalResourceId"] - PhysicalResourceId = event.get("PhysicalResourceId") - - print('RequestType: {}'.format(RequestType)) - print('PhysicalResourceId: {}'.format(PhysicalResourceId)) - print('LogicalResourceId: {}'.format(LogicalResourceId)) - - id = PhysicalResourceId - - data = {} - - tower = ControlTower(os.environ['AWSAPILIB_CONTROL_TOWER_ROLE_ARN']) - - if RequestType == CREATE: - tower.deploy(logging_account_email=os.environ['LOG_ARCHIVE_AWS_ACCOUNT_EMAIL'], security_account_email=os.environ['AUDIT_AWS_ACCOUNT_EMAIL'], retries=50, wait=5) - - cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) - - SetupControlTowerCustomResourceRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - - SetupControlTowerCustomResourceRolePolicy: - Type: AWS::IAM::Policy - Properties: - PolicyDocument: - Statement: - - Effect: Allow - Action: sts:AssumeRole - Resource: !GetAtt AwsApiLibRole.Arn - Version: 2012-10-17 - PolicyName: !Sub ${SetupControlTowerCustomResourceRole}Policy - Roles: - - !Ref SetupControlTowerCustomResourceRole - - AwsApiLibRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - AWS: !GetAtt SetupControlTowerCustomResourceRole.Arn - Action: sts:AssumeRole - ManagedPolicyArns: - - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess - - ControlTowerReadyHandle: - Type: AWS::CloudFormation::WaitConditionHandle - - ControlTowerReadyHandleWaitCondition: - Type: AWS::CloudFormation::WaitCondition - Properties: - Handle: !Ref ControlTowerReadyHandle - Timeout: "7200" - - SuperwerkerBootstrapFunction: - Type: AWS::Serverless::Function - Properties: - Events: - SetupLandingZone: # event from entirely fresh landing zone - Type: CloudWatchEvent - Properties: - InputPath: $.detail.serviceEventDetails.setupLandingZoneStatus - Pattern: - detail-type: - - AWS Service Event via CloudTrail - source: - - aws.controltower - detail: - serviceEventDetails: - setupLandingZoneStatus: - state: - - SUCCEEDED - eventName: - - SetupLandingZone - - Handler: index.handler - Runtime: python3.7 - Environment: - Variables: - SIGNAL_URL: !Ref ControlTowerReadyHandle - InlineCode: |- - import boto3 - import json - - ssm = boto3.client('ssm') - events = boto3.client('events') - import urllib3 - import os - - def handler(event, context): - for account in event['accounts']: - ssm.put_parameter( - Name='/superwerker/account_id_{}'.format(account['accountName'].lower().replace(' ', '')), - Value=account['accountId'], - Overwrite=True, - Type='String', - ) - - # signal cloudformation stack that control tower setup is complete - encoded_body = json.dumps({ - "Status": "SUCCESS", - "Reason": "Control Tower Setup completed", - "UniqueId": "doesthisreallyhavetobeunique", - "Data": "Control Tower Setup completed" - }) - http = urllib3.PoolManager() - http.request('PUT', os.environ['SIGNAL_URL'], body=encoded_body) - - # signal Control Tower Landing ZOne Setup/Update has finished - events.put_events( - Entries=[ - { - 'DetailType': 'superwerker-event', - 'Detail': json.dumps( - { - 'eventName': 'LandingZoneSetupOrUpdateFinished', - } - ), - 'Source': 'superwerker' - } - ] - ) - - Policies: - - Version: 2012-10-17 - Statement: - - Action: - - ssm:PutParameter - Effect: Allow - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/superwerker* - - Action: events:PutEvents - Effect: Allow - Resource: !Sub 'arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default' - -Metadata: - SuperwerkerVersion: 0.13.2 - cfn-lint: - config: - ignore_checks: - - E9007 diff --git a/templates/guardduty.yaml b/templates/guardduty.yaml deleted file mode 100644 index 7b4364a..0000000 --- a/templates/guardduty.yaml +++ /dev/null @@ -1,308 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 -Description: Sets up GuardDuty with a delegated administor and automatically enabled for all AWS accounts in the AWS Organization. (qs-1s3rsr7ln) - -Resources: - - LandingZoneSetupFinishedTrigger: - Type: AWS::Events::Rule - Properties: - EventPattern: - source: - - superwerker - detail: - eventName: - - LandingZoneSetupOrUpdateFinished - State: ENABLED - Targets: - - Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableGuardDutyOrganizations} - Id: EnableGuardDutyOrganizations - RoleArn: !GetAtt SSMAutomationExecutionRoleforCWEvents.Arn - - SSMAutomationExecutionRoleforCWEvents: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: events.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: AllowStartAutomationExecution - PolicyDocument: - Statement: - - Effect: Allow - Action: - - ssm:StartAutomationExecution - Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableGuardDutyOrganizations}:* - - EnableGuardDutyOrganizations: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - assumeRole: !GetAtt EnableGuardDutyOrganizationsRole.Arn - parameters: - AuditAccountId: - type: String - default: '{{ssm:/superwerker/account_id_audit}}' - LogArchiveAccountId: - type: String - default: '{{ssm:/superwerker/account_id_logarchive}}' - mainSteps: - - name: CheckIfOrganizationAdminAccountIsAlReadyRegistered - action: aws:executeAwsApi - inputs: - Service: guardduty - Api: ListOrganizationAdminAccounts - outputs: - - Name: AdminAccountId - Selector: $.AdminAccounts[0].AdminAccountId - nextStep: EnableOrganizationAdminAccountChoice - - name: EnableOrganizationAdminAccountChoice - action: aws:branch - inputs: - Choices: - - NextStep: EnableGuardDutyInManagementAccount - Variable: '{{ CheckIfOrganizationAdminAccountIsAlReadyRegistered.AdminAccountId }}' - StringEquals: '{{ AuditAccountId }}' - Default: EnableOrganizationAdminAccount - - name: EnableOrganizationAdminAccount - action: aws:executeAwsApi - inputs: - Service: guardduty - Api: EnableOrganizationAdminAccount - AdminAccountId: '{{ AuditAccountId }}' - - name: WaitForEnableOrganizationAdminAccount - timeoutSeconds: 60 - action: aws:waitForAwsResourceProperty - inputs: - Service: organizations - Api: ListDelegatedAdministrators - ServicePrincipal: guardduty.amazonaws.com - PropertySelector: $.DelegatedAdministrators[0].Status - DesiredValues: - - ACTIVE - - name: EnableGuardDutyInManagementAccount - action: aws:executeAwsApi - inputs: - Service: guardduty - Api: CreateDetector - Enable: true - - name: SleepEnableGuardDutyExistingAccounts # GuardDuty Org Admin needs to settle first, give it some time - action: aws:sleep - inputs: - Duration: PT120S - - name: EnableGuardDutyS3DataProtectionForOrganization - action: aws:executeAwsApi - inputs: - Service: ssm - Api: StartAutomationExecution - DocumentName: !Ref EnableGuardDutyS3DataProtectionForOrganization - TargetLocations: - - ExecutionRoleName: AWSControlTowerExecution - Accounts: - - '{{ AuditAccountId }}' - Regions: - - !Ref AWS::Region - outputs: - - Name: AutomationExecutionId - Selector: $.AutomationExecutionId - - - name: WaitForEnableGuardDutyS3DataProtectionForOrganization - timeoutSeconds: 60 - action: aws:waitForAwsResourceProperty - inputs: - Service: ssm - Api: DescribeAutomationExecutions - Filters: - - Key: ExecutionId - Values: - - '{{ EnableGuardDutyS3DataProtectionForOrganization.AutomationExecutionId }}' - PropertySelector: $.AutomationExecutionMetadataList[0].AutomationExecutionStatus - DesiredValues: - - Success - - name: EnableGuardDutyExistingAccounts - action: aws:executeAwsApi - inputs: - Service: ssm - Api: StartAutomationExecution - DocumentName: !Ref EnableGuardDutyExistingAccounts - TargetLocations: - - ExecutionRoleName: AWSControlTowerExecution - Accounts: - - '{{ AuditAccountId }}' - Regions: - - !Ref AWS::Region - Parameters: - LogArchiveAWSAccountId: - - '{{ LogArchiveAccountId }}' - ManagementAWSAccountId: - - !Sub "${AWS::AccountId}" - outputs: - - Name: AutomationExecutionId - Selector: $.AutomationExecutionId - - name: WaitForEnableGuardDutyExistingAccounts - timeoutSeconds: 60 - action: aws:waitForAwsResourceProperty - inputs: - Service: ssm - Api: DescribeAutomationExecutions - Filters: - - Key: ExecutionId - Values: - - '{{ EnableGuardDutyExistingAccounts.AutomationExecutionId }}' - PropertySelector: $.AutomationExecutionMetadataList[0].AutomationExecutionStatus - DesiredValues: - - Success - - EnableGuardDutyOrganizationsRole: - Type: AWS::IAM::Role - Metadata: - cfn-lint: - config: - ignore_checks: - - EIAMPolicyWildcardResource - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: ssm.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: EnableAWSServiceAccess - PolicyDocument: - Statement: - - Effect: Allow - Action: - - guardduty:EnableOrganizationAdminAccount - - guardduty:ListOrganizationAdminAccounts - - guardduty:CreateDetector - - organizations:EnableAWSServiceAccess - - organizations:ListAWSServiceAccessForOrganization - - organizations:ListDelegatedAdministrators - - organizations:RegisterDelegatedAdministrator - - organizations:DescribeOrganization - - ssm:DescribeAutomationExecutions - Resource: '*' - - PolicyName: AllowStartAutomationExecution - PolicyDocument: - Statement: - - Effect: Allow - Action: ssm:StartAutomationExecution - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableGuardDutyExistingAccounts}:* - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableGuardDutyS3DataProtectionForOrganization}:* - - PolicyName: AllowCallCrossAccountAutomation - PolicyDocument: - Statement: - - Effect: Allow - Action: sts:AssumeRole - Resource: !Sub arn:${AWS::Partition}:iam::*:role/AWSControlTowerExecution - - PolicyName: SSMParameters - PolicyDocument: - Statement: - - Effect: Allow - Action: ssm:GetParameters - Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/superwerker/* - - PolicyName: ServiceLinkedRole - PolicyDocument: - Statement: - - Effect: Allow - Action: iam:CreateServiceLinkedRole - Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aws-service-role/guardduty.amazonaws.com/AWSServiceRoleForAmazonGuardDuty - - EnableGuardDutyExistingAccounts: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - parameters: - LogArchiveAWSAccountId: - type: String - ManagementAWSAccountId: - type: String - mainSteps: - - name: GetDetectorId - action: aws:executeAwsApi - inputs: - Service: guardduty - Api: ListDetectors - outputs: - - Name: DetectorId - Selector: $.DetectorIds[0] - - name: ManagementAWSAccount - action: aws:executeAwsApi - inputs: - Service: organizations - Api: DescribeAccount - AccountId: '{{ ManagementAWSAccountId }}' - outputs: - - Name: EmailAddress - Selector: $.Account.Email - - name: LogArchiveAWSAccount - action: aws:executeAwsApi - inputs: - Service: organizations - Api: DescribeAccount - AccountId: '{{ LogArchiveAWSAccountId }}' - outputs: - - Name: EmailAddress - Selector: $.Account.Email - - name: CreateMembers - action: aws:executeAwsApi - inputs: - Service: guardduty - Api: CreateMembers - DetectorId: '{{ GetDetectorId.DetectorId }}' - AccountDetails: - - AccountId: '{{ ManagementAWSAccountId }}' - Email: '{{ ManagementAWSAccount.EmailAddress }}' - - AccountId: '{{ LogArchiveAWSAccountId }}' - Email: '{{ LogArchiveAWSAccount.EmailAddress }}' - - name: EnableGuardDutyExistingAccounts - action: aws:executeAwsApi - inputs: - Service: guardduty - Api: UpdateOrganizationConfiguration - DetectorId: '{{ GetDetectorId.DetectorId }}' - AutoEnable: true - - EnableGuardDutyS3DataProtectionForOrganization: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - mainSteps: - - name: GetDetectorId - action: aws:executeAwsApi - inputs: - Service: guardduty - Api: ListDetectors - outputs: - - Name: DetectorId - Selector: $.DetectorIds[0] - - name: EnableGuardDutyS3DataProtectionForOrganization - action: aws:executeAwsApi - inputs: - Service: guardduty - Api: UpdateOrganizationConfiguration - AutoEnable: true - DetectorId: '{{ GetDetectorId.DetectorId }}' - DataSources: - S3Logs: - AutoEnable: true - -Metadata: - SuperwerkerVersion: 0.13.2 - cfn-lint: - config: - ignore_checks: - - E9007 diff --git a/templates/living-documentation.yaml b/templates/living-documentation.yaml deleted file mode 100755 index 47e5f65..0000000 --- a/templates/living-documentation.yaml +++ /dev/null @@ -1,135 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 -Description: Sets up Living Documentation. (qs-1s3rsr7m0) - -Parameters: - SuperwerkerDomain: - Type: String - -Resources: - - DashboardGeneratorFunction: - Type: AWS::Serverless::Function - Properties: - Events: - Schedule: - Type: Schedule - Properties: - Schedule: rate(1 minute) # runs in Lambda free tier - - Handler: index.handler - Policies: - - SSMParameterReadPolicy: - ParameterName: 'superwerker/*' - - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: cloudwatch:PutDashboard - Resource: !Sub arn:${AWS::Partition}:cloudwatch::${AWS::AccountId}:dashboard/superwerker - - Effect: Allow - Action: cloudwatch:DescribeAlarms - Resource: !Sub arn:${AWS::Partition}:cloudwatch:${AWS::Region}:${AWS::AccountId}:alarm:superwerker-RootMailReady - Environment: - Variables: - SUPERWERKER_DOMAIN: !Ref SuperwerkerDomain - InlineCode: | - import boto3 - import json - import os - from datetime import datetime - cw = boto3.client("cloudwatch") - ssm = boto3.client("ssm") - - def handler(event, context): - dns_domain =os.environ['SUPERWERKER_DOMAIN'] - superwerker_config = {} - for ssm_parameter in ssm.get_parameters(Names=['/superwerker/domain_name_servers'])['Parameters']: - superwerker_config[ssm_parameter['Name']] = ssm_parameter['Value'] - - rootmail_ready_alarm_state = cw.describe_alarms(AlarmNames=['superwerker-RootMailReady'])['MetricAlarms'][0]['StateValue'] - if rootmail_ready_alarm_state == 'OK': - dns_delegation_text = """ - #### 🏠 {domain} - #### ✅ DNS configuration is set up correctly. - """.format( - domain=dns_domain, - ) - else: - if '/superwerker/domain_name_servers' in superwerker_config: - dns_delegation_text = """ - #### 🏠 {domain} - #### ❌ DNS configuration needed. - -   - - ### Next Steps - - Please create the following NS records for your domain: - - ``` - {ns[0]} - {ns[1]} - {ns[2]} - {ns[3]} - ``` - """.format(domain=dns_domain, ns=superwerker_config['/superwerker/domain_name_servers'].split(',')) - else: - dns_delegation_text = '### DNS Setup pending' - markdown = """ - # [superwerker](https://github.com/superwerker/superwerker) -   - - {dns_delegation} - -   - ## Next steps - finish setup -   - - ### SSO Setup - - - Check your e-mail inbox for "Invitation to join AWS Single Sign-On" and follow the setups to accept the invitation. After finishing, log in into AWS via the AWS SSO portal. - - [Configure AWS SSO with identity providers](https://docs.aws.amazon.com/singlesignon/latest/userguide/manage-your-identity-source-idp.html), e.g. [Azure AD](https://controltower.aws-management.tools/aa/sso/azure_ad/), [Google Workspace](https://controltower.aws-management.tools/aa/sso/google/), [Okta](https://controltower.aws-management.tools/aa/sso/okta/), [OneLogin](https://controltower.aws-management.tools/aa/sso/onelogin/), to login to AWS with your existing login mechanisms. - -   - ### Organizations Setup - - - Set up recommended organizational units via [Control Tower](/controltower/home/organizationunits?region={region}) acording to the [Organizing Your AWS Environment Using Multiple Accounts whitepaper](https://docs.aws.amazon.com/whitepapers/latest/organizing-your-aws-environment/production-starter-organization.html) - - Create a `Workloads_Prod` organizational unit for production workloads - - Create a `Workloads_Test` organizational unit for test/dev workloads - -   - ## What now? Standard operating procedures - - - Create AWS accounts for each of your workloads via the [Control Tower Account Factory](/controltower/home/accountfactory/createAccount?region={region}) (for "Account email" use `root+@{dns_domain}`) - - Check [OpsCenter for incoming events and messages](/systems-manager/opsitems?region={region}#list_ops_items_filters=Status:Equal:Open_InProgress&activeTab=OPS_ITEMS) - - Check [AWS Security Hub](/securityhub/home?region={region}) for security best practise violations (login to Audit Account via AWS SSO portal first) - - Check [Amazon GuardDuty](/guardduty/home?region={region}#/findings) for threats against your AWS accounts (login to Audit Account via AWS SSO portal first) - - Exclude resources from being backed-up by changing the `superwerker:backup` tag to `none` - -   - ## Help and more information - - - [superwerker on GitHub](https://github.com/superwerker/superwerker) - - [Architecture Decision Records](https://github.com/superwerker/superwerker/tree/main/docs/adrs) - - [#superwerker](https://og-aws.slack.com/archives/C01CQ34TC93) Slack channel in [og-aws](http://slackhatesthe.cloud) - - [Mailing list](https://groups.google.com/forum/#!forum/superwerker/join) - -   - - ``` - Updated at {current_time} (use browser reload to refresh) - ``` - """.format(dns_delegation=dns_delegation_text, current_time=datetime.now(), region=os.environ['AWS_REGION'], dns_domain=dns_domain) - cw.put_dashboard( - DashboardName='superwerker', - DashboardBody=json.dumps({"widgets": [{"type": "text","x": 0,"y": 0,"width": 24,"height": 20,"properties": {"markdown": markdown}}]}), - ) - Runtime: python3.7 - Timeout: 60 - -Metadata: - SuperwerkerVersion: 0.13.2 - cfn-lint: - config: - ignore_checks: - - E9007 diff --git a/templates/notifications.yaml b/templates/notifications.yaml deleted file mode 100644 index 1a3aa09..0000000 --- a/templates/notifications.yaml +++ /dev/null @@ -1,85 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 -Description: Sets up notifications. (qs-1s3rsr7mk) -Metadata: - cfn-lint: - config: - ignore_checks: - - E9007 - -Parameters: - NotificationsMail: - Type: String - -Outputs: - NotificationTopic: - Description: Notification topic ARN for ops center creation events - Value: !Ref NotificationTopic - -Resources: - NotificationTopic: - Type: AWS::SNS::Topic - Properties: - Subscription: - - Endpoint: !Ref NotificationsMail - Protocol: email - - NotificationOpsItemCreated: - Type: AWS::Serverless::Function - Properties: - Events: - Enable: - Type: CloudWatchEvent - Properties: - Pattern: - source: - - aws.ssm - detail-type: - - AWS API Call via CloudTrail - detail: - eventName: - - CreateOpsItem - eventSource: - - ssm.amazonaws.com - Handler: index.handler - Runtime: python3.7 - Policies: - - SNSPublishMessagePolicy: - TopicName: !GetAtt NotificationTopic.TopicName - Environment: - Variables: - TOPIC_ARN: !Ref NotificationTopic - InlineCode: !Sub |- - import boto3 - import json - import os - - client = boto3.client('sns') - - def handler(event, context): - id = event['detail']['responseElements']['opsItemId'] - desc = event['detail']['requestParameters']['description'] - title = event['detail']['requestParameters']['title'] - - url = "https://${AWS::Region}.console.aws.amazon.com/systems-manager/opsitems/{}".format(id) - - log({ - 'desc': desc, - 'event': event, - 'level': 'info', - 'msg': 'Publishing new ops item event from CloudTrail to SNS', - 'title': title, - 'url': url, - }) - - message_title = "New OpsItem: {}".format(title) - message_body = "{}\n\n{}".format(desc, url) - - client.publish( - Message=message_body, - Subject=message_title, - TopicArn=os.environ['TOPIC_ARN'], - ) - - def log(msg): - print(json.dumps(msg), flush=True) diff --git a/templates/rootmail.yaml b/templates/rootmail.yaml deleted file mode 100644 index 6234461..0000000 --- a/templates/rootmail.yaml +++ /dev/null @@ -1,784 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 -Description: Sets up root mail. (qs-1s3rsr7mr) - -Parameters: - Domain: - Type: String - Subdomain: - Type: String - -Outputs: - - DelegationTarget: - Description: Nameservers for the hosted zone delegation - Value: !Join [ ',', !GetAtt HostedZone.NameServers ] - EmailGeneratorFunction: - Description: Lambda function to verify email delegation and generate new email aliases - Value: !Ref EmailGeneratorFunction - -Resources: - - EmailBucket: - Type: AWS::S3::Bucket - Properties: - PublicAccessBlockConfiguration: - BlockPublicAcls: true - BlockPublicPolicy: true - IgnorePublicAcls: true - RestrictPublicBuckets: true - - EmailBucketPolicy: - Type: AWS::S3::BucketPolicy - Properties: - Bucket: !Ref EmailBucket - PolicyDocument: !Sub | - { - "Version": "2012-10-17", - "Statement": [ - { - "Action": "s3:PutObject", - "Condition": { - "StringEquals": { - "aws:Referer": "${AWS::AccountId}" - } - }, - "Effect": "Allow", - "Principal": { - "Service": "ses.amazonaws.com" - }, - "Resource": [ - "arn:${AWS::Partition}:s3:::${EmailBucket}/RootMail/*" - ], - "Sid": "EnableSESReceive" - } - ] - } - - EmailGeneratorFunction: - Type: AWS::Serverless::Function - Properties: # TODO: use environment variables instead of variables in !Sub - Handler: index.handler - InlineCode: !Sub | - import json - import uuid - - domain = "${Subdomain}.${Domain}" - - def handler(event, context): - - max = 64 - len(domain) - 1 - 5 - - alias = str(uuid.uuid4()) - alias = alias[:max] - - if len(alias) < 36: - - log({ - 'domain': domain, - 'msg': 'UUID local part was reduced in length because your domain is too long for Control Tower (64 characters in total) - this increases the chance of collisions', - 'level': 'warn', - 'length': len(alias), - 'max': 36, - }) - - email = 'root+{alias}@{domain}'.format(alias = alias, domain = domain) - - return { - 'email': email, - } - - def log(msg): - print(json.dumps(msg), flush=True) - - Runtime: python3.7 - Timeout: 260 # the timeout effectivly limits retries to 2^(n+1) - 1 = 9 attempts with backup - - HostedZone: - Type: AWS::Route53::HostedZone - Properties: - Name: !Sub ${Subdomain}.${Domain} - HostedZoneConfig: - Comment: Created by superwerker - - HostedZoneSSMParameter: - Type: AWS::SSM::Parameter - Properties: - Name: /superwerker/domain_name_servers - Value: !Join [',', !GetAtt HostedZone.NameServers] - Type: StringList - - HostedZoneDKIMAndVerificationRecords: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt HostedZoneDKIMAndVerificationRecordsCustomResource.Arn - Domain: !Sub ${Subdomain}.${Domain} - - HostedZoneDKIMAndVerificationRecordsCustomResource: - Type: AWS::Serverless::Function - Properties: - Timeout: 200 - Handler: index.handler - Runtime: python3.7 - Policies: - - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - ses:VerifyDomainDkim - - ses:VerifyDomainIdentity - Resource: '*' - InlineCode: | - import boto3 - import cfnresponse - - ses = boto3.client("ses", region_name="eu-west-1") # this is fixed to eu-west-1 until SES supports receive more globally (see #23) - - CREATE = 'Create' - DELETE = 'Delete' - UPDATE = 'Update' - - 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 handler(event, context): - RequestType = event["RequestType"] - Properties = event["ResourceProperties"] - LogicalResourceId = event["LogicalResourceId"] - PhysicalResourceId = event.get("PhysicalResourceId") - Domain = Properties["Domain"] - - print('RequestType: {}'.format(RequestType)) - print('PhysicalResourceId: {}'.format(PhysicalResourceId)) - print('LogicalResourceId: {}'.format(LogicalResourceId)) - - id = PhysicalResourceId - - data = {} - - if RequestType == CREATE: - - print('Creating Domain verification and DKIM records: {}'.format(LogicalResourceId)) - - response = ses.verify_domain_identity( - Domain=Domain, - ) - - data["VerificationToken"] = response["VerificationToken"] - - response = ses.verify_domain_dkim( - Domain=Domain, - ) - - data["DkimTokens"] = response["DkimTokens"] - - cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) - - HostedZoneDKIMTokenRecord0: - Type: AWS::Route53::RecordSet - Properties: - HostedZoneId: !Ref HostedZone - Name: !Sub - - "${Token}._domainkey.${Subdomain}.${Domain}" - - { Token: !Select [ 0, !GetAtt HostedZoneDKIMAndVerificationRecords.DkimTokens ]} - ResourceRecords: - - !Sub - - "${Token}.dkim.amazonses.com" - - { Token: !Select [ 0, !GetAtt HostedZoneDKIMAndVerificationRecords.DkimTokens ]} - TTL: 60 - Type: CNAME - - HostedZoneDKIMTokenRecord1: - Type: AWS::Route53::RecordSet - Properties: - HostedZoneId: !Ref HostedZone - Name: !Sub - - "${Token}._domainkey.${Subdomain}.${Domain}" - - { Token: !Select [ 1, !GetAtt HostedZoneDKIMAndVerificationRecords.DkimTokens ]} - ResourceRecords: - - !Sub - - "${Token}.dkim.amazonses.com" - - { Token: !Select [ 1, !GetAtt HostedZoneDKIMAndVerificationRecords.DkimTokens ]} - TTL: 60 - Type: CNAME - - HostedZoneDKIMTokenRecord2: - Type: AWS::Route53::RecordSet - Properties: - HostedZoneId: !Ref HostedZone - Name: !Sub - - "${Token}._domainkey.${Subdomain}.${Domain}" - - { Token: !Select [ 2, !GetAtt HostedZoneDKIMAndVerificationRecords.DkimTokens ]} - ResourceRecords: - - !Sub - - "${Token}.dkim.amazonses.com" - - { Token: !Select [ 2, !GetAtt HostedZoneDKIMAndVerificationRecords.DkimTokens ]} - TTL: 60 - Type: CNAME - - HostedZoneMXRecord: - Type: AWS::Route53::RecordSet - Properties: - HostedZoneId: !Ref HostedZone - Name: !Sub ${Subdomain}.${Domain}. - ResourceRecords: - - 10 inbound-smtp.eu-west-1.amazonaws.com # this is fixed to eu-west-1 until SES supports receive more globally (see #23) - TTL: 60 - Type: MX - - HostedZoneVerificationTokenRecord: - Type: AWS::Route53::RecordSet - Properties: - HostedZoneId: !Ref HostedZone - Name: !Sub _amazonses.${Subdomain}.${Domain}. - ResourceRecords: - - !Sub "\"${HostedZoneDKIMAndVerificationRecords.VerificationToken}\"" - TTL: 60 - Type: TXT - - RootMailReady: - Type: AWS::Serverless::Function - Properties: - Events: - Schedule: - Type: Schedule - Properties: - Schedule: rate(5 minutes) - Handler: index.handler - InlineCode: !Sub | - import boto3 - import itertools - import json - import time - - domain = "${Subdomain}.${Domain}" - ses = boto3.client("ses", region_name="eu-west-1") # this is fixed to eu-west-1 until SES supports receive more globally (see #23) - - def backoff(msg, res, n): - - wait = pow(2, n) - - log({ - 'level': 'info', - 'msg': msg, - 'res': res, - 'round': n, - 'waiting_in_seconds': wait, - }) - - time.sleep(n) - - def handler(event, context): - - log({ - 'event': event, - 'level': 'debug', - }) - - for n in itertools.count(start=1): - - res = ses.get_account_sending_enabled() - - if res.get('Enabled'): - break - else: - backoff('sending not yet enabled', res, n) - - for n in itertools.count(start=1): - - res = ses.get_identity_verification_attributes( - Identities=[ - domain, - ], - ) - - if res.get('VerificationAttributes', {}).get(domain, {}).get('VerificationStatus') == 'Success': - break - else: - backoff('verification not yet successful', res, n) - - for n in itertools.count(start=1): - - res = ses.get_identity_dkim_attributes( - Identities=[ - domain, - ], - ) - - if res.get('DkimAttributes', {}).get(domain, {}).get('DkimVerificationStatus') == 'Success': - break - else: - backoff('DKIM verification not yet successful', res, n) - - for n in itertools.count(start=1): - - res = ses.get_identity_notification_attributes( - Identities=[ - domain, - ], - ) - - if res.get('NotificationAttributes', {}).get(domain, {}).get('ForwardingEnabled') == True: - break - else: - backoff('forwarding not yet enabled', res, n) - - def log(msg): - print(json.dumps(msg), flush=True) - - Runtime: python3.7 - Policies: - - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - ses:GetIdentityVerificationAttributes - - ses:GetAccountSendingEnabled - - ses:GetIdentityDkimAttributes - - ses:GetIdentityNotificationAttributes - Resource: '*' - Timeout: 260 # the timeout effectivly limits retries to 2^(n+1) - 1 = 9 attempts with backup - - RootMailReadyAlert: - Type: AWS::CloudWatch::Alarm - Properties: - AlarmName: superwerker-RootMailReady - ComparisonOperator: GreaterThanOrEqualToThreshold - Dimensions: - - Name: FunctionName - Value: !Ref RootMailReady - EvaluationPeriods: 1 - MetricName: Errors - Namespace: AWS/Lambda - Period: 180 - Statistic: Sum - Threshold: 1 - - RootMailReadyHandle: - Type: AWS::CloudFormation::WaitConditionHandle - - RootMailReadyHandleWaitCondition: - Type: AWS::CloudFormation::WaitCondition - Properties: - Handle: !Ref RootMailReadyHandle - Timeout: "28800" # 8 hours time to wire DNS - - RootMailReadyTrigger: - Type: AWS::Serverless::Function - Properties: - Events: - EmailHealth: - Type: CloudWatchEvent - Properties: - Pattern: - detail-type: - - CloudWatch Alarm State Change - source: - - aws.cloudwatch - detail: - alarmName: - - !Ref RootMailReadyAlert - state: - value: - - OK - Handler: index.handler - Environment: - Variables: - SIGNAL_URL: !Ref RootMailReadyHandle - InlineCode: |- - import json - import os - import urllib3 - import uuid - - def handler(event, context): - - encoded_body = json.dumps({ - "Status": "SUCCESS", - "Reason": "RootMail Setup completed", - "UniqueId": str(uuid.uuid4()), - "Data": "RootMail Setup completed" - }) - - http = urllib3.PoolManager() - http.request('PUT', os.environ['SIGNAL_URL'], body=encoded_body) - Runtime: python3.7 - Timeout: 10 - - SESReceiveStack: - Type: AWS::CloudFormation::StackSet - Properties: - AdministrationRoleARN: !GetAtt StackSetAdministrationRole.Arn - ExecutionRoleName: !Ref StackSetExecutionRole - PermissionModel: SELF_MANAGED - Capabilities: - - CAPABILITY_IAM - StackInstancesGroup: - - DeploymentTargets: - Accounts: - - !Sub "${AWS::AccountId}" - Regions: - - eu-west-1 # this is fixed to eu-west-1 until SES supports receive more globally (see #23) - StackSetName: !Sub ${AWS::StackName}-ReceiveStack - TemplateBody: !Sub | - Resources: - SESReceiptRuleSetActivation: - Type: AWS::CloudFormation::CustomResource - Properties: - ServiceToken: !GetAtt SESReceiptRuleSetActivationCustomResource.Arn - - SESReceiptRuleSetActivationCustomResourceRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - Policies: - - PolicyName: AllowSesAccess - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: # TODO: least privilege - - ses:* - Resource: "*" - - SESReceiptRuleSetActivationCustomResource: - Type: AWS::Lambda::Function - Properties: - Timeout: 200 - Handler: index.handler - Runtime: python3.7 - Role: !GetAtt SESReceiptRuleSetActivationCustomResourceRole.Arn - Code: - ZipFile: !Sub | # TODO: adopt function to new ADR for inline python lambdas - import boto3 - import cfnresponse - - ses = boto3.client("ses") - - CREATE = 'Create' - DELETE = 'Delete' - UPDATE = 'Update' - - 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 handler(event, context): - RequestType = event["RequestType"] - Properties = event["ResourceProperties"] - LogicalResourceId = event["LogicalResourceId"] - PhysicalResourceId = event.get("PhysicalResourceId") - - print('RequestType: {}'.format(RequestType)) - print('PhysicalResourceId: {}'.format(PhysicalResourceId)) - print('LogicalResourceId: {}'.format(LogicalResourceId)) - - id = PhysicalResourceId - rule_set_name = 'RootMail' - rule_name = 'Receive' - - if RequestType == CREATE or RequestType == UPDATE: - ses.create_receipt_rule_set( - RuleSetName=rule_set_name - ) - - ses.create_receipt_rule( - RuleSetName=rule_set_name, - Rule = { - 'Name' : rule_name, - 'Enabled' : True, - 'TlsPolicy' : 'Require', - 'ScanEnabled': True, - 'Recipients': [ - 'root@${Subdomain}.${Domain}', - ], - 'Actions': [ - { - 'S3Action' : { - 'BucketName' : '${EmailBucket}', - 'ObjectKeyPrefix': 'RootMail' - }, - }, - { - 'LambdaAction': { - 'FunctionArn': '${!OpsSantaFunction.Arn}' - } - } - ], - } - ) - - print('Activating SES ReceiptRuleSet: {}'.format(LogicalResourceId)) - - ses.set_active_receipt_rule_set( - RuleSetName=rule_set_name, - ) - elif RequestType == DELETE: - print('Deactivating SES ReceiptRuleSet: {}'.format(LogicalResourceId)) - - ses.set_active_receipt_rule_set() - - ses.delete_receipt_rule( - RuleName=rule_name, - RuleSetName=rule_set_name, - ) - - ses.delete_receipt_rule_set( - RuleSetName=rule_set_name - ) - - - cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, id) - - OpsSantaFunctionSESPermissions: - Type: AWS::Lambda::Permission - Properties: - Action: lambda:InvokeFunction - FunctionName: !Ref OpsSantaFunction - Principal: ses.amazonaws.com - SourceAccount: "${AWS::AccountId}" - - OpsSantaFunctionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - Policies: - - PolicyName: OpsSantaFunctionRolePolicy - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - s3:GetObject - Resource: ${EmailBucket.Arn}/RootMail/* - - Effect: Allow - Action: - - ssm:CreateOpsItem - Resource: "*" - - Action: ssm:PutParameter - Effect: Allow - Resource: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/superwerker/* - - OpsSantaFunction: - Type: AWS::Lambda::Function - Properties: - Timeout: 60 - Handler: index.handler - Runtime: python3.7 - Role: !GetAtt OpsSantaFunctionRole.Arn - Code: - ZipFile: !Sub | - import boto3 - import email - from email import policy - import hashlib - import json - import re - import datetime - - s3 = boto3.client('s3') - ssm = boto3.client('ssm', region_name='${AWS::Region}') - - filtered_email_subjects = [ - 'Your AWS Account is Ready - Get Started Now', - 'Welcome to Amazon Web Services', - ] - - def handler(event, context): - - log({ - 'event': event, - 'level': 'debug', - }) - - for record in event['Records']: - - id = record['ses']['mail']['messageId'] - key = 'RootMail/{key}'.format(key=id) - receipt = record['ses']['receipt'] - - log({ - 'id': id, - 'level': 'debug', - 'key': key, - 'msg': 'processing mail', - }) - - verdicts = { - 'dkim': receipt['dkimVerdict']['status'], - 'spam': receipt['spamVerdict']['status'], - 'spf': receipt['spfVerdict']['status'], - 'virus': receipt['virusVerdict']['status'], - } - - for k, v in verdicts.items(): - - if not v == 'PASS': - - log({ - 'class': k, - 'id': id, - 'key': key, - 'level': 'warn', - 'msg': 'verdict failed - ops santa item skipped', - }) - - return - - response = s3.get_object( - Bucket="${EmailBucket}", - Key=key, - ) - - msg = email.message_from_bytes(response["Body"].read(), policy=policy.default) - - title=msg["subject"] - - source=recipient=event["Records"][0]["ses"]["mail"]["destination"][0] - - if title == 'Amazon Web Services Password Assistance': - description=msg.get_body('html').get_content() - pw_reset_link = re.search(r'(https://signin.aws.amazon.com/resetpassword(.*?))(?=
)', description).group() - rootmail_identifier = '/superwerker/rootmail/pw_reset_link/{}'.format(source.split('@')[0].split('root+')[1]) - ssm.put_parameter( - Name=rootmail_identifier, - Value=pw_reset_link, - Overwrite=True, - Type='String', - Tier='Advanced', - Policies=json.dumps([ - { - "Type":"Expiration", - "Version":"1.0", - "Attributes":{ - "Timestamp": (datetime.datetime.now() + datetime.timedelta(minutes = 10)).strftime('%Y-%m-%dT%H:%M:%SZ') # expire in 10 minutes - } - } - ]) - ) - return # no ops item for now - - if title in filtered_email_subjects: - log({ - 'level': 'info', - 'msg': 'filtered email', - 'title': title, - }) - return - - description=msg.get_body(preferencelist=('plain', 'html')).get_content() - - title=title[:1020] + " ..." * (len(title) > 1020) - - description=description[:1020] + " ..." * (len(description) > 1020) - - source=source[:60] + ' ...' * (len(source) > 60) - - operational_data={ - "/aws/dedup":{ - "Value":json.dumps( - { - "dedupString":id, - } - ), - "Type":"SearchableString", - }, - "/aws/resources":{ - "Value":json.dumps([ - { - "arn":"${EmailBucket.Arn}/{key}".format(key=key), - } - ]), - "Type":"SearchableString", - }, - } - - ssm.create_ops_item( - Description=description, - OperationalData=operational_data, - Source=source, - Title=title, - ) - - def log(msg): - print(json.dumps(msg), flush=True) - - StackSetExecutionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - AWS: - - !Sub "${AWS::AccountId}" - Action: - - sts:AssumeRole - Path: / - ManagedPolicyArns: - - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess - - StackSetAdministrationRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: cloudformation.amazonaws.com - Action: - - sts:AssumeRole - Path: / - Policies: - - PolicyName: AssumeRole-AWSCloudFormationStackSetExecutionRole - PolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Action: - - sts:AssumeRole - Resource: - - !GetAtt StackSetExecutionRole.Arn - -Metadata: - SuperwerkerVersion: 0.13.2 - cfn-lint: - config: - ignore_checks: - - E9007 - - EIAMPolicyWildcardResource diff --git a/templates/security-hub.yaml b/templates/security-hub.yaml deleted file mode 100644 index 4529ad2..0000000 --- a/templates/security-hub.yaml +++ /dev/null @@ -1,435 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 -Description: Sets up SecurityHub with a delegated administor and automatically enabled for all AWS accounts in the AWS Organization. (qs-1s3rsr7n9) - -Resources: - - LandingZoneSetupFinishedTrigger: - Type: AWS::Events::Rule - Properties: - EventPattern: - source: - - superwerker - detail: - eventName: - - LandingZoneSetupOrUpdateFinished - State: ENABLED - Targets: - - Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${CreateLandingZoneEnableSecurityHub} - Id: EnableSecurityHubOrganizations - RoleArn: !GetAtt SSMAutomationExecutionRoleforCWEvents.Arn - - CreateManagedAccountTrigger: - Type: AWS::Events::Rule - Properties: - EventPattern: - detail-type: - - AWS Service Event via CloudTrail - source: - - aws.controltower - detail: - serviceEventDetails: - createManagedAccountStatus: - state: - - SUCCEEDED - eventName: - - CreateManagedAccount - - State: ENABLED - Targets: - - Arn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableSecurityHubInOrgAccountAndAddAsMember} - Id: CreateManagedAccountTrigger - RoleArn: !GetAtt SSMAutomationExecutionRoleforCWEvents.Arn - InputTransformer: - InputPathsMap: - AwsAccountId: $.detail.serviceEventDetails.createManagedAccountStatus.account.accountId - InputTemplate: | - { - "MemberAWSAccountId": [] - } - - - SSMAutomationExecutionRoleforCWEvents: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: events.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: AllowStartAutomationExecution - PolicyDocument: - Statement: - - Effect: Allow - Action: - - ssm:StartAutomationExecution - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${CreateLandingZoneEnableSecurityHub}:* - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableSecurityHubInOrgAccountAndAddAsMember}:* - - CreateLandingZoneEnableSecurityHub: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - assumeRole: !GetAtt CreateLandingZoneEnableSecurityHubRole.Arn - parameters: - AuditAccountId: - type: String - default: '{{ssm:/superwerker/account_id_audit}}' - LogArchiveAccountId: - type: String - default: '{{ssm:/superwerker/account_id_logarchive}}' - mainSteps: - - name: EnableSecurityHubInAuditAccount - action: aws:executeAutomation - inputs: - DocumentName: !Ref EnableSecurityHubInOrgAccount - RuntimeParameters: - AWSAccountId: - - '{{ AuditAccountId }}' - - name: EnableSecurityHubMemberInOrgLogArchiveAccount - action: aws:executeAutomation - inputs: - DocumentName: !Ref EnableSecurityHubInOrgAccountAndAddAsMember - RuntimeParameters: - MemberAWSAccountId: - - '{{ LogArchiveAccountId }}' - - CreateLandingZoneEnableSecurityHubRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: ssm.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: AllowStartAutomationExecutionEnableSecurityHub - PolicyDocument: - Statement: - - Effect: Allow - Action: ssm:StartAutomationExecution - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableSecurityHubInOrgAccount}:* - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableSecurityHubInOrgAccountAndAddAsMember}:* - - Effect: Allow - Action: - - ssm:GetAutomationExecution - Resource: '*' - - PolicyName: SSMParameters - PolicyDocument: - Statement: - - Effect: Allow - Action: ssm:GetParameters - Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/superwerker/* - EnableSecurityHubInOrgAccountAndAddAsMember: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - assumeRole: !GetAtt EnableSecurityHubInOrgAccountAndAddAsMemberRole.Arn - parameters: - MemberAWSAccountId: - type: String - AuditAccountId: - type: String - default: '{{ssm:/superwerker/account_id_audit}}' - mainSteps: - - name: EnableSecurityHubInOrgAccount - action: aws:executeAutomation - inputs: - DocumentName: !Ref EnableSecurityHubInOrgAccount - RuntimeParameters: - AWSAccountId: - - '{{ MemberAWSAccountId }}' - - name: MemberAccount - action: aws:executeAwsApi - inputs: - Service: organizations - Api: DescribeAccount - AccountId: '{{ MemberAWSAccountId }}' - outputs: - - Name: EmailAddress - Selector: $.Account.Email - - name: InviteSecurityHubMember - action: aws:executeAwsApi - inputs: - Service: ssm - Api: StartAutomationExecution - DocumentName: !Ref InviteSecurityHubMember - Parameters: - MemberAWSAccountId: - - '{{ MemberAWSAccountId }}' - MemberAWSAccountEmail: - - '{{ MemberAccount.EmailAddress }}' - TargetLocations: - - ExecutionRoleName: AWSControlTowerExecution - Accounts: - - '{{ AuditAccountId }}' - Regions: - - !Ref AWS::Region - outputs: - - Name: AutomationExecutionId - Selector: $.AutomationExecutionId - nextStep: WaitForInviteSecurityHubMember - - name: WaitForInviteSecurityHubMember - timeoutSeconds: 60 - action: aws:waitForAwsResourceProperty - inputs: - Service: ssm - Api: DescribeAutomationExecutions - Filters: - - Key: ExecutionId - Values: - - '{{ InviteSecurityHubMember.AutomationExecutionId }}' - PropertySelector: $.AutomationExecutionMetadataList[0].AutomationExecutionStatus - DesiredValues: - - Success - - name: AcceptInvitation - action: aws:executeAwsApi - inputs: - Service: ssm - Api: StartAutomationExecution - DocumentName: !Ref AcceptSecurityHubInvitation - TargetLocations: - - ExecutionRoleName: AWSControlTowerExecution - Accounts: - - '{{ MemberAWSAccountId }}' - Regions: - - !Ref AWS::Region - outputs: - - Name: AutomationExecutionId - Selector: $.AutomationExecutionId - nextStep: WaitForAcceptSecurityHubMember - - name: WaitForAcceptSecurityHubMember - timeoutSeconds: 60 - action: aws:waitForAwsResourceProperty - inputs: - Service: ssm - Api: DescribeAutomationExecutions - Filters: - - Key: ExecutionId - Values: - - '{{ AcceptInvitation.AutomationExecutionId }}' - PropertySelector: $.AutomationExecutionMetadataList[0].AutomationExecutionStatus - DesiredValues: - - Success - - EnableSecurityHubInOrgAccountAndAddAsMemberRole: - Type: AWS::IAM::Role - Metadata: - cfn-lint: - config: - ignore_checks: - - EIAMPolicyWildcardResource - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: ssm.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: AllowStartAutomationExecutionEnableSecurityHub - PolicyDocument: - Statement: - - Effect: Allow - Action: ssm:StartAutomationExecution - Resource: - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableSecurityHubInOrgAccount}:* - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${InviteSecurityHubMember}:* - - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${AcceptSecurityHubInvitation}:* - - PolicyName: AllowDescribeSsmAutomationExecutionStatus - PolicyDocument: - Statement: - - Effect: Allow - Action: - - ssm:DescribeAutomationExecutions - - ssm:GetAutomationExecution - Resource: '*' - - PolicyName: AllowCallCrossAccountAutomation - PolicyDocument: - Statement: - - Effect: Allow - Action: sts:AssumeRole - Resource: !Sub arn:${AWS::Partition}:iam::*:role/AWSControlTowerExecution - - PolicyName: Organizations - PolicyDocument: - Statement: - - Effect: Allow - Action: organizations:DescribeAccount - Resource: '*' - - PolicyName: SSMParameters - PolicyDocument: - Statement: - - Effect: Allow - Action: ssm:GetParameters - Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/superwerker/* - - InviteSecurityHubMember: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - parameters: - MemberAWSAccountId: - type: String - MemberAWSAccountEmail: - type: String - mainSteps: - - name: CreateMembers - action: aws:executeAwsApi - inputs: - Service: securityhub - Api: CreateMembers - AccountDetails: - - AccountId: '{{ MemberAWSAccountId }}' - Email: '{{ MemberAWSAccountEmail }}' - - name: InviteMembers - action: aws:executeAwsApi - inputs: - Service: securityhub - Api: InviteMembers - AccountIds: - - '{{ MemberAWSAccountId }}' - - AcceptSecurityHubInvitation: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - mainSteps: - - name: Invitation - action: aws:executeAwsApi - inputs: - Service: securityhub - Api: ListInvitations - outputs: - - Name: InvitationId - Selector: $.Invitations[0].InvitationId - - Name: AccountId - Selector: $.Invitations[0].AccountId - - name: AcceptInvitation - action: aws:executeAwsApi - inputs: - Service: securityhub - Api: AcceptInvitation - InvitationId: '{{ Invitation.InvitationId }}' - MasterId: '{{ Invitation.AccountId }}' - - EnableSecurityHubInOrgAccount: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - assumeRole: !GetAtt EnableSecurityHubInOrgAccountRole.Arn - parameters: - AWSAccountId: - type: String - mainSteps: - - name: EnableSecurityHubInOrgAccount - action: aws:executeAwsApi - inputs: - Service: ssm - Api: StartAutomationExecution - DocumentName: !Ref EnableSecurityHub - TargetLocations: - - ExecutionRoleName: AWSControlTowerExecution - Accounts: - - '{{ AWSAccountId }}' - Regions: - - !Ref AWS::Region - outputs: - - Name: AutomationExecutionId - Selector: $.AutomationExecutionId - nextStep: WaitForEnableSecurityHubInOrgAccount - - name: WaitForEnableSecurityHubInOrgAccount - timeoutSeconds: 60 - action: aws:waitForAwsResourceProperty - inputs: - Service: ssm - Api: DescribeAutomationExecutions - Filters: - - Key: ExecutionId - Values: - - '{{ EnableSecurityHubInOrgAccount.AutomationExecutionId }}' - PropertySelector: $.AutomationExecutionMetadataList[0].AutomationExecutionStatus - DesiredValues: - - Success - - EnableSecurityHubInOrgAccountRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Effect: Allow - Principal: - Service: ssm.amazonaws.com - Action: sts:AssumeRole - Policies: - - PolicyName: AllowStartAutomationExecutionEnableSecurityHub - PolicyDocument: - Statement: - - Effect: Allow - Action: ssm:StartAutomationExecution - Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${EnableSecurityHub}:* - - PolicyName: AllowDescribeSsmAutomationExecutionStatus - PolicyDocument: - Statement: - - Effect: Allow - Action: - - ssm:DescribeAutomationExecutions - Resource: '*' - - PolicyName: AllowCallCrossAccountAutomation - PolicyDocument: - Statement: - - Effect: Allow - Action: sts:AssumeRole - Resource: !Sub arn:${AWS::Partition}:iam::*:role/AWSControlTowerExecution - - EnableSecurityHub: - Type: AWS::SSM::Document - Properties: - DocumentType: Automation - Content: - schemaVersion: '0.3' - mainSteps: - - name: CheckIfSecurityHubIsEnabled - action: aws:executeAwsApi - inputs: - Service: securityhub - Api: DescribeHub - isCritical: false # this step can fail on purpose - onFailure: step:EnableSecurityHub - nextStep: NoOp - - name: NoOp - action: aws:sleep # use sleep as a workaround for no-op - inputs: - Duration: PT0S - isEnd: true - - name: EnableSecurityHub - action: aws:executeAwsApi - inputs: - Service: securityhub - Api: EnableSecurityHub - isEnd: true - -Metadata: - SuperwerkerVersion: 0.13.2 - cfn-lint: - config: - ignore_checks: - - E9007 diff --git a/templates/service-control-policies.yaml b/templates/service-control-policies.yaml deleted file mode 100644 index 6df8154..0000000 --- a/templates/service-control-policies.yaml +++ /dev/null @@ -1,321 +0,0 @@ -# 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 diff --git a/templates/superwerker.template.yaml b/templates/superwerker.template.yaml index cf0f081..261dad2 100644 --- a/templates/superwerker.template.yaml +++ b/templates/superwerker.template.yaml @@ -1,335 +1,498 @@ -AWSTemplateFormatVersion: 2010-09-09 -Transform: AWS::Serverless-2016-10-31 Description: Automated Best Practices for AWS Cloud setups - https://superwerker.cloud (qs-1rhrhoi4t) Metadata: - cfn-lint: - config: - ignore_checks: - - E9007 - SuperwerkerVersion: 0.13.2 + SuperwerkerVersion: '0.15.0' QuickStartDocumentation: - EntrypointName: "Parameters for launching Superwerker" - Order: "1" - AWS::CloudFormation::Interface: - ParameterGroups: - - Label: - default: Features - Parameters: - - IncludeBudget - - IncludeGuardDuty - - IncludeSecurityHub - - IncludeBackup - - IncludeServiceControlPolicies - - Label: - default: Domain configuration - Parameters: - - Domain - - Subdomain - - Label: - default: Notifications - Parameters: - - NotificationsMail - - Label: - default: AWS Quick Start configuration - Parameters: - - QSS3BucketName - - QSS3KeyPrefix - - QSS3BucketRegion - ParameterLabels: - Domain: - default: Domain for automated DNS configuration - Subdomain: - default: Sub domain for automated DNS configuration - NotificationsMail: - default: Mail address used for important notification regarding your AWS account (leave empty for no notifications) - IncludeBudget: - default: Include AWS Budgets alarm - IncludeGuardDuty: - default: Include Amazon GuardDuty - IncludeSecurityHub: - default: Include AWS Security Hub - IncludeBackup: - default: Include Automated Backups - IncludeServiceControlPolicies: - default: Include service control policies - QSS3BucketName: - default: Quick Start S3 bucket name - QSS3BucketRegion: - default: Quick Start S3 bucket Region - QSS3KeyPrefix: - default: Quick Start S3 key prefix + EntrypointName: Parameters for launching Superwerker + Order: '1' Parameters: + QSS3BucketName: + Type: String + Default: '' + QSS3BucketRegion: + Type: String + Default: '' + QSS3KeyPrefix: + Type: String + Default: '' Domain: Type: String - Description: Domain used for root mail feature + Description: Domain used for root mail feature. Please see https://github.com/superwerker/superwerker/blob/main/README.md#technical-faq for more information Subdomain: Type: String Default: aws - Description: Subdomain used for root mail feature + Description: Subdomain used for root mail feature. Please see https://github.com/superwerker/superwerker/blob/main/README.md#technical-faq for more information NotificationsMail: Type: String - Description: Mail address used for notifications - Default: "" - AllowedPattern: "(^$|^.*@.*\\..*$)" + Default: '' + AllowedPattern: (^$|^.*@.*\..*$) + Description: Mail address used for notifications. Please see https://github.com/superwerker/superwerker/blob/main/README.md#technical-faq for more information IncludeBudget: + Type: String + Default: 'Yes' AllowedValues: - 'Yes' - 'No' - Default: 'Yes' - Type: String Description: Enable AWS Budgets alarm for monthly AWS spending IncludeGuardDuty: + Type: String + Default: 'Yes' AllowedValues: - 'Yes' - 'No' - Default: 'Yes' - Type: String Description: Enable Amazon GuardDuty IncludeSecurityHub: + Type: String + Default: 'Yes' AllowedValues: - 'Yes' - 'No' - Default: 'Yes' - Type: String Description: Enable AWS Security Hub IncludeBackup: + Type: String + Default: 'Yes' AllowedValues: - 'Yes' - 'No' - Default: 'Yes' - Type: String Description: Enable automated backups IncludeServiceControlPolicies: + Type: String + Default: 'Yes' AllowedValues: - 'Yes' - 'No' - Default: 'Yes' - Type: String Description: Enable service control policies in AWS organizations - QSS3BucketName: - AllowedPattern: '^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$' - ConstraintDescription: The Quick Start bucket name can include numbers, lowercase - letters, uppercase letters, and hyphens (-). It cannot start or end with a - hyphen (-). - Default: aws-quickstart - Description: Name of the S3 bucket for your copy of the Quick Start assets. - Keep the default name unless you are customizing the template. - Changing the name updates code references to point to a new Quick - Start location. This name can include numbers, lowercase letters, - uppercase letters, and hyphens, but do not start or end with a hyphen (-). - See https://aws-quickstart.github.io/option1.html. - Type: String - QSS3BucketRegion: - Default: 'us-east-1' - Description: 'AWS Region where the Quick Start S3 bucket (QSS3BucketName) is - hosted. Keep the default Region unless you are customizing the template. - Changing this Region updates code references to point to a new Quick Start location. - When using your own bucket, specify the Region. - See https://aws-quickstart.github.io/option1.html.' - Type: String - QSS3KeyPrefix: - AllowedPattern: '^([0-9a-zA-Z-.:]+/)*$' - ConstraintDescription: The Quick Start S3 key prefix can include numbers, lowercase letters, - uppercase letters, hyphens (-), colon (:), and forward slashes (/). - Default: quickstart-superwerker/ - Description: S3 key prefix that is used to simulate a directory for your copy of the - Quick Start assets. Keep the default prefix unless you are customizing - the template. Changing this prefix updates code references to point to - a new Quick Start location. This prefix can include numbers, lowercase - letters, uppercase letters, hyphens (-), colon (:), and forward slashes (/). - See https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html - and https://aws-quickstart.github.io/option1.html. - Type: String - -Conditions: - UsingDefaultBucket: !Equals [!Ref QSS3BucketName, 'aws-quickstart'] - IncludeNotifications: !Not [!Equals [ !Ref NotificationsMail, '' ]] - IncludeBudget: !Equals [ !Ref IncludeBudget, 'Yes' ] - IncludeGuardDuty: !Equals [ !Ref IncludeGuardDuty, 'Yes' ] - IncludeSecurityHub: !Equals [ !Ref IncludeSecurityHub, 'Yes' ] - IncludeBackup: !Equals [ !Ref IncludeBackup, 'Yes' ] - IncludeServiceControlPolicies: !Equals [ !Ref IncludeServiceControlPolicies, 'Yes' ] - Resources: - Budget: - Condition: IncludeBudget - Type: AWS::CloudFormation::Stack - Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/budget.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] - - ControlTower: - Type: AWS::CloudFormation::Stack + GeneratedAuditAWSAccountEmail426CA952: + Type: Custom::GenerateEmailAddress Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/control-tower.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] - Parameters: - AuditAWSAccountEmail: !GetAtt GeneratedAuditAWSAccountEmail.email - LogArchiveAWSAccountEmail: !GetAtt GeneratedLogArchiveAWSAccountEmail.email - - GeneratedAuditAWSAccountEmail: - Type: AWS::CloudFormation::CustomResource + ServiceToken: !GetAtt 'superwerkergenerateemailaddressproviderframeworkonEvent3AC8AF01.Arn' + Domain: !Join + - '' + - - !Ref 'Subdomain' + - . + - !Ref 'Domain' + Name: Audit + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/GeneratedAuditAWSAccountEmail/Resource/Default + superwerkergenerateemailaddressprovidergenerateemailaddressoneventServiceRoleC486FCF1: + Type: AWS::IAM::Role Properties: - ServiceToken: !GetAtt GenerateLogAndOrAuditEmailCustomResource.Arn - - GeneratedLogArchiveAWSAccountEmail: - Type: AWS::CloudFormation::CustomResource + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + - !Join + - '' + - - 'arn:' + - !Ref 'AWS::Partition' + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Metadata: + aws:cdk:path: SuperwerkerStack/superwerker.generate-email-address-provider/generate-email-address-on-event/ServiceRole/Resource + superwerkergenerateemailaddressprovidergenerateemailaddressoneventServiceRoleDefaultPolicyCA8C0038: + Type: AWS::IAM::Policy Properties: - ServiceToken: !GetAtt GenerateLogAndOrAuditEmailCustomResource.Arn - - GenerateLogAndOrAuditEmailCustomResource: - Type: AWS::Serverless::Function + PolicyDocument: + Statement: + - Action: organizations:ListAccounts + Effect: Allow + Resource: '*' + Version: '2012-10-17' + PolicyName: superwerkergenerateemailaddressprovidergenerateemailaddressoneventServiceRoleDefaultPolicyCA8C0038 + Roles: + - !Ref 'superwerkergenerateemailaddressprovidergenerateemailaddressoneventServiceRoleC486FCF1' + Metadata: + aws:cdk:path: SuperwerkerStack/superwerker.generate-email-address-provider/generate-email-address-on-event/ServiceRole/DefaultPolicy/Resource + superwerkergenerateemailaddressprovidergenerateemailaddressoneventE83D0932: + Type: AWS::Lambda::Function Properties: - Timeout: 200 + Code: + S3Bucket: !Sub 'superwerker-assets-${AWS::Region}' + S3Key: '0.15.0/42fec8cabe64f38c1f5223e088b90721a9282c15a96cf5ed8ad16e4b45c9bb6c.zip' + Role: !GetAtt 'superwerkergenerateemailaddressprovidergenerateemailaddressoneventServiceRoleC486FCF1.Arn' + Environment: + Variables: + AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1' Handler: index.handler - Runtime: python3.7 - Policies: - - LambdaInvokePolicy: - FunctionName: !GetAtt RootMail.Outputs.EmailGeneratorFunction - InlineCode: !Sub | - import boto3 - import cfnresponse - import json - - lambda_client = boto3.client("lambda") - - CREATE = 'Create' - DELETE = 'Delete' - UPDATE = 'Update' - - 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 handler(event, context): - RequestType = event["RequestType"] - Properties = event["ResourceProperties"] - LogicalResourceId = event["LogicalResourceId"] - PhysicalResourceId = event.get("PhysicalResourceId") - - print('RequestType: {}'.format(RequestType)) - print('PhysicalResourceId: {}'.format(PhysicalResourceId)) - print('LogicalResourceId: {}'.format(LogicalResourceId)) - - id = PhysicalResourceId - - data = {} - - if RequestType == CREATE or RequestType == UPDATE: - lambda_response = lambda_client.invoke( - FunctionName='${RootMail.Outputs.EmailGeneratorFunction}' - ) - - response_json = json.loads(lambda_response['Payload'].read().decode('utf-8')) - data['email'] = response_json['email'] - - cfnresponse.send(event, context, cfnresponse.SUCCESS, data, id) - - GuardDuty: - Condition: IncludeGuardDuty - Type: AWS::CloudFormation::Stack + Runtime: nodejs14.x + DependsOn: + - superwerkergenerateemailaddressprovidergenerateemailaddressoneventServiceRoleDefaultPolicyCA8C0038 + - superwerkergenerateemailaddressprovidergenerateemailaddressoneventServiceRoleC486FCF1 + Metadata: + aws:cdk:path: SuperwerkerStack/superwerker.generate-email-address-provider/generate-email-address-on-event/Resource + aws:asset:path: asset.42fec8cabe64f38c1f5223e088b90721a9282c15a96cf5ed8ad16e4b45c9bb6c + aws:asset:is-bundled: true + aws:asset:property: Code + superwerkergenerateemailaddressproviderframeworkonEventServiceRole116C3F83: + Type: AWS::IAM::Role Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/guardduty.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] - - LivingDocumentation: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + - !Join + - '' + - - 'arn:' + - !Ref 'AWS::Partition' + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Metadata: + aws:cdk:path: SuperwerkerStack/superwerker.generate-email-address-provider/generate-email-address-provider/framework-onEvent/ServiceRole/Resource + superwerkergenerateemailaddressproviderframeworkonEventServiceRoleDefaultPolicy38FE703D: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: lambda:InvokeFunction + Effect: Allow + Resource: + - !GetAtt 'superwerkergenerateemailaddressprovidergenerateemailaddressoneventE83D0932.Arn' + - !Join + - '' + - - !GetAtt 'superwerkergenerateemailaddressprovidergenerateemailaddressoneventE83D0932.Arn' + - :* + Version: '2012-10-17' + PolicyName: superwerkergenerateemailaddressproviderframeworkonEventServiceRoleDefaultPolicy38FE703D + Roles: + - !Ref 'superwerkergenerateemailaddressproviderframeworkonEventServiceRole116C3F83' + Metadata: + aws:cdk:path: SuperwerkerStack/superwerker.generate-email-address-provider/generate-email-address-provider/framework-onEvent/ServiceRole/DefaultPolicy/Resource + superwerkergenerateemailaddressproviderframeworkonEvent3AC8AF01: + Type: AWS::Lambda::Function + Properties: + Code: + S3Bucket: !Sub 'superwerker-assets-${AWS::Region}' + S3Key: '0.15.0/2157f4ab8972014e220d70707296b292b3b7301f163f7cd641ccda0ee663530f.zip' + Role: !GetAtt 'superwerkergenerateemailaddressproviderframeworkonEventServiceRole116C3F83.Arn' + Description: AWS CDK resource provider framework - onEvent (SuperwerkerStack/superwerker.generate-email-address-provider/generate-email-address-provider) + Environment: + Variables: + USER_ON_EVENT_FUNCTION_ARN: !GetAtt 'superwerkergenerateemailaddressprovidergenerateemailaddressoneventE83D0932.Arn' + Handler: framework.onEvent + Runtime: nodejs14.x + Timeout: 900 + DependsOn: + - superwerkergenerateemailaddressproviderframeworkonEventServiceRoleDefaultPolicy38FE703D + - superwerkergenerateemailaddressproviderframeworkonEventServiceRole116C3F83 + Metadata: + aws:cdk:path: SuperwerkerStack/superwerker.generate-email-address-provider/generate-email-address-provider/framework-onEvent/Resource + aws:asset:path: asset.2157f4ab8972014e220d70707296b292b3b7301f163f7cd641ccda0ee663530f + aws:asset:is-bundled: false + aws:asset:property: Code + GeneratedLogArchiveAWSAccountEmail92CF255B: + Type: Custom::GenerateEmailAddress + Properties: + ServiceToken: !GetAtt 'superwerkergenerateemailaddressproviderframeworkonEvent3AC8AF01.Arn' + Domain: !Join + - '' + - - !Ref 'Subdomain' + - . + - !Ref 'Domain' + Name: Log Archive + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/GeneratedLogArchiveAWSAccountEmail/Resource/Default + RootMail: Type: AWS::CloudFormation::Stack Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/living-documentation.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/399b019bfbda669f436273fbe4ac8fb66a80160133f51da692ca55f6de6d0042.json Parameters: - SuperwerkerDomain: !Sub '${Subdomain}.${Domain}' - - RootMail: + Domain: !Ref 'Domain' + Subdomain: !Ref 'Subdomain' + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/RootMail.NestedStack/RootMail.NestedStackResource + aws:asset:path: SuperwerkerStackRootMail79641A79.nested.template.json + aws:asset:property: TemplateURL + ControlTower: Type: AWS::CloudFormation::Stack Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/rootmail.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/af24eb9c16fe2a0804334630d27afb0588a6f0c04d13aa6f0e8e253d8348975a.json Parameters: - Domain: !Ref Domain - Subdomain: !Ref Subdomain - - SecurityHub: - Condition: IncludeSecurityHub + AuditAWSAccountEmail: !GetAtt 'GeneratedAuditAWSAccountEmail426CA952.Email' + LogArchiveAWSAccountEmail: !GetAtt 'GeneratedLogArchiveAWSAccountEmail92CF255B.Email' + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/ControlTower.NestedStack/ControlTower.NestedStackResource + aws:asset:path: SuperwerkerStackControlTowerA5957978.nested.template.json + aws:asset:property: TemplateURL + LivingDocumentation: Type: AWS::CloudFormation::Stack Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/security-hub.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] - + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/1bedaa55f1c84cc9d700606032a034b54b66bb8570238d7dfee8837475457644.json + Parameters: + SuperwerkerDomain: !Join + - '' + - - !Ref 'Subdomain' + - . + - !Ref 'Domain' + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/LivingDocumentation.NestedStack/LivingDocumentation.NestedStackResource + aws:asset:path: SuperwerkerStackLivingDocumentation7D832A3F.nested.template.json + aws:asset:property: TemplateURL Backup: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/3205efcddb004693d8caa05ecd262a3e4da3e913ec0ec1be1f26e67f8af6671c.json + DependsOn: + - ControlTower + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/Backup.NestedStack/Backup.NestedStackResource + aws:asset:path: SuperwerkerStackBackup2EDC043E.nested.template.json + aws:asset:property: TemplateURL Condition: IncludeBackup + Budget: Type: AWS::CloudFormation::Stack - DependsOn: ControlTower Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/backup.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] - + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/332e4889787559f58893a9ae8152c8e162842a4ce92416c9e36099a750cdb166.json + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/Budget.NestedStack/Budget.NestedStackResource + aws:asset:path: SuperwerkerStackBudget4EA13C09.nested.template.json + aws:asset:property: TemplateURL + Condition: IncludeBudget + GuardDuty: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/59a8f6f5c83e1f8486c9c733f4c368f722d6aae4dfe91947c059d5d9af24be6b.json + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/GuardDuty.NestedStack/GuardDuty.NestedStackResource + aws:asset:path: SuperwerkerStackGuardDutyF56F7940.nested.template.json + aws:asset:property: TemplateURL + Condition: IncludeGuardDuty + Notifications: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/1ab342ce26220ab3f325627f1681899ec5f528041c569ac5e8737a4fdab2ecc6.json + Parameters: + NotificationsMail: !Ref 'NotificationsMail' + DependsOn: + - RootMail + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/Notifications.NestedStack/Notifications.NestedStackResource + aws:asset:path: SuperwerkerStackNotifications153068C2.nested.template.json + aws:asset:property: TemplateURL + Condition: IncludeNotifications + SecurityHub: + Type: AWS::CloudFormation::Stack + Properties: + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/b746d1b3573f5de0c05787bcacfba2f0c7f434954fda58d4d935b2f3c4238799.json + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/SecurityHub.NestedStack/SecurityHub.NestedStackResource + aws:asset:path: SuperwerkerStackSecurityHubF52922D9.nested.template.json + aws:asset:property: TemplateURL + Condition: IncludeSecurityHub ServiceControlPolicies: - Condition: IncludeServiceControlPolicies Type: AWS::CloudFormation::Stack - DependsOn: ControlTower Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/service-control-policies.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] + TemplateURL: !Join + - '' + - - https://s3. + - !Ref 'AWS::Region' + - . + - !Ref 'AWS::URLSuffix' + - / + - !Sub 'superwerker-assets-${AWS::Region}' + - /0.15.0/356b73d985cb571592e5ba4afc88630f718ed31950af0bf90e2958cdff82f36a.json Parameters: IncludeSecurityHub: !If - IncludeSecurityHub - - true - - false + - 'true' + - 'false' IncludeBackup: !If - IncludeBackup - - true - - false - - Notifications: - Condition: IncludeNotifications - Type: AWS::CloudFormation::Stack - DependsOn: RootMail + - 'true' + - 'false' + DependsOn: + - ControlTower + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + Metadata: + aws:cdk:path: SuperwerkerStack/ServiceControlPolicies.NestedStack/ServiceControlPolicies.NestedStackResource + aws:asset:path: SuperwerkerStackServiceControlPolicies29C013D1.nested.template.json + aws:asset:property: TemplateURL + Condition: IncludeServiceControlPolicies + CDKMetadata: + Type: AWS::CDK::Metadata Properties: - TemplateURL: - Fn::Sub: - - 'https://${S3Bucket}.s3.${S3Region}.${AWS::URLSuffix}/${QSS3KeyPrefix}templates/notifications.yaml' - - S3Region: !If [UsingDefaultBucket, !Sub '${AWS::Region}', !Ref QSS3BucketRegion] - S3Bucket: !If [UsingDefaultBucket, !Sub '${QSS3BucketName}-${AWS::Region}', !Ref QSS3BucketName] - Parameters: - NotificationsMail: !Ref NotificationsMail - -Outputs: - RootMailDelegationTarget: - Description: Name servers for the hosted zone delegation - Value: !GetAtt RootMail.Outputs.DelegationTarget - CloudWatchDashboard: - Description: See the CloudWatch superwerker dashboard for post-deployment steps - Value: https://console.aws.amazon.com/cloudwatch/home#dashboards:name=superwerker + Analytics: >- + v2:deflate64:H4sIAAAAAAAA/z1QQWrEMAx8S++O2rSw0GM30OMS0gcEra1dlMQ2WPaWEvL32k6b04wQM5rRK5xaeHnCb2m0mZuFr7B+RdSz6m6ux4CWIgXVJYneDiQ+BU1ld/AsHdcF7dXg6LyhSeBS4TM5Hdk7xWhhHfyy6wr2fmH9U09Util5G1GEosBHgTzDOemZ4hmF1G4PaxYcrv9k29SRpWpz/Du7u9I18xj+lgJ98A82pc3NHR077wxXH1XSwyTPj/YE7Xv+yiTMTUgusiUYdvwFFlfcqDEBAAA= + Metadata: + aws:cdk:path: SuperwerkerStack/CDKMetadata/Default + Condition: CDKMetadataAvailable +Conditions: + IncludeBackup: !Equals + - !Ref 'IncludeBackup' + - 'Yes' + IncludeBudget: !Equals + - !Ref 'IncludeBudget' + - 'Yes' + IncludeGuardDuty: !Equals + - !Ref 'IncludeGuardDuty' + - 'Yes' + IncludeNotifications: !Not + - !Equals + - !Ref 'NotificationsMail' + - '' + IncludeSecurityHub: !Equals + - !Ref 'IncludeSecurityHub' + - 'Yes' + IncludeServiceControlPolicies: !Equals + - !Ref 'IncludeServiceControlPolicies' + - 'Yes' + CDKMetadataAvailable: !Or + - !Or + - !Equals + - !Ref 'AWS::Region' + - af-south-1 + - !Equals + - !Ref 'AWS::Region' + - ap-east-1 + - !Equals + - !Ref 'AWS::Region' + - ap-northeast-1 + - !Equals + - !Ref 'AWS::Region' + - ap-northeast-2 + - !Equals + - !Ref 'AWS::Region' + - ap-south-1 + - !Equals + - !Ref 'AWS::Region' + - ap-southeast-1 + - !Equals + - !Ref 'AWS::Region' + - ap-southeast-2 + - !Equals + - !Ref 'AWS::Region' + - ca-central-1 + - !Equals + - !Ref 'AWS::Region' + - cn-north-1 + - !Equals + - !Ref 'AWS::Region' + - cn-northwest-1 + - !Or + - !Equals + - !Ref 'AWS::Region' + - eu-central-1 + - !Equals + - !Ref 'AWS::Region' + - eu-north-1 + - !Equals + - !Ref 'AWS::Region' + - eu-south-1 + - !Equals + - !Ref 'AWS::Region' + - eu-west-1 + - !Equals + - !Ref 'AWS::Region' + - eu-west-2 + - !Equals + - !Ref 'AWS::Region' + - eu-west-3 + - !Equals + - !Ref 'AWS::Region' + - me-south-1 + - !Equals + - !Ref 'AWS::Region' + - sa-east-1 + - !Equals + - !Ref 'AWS::Region' + - us-east-1 + - !Equals + - !Ref 'AWS::Region' + - us-east-2 + - !Or + - !Equals + - !Ref 'AWS::Region' + - us-west-1 + - !Equals + - !Ref 'AWS::Region' + - us-west-2 From b154574ead234b4e43a9da1587f4c6f275c3efb9 Mon Sep 17 00:00:00 2001 From: Robert von Massow <11364448+skomp@users.noreply.github.com> Date: Thu, 16 Feb 2023 11:45:34 +0100 Subject: [PATCH 2/3] Remove unused parameters --- templates/superwerker.template.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/templates/superwerker.template.yaml b/templates/superwerker.template.yaml index 261dad2..1060319 100644 --- a/templates/superwerker.template.yaml +++ b/templates/superwerker.template.yaml @@ -5,15 +5,6 @@ Metadata: EntrypointName: Parameters for launching Superwerker Order: '1' Parameters: - QSS3BucketName: - Type: String - Default: '' - QSS3BucketRegion: - Type: String - Default: '' - QSS3KeyPrefix: - Type: String - Default: '' Domain: Type: String Description: Domain used for root mail feature. Please see https://github.com/superwerker/superwerker/blob/main/README.md#technical-faq for more information From 0f0a17280ded0108ed39739522e1661445baa99b Mon Sep 17 00:00:00 2001 From: Jan Brauer Date: Fri, 17 Mar 2023 12:09:47 +0100 Subject: [PATCH 3/3] add template format version --- templates/superwerker.template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/superwerker.template.yaml b/templates/superwerker.template.yaml index 1060319..9a8ac0c 100644 --- a/templates/superwerker.template.yaml +++ b/templates/superwerker.template.yaml @@ -1,3 +1,4 @@ +AWSTemplateFormatVersion: "2010-09-09" Description: Automated Best Practices for AWS Cloud setups - https://superwerker.cloud (qs-1rhrhoi4t) Metadata: SuperwerkerVersion: '0.15.0'