Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd850cd
feat(cloudfront): add support for mutual TLS (mTLS) configuration in …
badmintoncryer Nov 29, 2025
4171d17
add integ
badmintoncryer Nov 30, 2025
d5ece7b
update readme
badmintoncryer Nov 30, 2025
9393986
update unit test
badmintoncryer Nov 30, 2025
0b2c955
add validation
badmintoncryer Nov 30, 2025
7ab49a8
update integ
badmintoncryer Nov 30, 2025
4ade6c6
Merge branch 'main' into mtls
badmintoncryer Dec 1, 2025
08f1c7d
feat: add Trust Store for mTLS authentication in CloudFront
badmintoncryer Dec 5, 2025
fa6ab9d
fix(cloudfront): update TrustStore creation to use higher-level const…
badmintoncryer Dec 5, 2025
ef74c4a
fix(truststore): rename trustStoreName to name and add validation for…
badmintoncryer Dec 5, 2025
80e0ed4
Merge remote-tracking branch 'origin/main' into mtls
badmintoncryer Dec 5, 2025
a8a4248
fix(truststore): rename 'name' to 'trustStoreName' for consistency an…
badmintoncryer Dec 5, 2025
590a435
add snapshot
badmintoncryer Dec 5, 2025
d474f54
fix(truststore): update validation for trustStoreName length to allow…
badmintoncryer Dec 5, 2025
27454ef
Merge branch 'main' into mtls
badmintoncryer Dec 5, 2025
1032488
Update packages/aws-cdk-lib/aws-cloudfront/README.md
badmintoncryer Dec 5, 2025
f866228
fix(cloudfront): enforce HTTPS for mTLS configurations in Distribution
badmintoncryer Dec 5, 2025
5f7eab8
fix(truststore): add validation for S3 key to prevent empty strings
badmintoncryer Dec 5, 2025
7b11d15
fix(distribution): simplify mTLS test cases by removing expected values
badmintoncryer Dec 5, 2025
d9f166b
Update packages/aws-cdk-lib/aws-cloudfront/test/trust-store.test.ts
badmintoncryer Dec 5, 2025
4b500e4
feat(cloudfront): add TrustStore L2 construct and integration tests f…
badmintoncryer Dec 5, 2025
a2ac411
Merge remote-tracking branch 'cryer/mtls' into mtls
badmintoncryer Dec 5, 2025
a492868
Merge branch 'main' into mtls
badmintoncryer Dec 6, 2025
2658292
Update packages/aws-cdk-lib/aws-cloudfront/README.md
badmintoncryer Dec 7, 2025
e53cde6
fix(cloudfront): enforce viewerProtocolPolicy for mTLS configuration
badmintoncryer Dec 7, 2025
536c835
Merge branch 'main' into mtls
badmintoncryer Dec 7, 2025
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
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.*'),
}));
63 changes: 63 additions & 0 deletions packages/aws-cdk-lib/aws-cloudfront/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

@mazyu36 mazyu36 Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't access the link. It may have been changed.
Is this the link you were referring to?

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/mtls-authentication.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!


### Lambda@Edge

Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute
Expand Down
Loading
Loading