fix(auth): authorize query and REST requests under --enforce-auth - #1383
Merged
Merged
Conversation
Every signed AWS request is now authorized against the caller's IAM policies, bound to the handler dispatch will run. Query services, CloudWatch and SageMaker are checked per operation, JSON-RPC through X-Amz-Target only for handlers that route on it, and REST services at service level until each gets per-operation checks.
…als neutral Matches peeks now hand the whole body back (wire.PeekBody), and the probe body is no longer reset after Match, so IAMChecks and dispatch read the same bytes. An AssumeRole deny names the RoleArn as sent.
…-rest-authz # Conflicts: # server/gcp/filestore/handler.go # server/gcp/managedkafka/handler.go # server/gcp/securesourcemanager/handler.go # server/gcp/spanner/handler.go
NitinKumar004
marked this pull request as ready for review
October 4, 2026 09:33
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.
Closes tracker row AUTHZ-X1 (critical). This is AUTHZ-X1b from the approved plan.
Under
--enforce-auth, only requests with anX-Amz-Targetheader were authorized. Query and REST requests were authenticated and then let through. So a user whose only policy wasdynamodb:*could runiam create-user,s3 mb,ec2 run-instancesand create Lambda functions, API Gateway APIs and EKS clusters. AssumeRole was never authorized at all.What changed
The gate now finds the handler that dispatch will run (one shared
probeRoute, also used by the #1375 public exemption) and builds its plan from that handler:IAMService()Root and users with no policies keep the bootstrap shortcut on every plan except JSON deny. Role sessions stay strict.
server/wire/awsauthz:Check,CheckMode,Scope,Resolver,ServiceNamer,DenyWriter,Evaluation,QueryChecks,ConditionContext.server.Server.Handlers()returns a copy, for the completeness test.X-Amz-Targetis read only for the handlers wrapped withrpc(...)at registration inaws.New. A forged header on any other handler cannot steer the authorized action.QueryChecks, which reads the samer.Form.Get("Action")their switch reads.autoScalingRoutestable. Dispatch andIAMChecksboth read it, so those actions areautoscaling:and everything else isec2:.cloudwatchOp(r)used by both ServeHTTP andIAMChecks, after the same gzip decode.classify(r)used by both.IAMService()on every other handler, using the plan's prefix table. The JSON-RPC handlers use their table prefix.AWSInsightsIndexService.→ce,ServiceQuotasV20190624.→servicequotas,ElasticMapReduce.→elasticmapreduce. Before this, all three were unusable under--enforce-auth, even for root.UnauthorizedOperation, andAccessDeniedfor autoscaling actions.<Error><Code>AccessDenied; Route 53 and CloudFront use<ErrorResponse><Error><Code>AccessDenied.AccessDeniedfor query, JSONAccessDeniedExceptionotherwise.EvaluateServiceWidenow rejects anything that is not an IAM service prefix ("",a:b,s3:*,*, …) as an implicit deny (the X1a review note). The gate builds a service-wide plan only for a name matching^[a-z0-9][a-z0-9-]*$."*". A resource-scoped Deny (for example on one queue) is no longer silently skipped (plan §2).coveragegenfollowsNewinto the same-file function it hands its Drivers to.Newnow delegates tonewServer, which also returns the authz sets for the tests.docs/coverageis unchanged.aws.goEnforceAuth comment, the--enforce-authflag help, and a new section incontrib/server/README.md. Until a REST service reaches tier 1, a fine-grained or resource-scoped policy on it (for examples3:GetObjecton one bucket) is denied, fail closed. AdministratorAccess and<svc>:*users keep working.Unchanged, each with a regression test: the
/_cloudemu/*admin endpoints (unsigned health, snapshot and reset under--enforce-auth, in contrib), bootstrap users, and the #1375 public operations.TestEnforcedGateBypassAttemptsandTestEnforcedGateAdmitsUnsignedPublicOpspass unchanged.TestAuthzSkipsPublicOpsnow also covers a signed execute-api invoke from a caller with a policy.Unknown-op safety, per handler
authz_unknown_op_test.gocalls each query handler's ServeHTTP with an unknown Action, then sends the same request through the gate as a bootstrap user. It asserts a 4xx and an unchanged provider snapshot. The snapshot is taken withpersist.Exportand leaves out only CloudTrail's management-event log, which records every call. The default branches it exercises:Each one writes the error and returns, and none of them touches a driver.
Where this differs from the plan text
X-Amz-Targeton S3, IAM and RDS. With DynamoDB registered, that request is served by DynamoDB itself, because it registers first and matches on the target. It is authorized asdynamodb:PutItem, which is what runs, and fails there with a 400. The S3, IAM and RDS handlers refuse any request that carries a target. So the test asserts a non-2xx response and unchanged S3, IAM and RDS state, not a 403. The real bypass is a REST handler that ignores the header. That case is tested with Lambda on a server without DynamoDB: it is a 403 now and created the function on the old code.Verification
authz_matrix_test.gofailed there, plus the two state checks: dynOnly created IAM users, buckets, Lambda functions, REST APIs and EKS clusters; AssumeRole passed without permission; CBOR PutMetricData with?Action=DescribeAlarmsstored a metric for a DescribeAlarms-only caller; ce, servicequotas and EMR returned 403 for an unrestricted caller. All pass now.TestAuthOffResponsesUnchangedreplays 35 requests over the edited handlers (CloudWatch cbor/json/query/gzip, every autoscaling route, SageMaker runtime and feature store, EC2 edge cases) with EnforceAuth off. Each response is compared with a golden file recorded on the old code, masking only generated ids and CBOR key order.TestEveryHandlerDeclaresAuthorizationandTestHandlerIAMServicesMatchTablewalk every handler of a full server: each must declare a valid IAM service, each table prefix must route to a handler with the same service, and only Kubernetes may be authn-only. A mutation check (removing oneIAMService) fails both.go build ./..., andgo veton server/..., providers/aws/iam/... and services/iam/....go test -race -timeout 600son server/... (including server/wire/...), providers/aws/iam/... and services/iam/..., plus the contrib/server enforce-auth tests.golangci-lint --new-from-rev=origin/development: 0 issues in the root module and in contrib/server.go run ./internal/coveragegen: no diff.contrib/server/enforce_authz_test.go, real SDKs against serve with--enforce-auth, bootstrapped through the unsigned admin snapshot API):limited(dynamodb:*) gets AccessDenied on CreateUser, ListUsers and CreateAccessKey, while ListTables passes. Afteriam:ListUsersis added, ListUsers passes and CreateUser is still denied.ec2:Describe*user can DescribeInstances, gets UnauthorizedOperation on RunInstances, and AccessDenied on CreateAutoScalingGroup.sqs:SendMessageuser can send, gets AccessDeniedException on CreateQueue, and a hand-signed query-form SendMessage is a 403ec2:SendMessagethat never reaches SQS (the queue still holds one message).cloudemu serve --enforce-authon :65166:limited, these are denied:iam create-user(AccessDenied),s3 mb(AccessDenied,s3:*),sts assume-role(AccessDenied on the role ARN),ec2 run-instances(UnauthorizedOperation) andautoscaling create-auto-scaling-group(AccessDenied). No user or bucket was created.limited,dynamodb list-tables,sts get-caller-identityandsts get-session-tokenwork.~> 5.0the repo fixtures pin).Size
Production code is +1459/−322, about the plan's estimate. 300 of those lines are the one-line
IAMService()methods, at 4 lines each with their comment. Tests are +1918, including a 177-line golden file. I did not split the PR. The plan has no seam inside X1b that is both safe and compatible: shipping the gate without tier 0 would lock out every REST user, and shipping without the gate leaves the bypass open. The only standalone pieces (awsauthz,Handlers(), the coveragegen change) come to about 300 lines and change no behaviour on their own.Found, not fixed here
contrib/terraform/cloudemu-tfhas nos3controlendpoint. AWS provider 6.67 reads bucket tags through S3 Control ListTagsForResource, so that call goes to real AWS and fails. This is unrelated to auth and is why the TF run used the pinned 5.x provider.--enforce-auththis is now unmapped, which fails closed.com.amazonaws.cloudtrail.v20131101.CloudTrail_20131101.*is not in the target table, so it stays a JSON deny under--enforce-auth, as it was before this change.Not in this PR: S3 op-level checks (X1c), trust evaluation with the real caller and the session-kind restrictions (X1d), and resource ARNs for IAM, SQS, SNS and DynamoDB (X1e).