cdklabs / cdklabs/cdk-cicd-wrapper

[BUG] (compliance_log_bucket_policy doesn't allow CloudFront to put access logs)

Open
#76 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
TypeScript
Stars
34
Forks
9
Avg merge
2h 57m
Merged PRs (30d)
48

Description

### Describe the bug

compliance_log_bucket_policy (see below link) doesn't allow CloudFront to put access logs. Hence, can't use wrapper provided compliance bucket with CloudFront access logging.
https://github.com/cdklabs/cdk-cicd-wrapper/blob/main/packages/%40cdklabs/cdk-cicd-wrapper/src/stacks/compliance-bucket/lambda-functions/compliance_log_bucket_policy.py

### Expected Behavior

update the bucket policy to have ACL for CloudFront to be able to log access logs on compliance bucket

### Current Behavior

Resource handler returned message: "Invalid request provided: AWS::CloudFront::Distribution: The S3 bucket that you specified for CloudFront logs does not enable ACL access: compliance-log-xxxxxxx-eu-west-1.s3.eu-west-1.amazonaws.com (Service: CloudFront, Status Code: 400, Request ID:

### Reproduction Steps

Just pass complaince bucket to CloudFront construct and use for access log.
e.g.

import * as cdk from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins';
import * as iam from 'aws-cdk-lib/aws-iam';
import { IBucket, CfnBucket } from 'aws-cdk-lib/aws-s3';
import * as shield from 'aws-cdk-lib/aws-shield';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as nag from 'cdk-nag';
import { Construct } from 'constructs';

interface Props {
domainNames?: string[];
webAclArn: string;
certificate?: acm.ICertificate;
staticContentBucket: IBucket;
logBucket?: IBucket;
applicationName: string;
stageName: string;
defaultRootObject?: string;
}

export class CloudFrontConstruct extends Construct {
readonly distribution: cloudfront.Distribution;
readonly staticSourceBucket: IBucket;

constructor(scope: Construct, id: string, props: Props) {
super(scope, id);

if (props.domainNames == undefined && props.certificate) {
throw new Error(
'Custom certificate is provided but not custom domain name.',
);
}

if (props.certificate == undefined && props.domainNames) {
throw new Error(
'Custom domain name is provided but not custom certificate.',
);
}

const originAccessIdentity = new cloudfront.OriginAccessIdentity(
this,
'OriginAccessIdentity',
{
comment: `${props.applicationName}${props.stageName}OriginAccessIdentity`,
},
);

this.staticSourceBucket = props.staticContentBucket;
this.staticSourceBucket.grantRead(originAccessIdentity);

this.distribution = new cloudfront.Distribution(this, 'CloudFront', {
defaultBehavior: {
origin: new S3Origin(this.staticSourceBucket, { originAccessIdentity }),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
domainNames: props.domainNames,
certificate: props.certificate,
webAclId: props.webAclArn,
defaultRootObject: props.defaultRootObject ?? 'index.html',
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
comment: `${props.applicationName}${props.stageName}Distribution`,
enableLogging: props.logBucket != undefined,
sslSupportMethod: cloudfront.SSLMethod.SNI,
logBucket: props.logBucket,
errorResponses: [
{
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: '/index.html',
ttl: cdk.Duration.minutes(30),
},
{
httpStatus: 403,
responseHttpStatus: 403,
responsePagePath: '/index.html',
ttl: cdk.Duration.minutes(30),
},
{
httpStatus: 500,
responseHttpStatus: 500,
responsePagePath: '/index.html',
ttl: cdk.Duration.minutes(30),
},
],
});

if (props.logBucket) {
props.logBucket.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
actions: ['s3:PutObject'],
resources: [`${props.logBucket.bucketArn}/*`],
conditions: {
StringEquals: {
'aws:SecureTransport': 'true',
's3:x-amz-server-side-encryption': 'AES256',
'AWS:SourceArn': `arn:aws:cloudfront::${cdk.Aws.ACCOUNT_ID}:distribution/${this.distribution.distributionId}`,
},
},
}));
}

new ssm.StringParameter(this, 'CloudFrontDomainId', {
parameterName: `/${props.applicationName}/${props.stageName}/SiteCloudFrontDomainId`,
stringValue: this.distribution.distributionId,
});

new ssm.StringParameter(this, 'CloudFrontDomainName', {
parameterName: `/${props.applicationName}/${props.stageName}/SiteCloudFrontDomainName`,
stringValue: this.distribution.domainName,
});

// Attach AWS Shield Advanced for DDoS protection
new shield.CfnProtection(this, 'DDoSProtection', {
name: 'DDoSProtection',
resourceArn: this.getDistributionArn(),
});

nag.NagSuppressions.addResourceSuppressions(
this.distribution,
[
...(props.certificate
? []
: [
{
id: 'AwsSolutions-CFR4',
reason: 'CloudFront distribution is not using custom cert',
},
]),
{
id: 'AwsSolutions-CFR1',
reason: 'CloudFront distribution is not limited by Geo',
},
],
true,
);
}

private getDistributionArn(): string {
return `arn:aws:cloudfront::${cdk.Aws.ACCOUNT_ID}:distribution/${this.distribution.distributionId}`;
}
}
>

### Possible Solution

Add this to bucket policy =>
{
"Sid": "CloudFrontLogDeliveryPolicy",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::compliance-log-211826912675-eu-west-1/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::211826912675:distribution/*",
"aws:SecureTransport": "true",
"s3:x-amz-server-side-encryption": "AES256"
}
}
},

### Additional Information/Context

Ignore this if compliance bucket is not supposed to be used for CLOUDFRONT access logs.

### CDK CI/CD Wrapper version used

0.0.12

### Environment details (OS name and version, etc.)

MacOS Sonoma 14.5

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.