Motivation
The Lambda Function URL is public and unauthenticated (AuthType: NONE) — required today because SNS HTTP delivery cannot sign requests. The in-app boundary (SNS signature verification + topic allowlist) keeps the endpoint safe, but the URL itself:
- cannot have AWS WAF attached (no rate limiting / IP filtering),
- cannot use a custom domain,
- bills a Lambda invocation for every request, including junk POSTs from anyone who discovers the URL.
Fronting the Function URL with CloudFront closes those gaps: WAF attachment point, custom-domain support, and direct-access lockdown. No API Gateway — CloudFront talks to the Function URL directly.
Decision: OAC signing is not viable for this workload
CloudFront's Origin Access Control for Lambda origins (AuthType: AWS_IAM + SigV4-signed origin requests) was considered and rejected. Per the official docs:
If you use PUT or POST methods with your Lambda function URL, your users must compute the SHA256 of the body and include the payload hash value of the request body in the x-amz-content-sha256 header when sending the request to CloudFront. Lambda doesn't support unsigned payloads.
The docs' example computes the hash client-side; CloudFront does not compute it at the edge. Every SNS delivery (Notification, SubscriptionConfirmation, UnsubscribeConfirmation) is a POST, and SNS does not send that header — so every delivery would fail with InvalidSignatureException. CloudFront Functions cannot read the request body to inject the hash, and Lambda@Edge is out of scope by design. This is documented behavior, not a hypothesis — no spike needed. (Background: AWS blog on Lambda OAC.)
If Lambda ever supports unsigned payloads for OAC-signed requests, revisit — AWS_IAM + OAC is the stronger lockdown.
Design: shared-secret origin header
The Function URL stays AuthType: NONE; direct access is blocked by a secret only CloudFront knows:
- CloudFront distribution with the Function URL as origin: HTTPS only protocol, caching disabled (all traffic is POST), custom origin header
X-Origin-Verify: <secret> injected on every origin request.
- Secret stored in Secrets Manager / SSM and passed to the function via the existing
config.rs env-var path.
- The app rejects requests missing or mismatching the header with 403, in the router layer ahead of
VerifiedSns, so junk fails before any signature-verification work.
- Rotation: the app accepts two values (current + previous) so the CloudFront config and secret can roll without dropping deliveries.
- WAF attached to the distribution with a rate-based rule.
Tasks:
Non-goals
- API Gateway in the path (adds cost and config with no capability CloudFront lacks here).
- Lambda@Edge (explicitly excluded).
- Native SNS→Lambda subscription (separate discussion — removes the public endpoint entirely but sacrifices the loose cross-account topic wiring).
Motivation
The Lambda Function URL is public and unauthenticated (
AuthType: NONE) — required today because SNS HTTP delivery cannot sign requests. The in-app boundary (SNS signature verification + topic allowlist) keeps the endpoint safe, but the URL itself:Fronting the Function URL with CloudFront closes those gaps: WAF attachment point, custom-domain support, and direct-access lockdown. No API Gateway — CloudFront talks to the Function URL directly.
Decision: OAC signing is not viable for this workload
CloudFront's Origin Access Control for Lambda origins (
AuthType: AWS_IAM+ SigV4-signed origin requests) was considered and rejected. Per the official docs:The docs' example computes the hash client-side; CloudFront does not compute it at the edge. Every SNS delivery (Notification, SubscriptionConfirmation, UnsubscribeConfirmation) is a POST, and SNS does not send that header — so every delivery would fail with
InvalidSignatureException. CloudFront Functions cannot read the request body to inject the hash, and Lambda@Edge is out of scope by design. This is documented behavior, not a hypothesis — no spike needed. (Background: AWS blog on Lambda OAC.)If Lambda ever supports unsigned payloads for OAC-signed requests, revisit —
AWS_IAM+ OAC is the stronger lockdown.Design: shared-secret origin header
The Function URL stays
AuthType: NONE; direct access is blocked by a secret only CloudFront knows:X-Origin-Verify: <secret>injected on every origin request.config.rsenv-var path.VerifiedSns, so junk fails before any signature-verification work.Tasks:
template.yaml: CloudFront distribution + origin header, WAF WebACL, secret resource, new function env varconfig.rs: origin-verify secret(s) config fieldVerifiedSns)scripts/subscribe.shdocs to use the CloudFront domain; re-subscribe existing topicsscripts/e2e-ses-bounce.shpasses through the CloudFront endpoint; direct Function URL access without the header is rejectedNon-goals