Skip to main content
emnode / learn
Compliance Medium severity

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?

  1. Confirm the gap and inventory which distributions have function-URL origins without OAC.
  2. Create an OAC with SigV4 signing and attach it to the function-URL origin, then wait for the distribution to reach `Deployed`.
  3. 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.
  4. 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.

Part of the learning path Lock down access
  • 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