cdklabs / cdk-cicd-wrapper

This repository contains the infrastructure as code to wrap your AWS CDK project with CI/CD around it.
https://cdklabs.github.io/cdk-cicd-wrapper/
Apache License 2.0
21 stars 5 forks source link

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

Open mishdane opened 1 month ago

mishdane commented 1 month ago

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