Thesis: AWS WAF cannot associate a regional web ACL directly with a Lambda Function URL. Put Amazon CloudFront in front, use a CloudFront-scoped web ACL in us-east-1, and publish a branded hostname so your Content-Security-Policy never needs a wildcard for Function URL hosts.
What we tried first
Function URLs are attractive for a small POST/OPTIONS API: no API Gateway stage to babysit, IAM auth or public invoke, quick deploy. For a marketing-site form that posts JSON to the cloud, that shape is fine — until you ask for WAF.
AssociateWebACL against the Function URL ARN fails. Regional WAF expects Application Load Balancers, API Gateway REST/HTTP APIs, AppSync, Cognito user pools, and a few other resource types. Function URLs are not on that list.
The working shape
Browser
│ HTTPS → intake.example.com
▼
CloudFront (custom domain + ACM in us-east-1)
│ origin: Function URL (HTTPS-only)
│ WAF: scope = CLOUDFRONT (must live in us-east-1)
▼
Lambda Function URL
# AuthType NONE still needs both permissions on the resource policy:
# lambda:InvokeFunctionUrl
# lambda:InvokeFunction
Three details matter in practice:
- Scope. CloudFront distributions use CLOUDFRONT-scoped web ACLs. Those ACLs are created and managed in us-east-1 even if your Lambda runs elsewhere.
- Origin. Point CloudFront at the Function URL as a custom HTTPS origin. Prefer restricting who can invoke the Function URL so casual callers cannot bypass the distribution.
- Hostname. Attach ACM (also in us-east-1 for CloudFront) and an alias such as intake.example.com. Your marketing CSP connect-src then lists that host — not a wildcard for Function URL domains.
Why CSP cares
If the browser posts straight to a Function URL, teams often paper over the hostname with a CSP wildcard. That fingerprints the stack and widens the connect-src surface. A branded intake host is both cleaner for buyers reading your headers and stricter for what the page may call.
After cutover, verify with a simple header check on the marketing origin: connect-src should name the branded intake URL and any intentional analytics hosts — nothing broader.
Cost and ops notes
CloudFront plus WAF is not free. For a low-traffic form, treat it as a fixed control cost you turn on when the endpoint is public and worth defending — not as an afterthought once bots find the Function URL. Log WAF samples and CloudFront access logs to the same place you already watch for abuse.
Rate-based rules have AWS-side minimums; do not invent thresholds from memory — read the WAF documentation when you tune. Prefer starting with managed rule groups you understand, then add a conservative rate limit.
Two permissions for a public Function URL
With AuthType NONE, public callers still need both lambda:InvokeFunctionUrl and lambda:InvokeFunction on the resource policy. One without the other fails closed in surprising ways — CloudFront can look healthy while the origin rejects.
Checklist
- Do not AssociateWebACL on a Function URL ARN — plan for CloudFront first.
- Create the web ACL with CloudFront scope in us-east-1.
- Issue ACM in us-east-1 for the branded hostname.
- Pin marketing CSP connect-src to that hostname.
- Confirm the Function URL is not a useful bypass path for anonymous callers.
- For AuthType NONE, grant both InvokeFunctionUrl and InvokeFunction on the resource policy.
Next in this series: WAF did not protect the raw Function URL · keep SSH closed until break-glass. How we deliver: methodology.
Engineering commentary only — not audit, legal, or certification advice.