Skip to content
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(cloudfront): implement OriginAccessControl L2 construct #24861

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 69 additions & 40 deletions packages/@aws-cdk/aws-cloudfront-origins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,65 @@
This library contains convenience methods for defining origins for a CloudFront distribution. You can use this library to create origins from
S3 buckets, Elastic Load Balancing v2 load balancers, or any other domain name.

## S3 Bucket

An S3 bucket can be added as an origin. If the bucket is configured as a website endpoint, the distribution can use S3 redirects and S3 custom error
documents.
## From any generic HTTP endpoint

Any public HTTP or HTTPS endpoint can be used as an origin, as long as it has a static domain name.

```ts
const myBucket = new s3.Bucket(this, 'myBucket');
// Creates a distribution from an HTTP endpoint
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: { origin: new origins.S3Origin(myBucket) },
defaultBehavior: { origin: new origins.HttpOrigin('www-origin.example.com') },
});
```

The above will treat the bucket differently based on if `IBucket.isWebsite` is set or not. If the bucket is configured as a website, the bucket is
treated as an HTTP origin, and the built-in S3 redirects and error pages can be used. Otherwise, the bucket is handled as a bucket origin and
CloudFront's redirect and error handling will be used. In the latter case, the Origin will create an origin access identity and grant it access to the
underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront
URLs and not S3 URLs directly. Alternatively, a custom origin access identity can be passed to the S3 origin in the properties.
## From an S3 Bucket with IAM access controls

### Adding Custom Headers
Any S3 bucket can be used as an origin. If the bucket is not configured for public website hosting, the distribution
will access bucket contents through the standard S3 `GetObject` API.

You can configure CloudFront to add custom headers to the requests that it sends to your origin. These custom headers enable you to send and gather information from your origin that you don’t get with typical viewer requests. These headers can even be customized for each origin. CloudFront supports custom headers for both for custom and Amazon S3 origins.
CloudFront supports two methods of authenticating with S3: origin access control (OAC) and origin access identity (OAI). OAI
is a legacy authentication method which will not be supported in new regions. It is strongly recommended to use OAC for all
new deployments.

You can enable OAC by setting the `originAccessControl` property to `true`, or by explicitly supplying an `OriginAccessControl`
resource. If you do not specify anything, the legacy OAI method will be used by default for backwards-compatibility reasons.

```ts
// Creates a distribution from a private S3 bucket using Origin Access Control
const myBucket = new s3.Bucket(this, 'myBucket');
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: { origin: new origins.S3Origin(myBucket, {
customHeaders: {
Foo: 'bar',
},
})},
defaultBehavior: { origin: new origins.S3Origin(myBucket, { originAccessControl: true }) },
});
```

## ELBv2 Load Balancer
The `S3Origin` construct will automatically adjust the bucket resource policy to grant necessary `s3:GetObject` permissions.
When using OAC, any failure when adjusting permissions will result in an error message. If you cannot resolve the error, you
can set `autoResourcePolicy: false` to allow deployment and then manually adjust resource policies after deployment.

When using OAI, some types of failures when adjusting permissions will be silently ignored. You should verify that your
distribution is properly serving origin content after deployment, and then adjust resource policies if necessary.

See [Restricting access to an Amazon S3 origin](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) for more details about OAC, OAI, and resource policies.

## From a website-enabled S3 Bucket with public read access

A website-enabled S3 bucket can also be used as an origin. The distribution will access the bucket through
the public website endpoint and honor any existing S3 configuration for index documents, error documents,
and redirections.

```ts
// Creates a distribution from a public website-enabled S3 bucket
const publicWebBucket = new s3.Bucket(this, 'myWebsiteBucket', {
publicReadAccess: true,
websiteIndexDocument: 'index.html',
});
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: { origin: new origins.S3Origin(publicWebBucket) },
});
```

## From an ELBv2 Load Balancer

An Elastic Load Balancing (ELB) v2 load balancer may be used as an origin. In order for a load balancer to serve as an origin, it must be publicly
accessible (`internetFacing` is true). Both Application and Network load balancers are supported.
Expand Down Expand Up @@ -85,19 +110,28 @@ Note that the `readTimeout` and `keepaliveTimeout` properties can extend their v
quota has been approved in the target account; otherwise, values over 60 seconds will produce an error at deploy time. Consider that this value is
still limited to a maximum value of 180 seconds, which is a hard limit for that quota.

## From an HTTP endpoint
## From an API Gateway REST API

Origins can also be created from any other HTTP endpoint, given the domain name, and optionally, other origin properties.
Origins can be created from an API Gateway REST API. It is recommended to use a
[regional API](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html) in this case. The origin path will automatically be set as the stage name.

```ts
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: { origin: new origins.HttpOrigin('www.example.com') },
declare const api: apigateway.RestApi;
new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: { origin: new origins.RestApiOrigin(api) },
});
```

See the documentation of `@aws-cdk/aws-cloudfront` for more information.
If you want to use a different origin path, you can specify it in the `originPath` property.

```ts
declare const api: apigateway.RestApi;
new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: { origin: new origins.RestApiOrigin(api, { originPath: '/custom-origin-path' }) },
});
```

## Failover Origins (Origin Groups)
## From a failover configuration with multiple origins (Origin Groups)

You can set up CloudFront with origin failover for scenarios that require high availability.
To get started, you create an origin group with two origins: a primary and a secondary.
Expand All @@ -110,7 +144,7 @@ const myBucket = new s3.Bucket(this, 'myBucket');
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: {
origin: new origins.OriginGroup({
primaryOrigin: new origins.S3Origin(myBucket),
primaryOrigin: new origins.S3Origin(myBucket, { originAccessControl: true }),
fallbackOrigin: new origins.HttpOrigin('www.example.com'),
// optional, defaults to: 500, 502, 503 and 504
fallbackStatusCodes: [404],
Expand All @@ -119,23 +153,18 @@ new cloudfront.Distribution(this, 'myDist', {
});
```

## From an API Gateway REST API
## Adding Custom Headers

Origins can be created from an API Gateway REST API. It is recommended to use a
[regional API](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html) in this case. The origin path will automatically be set as the stage name.

```ts
declare const api: apigateway.RestApi;
new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: { origin: new origins.RestApiOrigin(api) },
});
```

If you want to use a different origin path, you can specify it in the `originPath` property.
You can configure CloudFront to add custom headers to the requests that it sends to your origin. These custom headers enable you to send and gather information from your origin that you don’t get with typical viewer requests. These headers can even be customized for each origin. CloudFront supports custom headers for both for custom and Amazon S3 origins.

```ts
declare const api: apigateway.RestApi;
new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: { origin: new origins.RestApiOrigin(api, { originPath: '/custom-origin-path' }) },
const myBucket = new s3.Bucket(this, 'myBucket');
new cloudfront.Distribution(this, 'myDist', {
defaultBehavior: { origin: new origins.S3Origin(myBucket, {
orginAccessControl: true,
customHeaders: {
Foo: 'bar',
},
})},
});
```
159 changes: 143 additions & 16 deletions packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,75 @@ import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
import { HttpOrigin } from './http-origin';
// eslint-disable-next-line import/order
import { DistributionPolicySetter } from '@aws-cdk/aws-cloudfront/lib/private/distribution-policy-setter';

/**
* Resource policy modification settings for S3 origins.
*/
export enum S3OriginAutoResourcePolicy {
/**
* No modifications are made to resource policies
*/
NONE = 'none',
/**
* Read (but not write) permissions are added to resource policies
*/
READ_ONLY = 'readonly',
/**
* Read and write permissions are added to resource policies.
* This setting cannot be used with origin access identity (OAI).
*/
READ_WRITE = 'readwrite',
};

/**
* Properties to use to customize an S3 Origin.
*/
export interface S3OriginProps extends cloudfront.OriginProps {
/**
* An optional Origin Access Identity of the origin identity cloudfront will use when calling your s3 bucket.
* Controls how the resource policies of origin buckets and keys should be automatically modified.
* The behavior is slightly different for "origin access control" (OAC) and "origin access identity"
* (OAI) origin configurations.
*
* @default - An Origin Access Identity will be created.
* If this property is NONE, then no modifications are made to any resource policies. S3 bucket
* policy must be configured manually to grant necessary permissions to the CloudFront distribution.
*
* If this property is READ_ONLY, then s3:GetObject and kms:Decrypt permissions are granted to the
* CloudFront distribution on the bucket and its associated KMS key, if any.
*
* If this property is READ_WRITE, then s3:PutObject, kms:Encrypt, and kms:GenerateDataKey* permissions
* are granted to the CloudFront distrubution on the bucket and its associated KMS key, if any.
*
* When used in combination with OAC, the described behavior is mandatory. If any resource policies
* cannot be set due to imported or cross-stack resources, an error will be raised.
*
* When used in a stack with a legacy OAI configuration, only a best-effort attempt will be made to set
* resource policies. Any failures due to imported or cross-stack resources will be ignored.
*
* `true` is a convenience alias for READ_ONLY and `false` is an alias for NONE.
*
* @default S3OriginAutoResourcePolicyConfig.READ_ONLY
*/
readonly autoResourcePolicy?: S3OriginAutoResourcePolicy | boolean;
/**
* An optional "origin access control" (OAC) resource which describes how the distribution should
* sign its requests for the S3 bucket origin. Can also be set to `true` to apply a default OAC.
* OAC is the preferred way to authenticate S3 requests and should be enabled whenever possible.
*
* @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html
*
* @default - OAC is disabled by default, `originAccessIdentity` settings will be used insead
*/
readonly originAccessControl?: cloudfront.IOriginAccessControl | true;
/**
* An optional "origin access identity" (OAI) that CloudFront will use to access the S3 bucket.
* OAI is a legacy feature which remains enabled by default for backwards-compatibility reasons.
* New origin configurations should use OAC instead, via the `originAccessControl` property.
*
* @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html
*
* @default - OAI is enabled with default settings, unless `originAccessControl` is set
*/
readonly originAccessIdentity?: cloudfront.IOriginAccessIdentity;
}
Expand Down Expand Up @@ -47,16 +107,75 @@ export class S3Origin implements cloudfront.IOrigin {
* Contains additional logic around bucket permissions and origin access identities.
*/
class S3BucketOrigin extends cloudfront.OriginBase {
private originAccessIdentity!: cloudfront.IOriginAccessIdentity;

constructor(private readonly bucket: s3.IBucket, { originAccessIdentity, ...props }: S3OriginProps) {
private originAccessControl?: cloudfront.IOriginAccessControl | true;
private originAccessIdentity?: cloudfront.IOriginAccessIdentity;
private autoResourcePolicy: S3OriginAutoResourcePolicy;

constructor(private readonly bucket: s3.IBucket, props: S3OriginProps) {
super(bucket.bucketRegionalDomainName, props);
if (originAccessIdentity) {
this.originAccessIdentity = originAccessIdentity;
if (props.originAccessControl && props.originAccessIdentity) {
throw new Error('The same origin cannot specify both originAccessControl and originAccessIdentity');
}
this.originAccessControl = props.originAccessControl;
this.originAccessIdentity = props.originAccessIdentity;
const autopolicy = props.autoResourcePolicy ?? true;
if (autopolicy === true) {
this.autoResourcePolicy = S3OriginAutoResourcePolicy.READ_ONLY;
} else if (autopolicy === false) {
this.autoResourcePolicy = S3OriginAutoResourcePolicy.NONE;
} else {
this.autoResourcePolicy = autopolicy;
}
}

private bindOAC(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
if (this.autoResourcePolicy != S3OriginAutoResourcePolicy.NONE) {
const readonly = this.autoResourcePolicy == S3OriginAutoResourcePolicy.READ_ONLY;
const dist = scope.node.scope as cloudfront.Distribution;
const lazyDistArn = cdk.Lazy.string({ produce: () => dist.distributionArn });
if (cdk.Stack.of(this.bucket) == cdk.Stack.of(scope)) {
// same stack - this "just works", the distribution does not depend on
// the resource policy (although it absolutely should, ideally)
const added = this.bucket.addToResourcePolicy(new iam.PolicyStatement({
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
actions: readonly ? ['s3:GetObject'] : ['s3:GetObject', 's3:PutObject'],
resources: [this.bucket.arnForObjects('*')],
conditions: { StringEquals: { 'aws:SourceArn': lazyDistArn } },
}));
if (!added.statementAdded) {
throw new Error('Cannot apply autoResourcePolicy to imported buckets');
}
} else {
if (!DistributionPolicySetter.configureBucket(dist, this.bucket, !readonly)) {
throw new Error('Cannot apply autoResourcePolicy to imported buckets');
}
}
if (this.bucket.encryptionKey) {
if (!DistributionPolicySetter.configureKey(dist, this.bucket.encryptionKey, !readonly)) {
throw new Error('Cannot apply autoResourcePolicy to imported KMS keys');
}
}
}
let oac = this.originAccessControl ?? true;
if (oac === true) {
oac = cloudfront.OriginAccessControl.fromS3Defaults(scope);
}
// Wrap base-class results and directly inject OAC Id property
const baseConfig = super.bind(scope, options);
return {
...baseConfig,
originProperty: {
...baseConfig.originProperty!,
originAccessControlId: oac.originAccessControlId,
},
};
}

public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
if (this.originAccessControl) {
return this.bindOAC(scope, options);
}
if (!this.originAccessIdentity) {
// Using a bucket from another stack creates a cyclic reference with
// the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal,
Expand All @@ -71,19 +190,27 @@ class S3BucketOrigin extends cloudfront.OriginBase {
comment: `Identity for ${options.originId}`,
});
}
// Used rather than `grantRead` because `grantRead` will grant overly-permissive policies.
// Only GetObject is needed to retrieve objects for the distribution.
// This also excludes KMS permissions; currently, OAI only supports SSE-S3 for buckets.
// Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/
this.bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [this.bucket.arnForObjects('*')],
actions: ['s3:GetObject'],
principals: [this.originAccessIdentity.grantPrincipal],
}));
if (this.autoResourcePolicy == S3OriginAutoResourcePolicy.READ_WRITE) {
throw new Error('S3OriginAutoResourcePolicy.READ_WRITE cannot be used with originAccessIdentity');
}
if (this.autoResourcePolicy != S3OriginAutoResourcePolicy.NONE) {
// Used rather than `grantRead` because `grantRead` will grant overly-permissive policies.
// Only GetObject is needed to retrieve objects for the distribution.
// This also excludes KMS permissions; currently, OAI only supports SSE-S3 for buckets.
// Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/
this.bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [this.bucket.arnForObjects('*')],
actions: ['s3:GetObject'],
principals: [this.originAccessIdentity.grantPrincipal],
}));
}
return super.bind(scope, options);
}

protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined {
return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` };
if (this.originAccessControl) {
return { };
}
return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity!.originAccessIdentityId}` };
}
}

This file was deleted.

Loading