Thesis: A public Lambda Function URL with AuthType NONE needs two statements on the function resource policy: lambda:InvokeFunctionUrl and lambda:InvokeFunction, both for principal *. The first without the second still returns 403.
What we tried first
A small POST endpoint in front of a multi-tenant evidence vault does not need an API stage on day one. A Function URL is a hostname, an auth type, and a resource policy. We set auth type to NONE so a marketing form could post JSON without SigV4, and we added a single permission: action lambda:InvokeFunctionUrl, principal *, function_url_auth_type = NONE.
That matches the older examples. The URL existed. The function was deployed. A browser POST returned 403 Forbidden. CloudWatch showed no invocation. The request never entered the handler, so application logs were the wrong place to debug it.
Auth type NONE means the request does not need a signature. It does not skip the resource policy. If no statement allows the call, Lambda refuses it before the handler runs.
The working shape
lambda:InvokeFunctionUrl is not a substitute for lambda:InvokeFunction. Public callers need both, for the same principal. The first statement must set function_url_auth_type to NONE, matching the URL.
The working pair:
resource "aws_lambda_permission" "url" {
action = "lambda:InvokeFunctionUrl"
function_name = aws_lambda_function.intake.function_name
principal = "*"
function_url_auth_type = "NONE"
}
resource "aws_lambda_permission" "invoke" {
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.intake.function_name
principal = "*"
}
After both statements exist, an unauthenticated POST reaches the handler. Until then, the 403 is from Lambda’s front door. The console can show auth type None and a live URL while the policy still lacks the second action. Count actions. One statement is not “the URL is public.”
If the log group is empty, stop editing the handler. The policy rejected the call. A missing CORS header looks different: the browser reports a failed preflight, not an invoke that never started. Switching the URL to AWS_IAM does not fix a public form. The browser cannot sign that request.
Checklist
- Auth type NONE on the Function URL, if the caller is a browser with no AWS credentials.
- Resource policy: lambda:InvokeFunctionUrl with function_url_auth_type = NONE.
- Resource policy: lambda:InvokeFunction for the same principal.
- Confirm the 403 is gone in the browser before you debug handler logic. No invocation log means the policy still rejected the call.
Related: put CloudFront in front of a Function URL. How we deliver: methodology.
Engineering commentary only — not audit, legal, or certification advice.