-
Notifications
You must be signed in to change notification settings - Fork 4.4k
feat(cloudfront): mutual TLS #36245
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
base: main
Are you sure you want to change the base?
feat(cloudfront): mutual TLS #36245
Changes from 12 commits
dd850cd
4171d17
d5ece7b
9393986
0b2c955
7ab49a8
4ade6c6
08f1c7d
fa6ab9d
ef74c4a
80e0ed4
a8a4248
590a435
d474f54
27454ef
1032488
f866228
5f7eab8
7b11d15
d9f166b
4b500e4
a2ac411
a492868
2658292
e53cde6
536c835
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| -----BEGIN CERTIFICATE----- | ||
| MIIDPTCCAiWgAwIBAgIUTNWxjW1IQhg/6aMz9pVzGtJ5rKcwDQYJKoZIhvcNAQEL | ||
| BQAwLTEUMBIGA1UEAwwLSW50ZWdUZXN0Q0ExFTATBgNVBAoMDEFXUy1DREstVGVz | ||
| dDAgFw0yNTExMzAwMDE5MzRaGA8yMTI1MTEwNjAwMTkzNFowLTEUMBIGA1UEAwwL | ||
| SW50ZWdUZXN0Q0ExFTATBgNVBAoMDEFXUy1DREstVGVzdDCCASIwDQYJKoZIhvcN | ||
| AQEBBQADggEPADCCAQoCggEBAM1nO3RgHLvnoEHnM38hdMqLJW94mPexvrzb2MeU | ||
| rWMlREMSJMJb+2oyNrgjWLMzTHEyGrXz1xpzylZi/22WofPhlmvlwQWNhh3nPmF4 | ||
| 7lwqREoZE6Sz0qsVRhomCx4iCGk/IhdHW1ykbcQp8pWOG7SDdMtvRJpMd/Cpt6bJ | ||
| YYQlQrlpZOfkkryCDYGEk0mQUsYSY8o5H+qJSs3iqcU1asKKrDz/AtTm5ltXvZWP | ||
| B2dn5WH7Y9jN3G44LBPIm5s66kNOooR1kbn4omiHl6WvEu16bMEYEv/KQpm9Y8iZ | ||
| G2qLnGNn0RukXN6dgWIPAWJ3GHIOPeHsKPhNKVzVQCFyJ08CAwEAAaNTMFEwHQYD | ||
| VR0OBBYEFJt1yzqghFLxRXGou2H59fC+C5CZMB8GA1UdIwQYMBaAFJt1yzqghFLx | ||
| RXGou2H59fC+C5CZMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB | ||
| AKBpFAAQrnE+uNAOj6y1dSRXCEpEZNywsZsKpBZ24ZXj0a/z5agdImXFGqxDaliP | ||
| 2rNcYiy7b+C4fUNBnzAGnKAnTeP0WBc8V4pkfZOQS4dGnnv0vhjqYBTgOqBr0ahG | ||
| 6eWRWT1PGL8JH/86YxMH6NUp/iIhTJJIB9yp3MrM7aUZcE4nNJ2hYfQkHqCfKgQx | ||
| 2sgyC6r3VE/wJIan42hVkdimBW4h4WuCMniSyabMLz2sG8fPuK/wwgVtUy2NZPdl | ||
| G/lHBgczsWx9+ElIz4oqXi3/Oe+gvMNjMPzk2Y+pzwY8IV8KtoUKpTpdlGs0/LZW | ||
| vudHJyYpz5EStIi478jiQlc= | ||
| -----END CERTIFICATE----- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| -----BEGIN CERTIFICATE----- | ||
| MIIDMDCCAhigAwIBAgIUfgH8ZDseoSmeNjjfxmCGrtuN20kwDQYJKoZIhvcNAQEL | ||
| BQAwLTEUMBIGA1UEAwwLSW50ZWdUZXN0Q0ExFTATBgNVBAoMDEFXUy1DREstVGVz | ||
| dDAgFw0yNTExMzAwMDE5MzRaGA8yMTI1MTEwNjAwMTkzNFowMTEYMBYGA1UEAwwP | ||
| SW50ZWdUZXN0Q2xpZW50MRUwEwYDVQQKDAxBV1MtQ0RLLVRlc3QwggEiMA0GCSqG | ||
| SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkHCyPtZpWBdDso4olYpTG64kpBXKySMl0 | ||
| 54tX+U7p3ROnVUIFNoWZBXV/qFhrfqWQgwowfCMKtTrqvHX3tXtRF+PtsKiz+3Mw | ||
| GK167D/v8EqMPxFLhuvcM9cROrJdQhd3DX4fJH97LI8jSWBWH/NWH/GtHmnYDemD | ||
| NdbqGkbRUtHcObGJSWS+n+NFwblnHQZzkpDkOPSQjwaGj+M61UqpaXImG6qIT0el | ||
| 2hZeDLHY1CmsEtFl4leQlLp48TlS5RoP0Bs/3nmlDdEEqBur+pPRHEu/GdvJ6GKX | ||
| K+UHdNTYsvv55k3vOZXvm47HvjYMZEZP2QslASCgS3hV7wqFlUxrAgMBAAGjQjBA | ||
| MB0GA1UdDgQWBBS036TLMcpNSb0HCZy1u61JK1aNWzAfBgNVHSMEGDAWgBSbdcs6 | ||
| oIRS8UVxqLth+fXwvguQmTANBgkqhkiG9w0BAQsFAAOCAQEAWdytMtdQwN/x/JkE | ||
| 8pk8sNHeO/dBwGUETN0FO76JpUwEx+jtlZC+qS/sIFmYvrmSggyDEs1ECIXeV+wS | ||
| Ku1uFYsscZm5Eb9I0izGllm7hrn7iEHRxmuLx9sj3AS4JknSI/k8tQB6r4FrIqDg | ||
| vE5AXAZrVZWZDgF4QGC+W0hIApR58KgRnsR5314Lykxc7YTs2cGz6xWkpUvdYNOC | ||
| Hn4/Xq0lGVS6VDQsu+V7nDYkc41z8MYI/raFn5IeWCjHuqsqC7ubGdUNn13Ng4Ws | ||
| smHEV7OPslLVJBme43OWvhe01S9zVelOHP5uUq4Qx65NU/BWMNpsZbeHtPVMebQ7 | ||
| YdbWrQ== | ||
| -----END CERTIFICATE----- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| -----BEGIN PRIVATE KEY----- | ||
| MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkHCyPtZpWBdDs | ||
| o4olYpTG64kpBXKySMl054tX+U7p3ROnVUIFNoWZBXV/qFhrfqWQgwowfCMKtTrq | ||
| vHX3tXtRF+PtsKiz+3MwGK167D/v8EqMPxFLhuvcM9cROrJdQhd3DX4fJH97LI8j | ||
| SWBWH/NWH/GtHmnYDemDNdbqGkbRUtHcObGJSWS+n+NFwblnHQZzkpDkOPSQjwaG | ||
| j+M61UqpaXImG6qIT0el2hZeDLHY1CmsEtFl4leQlLp48TlS5RoP0Bs/3nmlDdEE | ||
| qBur+pPRHEu/GdvJ6GKXK+UHdNTYsvv55k3vOZXvm47HvjYMZEZP2QslASCgS3hV | ||
| 7wqFlUxrAgMBAAECggEAH4gKR9JsxPp5Gf6SHHzEeTPoNqmFUBwMGRoj+9M7BlQj | ||
| 4pLPDJFdOfmCifSeiIjc5tdGreosEXKgz1lGkUGdYAdnE9RNlcdKaoNTUbryzfct | ||
| W6UCfbB+wOB1Aip0+tYoDAfbo8AvfkshdXYzvFiHOJUKqmidjZWeuEcv8+r4kF86 | ||
| XPrlBTqYvCB1pF/z6SLXw1tgThUlI0ZRfjhEVNB5nbNBGBJeO8H/4EvAg3QMMXZz | ||
| amn+MFrBI/cVAQuPzUMwtZqTpUo4KG7VYShuvyxL3bEVveVhByhEUjN6yK/7XAnH | ||
| 0qAjo/yBqF4DGHL+IEJJnsY6wddLIuLqNzKxBJ8SgQKBgQDb/zsEDJBCLfExEea3 | ||
| qXIQ/aEZDKrU5FYrUpMqdHKDuZVAu7faqYrdMNiEEz3Jx33CquLpvYBEiHvt18aN | ||
| 6EHgyyGrcYLhVNvgbY/nVljCoyfDmlsoppb6/IuP5LaKO1cqA4myJ9qUl7SZNVdX | ||
| pvp2kyLD1m1C+R5i2J6dH1rw7wKBgQC+94+26wC9Q7pe1MId2yGrfqmBaIWZzx68 | ||
| iLA6TE5Q5Sh3cnOfcNDdj8S5/kX35Qt8x/1PifMfF95kDCyapcdfJeAEduY3+Ybi | ||
| Rx5jBWMjRhad9Vo1Up7qJ/eSCsdRM4lIHTvz2O79/c4BJpvBtHMLPi3KO6tZDgGN | ||
| 9wiyiVtkRQKBgAL7GlmbsfizlqkLjQzABwOj7CLiwQ3Rajl8DPJuUX7nbNZLtNoJ | ||
| ohANMWHYwOWTBmk0145DOxGyp7s5ST9y/jGFjxc7moJjG/eWhHSl/t3kSA2mccXD | ||
| PBh6g4mXl8GXvD6dfagkUhnhVFkfF+fgI1Sn+bidly1pIrhEJyIJ4rmRAoGBAIK1 | ||
| /J6deCIUC6sBQeUhqogx6F/1ZQ0EYet2O/mLZUelsu8fXdjSZYRh9avBSa8GDbjZ | ||
| Qp+AsvKjactdMeKgeji8OfuUTrwdhZ+4QipQ86yfOrffymEZLkDkHmbODeOcvOip | ||
| afHcmKktYiYTgSUzGFOnubvk38HS/mlnn4Jk1jNpAoGAYjQP4tJzno0mYHwWD8QA | ||
| FfGJKzbt+bAaoAlL08t1gMQU7cw/Ipp8ubHsh1WxpqDw9zkkpABHY7xAej9yziJB | ||
| 1RfuJij8jac94QI4alwXRbZVmbOuPBS5CKJy6bH5dNOKz1lbBZg/UDgksCG+rmCp | ||
| IKwlJgdT4NwgDnpCFPCJtf8= | ||
| -----END PRIVATE KEY----- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head><title>mTLS Test</title></head> | ||
| <body><h1>Hello mTLS</h1></body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* eslint-disable import/no-extraneous-dependencies */ | ||
| import * as https from 'https'; | ||
| import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; | ||
|
|
||
| interface MtlsTestEvent { | ||
| distributionDomainName: string; | ||
| certBucket: string; | ||
| useCert: boolean; | ||
| } | ||
|
|
||
| interface MtlsTestResponse { | ||
| statusCode: number; | ||
| statusMessage: string; | ||
| } | ||
|
|
||
| class CertificateRetrievalError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = 'CertificateRetrievalError'; | ||
| } | ||
| } | ||
|
|
||
| export async function handler(event: MtlsTestEvent): Promise<MtlsTestResponse> { | ||
| let cert: string | undefined; | ||
| let key: string | undefined; | ||
|
|
||
| if (event.useCert) { | ||
| const s3 = new S3Client({}); | ||
|
|
||
| // Get client certificate from S3 | ||
| const certObj = await s3.send(new GetObjectCommand({ | ||
| Bucket: event.certBucket, | ||
| Key: 'client-cert.pem', | ||
| })); | ||
| const keyObj = await s3.send(new GetObjectCommand({ | ||
| Bucket: event.certBucket, | ||
| Key: 'client-key.pem', | ||
| })); | ||
|
|
||
| cert = await certObj.Body?.transformToString(); | ||
| key = await keyObj.Body?.transformToString(); | ||
|
|
||
| if (!cert || !key) { | ||
| throw new CertificateRetrievalError('Failed to retrieve client certificate or key from S3'); | ||
| } | ||
| } | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const options: https.RequestOptions = { | ||
| hostname: event.distributionDomainName, | ||
| port: 443, | ||
| path: '/index.html', | ||
| method: 'GET', | ||
| }; | ||
|
|
||
| // Add client certificate if useCert is true | ||
| if (event.useCert && cert && key) { | ||
| options.cert = cert; | ||
| options.key = key; | ||
| } | ||
|
|
||
| const req = https.request(options, (res) => { | ||
| resolve({ | ||
| statusCode: res.statusCode ?? 0, | ||
| statusMessage: res.statusMessage ?? '', | ||
| }); | ||
| }); | ||
|
|
||
| req.on('error', (err) => { | ||
| reject(err); | ||
| }); | ||
|
|
||
| req.end(); | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import * as path from 'path'; | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; | ||
| import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; | ||
| import * as lambda from 'aws-cdk-lib/aws-lambda'; | ||
| import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs'; | ||
| import * as s3 from 'aws-cdk-lib/aws-s3'; | ||
| import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment'; | ||
| import * as integ from '@aws-cdk/integ-tests-alpha'; | ||
| import { Construct } from 'constructs'; | ||
|
|
||
| /** | ||
| * Integration test for CloudFront Distribution with mTLS (Mutual TLS) authentication. | ||
| * | ||
| * This test: | ||
| * - Creates an S3 bucket and uploads a CA certificate bundle and client certificates | ||
| * - Creates a CloudFront TrustStore using the CA certificate | ||
| * - Creates a CloudFront Distribution with mTLS enabled in optional mode | ||
| * - Deploys test content to the origin bucket | ||
| * - Uses a Lambda function to make an mTLS request and verify the response | ||
| */ | ||
| class MtlsDistributionStack extends cdk.Stack { | ||
| public readonly distribution: cloudfront.Distribution; | ||
| public readonly certBucket: s3.Bucket; | ||
| public readonly mtlsTestFunction: lambda.IFunction; | ||
|
|
||
| constructor(scope: Construct, id: string, props?: cdk.StackProps) { | ||
| super(scope, id, props); | ||
|
|
||
| const assetsPath = path.join(__dirname, 'integ.distribution-mtls.assets'); | ||
|
|
||
| // S3 bucket for TrustStore CA certificate bundle and client certificates | ||
| this.certBucket = new s3.Bucket(this, 'CertBucket', { | ||
| removalPolicy: cdk.RemovalPolicy.DESTROY, | ||
| autoDeleteObjects: true, | ||
| }); | ||
|
|
||
| // Deploy CA certificate bundle and client certificates to S3 | ||
| const certDeployment = new s3deploy.BucketDeployment(this, 'DeployCerts', { | ||
| sources: [s3deploy.Source.asset(assetsPath, { | ||
| exclude: ['*.ts', '*.html'], | ||
| })], | ||
| destinationBucket: this.certBucket, | ||
| retainOnDelete: false, | ||
| }); | ||
|
|
||
| // Create TrustStore | ||
| const trustStore = new cloudfront.TrustStore(this, 'TrustStore', { | ||
| trustStoreName: 'integ-test-trust-store', | ||
| caCertificatesBundleS3Location: { | ||
| bucket: this.certBucket, | ||
| key: 'ca-bundle.pem', | ||
| }, | ||
| }); | ||
| // Ensure certificates are deployed before creating TrustStore | ||
| trustStore.node.addDependency(certDeployment); | ||
|
|
||
| // S3 bucket for CloudFront origin | ||
| const originBucket = new s3.Bucket(this, 'OriginBucket', { | ||
| removalPolicy: cdk.RemovalPolicy.DESTROY, | ||
| autoDeleteObjects: true, | ||
| }); | ||
|
|
||
| // Deploy test content to origin bucket | ||
| new s3deploy.BucketDeployment(this, 'DeployContent', { | ||
| sources: [s3deploy.Source.asset(assetsPath, { | ||
| exclude: ['*.ts', '*.pem'], | ||
| })], | ||
| destinationBucket: originBucket, | ||
| retainOnDelete: false, | ||
| }); | ||
|
|
||
| // CloudFront Distribution with mTLS enabled | ||
| // Note: mTLS requires HTTPS_ONLY - HTTP is not allowed with ViewerMutualAuthentication | ||
| this.distribution = new cloudfront.Distribution(this, 'Distribution', { | ||
| defaultBehavior: { | ||
| origin: origins.S3BucketOrigin.withOriginAccessControl(originBucket), | ||
| viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, | ||
| }, | ||
| viewerMtlsConfig: { | ||
| mode: cloudfront.MtlsMode.REQUIRED, | ||
| trustStore: trustStore, | ||
| }, | ||
| }); | ||
|
|
||
| // Lambda function to test mTLS request | ||
| this.mtlsTestFunction = new nodejs.NodejsFunction(this, 'MtlsTestFunction', { | ||
| entry: path.join(assetsPath, 'mtls-test-handler.ts'), | ||
| handler: 'handler', | ||
| runtime: lambda.Runtime.NODEJS_20_X, | ||
| timeout: cdk.Duration.seconds(30), | ||
| bundling: { | ||
| externalModules: ['@aws-sdk/client-s3'], | ||
| }, | ||
| }); | ||
|
|
||
| // Grant Lambda permission to read certificates from S3 | ||
| this.certBucket.grantRead(this.mtlsTestFunction); | ||
| } | ||
| } | ||
|
|
||
| const app = new cdk.App(); | ||
|
|
||
| const testCase = new MtlsDistributionStack(app, 'integ-distribution-mtls'); | ||
|
|
||
| const test = new integ.IntegTest(app, 'integ-test-distribution-mtls', { | ||
| testCases: [testCase], | ||
| }); | ||
|
|
||
| // Test 1: mTLS request WITH client certificate should return 200 | ||
| const mtlsWithCertAssertion = test.assertions.invokeFunction({ | ||
| functionName: testCase.mtlsTestFunction.functionName, | ||
| payload: JSON.stringify({ | ||
| distributionDomainName: testCase.distribution.distributionDomainName, | ||
| certBucket: testCase.certBucket.bucketName, | ||
| useCert: true, | ||
| }), | ||
| }); | ||
|
|
||
| mtlsWithCertAssertion.expect(integ.ExpectedResult.objectLike({ | ||
| Payload: integ.Match.stringLikeRegexp('.*"statusCode":200.*'), | ||
| })); | ||
|
|
||
| // Test 2: mTLS request WITHOUT client certificate should return 403 (mTLS REQUIRED mode) | ||
| const mtlsWithoutCertAssertion = test.assertions.invokeFunction({ | ||
| functionName: testCase.mtlsTestFunction.functionName, | ||
| payload: JSON.stringify({ | ||
| distributionDomainName: testCase.distribution.distributionDomainName, | ||
| certBucket: testCase.certBucket.bucketName, | ||
| useCert: false, | ||
| }), | ||
| }); | ||
|
|
||
| mtlsWithoutCertAssertion.expect(integ.ExpectedResult.objectLike({ | ||
| Payload: integ.Match.stringLikeRegexp('.*"statusCode":403.*'), | ||
| })); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -717,6 +717,69 @@ new cloudfront.Distribution(this, 'Dist', { | |
| }); | ||
| ``` | ||
|
|
||
| ### Trust Store | ||
|
|
||
| A Trust Store contains CA certificates that CloudFront uses to authenticate client certificates | ||
| during mutual TLS (mTLS) handshakes. The CA certificates bundle must be stored in an S3 bucket | ||
| in PEM format. | ||
|
|
||
| ```ts | ||
| declare const certsBucket: s3.Bucket; | ||
|
|
||
| const trustStore = new cloudfront.TrustStore(this, 'TrustStore', { | ||
| caCertificatesBundleS3Location: { | ||
| bucket: certsBucket, | ||
| key: 'ca-bundle.pem', | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| You can also import an existing Trust Store by its ID: | ||
|
|
||
| ```ts | ||
| const importedTrustStore = cloudfront.TrustStore.fromTrustStoreId( | ||
| this, | ||
| 'ImportedTrustStore', | ||
| 'ts_36QBpGQoOtQIsJUDB4IXYXogHoc', | ||
| ); | ||
| ``` | ||
|
|
||
| ### Mutual TLS (mTLS) Authentication | ||
|
|
||
| You can configure CloudFront to require mutual TLS (mTLS) authentication between viewers and CloudFront. | ||
| With mTLS, CloudFront authenticates viewers using client certificates. To enable mTLS, create a TrustStore | ||
| containing CA certificates and configure the `viewerMtlsConfig` property. | ||
|
|
||
| **Note:** mTLS requires HTTPS. Use `ViewerProtocolPolicy.HTTPS_ONLY` or `ViewerProtocolPolicy.REDIRECT_TO_HTTPS`. | ||
| **Note:** mTLS is not supported with HTTP/3. Use the `HttpVersion.HTTP2` or `HttpVersion.HTTP1_1`. | ||
|
|
||
| ```ts | ||
| declare const bucket: s3.Bucket; | ||
| declare const certsBucket: s3.Bucket; | ||
|
|
||
| const trustStore = new cloudfront.TrustStore(this, 'TrustStore', { | ||
| caCertificatesBundleS3Location: { | ||
| bucket: certsBucket, | ||
| key: 'ca-bundle.pem', | ||
| }, | ||
| }); | ||
|
|
||
| new cloudfront.Distribution(this, 'Dist', { | ||
| defaultBehavior: { | ||
| origin: origins.S3BucketOrigin.withOriginAccessControl(bucket), | ||
| viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, | ||
| }, | ||
| viewerMtlsConfig: { | ||
| mode: cloudfront.MtlsMode.REQUIRED, // or MtlsMode.OPTIONAL to allow requests without certificates | ||
| trustStore: trustStore, | ||
| advertiseTrustStoreCaNames: true, // Optional: advertise CA names during TLS handshake | ||
| ignoreCertificateExpiry: false, // Optional: accept expired certificates (use with caution) | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| See [Configuring mutual TLS authentication](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-mutual-tls.html) in the CloudFront User Guide. | ||
|
||
|
|
||
| ### Lambda@Edge | ||
|
|
||
| Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.