fix: validate SubjectAccessReview user identity and attributes#257
fix: validate SubjectAccessReview user identity and attributes#257
Conversation
Reject malformed SubjectAccessReview requests that lack both a user identity and group membership, or that have neither ResourceAttributes nor NonResourceAttributes set. These fields are always populated by a legitimate Kubernetes API server, so their absence indicates a misconfigured or rogue caller. Returns NoOpinion (Allowed=false, Denied=false) via HTTP 200 so other authorizers in the chain can still make a decision, and records the rejection in error metrics and structured logs for observability.
There was a problem hiding this comment.
Pull request overview
This PR hardens the webhook authorizer by validating incoming SubjectAccessReview (SAR) requests early in ServeHTTP, rejecting malformed requests before they reach evaluation logic.
Changes:
- Add
validateSAR()to reject SARs with empty identity (no user and no groups) or missing request attributes. - Add
writeNoOpinionResponse()to return a protocol-compatible “no opinion” SAR response for validation rejections. - Add unit tests covering the validation accept/reject paths.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| internal/webhook/authorization/webhook_authorizer.go | Adds SAR validation + “no opinion” response helper and integrates them into ServeHTTP. |
| internal/webhook/authorization/webhook_authorizer_test.go | Adds unit tests for malformed SAR rejection and valid edge cases (groups-only, non-resource attributes). |
| if err := json.NewDecoder(r.Body).Decode(&sar); err != nil { | ||
| wa.Log.Error(err, "failed to decode SubjectAccessReview request", | ||
| "latency", time.Since(start).String()) | ||
| if span := trace.SpanFromContext(ctx); span.IsRecording() { | ||
| span.RecordError(err) | ||
| span.SetStatus(codes.Error, "invalid request body") | ||
| } | ||
| wa.recordErrorMetrics(start) | ||
| http.Error(w, "invalid request body", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| if reason := validateSAR(&sar); reason != "" { | ||
| wa.Log.V(1).Info("rejecting malformed SubjectAccessReview", | ||
| "reason", reason, | ||
| "user", sar.Spec.User, | ||
| "latency", time.Since(start).String()) | ||
| wa.recordErrorMetrics(start) | ||
| wa.writeNoOpinionResponse(w, reason) | ||
| return | ||
| } |
There was a problem hiding this comment.
The PR description states the webhook should always return HTTP 200 (non-200 treated as infrastructure failure), but ServeHTTP still returns an HTTP 400 via http.Error on JSON decode errors. Consider aligning the decode-error path with the same HTTP 200 “no opinion” response (or adjust the PR description if 400 is intentional).
| "reason", reason, | ||
| "user", sar.Spec.User, | ||
| "latency", time.Since(start).String()) | ||
| wa.recordErrorMetrics(start) |
There was a problem hiding this comment.
The malformed-SAR rejection path returns a valid “no opinion” SAR (Allowed=false/Denied=false) but records metrics using recordErrorMetrics (decision=error). This conflicts with the semantic outcome and also contradicts recordErrorMetrics’ comment (it mentions decode/list failures, not validation rejections). Consider recording this as decision=no-opinion (and, if needed, add a dedicated counter for validation rejects) to avoid inflating error-rate dashboards/alerts.
| wa.recordErrorMetrics(start) |
| if strings.Contains(resp.Status.Reason, "empty user") { | ||
| t.Errorf("SAR with groups should not be rejected, got reason: %s", resp.Status.Reason) |
There was a problem hiding this comment.
TestServeHTTP_AcceptsEmptyUserWithGroups asserts the SAR wasn’t rejected by checking that the response reason does not contain the substring "empty user". This is brittle and could fail if future (unrelated) reasons include that phrase. Prefer asserting the reason is not equal to reasonEmptyIdentity/reasonMissingAttrs (or, better, assert the response status fields match the expected non-validation outcome).
| if strings.Contains(resp.Status.Reason, "empty user") { | |
| t.Errorf("SAR with groups should not be rejected, got reason: %s", resp.Status.Reason) | |
| if resp.Status.Reason == reasonEmptyIdentity || resp.Status.Reason == reasonMissingAttrs { | |
| t.Errorf("SAR with groups should not be rejected by validation, got reason: %s", resp.Status.Reason) |
📊 Output Delta ReportGenerated RBAC resources from ✅ No resource changes detected vs Prometheus Metrics (PR branch)📈 auth_operator_* metrics
|
Summary
Adds input validation to the webhook authorizer's
ServeHTTPhandler to reject malformed SubjectAccessReview requests before they reach the evaluation logic.Finding: AO-SEC-001 — "Webhook authorizer trusts caller-supplied user info without validation"
Changes
validateSAR()function that rejects SARs with:writeNoOpinionResponse()helper that returns HTTP 200 withAllowed=false, Denied=falseper K8s webhook protocolreasonEmptyIdentityandreasonMissingAttrsDesign Decisions
+optional, so group-only SARs are theoretically validServeHTTPcyclomatic complexity under gocyclo threshold (≤20)Verification
make lint— 0 issuesmake test— all tests passFiles Changed
internal/webhook/authorization/webhook_authorizer.gointernal/webhook/authorization/webhook_authorizer_test.go