Skip to content

Commit 44a4812

Browse files
authored
feat(cloudfront): remove headers and server timing (#23558)
Allow to remove headers and configure server timing in response headers policies. ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Construct Runtime Dependencies: * [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-construct-runtime-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent e0885db commit 44a4812

10 files changed

+156
-34
lines changed

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

+9-7
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ your domain name, and provide one (or more) domain names from the certificate fo
9191

9292
The certificate must be present in the AWS Certificate Manager (ACM) service in the US East (N. Virginia) region; the certificate
9393
may either be created by ACM, or created elsewhere and imported into ACM. When a certificate is used, the distribution will support HTTPS connections
94-
from SNI only and a minimum protocol version of TLSv1.2_2021 if the `@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021` feature flag is set, and TLSv1.2_2019 otherwise.
94+
from SNI only and a minimum protocol version of TLSv1.2_2021 if the `@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021` feature flag is set, and TLSv1.2_2019 otherwise.
9595

9696
```ts
9797
// To use your own domain name in a Distribution, you must associate a certificate
@@ -340,6 +340,8 @@ const myResponseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'Resp
340340
strictTransportSecurity: { accessControlMaxAge: Duration.seconds(600), includeSubdomains: true, override: true },
341341
xssProtection: { protection: true, modeBlock: true, reportUri: 'https://example.com/csp-report', override: true },
342342
},
343+
removeHeaders: ['Server'],
344+
serverTimingSamplingRate: 50,
343345
});
344346
new cloudfront.Distribution(this, 'myDistCustomPolicy', {
345347
defaultBehavior: {
@@ -620,7 +622,7 @@ configuration properties have been changed:
620622
| `loggingConfig` | `enableLogging`; configure with `logBucket` `logFilePrefix` and `logIncludesCookies` |
621623
| `viewerProtocolPolicy` | removed; set on each behavior instead. default changed from `REDIRECT_TO_HTTPS` to `ALLOW_ALL` |
622624

623-
After switching constructs, you need to maintain the same logical ID for the underlying [CfnDistribution](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-cloudfront.CfnDistribution.html) if you wish to avoid the deletion and recreation of your distribution.
625+
After switching constructs, you need to maintain the same logical ID for the underlying [CfnDistribution](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-cloudfront.CfnDistribution.html) if you wish to avoid the deletion and recreation of your distribution.
624626
To do this, use [escape hatches](https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html) to override the logical ID created by the new Distribution construct with the logical ID created by the old construct.
625627

626628
Example:
@@ -776,7 +778,7 @@ new cloudfront.CloudFrontWebDistribution(this, 'MyCfWebDistribution', {
776778
});
777779
```
778780

779-
Becomes:
781+
Becomes:
780782

781783
```ts
782784
declare const sourceBucket: s3.Bucket;
@@ -795,8 +797,8 @@ cfnDistribution.addPropertyOverride('ViewerCertificate.SslSupportMethod', 'sni-o
795797

796798
### Other changes
797799

798-
A number of default settings have changed on the new API when creating a new distribution, behavior, and origin.
799-
After making the major changes needed for the migration, run `cdk diff` to see what settings have changed.
800+
A number of default settings have changed on the new API when creating a new distribution, behavior, and origin.
801+
After making the major changes needed for the migration, run `cdk diff` to see what settings have changed.
800802
If no changes are desired during migration, you will at the least be able to use [escape hatches](https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html) to override what the CDK synthesizes, if you can't change the properties directly.
801803

802804
## CloudFrontWebDistribution API
@@ -1002,7 +1004,7 @@ The following example command uses OpenSSL to generate an RSA key pair with a le
10021004
openssl genrsa -out private_key.pem 2048
10031005
```
10041006

1005-
The resulting file contains both the public and the private key. The following example command extracts the public key from the file named `private_key.pem` and stores it in `public_key.pem`.
1007+
The resulting file contains both the public and the private key. The following example command extracts the public key from the file named `private_key.pem` and stores it in `public_key.pem`.
10061008

10071009
```bash
10081010
openssl rsa -pubout -in private_key.pem -out public_key.pem
@@ -1028,4 +1030,4 @@ new cloudfront.KeyGroup(this, 'MyKeyGroup', {
10281030
See:
10291031

10301032
* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html
1031-
* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html
1033+
* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html

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

+54-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Duration, Names, Resource } from '@aws-cdk/core';
1+
import { Duration, Names, Resource, Token } from '@aws-cdk/core';
22
import { Construct } from 'constructs';
33
import { CfnResponseHeadersPolicy } from './cloudfront.generated';
44

@@ -51,6 +51,22 @@ export interface ResponseHeadersPolicyProps {
5151
* @default - no security headers behavior
5252
*/
5353
readonly securityHeadersBehavior?: ResponseSecurityHeadersBehavior;
54+
55+
/**
56+
* A list of HTTP response headers that CloudFront removes from HTTP responses
57+
* that it sends to viewers.
58+
*
59+
* @default - no headers are removed
60+
*/
61+
readonly removeHeaders?: string[]
62+
63+
/**
64+
* The percentage of responses that you want CloudFront to add the Server-Timing
65+
* header to.
66+
*
67+
* @default - no Server-Timing header is added to HTTP responses
68+
*/
69+
readonly serverTimingSamplingRate?: number;
5470
}
5571

5672
/**
@@ -105,6 +121,8 @@ export class ResponseHeadersPolicy extends Resource implements IResponseHeadersP
105121
corsConfig: props.corsBehavior ? this._renderCorsConfig(props.corsBehavior) : undefined,
106122
customHeadersConfig: props.customHeadersBehavior ? this._renderCustomHeadersConfig(props.customHeadersBehavior) : undefined,
107123
securityHeadersConfig: props.securityHeadersBehavior ? this._renderSecurityHeadersConfig(props.securityHeadersBehavior) : undefined,
124+
removeHeadersConfig: props.removeHeaders ? this._renderRemoveHeadersConfig(props.removeHeaders) : undefined,
125+
serverTimingHeadersConfig: props.serverTimingSamplingRate ? this._renderServerTimingHeadersConfig(props.serverTimingSamplingRate) : undefined,
108126
},
109127
});
110128

@@ -142,6 +160,36 @@ export class ResponseHeadersPolicy extends Resource implements IResponseHeadersP
142160
xssProtection: behavior.xssProtection,
143161
};
144162
}
163+
164+
private _renderRemoveHeadersConfig(headers: string[]): CfnResponseHeadersPolicy.RemoveHeadersConfigProperty {
165+
const readonlyHeaders = ['content-encoding', 'content-length', 'transfer-encoding', 'warning', 'via'];
166+
167+
return {
168+
items: headers.map(header => {
169+
if (!Token.isUnresolved(header) && readonlyHeaders.includes(header.toLowerCase())) {
170+
throw new Error(`Cannot remove read-only header ${header}`);
171+
}
172+
return { header };
173+
}),
174+
};
175+
}
176+
177+
private _renderServerTimingHeadersConfig(samplingRate: number): CfnResponseHeadersPolicy.ServerTimingHeadersConfigProperty {
178+
if (!Token.isUnresolved(samplingRate)) {
179+
if ((samplingRate < 0 || samplingRate > 100)) {
180+
throw new Error(`Sampling rate must be between 0 and 100 (inclusive), received ${samplingRate}`);
181+
}
182+
183+
if (!hasMaxDecimalPlaces(samplingRate, 4)) {
184+
throw new Error(`Sampling rate can have up to four decimal places, received ${samplingRate}`);
185+
}
186+
}
187+
188+
return {
189+
enabled: true,
190+
samplingRate,
191+
};
192+
}
145193
}
146194

147195
/**
@@ -456,3 +504,8 @@ export enum HeadersReferrerPolicy {
456504
*/
457505
UNSAFE_URL = 'unsafe-url',
458506
}
507+
508+
function hasMaxDecimalPlaces(num: number, decimals: number): boolean {
509+
const parts = num.toString().split('.');
510+
return parts.length === 1 || parts[1].length <= decimals;
511+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"version":"20.0.0"}
1+
{"version":"22.0.0"}

packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.js.snapshot/integ-distribution-policies.assets.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"version": "20.0.0",
2+
"version": "22.0.0",
33
"files": {
4-
"d76653a864e13a2b26a7cf3d87d4ff2a401608c2059e4fecbd6771e44716ee5d": {
4+
"85cdf1d3cb389bbffb86daea3e968294cc2b3ab0ca95c300db0a6b907bed5589": {
55
"source": {
66
"path": "integ-distribution-policies.template.json",
77
"packaging": "file"
88
},
99
"destinations": {
1010
"current_account-current_region": {
1111
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12-
"objectKey": "d76653a864e13a2b26a7cf3d87d4ff2a401608c2059e4fecbd6771e44716ee5d.json",
12+
"objectKey": "85cdf1d3cb389bbffb86daea3e968294cc2b3ab0ca95c300db0a6b907bed5589.json",
1313
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
1414
}
1515
}

packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.js.snapshot/integ-distribution-policies.template.json

+12-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,18 @@
7676
"AccessControlMaxAgeSec": 600,
7777
"OriginOverride": true
7878
},
79-
"Name": "ACustomResponseHeadersPolicy"
79+
"Name": "ACustomResponseHeadersPolicy",
80+
"RemoveHeadersConfig": {
81+
"Items": [
82+
{
83+
"Header": "Server"
84+
}
85+
]
86+
},
87+
"ServerTimingHeadersConfig": {
88+
"Enabled": true,
89+
"SamplingRate": 50
90+
}
8091
}
8192
}
8293
},

packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.js.snapshot/integ.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "20.0.0",
2+
"version": "22.0.0",
33
"testCases": {
44
"integ.distribution-policies": {
55
"stacks": [

packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.js.snapshot/manifest.json

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
{
2-
"version": "20.0.0",
2+
"version": "22.0.0",
33
"artifacts": {
4-
"Tree": {
5-
"type": "cdk:tree",
6-
"properties": {
7-
"file": "tree.json"
8-
}
9-
},
104
"integ-distribution-policies.assets": {
115
"type": "cdk:asset-manifest",
126
"properties": {
@@ -23,7 +17,7 @@
2317
"validateOnSynth": false,
2418
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
2519
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
26-
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/d76653a864e13a2b26a7cf3d87d4ff2a401608c2059e4fecbd6771e44716ee5d.json",
20+
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/85cdf1d3cb389bbffb86daea3e968294cc2b3ab0ca95c300db0a6b907bed5589.json",
2721
"requiresBootstrapStackVersion": 6,
2822
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
2923
"additionalDependencies": [
@@ -77,6 +71,12 @@
7771
]
7872
},
7973
"displayName": "integ-distribution-policies"
74+
},
75+
"Tree": {
76+
"type": "cdk:tree",
77+
"properties": {
78+
"file": "tree.json"
79+
}
8080
}
8181
}
8282
}

packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.js.snapshot/tree.json

+39-12
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,6 @@
44
"id": "App",
55
"path": "",
66
"children": {
7-
"Tree": {
8-
"id": "Tree",
9-
"path": "Tree",
10-
"constructInfo": {
11-
"fqn": "constructs.Construct",
12-
"version": "10.1.85"
13-
}
14-
},
157
"integ-distribution-policies": {
168
"id": "integ-distribution-policies",
179
"path": "integ-distribution-policies",
@@ -135,6 +127,17 @@
135127
},
136128
"accessControlMaxAgeSec": 600,
137129
"originOverride": true
130+
},
131+
"removeHeadersConfig": {
132+
"items": [
133+
{
134+
"header": "Server"
135+
}
136+
]
137+
},
138+
"serverTimingHeadersConfig": {
139+
"enabled": true,
140+
"samplingRate": 50
138141
}
139142
}
140143
}
@@ -159,7 +162,7 @@
159162
"path": "integ-distribution-policies/Dist/Origin1",
160163
"constructInfo": {
161164
"fqn": "constructs.Construct",
162-
"version": "10.1.85"
165+
"version": "10.1.189"
163166
}
164167
},
165168
"Resource": {
@@ -209,17 +212,41 @@
209212
"fqn": "@aws-cdk/aws-cloudfront.Distribution",
210213
"version": "0.0.0"
211214
}
215+
},
216+
"BootstrapVersion": {
217+
"id": "BootstrapVersion",
218+
"path": "integ-distribution-policies/BootstrapVersion",
219+
"constructInfo": {
220+
"fqn": "@aws-cdk/core.CfnParameter",
221+
"version": "0.0.0"
222+
}
223+
},
224+
"CheckBootstrapVersion": {
225+
"id": "CheckBootstrapVersion",
226+
"path": "integ-distribution-policies/CheckBootstrapVersion",
227+
"constructInfo": {
228+
"fqn": "@aws-cdk/core.CfnRule",
229+
"version": "0.0.0"
230+
}
212231
}
213232
},
233+
"constructInfo": {
234+
"fqn": "@aws-cdk/core.Stack",
235+
"version": "0.0.0"
236+
}
237+
},
238+
"Tree": {
239+
"id": "Tree",
240+
"path": "Tree",
214241
"constructInfo": {
215242
"fqn": "constructs.Construct",
216-
"version": "10.1.85"
243+
"version": "10.1.189"
217244
}
218245
}
219246
},
220247
"constructInfo": {
221-
"fqn": "constructs.Construct",
222-
"version": "10.1.85"
248+
"fqn": "@aws-cdk/core.App",
249+
"version": "0.0.0"
223250
}
224251
}
225252
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(stack, 'Respo
2525
accessControlMaxAge: cdk.Duration.seconds(600),
2626
originOverride: true,
2727
},
28+
removeHeaders: ['Server'],
29+
serverTimingSamplingRate: 50,
2830
});
2931

3032
new cloudfront.Distribution(stack, 'Dist', {

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

+27
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ describe('ResponseHeadersPolicy', () => {
6464
strictTransportSecurity: { accessControlMaxAge: Duration.seconds(600), includeSubdomains: true, override: true },
6565
xssProtection: { protection: true, modeBlock: true, reportUri: 'https://example.com/csp-report', override: true },
6666
},
67+
removeHeaders: ['Server'],
68+
serverTimingSamplingRate: 12.3456,
6769
});
6870

6971
Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::ResponseHeadersPolicy', {
@@ -140,10 +142,35 @@ describe('ResponseHeadersPolicy', () => {
140142
ReportUri: 'https://example.com/csp-report',
141143
},
142144
},
145+
RemoveHeadersConfig: {
146+
Items: [{ Header: 'Server' }],
147+
},
148+
ServerTimingHeadersConfig: {
149+
Enabled: true,
150+
SamplingRate: 12.3456,
151+
},
143152
},
144153
});
145154
});
146155

156+
test('throws when removing read-only headers', () => {
157+
expect(() => new ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy', {
158+
removeHeaders: ['Content-Encoding'],
159+
})).toThrow('Cannot remove read-only header Content-Encoding');
160+
});
161+
162+
test('throws with out of range sampling rate', () => {
163+
expect(() => new ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy', {
164+
serverTimingSamplingRate: 110,
165+
})).toThrow('Sampling rate must be between 0 and 100 (inclusive), received 110');
166+
});
167+
168+
test('throws with sampling rate with more than 4 decimal places', () => {
169+
expect(() => new ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy', {
170+
serverTimingSamplingRate: 50.12345,
171+
})).toThrow('Sampling rate can have up to four decimal places, received 50.12345');
172+
});
173+
147174
test('it truncates long auto-generated names', () => {
148175
new ResponseHeadersPolicy(stack, 'AVeryLongIdThatMightSeemRidiculousButSometimesHappensInCdkPipelinesBecauseTheStageNamesConcatenatedWithTheRegionAreQuiteLongMuchLongerThanYouWouldExpect');
149176

0 commit comments

Comments
 (0)