# * # * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # * SPDX-License-Identifier: MIT-0 # * # * Permission is hereby granted, free of charge, to any person obtaining a copy of this # * software and associated documentation files (the "Software"), to deal in the Software # * without restriction, including without limitation the rights to use, copy, modify, # * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # * permit persons to whom the Software is furnished to do so. # * # * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # * AWSTemplateFormatVersion: '2010-09-09' Description: AWS CloudFormation template to create a scheduled multi-account and multi-region Automation patching operation. **WARNING** This template creates KMS key and related resources. You will be billed for the AWS resources used if you create a stack from this template. Parameters: ArtifactBucket: Type: String Description: Bucket name for artifact zip files AllowedPattern: ^[0-9a-z]+([0-9a-z-.]*[0-9a-z])*$ MinLength: 3 MaxLength: 63 OrgID: Description: Organization ID used for S3 bucket sharing Type: String AllowedPattern: ^o-[a-z0-9]{10,32} Resources: # Managed inventory resources # Best practices document for KMS : https://docs.aws.amazon.com/whitepapers/latest/kms-best-practices/key-policies.html # https://docs.aws.amazon.com/whitepapers/latest/kms-best-practices/least-privilege-separation-of-duties.html # https://docs.aws.amazon.com/kms/latest/developerguide/monitoring-overview.html EncryptionKeyforManagedInstancesData: Type: AWS::KMS::Key Properties: Description: Key used to encrypt instance data Enabled: True EnableKeyRotation: True KeyPolicy: Version: '2012-10-17' Id: AccountPolicy Statement: - Sid: Enable IAM User Permissions Effect: Allow Principal: AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root Action: kms:* Resource: '*' - Sid: Allow use of the key by Systems Manager Effect: Allow Principal: Service: ssm.amazonaws.com Action: - kms:DescribeKey - kms:Encrypt - kms:Decrypt - kms:ReEncrypt* - kms:GenerateDataKey - kms:GenerateDataKeyWithoutPlaintext Resource: '*' - Sid: Allow use of the key by service roles within the organization Effect: Allow Principal: "*" Action: - kms:Encrypt - kms:GenerateDataKey Resource: '*' Condition: StringEquals: aws:PrincipalOrgID: !Ref OrgID EncryptionKeyforManagedInstancesDataAlias: Type: AWS::KMS::Alias Properties: AliasName: alias/EncryptionKeyforManagedInstancesData TargetKeyId: !Ref EncryptionKeyforManagedInstancesData ManagedInstancesDataBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'managed-instances-data-${AWS::Region}-${AWS::AccountId}' AccessControl: BucketOwnerFullControl VersioningConfiguration: Status: Enabled BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: KMSMasterKeyID: !Ref EncryptionKeyforManagedInstancesData SSEAlgorithm: aws:kms LifecycleConfiguration: Rules: - Id: ResourceSyncGlacierRule Prefix: glacier Status: Enabled Transitions: - TransitionInDays: 90 StorageClass: GLACIER ExpirationInDays: 365 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true ManagedInstancesDataBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref ManagedInstancesDataBucket PolicyDocument: Statement: - Sid: SSMBucketPermissionsCheck Effect: Allow Principal: Service: ssm.amazonaws.com Action: s3:GetBucketAcl Resource: !GetAtt ManagedInstancesDataBucket.Arn - Sid: SSMBucketDelivery Effect: Allow Principal: Service: ssm.amazonaws.com Action: s3:PutObject Resource: !Sub arn:${AWS::Partition}:s3:::${ManagedInstancesDataBucket}/* Condition: StringEquals: s3:x-amz-server-side-encryption: aws:kms s3:x-amz-server-side-encryption-aws-kms-key-id: !GetAtt EncryptionKeyforManagedInstancesData.Arn - Sid: SSMBucketDeliveryTagging Effect: Allow Principal: Service: ssm.amazonaws.com Action: s3:PutObjectTagging Resource: !Sub arn:${AWS::Partition}:s3:::${ManagedInstancesDataBucket}/*/accountid=*/* - Sid: SSMWrite Effect: Allow Principal: "*" Action: s3:PutObject Resource: !Sub arn:${AWS::Partition}:s3:::${ManagedInstancesDataBucket}/* Condition: StringEquals: aws:PrincipalOrgID: !Ref OrgID # Patching resources PatchingExecutionLogsBucket: # please make sure to follow best practices reccommended by AWS for creating S3 buckets # https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-best-practices.html Type: AWS::S3::Bucket Properties: BucketName: !Sub 'patching-execution-logs-${AWS::Region}-${AWS::AccountId}' AccessControl: BucketOwnerFullControl VersioningConfiguration: Status: Enabled BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 LifecycleConfiguration: Rules: - Id: ExecutionGlacierRule Prefix: glacier Status: Enabled Transitions: - TransitionInDays: 30 StorageClass: GLACIER ExpirationInDays: 365 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true PatchingExecutionLogsBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref PatchingExecutionLogsBucket PolicyDocument: Statement: - Sid: SSMWrite Effect: Allow Principal: "*" Action: - s3:PutObject - s3:PutObjectAcl Resource: - !Join [ '', [!GetAtt PatchingExecutionLogsBucket.Arn, '/*'] ] Condition: StringEquals: aws:PrincipalOrgID: !Ref OrgID PatchingWindowProduct: Type: AWS::ServiceCatalog::CloudFormationProduct Properties: Owner: AWS ProvisioningArtifactParameters: - Info: LoadTemplateFromURL: !Sub - 'https://${bucket}.s3.amazonaws.com/patching_window.yml' - bucket: !Ref ArtifactBucket Name: 'v1' Name: Patching maintenance window product PatchingPortfolio: Type: AWS::ServiceCatalog::Portfolio Properties: DisplayName: Patching portfolio ProviderName: AWS AssociatePatchingproduct: Type: AWS::ServiceCatalog::PortfolioProductAssociation Properties: PortfolioId: !Ref PatchingPortfolio ProductId: !Ref PatchingWindowProduct # Emergency patching EmergencyPatchingFunctionRole: Type: AWS::IAM::Role Properties: RoleName: EmergencyPatchingFunctionRole AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole Path: "/" ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: OrgActions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - organizations:ListRoots - organizations:ListAccounts Resource: "*" - PolicyName: AssumeRole PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sts:AssumeRole Resource: !Sub arn:${AWS::Partition}:iam::*:role/EmergencyPatchingRole EmergencyPatchingFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${EmergencyPatchingFunction} RetentionInDays: 7 EmergencyPatchingFunction: # Best practices for working with AWS Lambda Functions: https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html # tracing and visulaizing Lambda functions with AWS x-ray: https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html Type: AWS::Lambda::Function Properties: Code: S3Bucket: Ref: ArtifactBucket S3Key: emergency_patching.zip FunctionName: emergency_patching Environment: Variables: TASK_LAMBDA_NAME: MaintenanceWindowTaskFunction ASG_TASK_LAMBDA_NAME: MaintenanceWindowASGTaskFunction DEPLOYMENT_REGION: !Ref AWS::Region PATCHING_TEMPLATE_REGION: !Ref AWS::Region CHILD_ACCOUNT_ROLE: EmergencyPatchingRole Handler: emergency_patching.lambda_handler Role: !GetAtt EmergencyPatchingFunctionRole.Arn Timeout: 60 MemorySize: 128 Runtime: python3.8 EmergencyPatchingStateMachineRole: Type: AWS::IAM::Role Properties: RoleName: EmergencyPatchingStateMachineRole AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - states.amazonaws.com Action: sts:AssumeRole Path: "/" Policies: - PolicyName: InvokeFunction PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - lambda:InvokeFunction Resource: - !GetAtt EmergencyPatchingFunction.Arn - PolicyName: CloudWatchLogs PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" EmergencyPatchingStateMachine: Type: AWS::StepFunctions::StateMachine Properties: StateMachineName: EmergencyPatchingStateMachine DefinitionString: !Sub - |- { "StartAt": "Invoke Emergency Lambda", "States": { "Invoke Emergency Lambda": { "Type": "Task", "Resource": "${emergency_lambda}", "End": true } } } - emergency_lambda: !GetAtt EmergencyPatchingFunction.Arn RoleArn: !GetAtt EmergencyPatchingStateMachineRole.Arn PatchBaselineOverrideBucket: # please make sure to follow best practices reccommended by AWS for creating S3 buckets # https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-best-practices.html Type: AWS::S3::Bucket Properties: BucketName: !Sub 'patch-baseline-override-${AWS::Region}-${AWS::AccountId}' AccessControl: BucketOwnerFullControl VersioningConfiguration: Status: Enabled BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 LifecycleConfiguration: Rules: - Id: InventoryGlacierRule Prefix: glacier Status: Enabled Transitions: - TransitionInDays: 30 StorageClass: GLACIER ExpirationInDays: 365 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true PatchBaselineOverrideBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref PatchBaselineOverrideBucket PolicyDocument: Statement: - Sid: OrgRead Effect: Allow Principal: "*" Action: s3:GetObject Resource: !Sub arn:${AWS::Partition}:s3:::${PatchBaselineOverrideBucket}/* Condition: StringEquals: aws:PrincipalOrgID: !Ref OrgID # Reporting resources GlueDatabase: Type: AWS::Glue::Database Properties: CatalogId: !Ref AWS::AccountId DatabaseInput: Name: managed_instances_database Description: Database used for patch reporting GlueCrawler: Type: AWS::Glue::Crawler Properties: DatabaseName: !Ref GlueDatabase Description: Crawler for managed instances data Name: Managed-Instances-GlueCrawler Role: !GetAtt GlueCrawlerRole.Arn Schedule: ScheduleExpression: 'cron(0 8 * * ? *)' Targets: S3Targets: - Path: !Ref ManagedInstancesDataBucket Exclusions: - AWS:InstanceInformation/accountid=*/test.json GlueCrawlerRole: Type: AWS::IAM::Role Properties: RoleName: Managed-Instances-GlueCrawlerRole AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - glue.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSGlueServiceRole Path: "/service-role/" Description: Role created for Glue to access managed instances data S3 bucket Policies: - PolicyName: S3Actions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:GetObject - s3:PutObject Resource: !Sub ${ManagedInstancesDataBucket.Arn}/* - Effect: Allow Action: - kms:Decrypt Resource: !GetAtt EncryptionKeyforManagedInstancesData.Arn DeleteGlueColumnRole: Type: AWS::IAM::Role Properties: RoleName: DeleteGlueColumnRole 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 Policies: - PolicyName: GlueActions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - glue:GetTable - glue:UpdateTable Resource: - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:catalog - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:database/${GlueDatabase} - !Sub arn:${AWS::Partition}:glue:${AWS::Region}:${AWS::AccountId}:table/${GlueDatabase}/aws_instanceinformation DeleteGlueColumnFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${DeleteGlueColumnFunction} RetentionInDays: 7 DeleteGlueColumnFunction: # Best practices for working with AWS Lambda Functions: https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html # tracing and visulaizing Lambda functions with AWS x-ray: https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html Type: AWS::Lambda::Function Properties: FunctionName: DeleteGlueColumnFunction Runtime: python3.7 Handler: index.lambda_handler MemorySize: 128 Timeout: 600 Role: !GetAtt DeleteGlueColumnRole.Arn Environment: Variables: CRAWLER_NAME: !Ref GlueCrawler DATABASE_NAME: !Ref GlueDatabase Code: ZipFile: | import json import os import boto3 CRAWLER_NAME = os.environ['CRAWLER_NAME'] DATABASE_NAME = os.environ['DATABASE_NAME'] TABLE_NAME = 'aws_instanceinformation' COLUMN_NAME = 'resourcetype' glue_client = boto3.client('glue') def lambda_handler(event, context): event_crawler_name = event['detail']['crawlerName'] if event_crawler_name == CRAWLER_NAME: response = glue_client.get_table( CatalogId=context.invoked_function_arn.split(":")[4], DatabaseName=DATABASE_NAME, Name=TABLE_NAME ) # Update the column name if the table exists. if response['Table']: table = response['Table'] columns = table['StorageDescriptor']['Columns'] # Remove the column. updated_columns = [i for i in columns if not (i['Name'] == COLUMN_NAME)] table['StorageDescriptor']['Columns'] = updated_columns table.pop('DatabaseName', None) table.pop('CreatedBy', None) table.pop('CreateTime', None) table.pop('UpdateTime', None) table.pop('IsRegisteredWithLakeFormation', None) table.pop('CatalogId', None) response = glue_client.update_table( CatalogId=context.invoked_function_arn.split(":")[4], DatabaseName=DATABASE_NAME, TableInput=table ) DeleteGlueColumnFunctionEventRule: Type: AWS::Events::Rule Properties: Name: DeleteGlueColumn Description: Deletes resourcetype from Glue table EventPattern: source: - aws.glue detail-type: - Glue Crawler State Change detail: state: - Succeeded Targets: - Arn: !GetAtt DeleteGlueColumnFunction.Arn Id: "TargetFunctionV1" DeleteGlueColumnFunctionCloudWatchPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref DeleteGlueColumnFunction Action: lambda:InvokeFunction Principal: events.amazonaws.com SourceArn: !GetAtt DeleteGlueColumnFunctionEventRule.Arn # Athena resources AthenaQueryResultsBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub athena-query-results-${AWS::Region}-${AWS::AccountId} AccessControl: BucketOwnerFullControl BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: KMSMasterKeyID: !Ref EncryptionKeyforManagedInstancesData SSEAlgorithm: aws:kms LifecycleConfiguration: Rules: - Id: AthenaGlacierRule Prefix: glacier Status: Enabled Transitions: - TransitionInDays: 30 StorageClass: GLACIER ExpirationInDays: 365 PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true AthenaQueryResultsBucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref AthenaQueryResultsBucket PolicyDocument: Statement: - Sid: AthenaQuery Effect: Allow Principal: Service: athena.amazonaws.com Action: - s3:GetObject - s3:PutObject Resource: !Sub arn:${AWS::Partition}:s3:::${AthenaQueryResultsBucket}/* AthenaQueryNonCompliantPatch: Type: AWS::Athena::NamedQuery Properties: Database: !Join [ '-', [!Ref ManagedInstancesDataBucket, 'database'] ] Description: Example query to list managed instances that are non-compliant for patching. Name: QueryNonCompliantPatch QueryString: | SELECT * FROM aws_complianceitem WHERE status='NON_COMPLIANT' AND compliancetype='Patch' LIMIT 20 AthenaQueryInstanceList: Type: AWS::Athena::NamedQuery Properties: Database: !Join [ '-', [!Ref ManagedInstancesDataBucket, 'database'] ] Description: Example query to return a list of non-terminated instances. Name: QueryInstanceList QueryString: | SELECT * FROM aws_instanceinformation WHERE instancestatus IS NULL; AthenaQueryManagedInstanceInventory: Type: AWS::Athena::NamedQuery Properties: Database: !Join [ '-', [!Ref ManagedInstancesDataBucket, 'database'] ] Description: Query to extract Managed instance inventory across all the child accounts along with the compliance status and platform_install_patch tag information Name: PatchComplianceReport QueryString: | SELECT aws_complianceitem.resourceid, aws_complianceitem.status, aws_complianceitem.compliancetype, aws_complianceitem.classification, aws_complianceitem.id, aws_complianceitem.patchstate, aws_complianceitem.accountid, aws_complianceitem.region, aws_complianceitem.installedtime, aws_complianceitem.capturetime, aws_instanceinformation.platformname, aws_instanceinformation.ipaddress, aws_instanceinformation.platformtype, aws_instanceinformation.instancestatus FROM aws_complianceitem INNER JOIN aws_instanceinformation ON aws_instanceinformation.resourceid = aws_complianceitem.resourceid WHERE compliancetype='Patch' AthenaQueryQuickSightInventoryCompliance: Type: AWS::Athena::NamedQuery Properties: Database: !Join [ '-', [!Ref ManagedInstancesDataBucket, 'database'] ] Description: Query to extract compliance status for quicksight dashboard Name: InventoryComplianceQuicksight QueryString: | CREATE OR REPLACE VIEW patch_table as SELECT aws_compliancesummary.resourceid, aws_compliancesummary.status, aws_compliancesummary.compliancetype,aws_compliancesummary.accountid, aws_compliancesummary.region,aws_instanceinformation.platformname, aws_instanceinformation.ipaddress, aws_instanceinformation.platformtype, aws_instanceinformation.instancestatus FROM aws_compliancesummary INNER JOIN aws_instanceinformation ON aws_instanceinformation.resourceid = aws_compliancesummary.resourceid WHERE compliancetype='Patch' Outputs: PatchingTemplateStackAccountId: Description: 'The account ID of the patching template.' Value: !Ref "AWS::AccountId" PatchingExecutionLogsBucketName: Description: The name of the S3 bucket used to store execution logs centrally. Value: !Ref PatchingExecutionLogsBucket ManagedInstancesDataBucketName: Description: The name of the S3 bucket used to store resource data sync details. Value: !Ref ManagedInstancesDataBucket BaselineOverrideBucket: Description: The ARN of the S3 bucket used to store patch baseline override list. Value: !GetAtt PatchBaselineOverrideBucket.Arn PatchingTemplateStackRegion: Description: 'The region of the patching template.' Value: !Ref "AWS::Region" ManagedInstancesDataEncryptionKey: Description: The ARN of the KMS key used to encrypt resource data sync logs. Value: !GetAtt EncryptionKeyforManagedInstancesData.Arn