-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: ele-4721 add support for security headers for cdn site hosting #60
Changes from 16 commits
203b436
d5c31cc
7924b6d
d8ffac8
dc1c06c
00b2b56
47aace0
b6c25bf
82e5947
fb16b67
bc9a5bf
3e96297
898175a
15356f0
8fceec4
7258ca3
3e12d8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ import * as certificatemanager from "@aws-cdk/aws-certificatemanager"; | |
import * as cloudfront from "@aws-cdk/aws-cloudfront"; | ||
import * as s3 from "@aws-cdk/aws-s3"; | ||
import * as s3deploy from "@aws-cdk/aws-s3-deployment"; | ||
import * as origins from "@aws-cdk/aws-cloudfront-origins"; | ||
import { getSiteDomain } from "./utils"; | ||
import { CommonCdnSiteHostingProps } from "./cdn-site-hosting-props"; | ||
|
||
|
@@ -22,7 +23,7 @@ export interface CdnSiteHostingConstructProps | |
*/ | ||
export class CdnSiteHostingConstruct extends cdk.Construct { | ||
public readonly s3Bucket: s3.Bucket; | ||
public readonly cloudfrontWebDistribution: cloudfront.CloudFrontWebDistribution; | ||
public readonly cloudfrontDistribution: cloudfront.Distribution; | ||
|
||
constructor( | ||
scope: cdk.Construct, | ||
|
@@ -35,18 +36,10 @@ export class CdnSiteHostingConstruct extends cdk.Construct { | |
|
||
const siteDomain = getSiteDomain(props); | ||
|
||
// certificate | ||
const viewerCertificate = cloudfront.ViewerCertificate.fromAcmCertificate( | ||
certificatemanager.Certificate.fromCertificateArn( | ||
this, | ||
`${siteDomain}-cert`, | ||
props.certificateArn | ||
), | ||
{ | ||
aliases: [siteDomain], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Has this been renamed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. |
||
sslMethod: cloudfront.SSLMethod.SNI, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Has this been renamed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. |
||
securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Has this been renamed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. |
||
} | ||
const certificate = certificatemanager.Certificate.fromCertificateArn( | ||
this, | ||
`${siteDomain}-cert`, | ||
props.certificateArn | ||
); | ||
|
||
let websiteErrorDocument: string | undefined = props.websiteErrorDocument; | ||
|
@@ -67,36 +60,75 @@ export class CdnSiteHostingConstruct extends cdk.Construct { | |
}); | ||
new cdk.CfnOutput(this, "Bucket", { value: this.s3Bucket.bucketName }); | ||
|
||
const defaultSecurityHeaders: cloudfront.ResponseSecurityHeadersBehavior = { | ||
Crevitus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src | ||
contentSecurityPolicy: { | ||
contentSecurityPolicy: "default-src 'self';", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: how have we decided on the defaults for all of these headers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've just taken all the defaults from the recommended from commented link above it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only setting https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... though it's good it's covering the major fetch directives... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How will this affect authentication in our SPAs? |
||
override: true, | ||
}, | ||
// https://web.dev/security-headers/#xcto | ||
contentTypeOptions: { override: true }, | ||
// https://web.dev/security-headers/#recommended-usages-4 | ||
frameOptions: { | ||
frameOption: cloudfront.HeadersFrameOption.DENY, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comment: I guess we'll have to override this in Otis, for LTI, etc? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, but for most apps it should be deny which is why I have set it as DENY (recommended). |
||
override: true, | ||
}, | ||
// https://web.dev/referrer-best-practices/#setting-your-referrer-policy:-best-practices | ||
referrerPolicy: { | ||
referrerPolicy: | ||
cloudfront.HeadersReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, | ||
override: true, | ||
}, | ||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#examples | ||
strictTransportSecurity: { | ||
accessControlMaxAge: cdk.Duration.days(365 * 2), | ||
includeSubdomains: true, | ||
preload: true, | ||
override: true, | ||
}, | ||
// xxs-protection is overridden by the contentSecurityPolicy in modern browsers | ||
Crevitus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection | ||
xssProtection: { protection: false, override: true }, | ||
}; | ||
|
||
const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy( | ||
this, | ||
"ResponseHeadersPolicy", | ||
{ | ||
securityHeadersBehavior: { | ||
...defaultSecurityHeaders, | ||
...props.securityHeaders, | ||
}, | ||
} | ||
); | ||
|
||
// Cloudfront distribution | ||
this.cloudfrontWebDistribution = new cloudfront.CloudFrontWebDistribution( | ||
this.cloudfrontDistribution = new cloudfront.Distribution( | ||
this, | ||
"SiteDistribution", | ||
{ | ||
viewerCertificate, | ||
originConfigs: [ | ||
{ | ||
// We use a custom origin rather than S3 origin because the latter | ||
// does not seem to support websiteErrorDocument correctly | ||
Comment on lines
-78
to
-79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Is this no longer an issue? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no custom origin support in the new class as far as I am aware, I tried going to a bogus page in Otis in a stack I bought up and it returned the styled Otis error page rather than something from s3 so assume it works in the new class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the This may be another benefit of the change, rather than something we need to recreate. |
||
customOriginSource: { | ||
domainName: this.s3Bucket.bucketWebsiteDomainName, | ||
originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY, | ||
}, | ||
behaviors: [{ isDefaultBehavior: true }], | ||
}, | ||
], | ||
errorConfigurations: props.isRoutedSpa | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Has this been renamed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. |
||
sslSupportMethod: cloudfront.SSLMethod.SNI, | ||
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016, | ||
certificate, | ||
domainNames: [siteDomain], | ||
defaultRootObject: props.websiteIndexDocument, | ||
defaultBehavior: { | ||
origin: new origins.S3Origin(this.s3Bucket), | ||
responseHeadersPolicy: responseHeadersPolicy, | ||
}, | ||
errorResponses: props.isRoutedSpa | ||
? [ | ||
{ | ||
errorCode: 404, | ||
responseCode: 200, | ||
Comment on lines
-90
to
-91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Have these been renamed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. |
||
httpStatus: 404, | ||
responseHttpStatus: 200, | ||
responsePagePath: `/${props.websiteIndexDocument}`, | ||
}, | ||
] | ||
: undefined, | ||
} | ||
); | ||
new cdk.CfnOutput(this, "DistributionId", { | ||
value: this.cloudfrontWebDistribution.distributionId, | ||
value: this.cloudfrontDistribution.distributionId, | ||
}); | ||
|
||
// Deploy site contents to S3 bucket | ||
|
@@ -125,7 +157,7 @@ export class CdnSiteHostingConstruct extends cdk.Construct { | |
prune: isSingleDeploymentStep, | ||
destinationBucket: this.s3Bucket, | ||
distribution: isInvalidationRequired | ||
? this.cloudfrontWebDistribution | ||
? this.cloudfrontDistribution | ||
: undefined, | ||
distributionPaths: isInvalidationRequired | ||
? distributionPathsToInvalidate | ||
|
@@ -145,7 +177,7 @@ export class CdnSiteHostingConstruct extends cdk.Construct { | |
new s3deploy.BucketDeployment(this, "DeployAndInvalidate", { | ||
sources: props.sources, | ||
destinationBucket: this.s3Bucket, | ||
distribution: this.cloudfrontWebDistribution, | ||
distribution: this.cloudfrontDistribution, | ||
distributionPaths: ["/*"], | ||
}); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,9 +58,7 @@ export class CdnSiteHostingWithDnsConstruct extends cdk.Construct { | |
new route53.ARecord(this, `${siteDomain}-SiteAliasRecord`, { | ||
recordName: siteDomain, | ||
target: route53.RecordTarget.fromAlias( | ||
new targets.CloudFrontTarget( | ||
this.cdnSiteHosting.cloudfrontWebDistribution | ||
) | ||
new targets.CloudFrontTarget(this.cdnSiteHosting.cloudfrontDistribution) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, this will be used for non-production stacks, so we could use |
||
), | ||
zone, | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,7 +12,9 @@ import { Template } from "@aws-cdk/assertions"; | |
|
||
// hosted-zone requires an environment be attached to the Stack | ||
const testEnv: Environment = { | ||
region: "eu-west-1", | ||
// region for Cloudfront Distribution certificates must be us-east-1 | ||
// https://stackoverflow.com/questions/37289994/aws-certificate-manager-do-regions-matter | ||
region: "us-east-1", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is intended to represent the environment in which we are provisioning the entire stack, not just a Cloudfront Distribution certificate. Let's say we wanted to add more resources to the construct that are region-specific, so we have a mixture of things that are supposed to be statically
For this reason, I'd be reluctant to hard-code to (To mitigate point 1, we should ultimately be testing to ensure that it respects different specified regions.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your broader point about |
||
account: "abcdefg12345", | ||
}; | ||
const fakeCertificateArn = `arn:aws:acm:${testEnv.region}:${testEnv.account}:certificate/123456789012-1234-1234-1234-12345678`; | ||
|
@@ -74,6 +76,44 @@ describe("CdnSiteHostingConstruct", () => { | |
); | ||
}); | ||
|
||
test("provisions a ResponseHeadersPolicy with default values", () => { | ||
const template = Template.fromStack(stack); | ||
|
||
template.hasResource("AWS::CloudFront::ResponseHeadersPolicy", { | ||
Properties: { | ||
ResponseHeadersPolicyConfig: { | ||
SecurityHeadersConfig: { | ||
ContentSecurityPolicy: { | ||
ContentSecurityPolicy: "default-src self;", | ||
Override: true, | ||
}, | ||
ContentTypeOptions: { | ||
Override: true, | ||
}, | ||
FrameOptions: { | ||
FrameOption: "DENY", | ||
Override: true, | ||
}, | ||
ReferrerPolicy: { | ||
Override: true, | ||
ReferrerPolicy: "strict-origin-when-cross-origin", | ||
}, | ||
StrictTransportSecurity: { | ||
AccessControlMaxAgeSec: 31536, | ||
IncludeSubdomains: true, | ||
Override: true, | ||
Preload: true, | ||
}, | ||
XSSProtection: { | ||
Override: true, | ||
Protection: false, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
}); | ||
|
||
test("issues a bucket deployment with CloudFront invalidation for the specified sources", () => { | ||
expectCDK(stack).to(countResources("Custom::CDKBucketDeployment", 1)); | ||
expectCDK(stack).to( | ||
|
@@ -307,4 +347,62 @@ describe("CdnSiteHostingConstruct", () => { | |
expect(secondDeployment.DependsOn).toContain(firstDeploymentId); | ||
}); | ||
}); | ||
|
||
describe("When securityHeaders are provided", () => { | ||
let stack: Stack; | ||
|
||
beforeAll(() => { | ||
const app = new cdk.App(); | ||
stack = new cdk.Stack(app, "TestStack", { env: testEnv }); | ||
new CdnSiteHostingConstruct(stack, "MyTestConstruct", { | ||
certificateArn: fakeCertificateArn, | ||
siteSubDomain: fakeSiteSubDomain, | ||
domainName: fakeDomain, | ||
removalPolicy: RemovalPolicy.DESTROY, | ||
sources: [s3deploy.Source.asset("./")], | ||
websiteIndexDocument: "index.html", | ||
securityHeaders: { | ||
contentSecurityPolicy: { | ||
contentSecurityPolicy: "default-src 'none';", | ||
override: true, | ||
}, | ||
frameOptions: undefined, | ||
}, | ||
}); | ||
}); | ||
|
||
test("provisions a ResponseHeadersPolicy with overridden default values", () => { | ||
const template = Template.fromStack(stack); | ||
|
||
template.hasResource("AWS::CloudFront::ResponseHeadersPolicy", { | ||
Properties: { | ||
ResponseHeadersPolicyConfig: { | ||
SecurityHeadersConfig: { | ||
ContentSecurityPolicy: { | ||
ContentSecurityPolicy: "default-src 'none';", | ||
Override: true, | ||
}, | ||
ContentTypeOptions: { | ||
Override: true, | ||
}, | ||
ReferrerPolicy: { | ||
Override: true, | ||
ReferrerPolicy: "strict-origin-when-cross-origin", | ||
}, | ||
StrictTransportSecurity: { | ||
AccessControlMaxAgeSec: 31536, | ||
IncludeSubdomains: true, | ||
Override: true, | ||
Preload: true, | ||
}, | ||
XSSProtection: { | ||
Override: true, | ||
Protection: false, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have we checked the projects to see if we're relying on this in some way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't but as I was intending a breaking change to the project I thought it wouldn't matter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might just help highlight anything where we're relying on this for something that will no longer be available. In all likelihood, we won't be relying upon it in this way, but if we were doing that and several of the apps were then blocked from upgrading, the construct's usefulness would substantially lower.
Might be worth emailing round the devs to ask anyone who is using this to take a look at their own project and feed back (by a short deadline). I'll look at user-utils, for instance.