Skip to content

Commit cc28312

Browse files
authored
fix(cloudfront): bucket policy for Origin Access Identities is overly permissive (#13087)
For both `CloudFrontWebDistribution` and `Distribution`, if an origin access identity (OAI) is used to access a S3 bucket origin, the default `bucket.grantRead()` method is used to grant read access to the bucket for the OAI. `grantRead` will create a statement in the bucket policy that grants: `s3:GetObject*`, `s3:GetBucket*`, and `s3:List*` to the OAI. The [official documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) in the developer guide states that to allow the OAI to read objects in the bucket, only `s3:GetObject` is necessary. The impact of the additional permissions is that if a user navigates to the root of the distribution (e.g., https://d12345abcdef.cloudfront.net/) *AND* no default root object (e.g., index.html) is set for the distribution, the user will receive an XML list of the objects in the bucket. This is likely undesired behavior. This same policy has been the behavior for OAI + S3 buckets since the first introduction of the `CloudFrontWebDistribution` construct years ago. However, the `CloudFrontWebDistribution` also always defaults the root object to 'index.html' (see #3486 for discussion on that). The side-effect of this is that unless the user selects to use an OAI and overrides the default root object to an empty string, visiting the root will result either in valid content or an AccessDenied message. However, since the `Distribution` constructs do *not* default the root object, exposing the bucket index becomes much more likely. This change restricts the permissions granted to the OAI user to just the necessary `s3:GetObject` permissions. fixes #13086 --- **NOTE:** This PR corrects the behavior without introducing a new feature flag. Any customers who rely on the bucket listing behavior would be broken by this change. However, I anticipate that represents a very small minority, versus the number of customers who may accidentally expose their bucket contents. I would very much like to have a discussion about whether this is the correct choice. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 72c22dc commit cc28312

File tree

8 files changed

+125
-85
lines changed

8 files changed

+125
-85
lines changed

packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as cloudfront from '@aws-cdk/aws-cloudfront';
2+
import * as iam from '@aws-cdk/aws-iam';
23
import * as s3 from '@aws-cdk/aws-s3';
34
import * as cdk from '@aws-cdk/core';
45
import { HttpOrigin } from './http-origin';
@@ -68,7 +69,7 @@ class S3BucketOrigin extends cloudfront.OriginBase {
6869
public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
6970
if (!this.originAccessIdentity) {
7071
// Using a bucket from another stack creates a cyclic reference with
71-
// the bucket taking a dependency on the generated S3CanonicalUserId when `grantRead` is called,
72+
// the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal,
7273
// and the distribution having a dependency on the bucket's domain name.
7374
// Fix this by parenting the OAI in the bucket's stack when cross-stack usage is detected.
7475
const bucketStack = cdk.Stack.of(this.bucket);
@@ -79,7 +80,16 @@ class S3BucketOrigin extends cloudfront.OriginBase {
7980
this.originAccessIdentity = new cloudfront.OriginAccessIdentity(oaiScope, oaiId, {
8081
comment: `Identity for ${options.originId}`,
8182
});
82-
this.bucket.grantRead(this.originAccessIdentity);
83+
84+
// Used rather than `grantRead` because `grantRead` will grant overly-permissive policies.
85+
// Only GetObject is needed to retrieve objects for the distribution.
86+
// This also excludes KMS permissions; currently, OAI only supports SSE-S3 for buckets.
87+
// Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/
88+
this.bucket.addToResourcePolicy(new iam.PolicyStatement({
89+
resources: [this.bucket.arnForObjects('*')],
90+
actions: ['s3:GetObject'],
91+
principals: [this.originAccessIdentity.grantPrincipal],
92+
}));
8393
}
8494
return super.bind(scope, options);
8595
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"dependencies": {
8080
"@aws-cdk/aws-cloudfront": "0.0.0",
8181
"@aws-cdk/aws-elasticloadbalancingv2": "0.0.0",
82+
"@aws-cdk/aws-iam": "0.0.0",
8283
"@aws-cdk/aws-s3": "0.0.0",
8384
"@aws-cdk/core": "0.0.0",
8485
"constructs": "^3.2.0"
@@ -88,8 +89,9 @@
8889
"@aws-cdk/core": "0.0.0",
8990
"constructs": "^3.2.0",
9091
"@aws-cdk/aws-cloudfront": "0.0.0",
91-
"@aws-cdk/aws-s3": "0.0.0",
92-
"@aws-cdk/aws-elasticloadbalancingv2": "0.0.0"
92+
"@aws-cdk/aws-elasticloadbalancingv2": "0.0.0",
93+
"@aws-cdk/aws-iam": "0.0.0",
94+
"@aws-cdk/aws-s3": "0.0.0"
9395
},
9496
"engines": {
9597
"node": ">= 10.13.0 <13 || >=13.7.0"

packages/@aws-cdk/aws-cloudfront-origins/test/integ.origin-group.expected.json

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@
1414
"PolicyDocument": {
1515
"Statement": [
1616
{
17-
"Action": [
18-
"s3:GetObject*",
19-
"s3:GetBucket*",
20-
"s3:List*"
21-
],
17+
"Action": "s3:GetObject",
2218
"Effect": "Allow",
2319
"Principal": {
2420
"CanonicalUser": {
@@ -28,28 +24,20 @@
2824
]
2925
}
3026
},
31-
"Resource": [
32-
{
33-
"Fn::GetAtt": [
34-
"Bucket83908E77",
35-
"Arn"
36-
]
37-
},
38-
{
39-
"Fn::Join": [
40-
"",
41-
[
42-
{
43-
"Fn::GetAtt": [
44-
"Bucket83908E77",
45-
"Arn"
46-
]
47-
},
48-
"/*"
49-
]
27+
"Resource": {
28+
"Fn::Join": [
29+
"",
30+
[
31+
{
32+
"Fn::GetAtt": [
33+
"Bucket83908E77",
34+
"Arn"
35+
]
36+
},
37+
"/*"
5038
]
51-
}
52-
]
39+
]
40+
}
5341
}
5442
],
5543
"Version": "2012-10-17"

packages/@aws-cdk/aws-cloudfront-origins/test/integ.s3-origin.expected.json

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@
1414
"PolicyDocument": {
1515
"Statement": [
1616
{
17-
"Action": [
18-
"s3:GetObject*",
19-
"s3:GetBucket*",
20-
"s3:List*"
21-
],
17+
"Action": "s3:GetObject",
2218
"Effect": "Allow",
2319
"Principal": {
2420
"CanonicalUser": {
@@ -28,28 +24,20 @@
2824
]
2925
}
3026
},
31-
"Resource": [
32-
{
33-
"Fn::GetAtt": [
34-
"Bucket83908E77",
35-
"Arn"
36-
]
37-
},
38-
{
39-
"Fn::Join": [
40-
"",
41-
[
42-
{
43-
"Fn::GetAtt": [
44-
"Bucket83908E77",
45-
"Arn"
46-
]
47-
},
48-
"/*"
49-
]
27+
"Resource": {
28+
"Fn::Join": [
29+
"",
30+
[
31+
{
32+
"Fn::GetAtt": [
33+
"Bucket83908E77",
34+
"Arn"
35+
]
36+
},
37+
"/*"
5038
]
51-
}
52-
]
39+
]
40+
}
5341
}
5442
],
5543
"Version": "2012-10-17"

packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,13 @@ describe('With bucket', () => {
8787
expect(stack).toHaveResourceLike('AWS::S3::BucketPolicy', {
8888
PolicyDocument: {
8989
Statement: [{
90+
Action: 's3:GetObject',
9091
Principal: {
9192
CanonicalUser: { 'Fn::GetAtt': ['DistOrigin1S3Origin87D64058', 'S3CanonicalUserId'] },
9293
},
94+
Resource: {
95+
'Fn::Join': ['', [{ 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] }, '/*']],
96+
},
9397
}],
9498
},
9599
});

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,9 +1027,15 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu
10271027
// first case for backwards compatibility
10281028
if (originConfig.s3OriginSource.originAccessIdentity) {
10291029
// grant CloudFront OriginAccessIdentity read access to S3 bucket
1030-
originConfig.s3OriginSource.s3BucketSource.grantRead(
1031-
originConfig.s3OriginSource.originAccessIdentity,
1032-
);
1030+
// Used rather than `grantRead` because `grantRead` will grant overly-permissive policies.
1031+
// Only GetObject is needed to retrieve objects for the distribution.
1032+
// This also excludes KMS permissions; currently, OAI only supports SSE-S3 for buckets.
1033+
// Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/
1034+
originConfig.s3OriginSource.s3BucketSource.addToResourcePolicy(new iam.PolicyStatement({
1035+
resources: [originConfig.s3OriginSource.s3BucketSource.arnForObjects('*')],
1036+
actions: ['s3:GetObject'],
1037+
principals: [originConfig.s3OriginSource.originAccessIdentity.grantPrincipal],
1038+
}));
10331039

10341040
s3OriginConfig = {
10351041
originAccessIdentity: `origin-access-identity/cloudfront/${originConfig.s3OriginSource.originAccessIdentity.originAccessIdentityName}`,

packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-s3.expected.json

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@
1414
"PolicyDocument": {
1515
"Statement": [
1616
{
17-
"Action": [
18-
"s3:GetObject*",
19-
"s3:GetBucket*",
20-
"s3:List*"
21-
],
17+
"Action": "s3:GetObject",
2218
"Effect": "Allow",
2319
"Principal": {
2420
"AWS": {
@@ -37,28 +33,20 @@
3733
]
3834
}
3935
},
40-
"Resource": [
41-
{
42-
"Fn::GetAtt": [
43-
"Bucket83908E77",
44-
"Arn"
36+
"Resource": {
37+
"Fn::Join": [
38+
"",
39+
[
40+
{
41+
"Fn::GetAtt": [
42+
"Bucket83908E77",
43+
"Arn"
44+
]
45+
},
46+
"/*"
4547
]
46-
},
47-
{
48-
"Fn::Join": [
49-
"",
50-
[
51-
{
52-
"Fn::GetAtt": [
53-
"Bucket83908E77",
54-
"Arn"
55-
]
56-
},
57-
"/*"
58-
]
59-
]
60-
}
61-
]
48+
]
49+
}
6250
}
6351
],
6452
"Version": "2012-10-17"

packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
GeoRestriction,
1111
KeyGroup,
1212
LambdaEdgeEventType,
13+
OriginAccessIdentity,
1314
PublicKey,
1415
SecurityPolicyProtocol,
1516
SSLMethod,
@@ -197,6 +198,59 @@ nodeunitShim({
197198
test.done();
198199
},
199200

201+
'distribution with bucket and OAI'(test: Test) {
202+
const stack = new cdk.Stack();
203+
const s3BucketSource = new s3.Bucket(stack, 'Bucket');
204+
const originAccessIdentity = new OriginAccessIdentity(stack, 'OAI');
205+
206+
new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', {
207+
originConfigs: [{
208+
s3OriginSource: { s3BucketSource, originAccessIdentity },
209+
behaviors: [{ isDefaultBehavior: true }],
210+
}],
211+
});
212+
213+
expect(stack).to(haveResourceLike('AWS::CloudFront::Distribution', {
214+
DistributionConfig: {
215+
Origins: [
216+
{
217+
ConnectionAttempts: 3,
218+
ConnectionTimeout: 10,
219+
DomainName: {
220+
'Fn::GetAtt': [
221+
'Bucket83908E77',
222+
'RegionalDomainName',
223+
],
224+
},
225+
Id: 'origin1',
226+
S3OriginConfig: {
227+
OriginAccessIdentity: {
228+
'Fn::Join': ['', ['origin-access-identity/cloudfront/', { Ref: 'OAIE1EFC67F' }]],
229+
},
230+
},
231+
},
232+
],
233+
},
234+
}));
235+
236+
expect(stack).to(haveResourceLike('AWS::S3::BucketPolicy', {
237+
PolicyDocument: {
238+
Statement: [{
239+
Action: 's3:GetObject',
240+
Principal: {
241+
CanonicalUser: { 'Fn::GetAtt': ['OAIE1EFC67F', 'S3CanonicalUserId'] },
242+
},
243+
Resource: {
244+
'Fn::Join': ['', [{ 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] }, '/*']],
245+
},
246+
}],
247+
},
248+
}));
249+
250+
test.done();
251+
},
252+
253+
200254
'distribution with trusted signers on default distribution'(test: Test) {
201255
const stack = new cdk.Stack();
202256
const sourceBucket = new s3.Bucket(stack, 'Bucket');

0 commit comments

Comments
 (0)