diff --git a/source/packages/@aws-accelerator/accelerator/lib/accelerator-resource-names.ts b/source/packages/@aws-accelerator/accelerator/lib/accelerator-resource-names.ts index afbebca4ea..c68e6c3fd7 100644 --- a/source/packages/@aws-accelerator/accelerator/lib/accelerator-resource-names.ts +++ b/source/packages/@aws-accelerator/accelerator/lib/accelerator-resource-names.ts @@ -73,6 +73,7 @@ interface BucketPrefixes { elbLogs: string; costUsage: string; s3AccessLogs: string; + awsConfig: string; auditManager: string; vpcFlowLogs: string; metadata: string; @@ -137,6 +138,7 @@ export class AcceleratorResourceNames { elbLogs: 'PLACE_HOLDER', costUsage: 'PLACE_HOLDER', s3AccessLogs: 'PLACE_HOLDER', + awsConfig: 'PLACE_HOLDER', auditManager: 'PLACE_HOLDER', vpcFlowLogs: 'PLACE_HOLDER', metadata: 'PLACE_HOLDER', @@ -256,6 +258,7 @@ export class AcceleratorResourceNames { this.bucketPrefixes.elbLogs = props.prefixes.bucketName + '-elb-access-logs'; this.bucketPrefixes.costUsage = props.prefixes.bucketName + '-cur'; this.bucketPrefixes.s3AccessLogs = props.prefixes.bucketName + '-s3-access-logs'; + this.bucketPrefixes.awsConfig = props.prefixes.bucketName + '-aws-config'; this.bucketPrefixes.auditManager = props.prefixes.bucketName + '-auditmgr'; this.bucketPrefixes.vpcFlowLogs = props.prefixes.bucketName + '-vpc'; this.bucketPrefixes.metadata = props.prefixes.bucketName + '-metadata'; diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/logging-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/logging-stack.ts index d17e2ed11a..37cbffab44 100644 --- a/source/packages/@aws-accelerator/accelerator/lib/stacks/logging-stack.ts +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/logging-stack.ts @@ -254,6 +254,65 @@ export class LoggingStack extends AcceleratorStack { // Create VPC Flow logs destination bucket this.createVpcFlowLogsBucket(s3Key, serverAccessLogsBucket, replicationProps); + if ( + cdk.Stack.of(this).account === this.props.accountsConfig.getLogArchiveAccountId() && + this.props.securityConfig.awsConfig.enableDeliveryChannel && + this.props.securityConfig.awsConfig.useSeparateBucket + ) { + const configBucket = new Bucket(this, 'ConfigBucket', { + encryptionType: BucketEncryptionType.SSE_KMS, + s3BucketName: `${this.acceleratorResourceNames.bucketPrefixes.awsConfig}-${cdk.Stack.of(this).account}-${ + cdk.Stack.of(this).region + }`, + kmsKey: this.centralLogsBucket?.getS3Bucket().getKey(), + serverAccessLogsBucket: serverAccessLogsBucket.getS3Bucket(), + replicationProps, + s3LifeCycleRules: this.getS3LifeCycleRules(this.props.globalConfig.logging.configBucket?.lifecycleRules), + }); + + // To make sure central log bucket created before elb access log bucket, this is required when logging stack executes in home region + if (this.centralLogsBucket) { + configBucket.node.addDependency(this.centralLogsBucket); + } + + // AwsSolutions-IAM5: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. + NagSuppressions.addResourceSuppressionsByPath( + this, + `/${this.stackName}/ConfigBucket/ConfigBucketReplication/` + + pascalCase(this.centralLogsBucketName) + + '-ReplicationRole/DefaultPolicy/Resource', + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Allows only specific policy.', + }, + ], + ); + + configBucket.getS3Bucket().addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + principals: [new cdk.aws_iam.ServicePrincipal('config.amazonaws.com')], + actions: ['s3:PutObject'], + resources: [configBucket.getS3Bucket().arnForObjects('*')], + conditions: { + StringEquals: { + 's3:x-amz-acl': 'bucket-owner-full-control', + }, + }, + }), + ); + + configBucket.getS3Bucket().addToResourcePolicy( + new cdk.aws_iam.PolicyStatement({ + principals: [new cdk.aws_iam.ServicePrincipal('config.amazonaws.com')], + actions: ['s3:GetBucketAcl', 's3:ListBucket'], + resources: [configBucket.getS3Bucket().bucketArn], + }), + ); + + this.configBucketAddResourcePolicies(configBucket.getS3Bucket()); + } + /** * Create S3 Bucket for ELB Access Logs, this is created in log archive account * For ELB to write access logs bucket is needed to have SSE-S3 server-side encryption @@ -1771,6 +1830,24 @@ export class LoggingStack extends AcceleratorStack { } } + private configBucketAddResourcePolicies(configBucket: cdk.aws_s3.IBucket) { + this.logger.info('Adding AWS Config bucket resource policies to S3'); + for (const attachment of this.props.globalConfig.logging.configBucket?.s3ResourcePolicyAttachments ?? []) { + const policyDocument = JSON.parse( + this.generatePolicyReplacements(path.join(this.props.configDirPath, attachment.policy), false), + ); + // Create a statements list using the PolicyStatement factory + const statements: cdk.aws_iam.PolicyStatement[] = []; + for (const statement of policyDocument.Statement) { + statements.push(cdk.aws_iam.PolicyStatement.fromJson(statement)); + } + + for (const statement of statements) { + configBucket.addToResourcePolicy(statement); + } + } + } + private elbLogBucketAddResourcePolicies(elbLogBucket: cdk.aws_s3.IBucket) { this.logger.info(`Adding elb log bucket resource policies to S3`); for (const attachment of this.props.globalConfig.logging.elbLogBucket?.s3ResourcePolicyAttachments ?? []) { diff --git a/source/packages/@aws-accelerator/accelerator/lib/stacks/security-resources-stack.ts b/source/packages/@aws-accelerator/accelerator/lib/stacks/security-resources-stack.ts index a137274e01..2f7caefe0a 100644 --- a/source/packages/@aws-accelerator/accelerator/lib/stacks/security-resources-stack.ts +++ b/source/packages/@aws-accelerator/accelerator/lib/stacks/security-resources-stack.ts @@ -544,7 +544,11 @@ export class SecurityResourcesStack extends AcceleratorStack { }); this.deliveryChannel = new cdk.aws_config.CfnDeliveryChannel(this, 'ConfigDeliveryChannel', { - s3BucketName: `${this.acceleratorResourceNames.bucketPrefixes.centralLogs}-${this.logArchiveAccountId}-${this.props.centralizedLoggingRegion}`, + s3BucketName: `${ + this.props.securityConfig.awsConfig.useSeparateBucket + ? this.acceleratorResourceNames.bucketPrefixes.awsConfig + : this.acceleratorResourceNames.bucketPrefixes.centralLogs + }-${this.logArchiveAccountId}-${this.props.centralizedLoggingRegion}`, configSnapshotDeliveryProperties: { deliveryFrequency: 'One_Hour', }, @@ -553,7 +557,11 @@ export class SecurityResourcesStack extends AcceleratorStack { if (this.props.securityConfig.awsConfig.overrideExisting) { const configServiceUpdater = new ConfigServiceRecorder(this, 'ConfigRecorderDeliveryChannel', { - s3BucketName: `${this.acceleratorResourceNames.bucketPrefixes.centralLogs}-${this.logArchiveAccountId}-${this.props.centralizedLoggingRegion}`, + s3BucketName: `${ + this.props.securityConfig.awsConfig.useSeparateBucket + ? this.acceleratorResourceNames.bucketPrefixes.awsConfig + : this.acceleratorResourceNames.bucketPrefixes.centralLogs + }-${this.logArchiveAccountId}-${this.props.centralizedLoggingRegion}`, s3BucketKmsKey: this.centralLogS3Key, logRetentionInDays: this.props.globalConfig.cloudwatchLogRetentionInDays, configRecorderRoleArn: configRecorderRole.roleArn, diff --git a/source/packages/@aws-accelerator/accelerator/test/__snapshots__/logging-stack.test.ts.snap b/source/packages/@aws-accelerator/accelerator/test/__snapshots__/logging-stack.test.ts.snap index 2b3000a343..e3d228bd85 100644 --- a/source/packages/@aws-accelerator/accelerator/test/__snapshots__/logging-stack.test.ts.snap +++ b/source/packages/@aws-accelerator/accelerator/test/__snapshots__/logging-stack.test.ts.snap @@ -1022,6 +1022,338 @@ exports[`LoggingStack Construct(LoggingStack): Snapshot Test 1`] = ` }, "Type": "AWS::IAM::ServiceLinkedRole", }, + "ConfigBucketCmkC7CFE158": { + "DeletionPolicy": "Retain", + "Properties": { + "EnableKeyRotation": true, + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::333333333333:root", + ], + ], + }, + }, + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::KMS::Key", + "UpdateReplacePolicy": "Retain", + }, + "ConfigBucketConfigBucketReplicationAwsAcceleratorCentralLogs333333333333UsWest2ReplicationRole9722BD80": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Path": "/service-role/", + }, + "Type": "AWS::IAM::Role", + }, + "ConfigBucketConfigBucketReplicationAwsAcceleratorCentralLogs333333333333UsWest2ReplicationRoleDefaultPolicyE9501E79": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "id": "AwsSolutions-IAM5", + "reason": "Allows only specific policy.", + }, + ], + }, + }, + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObjectLegalHold", + "s3:GetObjectRetention", + "s3:GetObjectVersion", + "s3:GetObjectVersionAcl", + "s3:GetObjectVersionForReplication", + "s3:GetObjectVersionTagging", + "s3:GetReplicationConfiguration", + "s3:ListBucket", + "s3:ReplicateDelete", + "s3:ReplicateObject", + "s3:ReplicateTags", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ConfigBucketE7CC15D9", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ConfigBucketE7CC15D9", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + { + "Action": [ + "s3:GetBucketVersioning", + "s3:GetObjectVersionTagging", + "s3:ObjectOwnerOverrideToBucketOwner", + "s3:PutBucketVersioning", + "s3:ReplicateDelete", + "s3:ReplicateObject", + "s3:ReplicateTags", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::aws-accelerator-central-logs-333333333333-us-west-2/*", + ], + ], + }, + }, + { + "Action": "kms:Decrypt", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "ConfigBucketCmkC7CFE158", + "Arn", + ], + }, + }, + { + "Action": "kms:Encrypt", + "Effect": "Allow", + "Resource": { + "Ref": "AcceleratorCentralLogBucketKeyLookup26F8C418", + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "ConfigBucketConfigBucketReplicationAwsAcceleratorCentralLogs333333333333UsWest2ReplicationRoleDefaultPolicyE9501E79", + "Roles": [ + { + "Ref": "ConfigBucketConfigBucketReplicationAwsAcceleratorCentralLogs333333333333UsWest2ReplicationRole9722BD80", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "ConfigBucketConfigBucketReplicationB49EB5CE": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "CustomS3PutBucketReplicationCustomResourceProviderLogGroup6A67905E", + ], + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3PutBucketReplicationCustomResourceProviderHandler1D75398C", + "Arn", + ], + }, + "destinationAccountId": "333333333333", + "destinationBucketArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::aws-accelerator-central-logs-333333333333-us-west-2", + ], + ], + }, + "destinationBucketKeyArn": { + "Ref": "AcceleratorCentralLogBucketKeyLookup26F8C418", + }, + "prefix": "", + "replicationRoleArn": { + "Fn::GetAtt": [ + "ConfigBucketConfigBucketReplicationAwsAcceleratorCentralLogs333333333333UsWest2ReplicationRole9722BD80", + "Arn", + ], + }, + "sourceBucketName": { + "Ref": "ConfigBucketE7CC15D9", + }, + }, + "Type": "Custom::S3PutBucketReplication", + "UpdateReplacePolicy": "Delete", + }, + "ConfigBucketE7CC15D9": { + "DeletionPolicy": "Retain", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "ConfigBucketCmkC7CFE158", + "Arn", + ], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + "BucketName": "aws-accelerator-aws-config-333333333333-us-east-1", + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "AccessLogsBucketFA218D2A", + }, + "LogFilePrefix": "aws-accelerator-aws-config-333333333333-us-east-1/", + }, + "OwnershipControls": { + "Rules": [ + { + "ObjectOwnership": "BucketOwnerPreferred", + }, + ], + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "ConfigBucketPolicy97E52C2B": { + "Properties": { + "Bucket": { + "Ref": "ConfigBucketE7CC15D9", + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": { + "AWS": "*", + }, + "Resource": [ + { + "Fn::GetAtt": [ + "ConfigBucketE7CC15D9", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ConfigBucketE7CC15D9", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + "Sid": "deny-insecure-connections", + }, + { + "Action": "s3:PutObject", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control", + }, + }, + "Effect": "Allow", + "Principal": { + "Service": "config.amazonaws.com", + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ConfigBucketE7CC15D9", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + { + "Action": [ + "s3:GetBucketAcl", + "s3:ListBucket", + ], + "Effect": "Allow", + "Principal": { + "Service": "config.amazonaws.com", + }, + "Resource": { + "Fn::GetAtt": [ + "ConfigBucketE7CC15D9", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, "CrossAccountCentralSnsTopicKMSArnSsmParamAccessRoleFA0EB249": { "Metadata": { "cdk_nag": { diff --git a/source/packages/@aws-accelerator/accelerator/test/__snapshots__/security-resources-stack.test.ts.snap b/source/packages/@aws-accelerator/accelerator/test/__snapshots__/security-resources-stack.test.ts.snap index f73cd665c6..ea1fbc3ba7 100644 --- a/source/packages/@aws-accelerator/accelerator/test/__snapshots__/security-resources-stack.test.ts.snap +++ b/source/packages/@aws-accelerator/accelerator/test/__snapshots__/security-resources-stack.test.ts.snap @@ -2764,7 +2764,7 @@ exports[`SecurityResourcesStack Construct(SecurityResourcesStack): Snapshot Tes "ConfigSnapshotDeliveryProperties": { "DeliveryFrequency": "One_Hour", }, - "S3BucketName": "aws-accelerator-central-logs-333333333333-us-west-2", + "S3BucketName": "aws-accelerator-aws-config-333333333333-us-west-2", }, "Type": "AWS::Config::DeliveryChannel", }, diff --git a/source/packages/@aws-accelerator/accelerator/test/configs/all-enabled/security-config.yaml b/source/packages/@aws-accelerator/accelerator/test/configs/all-enabled/security-config.yaml index fb46101cec..1d7af4638f 100644 --- a/source/packages/@aws-accelerator/accelerator/test/configs/all-enabled/security-config.yaml +++ b/source/packages/@aws-accelerator/accelerator/test/configs/all-enabled/security-config.yaml @@ -129,6 +129,7 @@ iamPasswordPolicy: awsConfig: enableConfigurationRecorder: true enableDeliveryChannel: true + useSeparateBucket: true ruleSets: - deploymentTargets: organizationalUnits: diff --git a/source/packages/@aws-accelerator/config/lib/global-config.ts b/source/packages/@aws-accelerator/config/lib/global-config.ts index f5da63e216..7f6098202a 100644 --- a/source/packages/@aws-accelerator/config/lib/global-config.ts +++ b/source/packages/@aws-accelerator/config/lib/global-config.ts @@ -82,6 +82,11 @@ export abstract class GlobalConfigTypes { kmsResourcePolicyAttachments: t.optional(t.array(t.resourcePolicyStatement)), }); + static readonly configBucketConfig = t.interface({ + lifecycleRules: t.array(t.lifecycleRuleConfig), + s3ResourcePolicyAttachments: t.optional(t.array(t.resourcePolicyStatement)), + }); + static readonly elbLogBucketConfig = t.interface({ lifecycleRules: t.array(t.lifecycleRuleConfig), s3ResourcePolicyAttachments: t.optional(t.array(t.resourcePolicyStatement)), @@ -602,6 +607,40 @@ export class CentralLogBucketConfig implements t.TypeOf { + /** + * Declaration of (S3 Bucket) Lifecycle rules. + * Configure additional resource policy attachments + */ + readonly lifecycleRules: t.LifeCycleRule[] = []; + readonly s3ResourcePolicyAttachments: t.ResourcePolicyStatement[] | undefined = undefined; +} + /** * *{@link GlobalConfig} / {@link LoggingConfig} / {@link ElbLogBucketConfig}* * @@ -783,6 +822,10 @@ export class LoggingConfig implements t.TypeOf * iam permission for the iam role name {acceleratorPrefix}Config */ readonly overrideExisting: boolean | undefined; + /** + * Indicates whether to create a separate bucket to point delivery channel at. + * + * AWS Config delivery channel will point to this bucket and have its contents then replicated to the central logs bucket. This enables the option to enable Object Lock on the central logs bucket and still receiving data from the delivery channel. + */ + readonly useSeparateBucket = false; /** * Config Recorder Aggregation configuration */