Repository navigation
feat(aws-lambda): op-level IAM authorization under --enforce-auth (AUTHZ-X1f) - #1530
Merged
Merged
Conversation
NitinKumar004
marked this pull request as ready for review
October 10, 2026 14:45
This was referenced Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #1495 (P0 "Auth (--enforce-auth)"), tracker row AUTHZ-X1f.
Summary
Under
cloudemu serve --enforce-auth, Lambda was authorized at service level only: justlambda:*on*passed, so any fine-grained Lambda policy was denied and a Deny on one action blocked all of Lambda. Lambda is now authorized per operation, on the ARNs and condition keys AWS uses, together with the function's resource-based policy.classify(r)picks the operation.ServeHTTPandIAMChecksboth use it. It reads the Host header first (function URLs), then the path prefixes in dispatch order. It never readsX-Amz-Targetor the signing scope. This refactor is its own commit, and the existing Lambda suites are unchanged.name:prod, a qualified ARN, or?Qualifier=): GetFunction, GetFunctionConfiguration, Invoke, DeleteFunction, AddPermission, GetPolicy, RemovePermission, and the function URL, event invoke config and provisioned concurrency operations. A policy onfunction:apptherefore does not coverfunction:app:prod, as AWS documents.$LATESTis the unpublished function itself, so it is checked on the unqualified ARN everywhere.layer:nameorlayer:name:version, and event source mappings onevent-source-mapping:uuid.$LATESTare rejected with InvalidParameterValueException. The AddPermission API reference says "Lambda does not support adding policies to version $LATEST". Before this, they wrote into the unqualified function's policy.authz_matrix_lambda_qualifier_test.go) cover a user with Allowlambda:*and Denylambda:*onfunction:f. Withf:prod,f:1,f:$LATEST, a qualified ARN and?Qualifier=for prod, 1 and$LATEST, every function-level operation is denied and f is unchanged.fandf:$LATESTis denied. Invoke off:prodandf:1is allowed, because a policy on the unqualified ARN does not match a version or alias.$LATESTAddPermission escalation is closed.lambda:TagResource.lambda:GetLayerVersionon each layer version.lambda:GetLayerVersion.lambda:FunctionUrlAuthType,lambda:FunctionArn,lambda:Principal,lambda:Layer,lambda:SubnetIds,lambda:SecurityGroupIds,lambda:CodeSigningConfigArn,lambda:InvokedViaFunctionUrl,aws:RequestTag/*,aws:TagKeysandaws:ResourceTag/*.awsauthz.ContextResolver.lambda:keys and the tag keys, and the gate's global keys always win.ResourcePolicymode. Within one account the call is allowed when either the identity policy or the function policy for that version or alias allows it, and nothing explicitly denies it.lambda:InvokeFunctionUrlandlambda:InvokeFunction, each from either source.PublicRequester). Under--enforce-auththey run only when the function policy grants bothlambda:InvokeFunctionUrlandlambda:InvokeFunctionto"*", with thelambda:FunctionUrlAuthType/lambda:InvokedViaFunctionUrlconditions satisfied. Otherwise they get 403 Forbidden.aws_lambda_permissionread them back as drift. They are now stored, persisted and returned as AWS returns them:ArnLike AWS:SourceArn;StringEquals AWS:SourceAccount,aws:PrincipalOrgID,lambda:EventSourceTokenandlambda:FunctionUrlAuthType;Bool lambda:InvokedViaFunctionUrl. An invalid FunctionUrlAuthType is rejected.X-Amzn-Errortype: AccessDeniedExceptionand body{"Type":"User","Message":"User: ... is not authorized to perform: lambda:X on resource: ..."}. Function URL hosts get{"Message":"Forbidden. For troubleshooting Function URL authorization issues, see: https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html"}.Sources
lambda:InvokeFunctionUrlandlambda:InvokeFunctionpermissions." For NONE: "your function's resource-based policy is always in effect and must grant public access before your function URL can receive requests", and "If a function's resource-based policy doesn't grantlambda:invokeFunctionUrlandlambda:InvokeFunctionpermissions, users get a 403 Forbidden error code when they try to invoke your function URL. This occurs even if the function URL uses theNONEauth type."Live repro of the Gate 2 findings (cloudemu serve --enforce-auth, aws CLI)
All 21 checks pass. bob has Allow
lambda:*on*and Denylambda:*onfunction:f.update-function-configurationonf:prod,f:$LATESTandf:1;update-function-code f:prodandpublish-version f:prod;create-alias --function-name f:prod,delete-alias f:1andupdate-alias f:prod;list-versions-by-function f:prod;add-permission --qualifier '$LATEST';invoke f:$LATEST;tag-resourceon the alias ARN.add-permissionandremove-permissionwith--qualifier '$LATEST'give InvalidParameterValueException.invoke f, and f has no policy.create-function g:prodgives InvalidParameterValueException.invoke f:prodby bob still returns 200.Unrouted Lambda APIs (checked, no fail-open)
I verified what happens today, before deferring them. A Lambda-signed request to InvokeAsync (
/2014-11-13/...), InvokeWithResponseStream (/2021-11-15/...), GetAccountSettings (/2016-08-19/account-settings), code-signing-config CRUD (/2020-04-22/...) or the runtime management config (/2021-07-20/...) is not matched by any handler. The S3 catch-all declines other services' SigV4 scope (#1301), so the request gets a clean 501 and nothing runs.TestAuthzMatrixLambdaUnroutedOpspins this and checks no S3 bucket is created. The live e2e repeats it for GetAccountSettings. The missing routes are a feature gap, recorded below as LAMBDA-ROUTES.Tests
server/aws/lambda.classify_test.go: every route, error path and ordering edge case; classify never changes the URL.iam_actions_test.go: action and resource for every operation, the unknown error paths, condition keys, and a check that the body is kept for dispatch.resource_policy_test.go: principal, condition and action matching.permission_conditions_sdk_test.go: AddPermission and GetPolicy round trip through the SDK.providers/aws/lambda/policy_conditions_test.go.server/aws/authz_matrix_lambda_test.gocovers:server/wire/awsauthz/merge_context_test.gocovers the key merge.TestEnforceAuthLambdaruns the real Lambda SDK againstserve --enforce-auth.go build ./...andgo vetpass.go test -racepasses onserver/aws/...,server/wire/...,providers/aws/lambda/...andpersist/....go -C contrib/server test -run Enforcepasses.golangci-lint --new-from-rev=origin/developmentreports 0 issues.go generateproduces no docs diff.cloudemu serve --enforce-auth: 28 of 28 checks pass.--qualifier prod,app:prodand the alias ARN works for a user allowed onfunction:app:prod.add-permission --principal arn:aws:iam::000000000000:user/namedlets that user invoke.curl --aws-sigv4 "aws:amz:us-east-1:lambda":add-permission --principal '*' --function-url-auth-type NONEand--invoked-via-function-url.aws_lambda_function,aws_lambda_function_url(NONE) andaws_lambda_permission(function_url_auth_type, invoked_via_function_url, source_arn and source_account):lambda:AddPermissionfails the apply with AccessDeniedException naming that action.Blast radius
lambda:*users see no change.--enforce-auth; before, every unsigned call was rejected. They run only with the public grant, as in AWS.server/wire/awsauthzgainsContextResolverandMergeContext.server/aws/authzgate.goprefersContextResolverand merges its keys.server/aws/aws.gopassesWithEnforceAuth.services/serverless/driver.PermissionStatementgains fields. They persist through the existing snapshot struct.TestAuthOffResponsesUnchanged, plus the unchanged Lambda suites).Deferrals (new tracker rows)
principalGrantedlambda:ListTagsis explicitly allowed (API reference, GetFunction Tags)get; awsauthz CheckModelambda:VpcIdsis not setvpc{"Message":"Forbidden..."}(status 403 matches)writeAuthErroraws:SourceArnstatement condition is compared exactly, not with ArnLike wildcardsconditionsHoldiam:PassRoleon CreateFunction / UpdateFunctionConfiguration Role, and the S3 code fetch on the caller's behalfresolveCodeMatches, classify.go