@clementvial

Developer from Canada ๐Ÿ‡จ๐Ÿ‡ฆ
Product, infrastructure, AI, and web3.
Mostly on AWS and Cloudflare.

All notes

Moving CloudFront from OAI to OAC in CDK

Origin Access Identity is the legacy way to let CloudFront read a private S3 bucket. Origin Access Control replaced it a year ago: it signs with SigV4, works in every region, and can read objects encrypted with SSE-KMS, which OAI never could.

CDK has no L2 for it. S3Origin still creates an OAI for you, so the migration is an L1 construct plus two overrides.

cdn-stack.ts
const oac = new cloudfront.CfnOriginAccessControl(this, 'OAC', {
originAccessControlConfig: {
name: 'site-oac',
originAccessControlOriginType: 's3',
signingBehavior: 'always',
signingProtocol: 'sigv4',
},
});
const distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: { origin: new origins.S3Origin(bucket) },
});
const cfnDistribution = distribution.node.defaultChild as cloudfront.CfnDistribution;
const origin = 'DistributionConfig.Origins.0';
cfnDistribution.addPropertyOverride(`${origin}.OriginAccessControlId`, oac.attrId);
cfnDistribution.addPropertyOverride(`${origin}.S3OriginConfig.OriginAccessIdentity`, '');

That empty string is the part people miss. If you leave the generated OAI in place, CloudFront keeps using it and your OAC does nothing.

Then grant the service principal instead of the identity, scoped to this distribution so nobody elseโ€™s can read your bucket:

cdn-stack.ts
bucket.addToResourcePolicy(new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [bucket.arnForObjects('*')],
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
conditions: {
StringEquals: {
'AWS:SourceArn': `arn:aws:cloudfront::${this.account}:distribution/${distribution.distributionId}`,
},
},
}));

Then the 404 page stopped working

S3 only returns NoSuchKey to a principal allowed to call s3:ListBucket. OAC has s3:GetObject and nothing else, so a missing file comes back as 403 AccessDenied.

CloudFront passes that straight through. If your error responses only map 404, they will never fire.

cdn-stack.ts
errorResponses: [
{ httpStatus: 403, responseHttpStatus: 404, responsePagePath: '/404.html' },
]

Granting s3:ListBucket would fix the status code honestly, but it also lets anyone with the origin URL enumerate your bucket. Mapping the 403 is the better trade.

Worth doing the migration even on a distribution that works. OAI is in maintenance mode, and the day you turn on KMS encryption is a bad day to discover it never supported it.