AWS Security Hub · CloudFront
CloudFront.16: OAC for Lambda function URL origins
Written and reviewed by Emnode · Last reviewed
What does AWS Security Hub CloudFront.16 check?
CloudFront.16 checks that a distribution using a Lambda function URL as an origin has origin access control (OAC) enabled. It reports FAILED whenever a function-URL origin exists without OAC, leaving the publicly-reachable `lambda-url.<region>.on.aws` endpoint callable directly, around the CDN.
Why does CloudFront.16 matter?
A function URL stays public so CloudFront's edge can reach it, so without OAC the endpoint is duplicated, not hidden. Anyone with the raw URL can invoke the function directly — no WAF, no edge rate limiting, no caching, no geo-restriction. Because Lambda bills per invocation and GB-second, an unfenced URL is a denial-of-wallet surface as much as an attack surface, and the uncached calls multiply downstream cost too.
How do I fix CloudFront.16?
- Confirm the gap and inventory which distributions have function-URL origins without OAC.
- Create an OAC with SigV4 signing and attach it to the function-URL origin, then wait for the distribution to reach `Deployed`.
- Add a resource-based policy granting `lambda:InvokeFunctionUrl` to the `cloudfront.amazonaws.com` principal scoped by `aws:SourceArn`, and set the function's `AuthType` to `AWS_IAM` — in that order, after OAC has deployed.
- Verify the CDN path works and a direct curl returns 403 before closing the finding.
Remediation script · bash
# 1. Lock an S3 origin: only THIS distribution may read the bucket.
cat > bucket-policy.json <<'JSON'
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-downloads-bucket/*",
"Condition": { "StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E2QWRUHAPOMQZL"
} }
}]
}
JSON
aws s3api put-bucket-policy --bucket my-downloads-bucket --policy file://bucket-policy.json
# 2. Raise the minimum TLS version (the field is nested in ViewerCertificate, so send the whole config back).
aws cloudfront get-distribution-config --id E2QWRUHAPOMQZL > dist.json
ETAG=$(python3 -c "import json;print(json.load(open('dist.json'))['ETag'])")
# ... edit dist.json: ViewerCertificate.MinimumProtocolVersion = TLSv1.2_2021, ViewerProtocolPolicy = redirect-to-https ...
aws cloudfront update-distribution --id E2QWRUHAPOMQZL \
--distribution-config file://distribution-config.json --if-match "$ETAG"
# 3. Confirm the second door is shut: a raw S3 GET should now return 403.
curl -s -o /dev/null -w '%{http_code}\n' https://my-downloads-bucket.s3.amazonaws.com/file.pdf Full walkthrough (console steps, edge cases and verification) in the lesson Protect CloudFront distributions and origins.
Is CloudFront.16 a false positive?
Order is the trap, not a true false positive: tightening the function's policy or AuthType before OAC has deployed and started signing breaks live traffic, because CloudFront is still sending unsigned requests.
More CloudFront controls
- CloudFront.1 No default root object, exposing the distribution listing
- CloudFront.3 Distributions should require encryption in transit
- CloudFront.5 Distributions should have logging enabled
- CloudFront.6 Distributions should have WAF enabled
- CloudFront.9 Distributions should encrypt traffic to custom origins
- CloudFront.10 No deprecated SSL protocols to custom origins
- CloudFront.12 A distribution points at a non-existent S3 origin (takeover risk)
- CloudFront.13 Distributions should use origin access control
- CloudFront.15 Distributions should use recommended TLS policy
- CloudFront.17 Use trusted key groups for signed URLs/cookies