diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 70bff02cf7ea8..cc3132a9a072a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -34,6 +34,7 @@ Higher level constructs for Websocket APIs | ![Experimental](https://img.shields - [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) - [Publishing HTTP APIs](#publishing-http-apis) - [Custom Domain](#custom-domain) + - [Mutual TLS](#mutual-tls-mtls) - [Managing access](#managing-access) - [Metrics](#metrics) - [VPC Link](#vpc-link) @@ -254,6 +255,29 @@ declare const apiDemo: apigwv2.HttpApi; const demoDomainUrl = apiDemo.defaultStage?.domainUrl; // returns "https://example.com/demo" ``` +## Mutual TLS (mTLS) + +Mutual TLS can be configured to limit access to your API based by using client certificates instead of (or as an extension of) using authorization headers. + +```ts +import * as s3 from '@aws-cdk/aws-s3'; +const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; +const domainName = 'example.com'; +const bucket = new s3.Bucket.fromBucketName(stack, 'TrustStoreBucket', ...); + +new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + mtls: { + bucket, + key: 'someca.pem', + version: 'version', + }, +}) +``` + +Instructions for configuring your trust store can be found [here](https://aws.amazon.com/blogs/compute/introducing-mutual-tls-authentication-for-amazon-api-gateway/) + ### Managing access API Gateway supports multiple mechanisms for [controlling and managing access to your HTTP diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts index 6b1123512c678..e550a21f915f3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/domain-name.ts @@ -1,4 +1,5 @@ import { ICertificate } from '@aws-cdk/aws-certificatemanager'; +import { IBucket } from '@aws-cdk/aws-s3'; import { IResource, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDomainName, CfnDomainNameProps } from '../apigatewayv2.generated'; @@ -59,6 +60,32 @@ export interface DomainNameProps { * The ACM certificate for this domain name */ readonly certificate: ICertificate; + /** + * The mutual TLS authentication configuration for a custom domain name. + * @default - mTLS is not configured. + */ + readonly mtls?: MTLSConfig +} + +/** + * The mTLS authentication configuration for a custom domain name. + */ +export interface MTLSConfig { + /** + * The bucket that the trust store is hosted in. + */ + readonly bucket: IBucket; + /** + * The key in S3 to look at for the trust store + */ + readonly key: string; + + /** + * The version of the S3 object that contains your truststore. + * To specify a version, you must have versioning enabled for the S3 bucket. + * @default - latest version + */ + readonly version?: string; } /** @@ -88,6 +115,7 @@ export class DomainName extends Resource implements IDomainName { throw new Error('empty string for domainName not allowed'); } + const mtlsConfig = this.configureMTLS(props.mtls); const domainNameProps: CfnDomainNameProps = { domainName: props.domainName, domainNameConfigurations: [ @@ -96,10 +124,19 @@ export class DomainName extends Resource implements IDomainName { endpointType: 'REGIONAL', }, ], + mutualTlsAuthentication: mtlsConfig, }; const resource = new CfnDomainName(this, 'Resource', domainNameProps); this.name = resource.ref; this.regionalDomainName = Token.asString(resource.getAtt('RegionalDomainName')); this.regionalHostedZoneId = Token.asString(resource.getAtt('RegionalHostedZoneId')); } + + private configureMTLS(mtlsConfig?: MTLSConfig): CfnDomainName.MutualTlsAuthenticationProperty | undefined { + if (!mtlsConfig) return undefined; + return { + truststoreUri: mtlsConfig.bucket.s3UrlForObject(mtlsConfig.key), + truststoreVersion: mtlsConfig.version, + }; + } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 4ce2dbc057014..121096724bdfd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -89,6 +89,7 @@ "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, @@ -97,6 +98,7 @@ "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts index 2d0d856c7ae15..9c60f7b5196e3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/domain-name.test.ts @@ -1,5 +1,6 @@ import { Template } from '@aws-cdk/assertions'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; +import { Bucket } from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; import { DomainName, HttpApi } from '../../lib'; @@ -168,4 +169,66 @@ describe('DomainName', () => { expect(t).toThrow('defaultDomainMapping not supported with createDefaultStage disabled'); }); + + test('accepts a mutual TLS configuration', () => { + // GIVEN + const stack = new Stack(); + const bucket = Bucket.fromBucketName(stack, 'testBucket', 'example-bucket'); + + // WHEN + new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + mtls: { + bucket, + key: 'someca.pem', + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::DomainName', { + DomainName: 'example.com', + DomainNameConfigurations: [ + { + CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate', + EndpointType: 'REGIONAL', + }, + ], + MutualTlsAuthentication: { + TruststoreUri: 's3://example-bucket/someca.pem', + }, + }); + }); + + test('mTLS should allow versions to be set on the s3 bucket', () => { + // GIVEN + const stack = new Stack(); + const bucket = Bucket.fromBucketName(stack, 'testBucket', 'example-bucket'); + + // WHEN + new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + mtls: { + bucket, + key: 'someca.pem', + version: 'version', + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::DomainName', { + DomainName: 'example.com', + DomainNameConfigurations: [ + { + CertificateArn: 'arn:aws:acm:us-east-1:111111111111:certificate', + EndpointType: 'REGIONAL', + }, + ], + MutualTlsAuthentication: { + TruststoreUri: 's3://example-bucket/someca.pem', + TruststoreVersion: 'version', + }, + }); + }); });