Skip to content

Commit ea0acff

Browse files
authoredNov 24, 2021
feat(cloudfront): Add support for response headers policy (#17359)
feat(cloudfront): Add support for response headers policy closes #17290 Notes: ~1. Currently the CFNSpec is not up-to-date with the latest available cloudformation changes for `ResponseHeadersPolicyId` in `AWS::CloudFront::Distribution CacheBehavior`. Some aspects of the same are added to the PR but are left commented. Would update the PR once the spec is updated.~ Refs: 1. https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-response-headers.html 2. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-responseheaderspolicy.html 3. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-cachebehavior.html#cfn-cloudfront-distribution-cachebehavior-responseheaderspolicyid ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 0138292 commit ea0acff

9 files changed

+722
-2
lines changed
 

‎packages/@aws-cdk/aws-cloudfront/README.md

+52
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,58 @@ new cloudfront.Distribution(this, 'myDistCustomPolicy', {
260260
});
261261
```
262262

263+
### Customizing Response Headers with Response Headers Policies
264+
265+
You can configure CloudFront to add one or more HTTP headers to the responses that it sends to viewers (web browsers or other clients), without making any changes to the origin or writing any code.
266+
To specify the headers that CloudFront adds to HTTP responses, you use a response headers policy. CloudFront adds the headers regardless of whether it serves the object from the cache or has to retrieve the object from the origin. If the origin response includes one or more of the headers that’s in a response headers policy, the policy can specify whether CloudFront uses the header it received from the origin or overwrites it with the one in the policy.
267+
See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-response-headers.html
268+
269+
```ts
270+
// Using an existing managed response headers policy
271+
declare const bucketOrigin: origins.S3Origin;
272+
new cloudfront.Distribution(this, 'myDistManagedPolicy', {
273+
defaultBehavior: {
274+
origin: bucketOrigin,
275+
responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
276+
},
277+
});
278+
279+
// Creating a custom response headers policy -- all parameters optional
280+
const myResponseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', {
281+
responseHeadersPolicyName: 'MyPolicy',
282+
comment: 'A default policy',
283+
corsBehavior: {
284+
accessControlAllowCredentials: false,
285+
accessControlAllowHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'],
286+
accessControlAllowMethods: ['GET', 'POST'],
287+
accessControlAllowOrigins: ['*'],
288+
accessControlExposeHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'],
289+
accessControlMaxAge: Duration.seconds(600),
290+
originOverride: true,
291+
},
292+
customHeadersBehavior: {
293+
customHeaders: [
294+
{ header: 'X-Amz-Date', value: 'some-value', override: true },
295+
{ header: 'X-Amz-Security-Token', value: 'some-value', override: false },
296+
],
297+
},
298+
securityHeadersBehavior: {
299+
contentSecurityPolicy: { contentSecurityPolicy: 'default-src https:;', override: true },
300+
contentTypeOptions: { override: true },
301+
frameOptions: { frameOption: cloudfront.HeadersFrameOption.DENY, override: true },
302+
referrerPolicy: { referrerPolicy: cloudfront.HeadersReferrerPolicy.NO_REFERRER, override: true },
303+
strictTransportSecurity: { accessControlMaxAge: Duration.seconds(600), includeSubdomains: true, override: true },
304+
xssProtection: { protection: true, modeBlock: true, reportUri: 'https://example.com/csp-report', override: true },
305+
},
306+
});
307+
new cloudfront.Distribution(this, 'myDistCustomPolicy', {
308+
defaultBehavior: {
309+
origin: bucketOrigin,
310+
responseHeadersPolicy: myResponseHeadersPolicy,
311+
},
312+
});
313+
```
314+
263315
### Validating signed URLs or signed cookies with Trusted Key Groups
264316

265317
CloudFront Distribution supports validating signed URLs or signed cookies using key groups.

‎packages/@aws-cdk/aws-cloudfront/lib/distribution.ts

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { IKeyGroup } from './key-group';
1212
import { IOrigin, OriginBindConfig, OriginBindOptions } from './origin';
1313
import { IOriginRequestPolicy } from './origin-request-policy';
1414
import { CacheBehavior } from './private/cache-behavior';
15+
import { IResponseHeadersPolicy } from './response-headers-policy';
1516

1617
// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch.
1718
// eslint-disable-next-line
@@ -700,6 +701,13 @@ export interface AddBehaviorOptions {
700701
*/
701702
readonly originRequestPolicy?: IOriginRequestPolicy;
702703

704+
/**
705+
* The response headers policy for this behavior. The response headers policy determines which headers are included in responses
706+
*
707+
* @default - none
708+
*/
709+
readonly responseHeadersPolicy?: IResponseHeadersPolicy;
710+
703711
/**
704712
* Set this to true to indicate you want to distribute media files in the Microsoft Smooth Streaming format using this behavior.
705713
*

‎packages/@aws-cdk/aws-cloudfront/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './origin';
77
export * from './origin-access-identity';
88
export * from './origin-request-policy';
99
export * from './public-key';
10+
export * from './response-headers-policy';
1011
export * from './web-distribution';
1112

1213
export * as experimental from './experimental';

‎packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export class CacheBehavior {
4848
cachePolicyId: (this.props.cachePolicy ?? CachePolicy.CACHING_OPTIMIZED).cachePolicyId,
4949
compress: this.props.compress ?? true,
5050
originRequestPolicyId: this.props.originRequestPolicy?.originRequestPolicyId,
51+
responseHeadersPolicyId: this.props.responseHeadersPolicy?.responseHeadersPolicyId,
5152
smoothStreaming: this.props.smoothStreaming,
5253
viewerProtocolPolicy: this.props.viewerProtocolPolicy ?? ViewerProtocolPolicy.ALLOW_ALL,
5354
functionAssociations: this.props.functionAssociations?.map(association => ({

‎packages/@aws-cdk/aws-cloudfront/lib/response-headers-policy.ts

+456
Large diffs are not rendered by default.

‎packages/@aws-cdk/aws-cloudfront/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,10 @@
169169
"resource-attribute:@aws-cdk/aws-cloudfront.KeyGroup.keyGroupLastModifiedTime",
170170
"resource-attribute:@aws-cdk/aws-cloudfront.PublicKey.publicKeyCreatedTime",
171171
"resource-attribute:@aws-cdk/aws-cloudfront.OriginAccessIdentity.cloudFrontOriginAccessIdentityId",
172-
"resource-attribute:@aws-cdk/aws-cloudfront.Function.functionMetadataFunctionArn"
172+
"resource-attribute:@aws-cdk/aws-cloudfront.Function.functionMetadataFunctionArn",
173+
"construct-interface-extends-iconstruct:@aws-cdk/aws-cloudfront.IResponseHeadersPolicy",
174+
"resource-interface-extends-resource:@aws-cdk/aws-cloudfront.IResponseHeadersPolicy",
175+
"resource-attribute:@aws-cdk/aws-cloudfront.ResponseHeadersPolicy.responseHeadersPolicyLastModifiedTime"
173176
]
174177
},
175178
"awscdkio": {

‎packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.expected.json

+40-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"CookiesConfig": {
1313
"CookieBehavior": "none"
1414
},
15-
"EnableAcceptEncodingGzip": false,
1615
"EnableAcceptEncodingBrotli": false,
16+
"EnableAcceptEncodingGzip": false,
1717
"HeadersConfig": {
1818
"HeaderBehavior": "none"
1919
},
@@ -44,6 +44,42 @@
4444
}
4545
}
4646
},
47+
"ResponseHeadersPolicy13DBF9E0": {
48+
"Type": "AWS::CloudFront::ResponseHeadersPolicy",
49+
"Properties": {
50+
"ResponseHeadersPolicyConfig": {
51+
"CorsConfig": {
52+
"AccessControlAllowCredentials": false,
53+
"AccessControlAllowHeaders": {
54+
"Items": [
55+
"X-Custom-Header-1",
56+
"X-Custom-Header-2"
57+
]
58+
},
59+
"AccessControlAllowMethods": {
60+
"Items": [
61+
"GET",
62+
"POST"
63+
]
64+
},
65+
"AccessControlAllowOrigins": {
66+
"Items": [
67+
"*"
68+
]
69+
},
70+
"AccessControlExposeHeaders": {
71+
"Items": [
72+
"X-Custom-Header-1",
73+
"X-Custom-Header-2"
74+
]
75+
},
76+
"AccessControlMaxAgeSec": 600,
77+
"OriginOverride": true
78+
},
79+
"Name": "ACustomResponseHeadersPolicy"
80+
}
81+
}
82+
},
4783
"DistB3B78991": {
4884
"Type": "AWS::CloudFront::Distribution",
4985
"Properties": {
@@ -56,6 +92,9 @@
5692
"OriginRequestPolicyId": {
5793
"Ref": "OriginRequestPolicy3EFDB4FA"
5894
},
95+
"ResponseHeadersPolicyId": {
96+
"Ref": "ResponseHeadersPolicy13DBF9E0"
97+
},
5998
"TargetOriginId": "integdistributionpoliciesDistOrigin17849EF2C",
6099
"ViewerProtocolPolicy": "allow-all"
61100
},

‎packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.ts

+14
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,25 @@ const originRequestPolicy = new cloudfront.OriginRequestPolicy(stack, 'OriginReq
1414
headerBehavior: cloudfront.OriginRequestHeaderBehavior.all('CloudFront-Forwarded-Proto'),
1515
});
1616

17+
const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy', {
18+
responseHeadersPolicyName: 'ACustomResponseHeadersPolicy',
19+
corsBehavior: {
20+
accessControlAllowCredentials: false,
21+
accessControlAllowHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'],
22+
accessControlAllowMethods: ['GET', 'POST'],
23+
accessControlAllowOrigins: ['*'],
24+
accessControlExposeHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'],
25+
accessControlMaxAge: cdk.Duration.seconds(600),
26+
originOverride: true,
27+
},
28+
});
29+
1730
new cloudfront.Distribution(stack, 'Dist', {
1831
defaultBehavior: {
1932
origin: new TestOrigin('www.example.com'),
2033
cachePolicy,
2134
originRequestPolicy,
35+
responseHeadersPolicy,
2236
},
2337
});
2438

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import '@aws-cdk/assert-internal/jest';
2+
import { App, Duration, Stack } from '@aws-cdk/core';
3+
import { HeadersFrameOption, HeadersReferrerPolicy, ResponseHeadersPolicy } from '../lib';
4+
5+
describe('ResponseHeadersPolicy', () => {
6+
let app: App;
7+
let stack: Stack;
8+
9+
beforeEach(() => {
10+
app = new App();
11+
stack = new Stack(app, 'Stack', {
12+
env: { account: '123456789012', region: 'testregion' },
13+
});
14+
});
15+
16+
test('import existing policy by id', () => {
17+
const responseHeadersPolicyId = '344f6fe5-7ce5-4df0-a470-3f14177c549c';
18+
const responseHeadersPolicy = ResponseHeadersPolicy.fromResponseHeadersPolicyId(stack, 'MyPolicy', responseHeadersPolicyId);
19+
expect(responseHeadersPolicy.responseHeadersPolicyId).toEqual(responseHeadersPolicyId);
20+
});
21+
22+
test('managed policies are provided', () => {
23+
expect(ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS.responseHeadersPolicyId).toEqual('60669652-455b-4ae9-85a4-c4c02393f86c');
24+
expect(ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT.responseHeadersPolicyId).toEqual('5cc3b908-e619-4b99-88e5-2cf7f45965bd');
25+
expect(ResponseHeadersPolicy.SECURITY_HEADERS.responseHeadersPolicyId).toEqual('67f7725c-6f97-4210-82d7-5512b31e9d03');
26+
expect(ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_AND_SECURITY_HEADERS.responseHeadersPolicyId).toEqual('e61eb60c-9c35-4d20-a928-2b84e02af89c');
27+
expect(ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS.responseHeadersPolicyId).toEqual('eaab4381-ed33-4a86-88ca-d9558dc6cd63');
28+
});
29+
30+
test('minimal example', () => {
31+
new ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy');
32+
33+
expect(stack).toHaveResource('AWS::CloudFront::ResponseHeadersPolicy', {
34+
ResponseHeadersPolicyConfig: {
35+
Name: 'StackResponseHeadersPolicy7B76F936',
36+
},
37+
});
38+
});
39+
40+
test('maximum example', () => {
41+
new ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy', {
42+
responseHeadersPolicyName: 'MyPolicy',
43+
comment: 'A default policy',
44+
corsBehavior: {
45+
accessControlAllowCredentials: false,
46+
accessControlAllowHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'],
47+
accessControlAllowMethods: ['GET', 'POST'],
48+
accessControlAllowOrigins: ['*'],
49+
accessControlExposeHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'],
50+
accessControlMaxAge: Duration.seconds(600),
51+
originOverride: true,
52+
},
53+
customHeadersBehavior: {
54+
customHeaders: [
55+
{ header: 'X-Custom-Header-1', value: 'application/json', override: true },
56+
{ header: 'X-Custom-Header-2', value: '0', override: false },
57+
],
58+
},
59+
securityHeadersBehavior: {
60+
contentSecurityPolicy: { contentSecurityPolicy: 'default-src https:;', override: true },
61+
contentTypeOptions: { override: true },
62+
frameOptions: { frameOption: HeadersFrameOption.DENY, override: true },
63+
referrerPolicy: { referrerPolicy: HeadersReferrerPolicy.NO_REFERRER, override: true },
64+
strictTransportSecurity: { accessControlMaxAge: Duration.seconds(600), includeSubdomains: true, override: true },
65+
xssProtection: { protection: true, modeBlock: true, reportUri: 'https://example.com/csp-report', override: true },
66+
},
67+
});
68+
69+
expect(stack).toHaveResource('AWS::CloudFront::ResponseHeadersPolicy', {
70+
ResponseHeadersPolicyConfig: {
71+
Comment: 'A default policy',
72+
CorsConfig: {
73+
AccessControlAllowCredentials: false,
74+
AccessControlAllowHeaders: {
75+
Items: [
76+
'X-Custom-Header-1',
77+
'X-Custom-Header-2',
78+
],
79+
},
80+
AccessControlAllowMethods: {
81+
Items: [
82+
'GET',
83+
'POST',
84+
],
85+
},
86+
AccessControlAllowOrigins: {
87+
Items: [
88+
'*',
89+
],
90+
},
91+
AccessControlExposeHeaders: {
92+
Items: [
93+
'X-Custom-Header-1',
94+
'X-Custom-Header-2',
95+
],
96+
},
97+
AccessControlMaxAgeSec: 600,
98+
OriginOverride: true,
99+
},
100+
CustomHeadersConfig: {
101+
Items: [
102+
{
103+
Header: 'X-Custom-Header-1',
104+
Override: true,
105+
Value: 'application/json',
106+
},
107+
{
108+
Header: 'X-Custom-Header-2',
109+
Override: false,
110+
Value: '0',
111+
},
112+
],
113+
},
114+
Name: 'MyPolicy',
115+
SecurityHeadersConfig: {
116+
ContentSecurityPolicy: {
117+
ContentSecurityPolicy: 'default-src https:;',
118+
Override: true,
119+
},
120+
ContentTypeOptions: {
121+
Override: true,
122+
},
123+
FrameOptions: {
124+
FrameOption: 'DENY',
125+
Override: true,
126+
},
127+
ReferrerPolicy: {
128+
Override: true,
129+
ReferrerPolicy: 'no-referrer',
130+
},
131+
StrictTransportSecurity: {
132+
AccessControlMaxAgeSec: 600,
133+
IncludeSubdomains: true,
134+
Override: true,
135+
},
136+
XSSProtection: {
137+
ModeBlock: true,
138+
Override: true,
139+
Protection: true,
140+
ReportUri: 'https://example.com/csp-report',
141+
},
142+
},
143+
},
144+
});
145+
});
146+
});

0 commit comments

Comments
 (0)
Please sign in to comment.