Skip to content
82 changes: 82 additions & 0 deletions packages/cdk/resources/Cloudfront/CustomSecurityHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {Construct} from "constructs"
import {Duration} from "aws-cdk-lib"
import {ResponseHeadersPolicy, HeadersFrameOption, HeadersReferrerPolicy} from "aws-cdk-lib/aws-cloudfront"

export interface CustomResponseHeadersPolicyProps {
policyName: string
}

export class CustomSecurityHeadersPolicy extends Construct {
public readonly policy: ResponseHeadersPolicy

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

this.policy = new ResponseHeadersPolicy(this, "CustomSecurityHeadersPolicy", {
responseHeadersPolicyName: props.policyName,
comment: "Security headers policy with inclusion of CSP",
removeHeaders: [
"x-amz-server-side-encryption",
"x-amz-server-side-encryption-aws-kms-key-id",
"x-amz-server-side-encryption-bucket-key-enabled"
],
securityHeadersBehavior: {
contentSecurityPolicy: {
contentSecurityPolicy: `
default-src 'self';
script-src 'self' https://assets.nhs.uk;
style-src 'self' 'unsafe-inline' https://assets.nhs.uk;
font-src 'self' https://assets.nhs.uk;
img-src 'self' data: https://assets.nhs.uk;
connect-src 'self'
https://*.amazonaws.com
https://*.amazoncognito.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
`.replace(/\s+/g, " ").trim(),
override: true
},
strictTransportSecurity: {
accessControlMaxAge: Duration.days(365),
includeSubdomains: true,
preload: true,
override: true
},
contentTypeOptions: {
override: true
},
frameOptions: {
frameOption: HeadersFrameOption.DENY,
override: true
},
referrerPolicy: {
referrerPolicy: HeadersReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
override: true
},
xssProtection: {
protection: true,
modeBlock: true,
override: true
}
},
customHeadersBehavior: {
customHeaders: [
{
header: "Permissions-Policy",
value: "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), \
cross-origin-isolated=(),display-capture=(), document-domain=(), encrypted-media=(),\
execution-while-not-rendered=(), execution-while-out-of-viewport=(),\
gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), \
keyboard-map=(), magnetometer=(), microphone=(), midi=(),\
otp-credentials=(), payment=(), picture-in-picture=(), \
publickey-credentials-get=(), screen-wake-lock=(), serial=(), speaker-selection=(),\
sync-xhr=(), usb=(), vertical-scroll=(), web-share=(),\
window-placement=(), xr-spatial-tracking=()",
override: true
}
]
}
})
}
}
23 changes: 12 additions & 11 deletions packages/cdk/resources/CloudfrontBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import {
IOrigin,
KeyValueStore,
OriginRequestPolicy,
ViewerProtocolPolicy,
ResponseHeadersPolicy
ViewerProtocolPolicy
} from "aws-cdk-lib/aws-cloudfront"
import {RestApiOrigin} from "aws-cdk-lib/aws-cloudfront-origins"

import {CustomSecurityHeadersPolicy} from "./Cloudfront/CustomSecurityHeaders"
/**
* Resources for cloudfront behaviors

Expand All @@ -27,7 +26,6 @@ export interface CloudfrontBehaviorsProps {
readonly oauth2GatewayOrigin: RestApiOrigin
readonly oauth2GatewayRequestPolicy: OriginRequestPolicy
readonly staticContentBucketOrigin: IOrigin
readonly responseHeadersPolicy: ResponseHeadersPolicy
}

/**
Expand All @@ -42,7 +40,6 @@ export class CloudfrontBehaviors extends Construct{
public readonly s3StaticContentUriRewriteFunction: CloudfrontFunction
public readonly s3StaticContentRootSlashRedirect: CloudfrontFunction
public readonly keyValueStore: KeyValueStore
public readonly responseHeadersPolicy: ResponseHeadersPolicy

public constructor(scope: Construct, id: string, props: CloudfrontBehaviorsProps){
super(scope, id)
Expand Down Expand Up @@ -188,6 +185,10 @@ export class CloudfrontBehaviors extends Construct{
on how many can be created simultaneously */
s3StaticContentRootSlashRedirect.node.addDependency(s3JwksUriRewriteFunction)

const headersPolicy = new CustomSecurityHeadersPolicy(this, "AdditionalBehavioursHeadersPolicy", {
policyName: `${props.serviceName}-AdditionalBehavioursCustomSecurityHeaders`
})

const additionalBehaviors = {
"/site*": {
origin: props.staticContentBucketOrigin,
Expand All @@ -199,7 +200,7 @@ export class CloudfrontBehaviors extends Construct{
eventType: FunctionEventType.VIEWER_REQUEST
}
],
responseHeadersPolicy: this.responseHeadersPolicy
responseHeadersPolicy: headersPolicy.policy
},
"/api/*": {
origin: props.apiGatewayOrigin,
Expand All @@ -213,7 +214,7 @@ export class CloudfrontBehaviors extends Construct{
eventType: FunctionEventType.VIEWER_REQUEST
}
],
responseHeadersPolicy: this.responseHeadersPolicy
responseHeadersPolicy: headersPolicy.policy
},
"/oauth2/*": {
origin: props.oauth2GatewayOrigin,
Expand All @@ -227,7 +228,7 @@ export class CloudfrontBehaviors extends Construct{
eventType: FunctionEventType.VIEWER_REQUEST
}
],
responseHeadersPolicy: this.responseHeadersPolicy
responseHeadersPolicy: headersPolicy.policy
},
"/jwks/": {/* matches exactly <url>/jwks and will only serve the jwks json (via cf function) */
origin: props.staticContentBucketOrigin,
Expand All @@ -239,7 +240,7 @@ export class CloudfrontBehaviors extends Construct{
eventType: FunctionEventType.VIEWER_REQUEST
}
],
responseHeadersPolicy: this.responseHeadersPolicy
responseHeadersPolicy: headersPolicy.policy
},
"/500.html": { // matches exactly <url>/500.html and will only serve the 500.html page (via cf function)
origin: props.staticContentBucketOrigin,
Expand All @@ -251,7 +252,7 @@ export class CloudfrontBehaviors extends Construct{
eventType: FunctionEventType.VIEWER_REQUEST
}
],
responseHeadersPolicy: this.responseHeadersPolicy
responseHeadersPolicy: headersPolicy.policy
},
"/404.css": {
origin: props.staticContentBucketOrigin,
Expand All @@ -261,7 +262,7 @@ export class CloudfrontBehaviors extends Construct{
origin: props.staticContentBucketOrigin,
allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
responseHeadersPolicy: this.responseHeadersPolicy,
responseHeadersPolicy: headersPolicy.policy,
functionAssociations: [
{
function: s3StaticContentRootSlashRedirect.function,
Expand Down
63 changes: 6 additions & 57 deletions packages/cdk/stacks/StatelessResourcesStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ import {
OriginRequestHeaderBehavior,
OriginRequestPolicy,
OriginRequestQueryStringBehavior,
ViewerProtocolPolicy,
ResponseHeadersPolicy,
HeadersReferrerPolicy,
HeadersFrameOption
ViewerProtocolPolicy
} from "aws-cdk-lib/aws-cloudfront"
import {RestApiOrigin, S3BucketOrigin} from "aws-cdk-lib/aws-cloudfront-origins"
import {Bucket} from "aws-cdk-lib/aws-s3"
Expand All @@ -42,7 +39,7 @@ import {Certificate} from "aws-cdk-lib/aws-certificatemanager"
import {WebACL} from "../resources/WebApplicationFirewall"
import {CfnWebACLAssociation} from "aws-cdk-lib/aws-wafv2"
import {ukRegionLogGroups} from "../resources/ukRegionLogGroups"

import {CustomSecurityHeadersPolicy} from "../resources/Cloudfront/CustomSecurityHeaders"
export interface StatelessResourcesStackProps extends StackProps {
readonly serviceName: string
readonly stackName: string
Expand Down Expand Up @@ -454,55 +451,8 @@ export class StatelessResourcesStack extends Stack {
})

// --- CloudfrontBehaviors
const responseHeadersPolicy = new ResponseHeadersPolicy(this, "CustomSecurityHeadersPolicy", {
responseHeadersPolicyName: `${props.serviceName}-CustomSecurityHeaders`,
comment: "Security headers policy with inclusion of CSP",
securityHeadersBehavior: {
contentSecurityPolicy: {
contentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
"object-src 'none'; base-uri 'self'; frame-ancestors 'none';",
override: true
},
strictTransportSecurity: {
accessControlMaxAge: Duration.days(365),
includeSubdomains: true,
preload: true,
override: true
},
contentTypeOptions: {
override: true
},
frameOptions: {
frameOption: HeadersFrameOption.DENY,
override: true
},
referrerPolicy: {
referrerPolicy: HeadersReferrerPolicy.NO_REFERRER,
override: true
},
xssProtection: {
protection: true,
modeBlock: true,
override: true
}
},
customHeadersBehavior: {
customHeaders: [
{
header: "Permissions-Policy",
value: "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), \
cross-origin-isolated=(),display-capture=(), document-domain=(), encrypted-media=(),\
execution-while-not-rendered=(), execution-while-out-of-viewport=(),\
gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), \
keyboard-map=(), magnetometer=(), microphone=(), midi=(),\
otp-credentials=(), payment=(), picture-in-picture=(), \
publickey-credentials-get=(), screen-wake-lock=(), serial=(), speaker-selection=(),\
sync-xhr=(), usb=(), vertical-scroll=(), web-share=(),\
window-placement=(), xr-spatial-tracking=()",
override: true
}
]
}
const headersPolicy = new CustomSecurityHeadersPolicy(this, "DefaultBehaviourHeadersPolicy", {
policyName: `${props.serviceName}-CustomSecurityHeaders`
})

const cloudfrontBehaviors = new CloudfrontBehaviors(this, "CloudfrontBehaviors", {
Expand All @@ -512,8 +462,7 @@ export class StatelessResourcesStack extends Stack {
apiGatewayRequestPolicy: apiGatewayRequestPolicy,
oauth2GatewayOrigin: oauth2GatewayOrigin,
oauth2GatewayRequestPolicy: oauth2GatewayRequestPolicy,
staticContentBucketOrigin: staticContentBucketOrigin,
responseHeadersPolicy: responseHeadersPolicy
staticContentBucketOrigin: staticContentBucketOrigin
})

// --- Distribution
Expand All @@ -538,7 +487,7 @@ export class StatelessResourcesStack extends Stack {
eventType: FunctionEventType.VIEWER_RESPONSE
}
],
responseHeadersPolicy: responseHeadersPolicy
responseHeadersPolicy: headersPolicy.policy
},
additionalBehaviors: cloudfrontBehaviors.additionalBehaviors,
errorResponses: [
Expand Down