From 313a7eda4d75f2d25284729bc52891f2c3c6b4e4 Mon Sep 17 00:00:00 2001 From: "maksim.nabokikh" Date: Wed, 8 Apr 2026 12:43:34 +0200 Subject: [PATCH] KEP-6001: Structured authorization decision --- .../README.md | 928 ++++++++++++++++++ .../kep.yaml | 22 + 2 files changed, 950 insertions(+) create mode 100644 keps/sig-auth/6001-structured-authorization-decision/README.md create mode 100644 keps/sig-auth/6001-structured-authorization-decision/kep.yaml diff --git a/keps/sig-auth/6001-structured-authorization-decision/README.md b/keps/sig-auth/6001-structured-authorization-decision/README.md new file mode 100644 index 000000000000..499fc1d8aab6 --- /dev/null +++ b/keps/sig-auth/6001-structured-authorization-decision/README.md @@ -0,0 +1,928 @@ +# KEP-6001: Structured Authorization Decision + + +- [Release Signoff Checklist](#release-signoff-checklist) +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) +- [Proposal](#proposal) + - [User Stories](#user-stories) + - [Story 1: Security audit automation](#story-1-security-audit-automation) + - [Story 2: Identifying overly broad roles](#story-2-identifying-overly-broad-roles) + - [Story 3: Compliance reporting](#story-3-compliance-reporting) + - [Notes/Constraints/Caveats](#notesconstraintscaveats) + - [Risks and Mitigations](#risks-and-mitigations) +- [Design Details](#design-details) + - [Audit Event Changes](#audit-event-changes) + - [AuthorizationDecision Type](#authorizationdecision-type) + - [Authorizer Types](#authorizer-types) + - [RBAC Decision](#rbac-decision) + - [Webhook Decision](#webhook-decision) + - [Node Decision](#node-decision) + - [ABAC, AlwaysAllow, AlwaysDeny Decisions](#abac-alwaysallow-alwaysdeny-decisions) + - [SubjectAccessReview Status Extension](#subjectaccessreview-status-extension) + - [Implementation in the Authorization Filter](#implementation-in-the-authorization-filter) + - [Test Plan](#test-plan) + - [Prerequisite testing updates](#prerequisite-testing-updates) + - [Unit tests](#unit-tests) + - [Integration tests](#integration-tests) + - [e2e tests](#e2e-tests) + - [Graduation Criteria](#graduation-criteria) + - [Alpha](#alpha) + - [Beta](#beta) + - [GA](#ga) + - [Upgrade / Downgrade Strategy](#upgrade--downgrade-strategy) + - [Version Skew Strategy](#version-skew-strategy) +- [Production Readiness Review Questionnaire](#production-readiness-review-questionnaire) + - [Feature Enablement and Rollback](#feature-enablement-and-rollback) + - [Rollout, Upgrade and Rollback Planning](#rollout-upgrade-and-rollback-planning) + - [Monitoring Requirements](#monitoring-requirements) + - [Dependencies](#dependencies) + - [Scalability](#scalability) + - [Troubleshooting](#troubleshooting) +- [Implementation History](#implementation-history) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) + + +## Release Signoff Checklist + +Items marked with (R) are required *prior to targeting to a milestone / release*. + +- [ ] (R) Enhancement issue in release milestone, which links to KEP dir in [kubernetes/enhancements] (not the initial KEP PR) +- [ ] (R) KEP approvers have approved the KEP status as `implementable` +- [ ] (R) Design details are appropriately documented +- [ ] (R) Test plan is in place, giving consideration to SIG Architecture and SIG Testing input (including test refactors) + - [ ] e2e Tests for all Beta API Operations (endpoints) + - [ ] (R) Ensure GA e2e tests meet requirements for [Conformance Tests](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/conformance-tests.md) + - [ ] (R) Minimum Two Week Window for GA e2e tests to prove flake free +- [ ] (R) Graduation criteria is in place + - [ ] (R) [all GA Endpoints](https://github.com/kubernetes/community/pull/1806) must be hit by [Conformance Tests](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/conformance-tests.md) +- [ ] (R) Production readiness review completed +- [ ] (R) Production readiness review approved +- [ ] "Implementation History" section is up-to-date for milestone +- [ ] User-facing documentation has been created in [kubernetes/website], for publication to [kubernetes.io] +- [ ] Supporting documentation—e.g., additional design documents, links to mailing list discussions/SIG meetings, relevant PRs/issues, release notes + +[kubernetes.io]: https://kubernetes.io/ +[kubernetes/enhancements]: https://git.k8s.io/enhancements +[kubernetes/kubernetes]: https://git.k8s.io/kubernetes +[kubernetes/website]: https://git.k8s.io/website + +## Summary + +Today, when the kube-apiserver makes an authorization decision, it records the +outcome as audit annotations: `authorization.k8s.io/decision` ("allow" or +"forbid") and `authorization.k8s.io/reason` (a free-form human-readable +string). For example, the RBAC authorizer produces: + +``` +RBAC: allowed by ClusterRoleBinding "system:basic-user" of ClusterRole +"system:basic-user" to Group "system:authenticated" +``` + +This string is generated by `fmt.Sprintf` inside the authorizer and has no +stability guarantees. It is not suitable for programmatic consumption. + +This KEP proposes adding a new structured field `authorizationDecision` to the +audit `Event` type. The field contains a machine-readable representation of the +authorization decision, including the authorizer type, and authorizer-specific +details such as the matched RBAC role, binding, and subject. + +For webhook authorizers, this KEP also extends the `SubjectAccessReviewStatus` +with an optional field so that webhooks can return structured decision metadata +back to the kube-apiserver. + +The existing annotations remain unchanged for backward compatibility. + +## Motivation + +Cluster operators and security teams need to analyze authorization decisions at +scale. Common tasks include: + +- finding all requests allowed by a specific ClusterRole +- detecting unused or overly broad role bindings +- identifying which subject (user, group, service account) matched a binding +- generating compliance reports mapping actions to the exact policy that + permitted them +- alerting on unexpected authorizer decisions + +All of these require parsing the free-form `authorization.k8s.io/reason` string. +This string has no stability guarantees, varies between authorizer +implementations, and changes to the message format can silently break tooling. + +### Goals + +- add a new `authorizationDecision` field to the audit `Event` struct containing + a structured, machine-readable authorization decision +- cover all six authorizer types: `RBAC`, `Webhook`, `Node`, `ABAC`, + `AlwaysAllow`, `AlwaysDeny` +- for RBAC: include the role reference, binding reference, and matched subject +- for Webhook: include the authorizer name from the authorization configuration + and allow the webhook to return its own structured decision metadata via + a new field in `SubjectAccessReviewStatus` +- for Node, ABAC, AlwaysAllow, AlwaysDeny: include the authorizer type +- gate the feature behind a feature flag +- keep the existing `authorization.k8s.io/reason` and + `authorization.k8s.io/decision` annotations unchanged + +### Non-Goals + +- changing or deprecating the existing audit annotations +- exposing structured decisions in `SubjectAccessReview` responses returned to + SAR clients (this may be a future extension) + +## Proposal + +A new optional field `authorizationDecision` is added to the audit `Event` +struct. The field is populated by the authorization filter after a decision is +made. It contains the decision outcome, the authorizer type, and +authorizer-specific details. + +For webhook authorizers, a new optional field `authorizationDetails` is added to +`SubjectAccessReviewStatus` so that webhooks can return structured decision +metadata alongside the existing `reason` string. + +### User Stories + +#### Story 1: Security audit automation + +As a security engineer, I want to query audit logs to find all requests allowed +by a specific ClusterRole, so that I can assess the blast radius when that role +is modified or compromised. + +Today, I parse free-text reason strings with fragile regular expressions. With +this KEP, I filter on +`authorizationDecision.rbac.role.name == "cluster-admin"` directly. + +#### Story 2: Identifying overly broad roles + +As a cluster administrator, I want to aggregate authorization decisions by role +binding, so that I can identify bindings that are never used. + +With structured decision data, I build a dashboard showing usage counts per +binding, enabling confident cleanup of unused bindings. + +#### Story 3: Compliance reporting + +As a compliance officer, I want to generate reports showing exactly which policy +granted each privileged action, so that I can satisfy audit requirements from +standards like SOC 2 or PCI DSS. + +Structured data makes this straightforward without relying on unstable message +formats. + +### Notes/Constraints/Caveats + +- when multiple authorizers are configured in a chain (via structured + authorization configuration, KEP-3221), only the authorizer that made the + final "allow" or "deny" decision is recorded +- the `authorizationDecision` field is populated for both "allow" and explicit + "deny" decisions. For implicit denials (all authorizers returned "no opinion"), + the field records `decision: "deny"` with `authorizer: ""` since there is no + single authorizer to attribute +- the field is only populated when the feature gate is enabled +- the existing annotations continue to be set regardless of the feature gate + +### Risks and Mitigations + +- **risk**: adding a new field to the audit Event type is an API change + - **mitigation**: the field is optional and omitted when empty. Existing audit + consumers that do not read this field are not affected. The audit API has a + history of additive field additions. + +- **risk**: the structured field increases the size of audit events + - **mitigation**: the feature is gated behind a feature flag. The structured + data is compact - typically under 300 bytes per event. + +- **risk**: exposing authorizer internals could leak security-sensitive + information about cluster configuration + - **mitigation**: audit logs already contain the same information in the + free-form reason string. The structured field does not expose anything new. + Access to audit logs is already restricted to administrators. + +- **risk**: webhook authorizers may not provide structured data + - **mitigation**: for webhooks, the kube-apiserver always records the + authorizer name from the authorization configuration. The webhook-provided + extra metadata is optional. + +## Design Details + +### Audit Event Changes + +A new field is added to the audit `Event` struct in both the internal and +external (`audit.k8s.io/v1`) versions: + +```go +type Event struct { + // ... existing fields ... + + // AuthorizationDecision contains structured information about the + // authorization decision for this request. Populated when the + // StructuredAuthorizationDecision feature gate is enabled. + // +optional + AuthorizationDecision *AuthorizationDecision `json:"authorizationDecision,omitempty"` +} +``` + +### AuthorizationDecision Type + +```go +// AuthorizationDecision contains structured information about the +// authorization decision for a request. +type AuthorizationDecision struct { + // Decision is the authorization outcome: "allow" or "deny". + Decision string `json:"decision"` + + // Authorizer is the type of authorizer that made the decision. + // Known values: "RBAC", "Webhook", "Node", "ABAC", "AlwaysAllow", + // "AlwaysDeny". + // Empty when all authorizers returned no opinion (implicit deny). + // +optional + Authorizer string `json:"authorizer,omitempty"` + + // RBAC contains details specific to an RBAC authorization decision. + // Present only when Authorizer is "RBAC". + // +optional + RBAC *RBACDecision `json:"rbac,omitempty"` + + // Webhook contains details specific to a Webhook authorization decision. + // Present only when Authorizer is "Webhook". + // +optional + Webhook *WebhookDecision `json:"webhook,omitempty"` +} +``` + +### Authorizer Types + +The authorizer type values match the authorization mode constants defined in +`k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes`: + +| Authorizer type | Value | Additional details | +|-----------------|----------------|--------------------| +| RBAC | `"RBAC"` | role, binding, matched subject | +| Webhook | `"Webhook"` | authorizer name, authorization details from webhook response | +| Node | `"Node"` | none | +| ABAC | `"ABAC"` | none | +| AlwaysAllow | `"AlwaysAllow"`| none | +| AlwaysDeny | `"AlwaysDeny"` | none | + +### RBAC Decision + +The RBAC authorizer already has all the information needed. The +`VisitRulesFor` method in `pkg/registry/rbac/validation/rule.go` passes +a `clusterRoleBindingDescriber` or `roleBindingDescriber` to the visitor, which +contains the binding, the role reference, and the matched subject. Today this +is formatted into a string by `fmt.Sprintf`. With this KEP, the same data is +captured in a typed struct. + +```go +// RBACDecision contains the role, binding, and matched subject for an RBAC +// authorization decision. +type RBACDecision struct { + // Role identifies the role that granted access. + Role RBACRoleRef `json:"role"` + + // Binding identifies the role binding that matched. + Binding RBACBindingRef `json:"binding"` +} + +// RBACRoleRef identifies a Role or ClusterRole. +type RBACRoleRef struct { + // Kind is "Role" or "ClusterRole". + Kind string `json:"kind"` + // Name is the name of the role. + Name string `json:"name"` + // Namespace is the namespace of the Role. Empty for ClusterRole. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// RBACBindingRef identifies a RoleBinding or ClusterRoleBinding and +// the subject that matched. +type RBACBindingRef struct { + // Kind is "RoleBinding" or "ClusterRoleBinding". + Kind string `json:"kind"` + // Name is the name of the binding. + Name string `json:"name"` + // Namespace is the namespace of the RoleBinding. + // Empty for ClusterRoleBinding. + // +optional + Namespace string `json:"namespace,omitempty"` + // MatchedSubject is the subject entry from the binding that matched + // the requesting user. + MatchedSubject RBACSubjectRef `json:"matchedSubject"` +} + +// RBACSubjectRef identifies a subject in a binding. +type RBACSubjectRef struct { + // Kind is "User", "Group", or "ServiceAccount". + Kind string `json:"kind"` + // Name is the name of the subject. + Name string `json:"name"` + // Namespace is the namespace of the ServiceAccount. + // Empty for User and Group. + // +optional + Namespace string `json:"namespace,omitempty"` +} +``` + +Example for a cluster-scoped RBAC decision: + +```json +{ + "authorizationDecision": { + "decision": "allow", + "authorizer": "RBAC", + "rbac": { + "role": { + "kind": "ClusterRole", + "name": "developer" + }, + "binding": { + "kind": "ClusterRoleBinding", + "name": "developers-binding", + "matchedSubject": { + "kind": "Group", + "name": "developers" + } + } + } + } +} +``` + +Example for a namespace-scoped RBAC decision: + +```json +{ + "authorizationDecision": { + "decision": "allow", + "authorizer": "RBAC", + "rbac": { + "role": { + "kind": "ClusterRole", + "name": "admin" + }, + "binding": { + "kind": "RoleBinding", + "name": "admin-binding", + "namespace": "production", + "matchedSubject": { + "kind": "ServiceAccount", + "name": "deploy-bot", + "namespace": "production" + } + } + } + } +} +``` + +### Webhook Decision + +For webhook authorizers, the kube-apiserver knows the authorizer name from the +authorization configuration (the `name` field in `AuthorizerConfiguration`). +This name is already passed to `InstrumentedAuthorizer` for metrics. + +Additionally, the webhook can optionally return structured decision metadata in +the `SubjectAccessReview` response via a new `authorizationDetails` field on +`SubjectAccessReviewStatus` (see +[SubjectAccessReview Status Extension](#subjectaccessreview-status-extension)). +The kube-apiserver reads this field from the webhook response and records it in +the audit event. This is the only mechanism needed for webhooks - no context +passing is required since the data arrives in the SAR response. + +```go +// WebhookDecision contains the authorizer name and optional decision metadata +// returned by the webhook. +type WebhookDecision struct { + // Name is the name of the webhook authorizer as defined in the + // authorization configuration (--authorization-config). + Name string `json:"name"` + + // Details contains key-value metadata about the decision provided by + // the webhook in the SubjectAccessReview response + // (SubjectAccessReviewStatus.AuthorizationDetails). + // +optional + Details map[string]ExtraValue `json:"details,omitempty"` +} +``` + +Example for a webhook that returns structured details: + +```json +{ + "authorizationDecision": { + "decision": "allow", + "authorizer": "Webhook", + "webhook": { + "name": "corporate-authz", + "details": { + "policy.example.com/name": ["allow-developers-readonly"], + "policy.example.com/engine": ["opa"], + "policy.example.com/version": ["v2.3"] + } + } + } +} +``` + +Example for a webhook without structured details: + +```json +{ + "authorizationDecision": { + "decision": "deny", + "authorizer": "Webhook", + "webhook": { + "name": "corporate-authz" + } + } +} +``` + +### Node Decision + +The Node authorizer does not provide additional details. The `authorizer` field +set to `"Node"` is sufficient to identify that the request was authorized by the +Node authorizer for kubelet API access. + +```json +{ + "authorizationDecision": { + "decision": "allow", + "authorizer": "Node" + } +} +``` + +### ABAC, AlwaysAllow, AlwaysDeny Decisions + +These authorizers do not provide additional details beyond the decision and +authorizer type. + +```json +{ + "authorizationDecision": { + "decision": "allow", + "authorizer": "AlwaysAllow" + } +} +``` + +```json +{ + "authorizationDecision": { + "decision": "deny", + "authorizer": "AlwaysDeny" + } +} +``` + +```json +{ + "authorizationDecision": { + "decision": "allow", + "authorizer": "ABAC" + } +} +``` + +### SubjectAccessReview Status Extension + +A new optional `authorizationDetails` field is added to +`SubjectAccessReviewStatus` in the `authorization.k8s.io/v1` API. This field +allows webhook authorizers to return machine-readable metadata about their +authorization decision alongside the existing human-readable `reason` string. + +The field uses `map[string]ExtraValue` (where `ExtraValue` is `[]string`), +following the same convention as `SubjectAccessReviewSpec.Extra` for user +attributes. Keys should use domain-prefixed naming to avoid collisions between +different webhook implementations. + +```go +type SubjectAccessReviewStatus struct { + // Allowed is required. True if the action would be allowed, false + // otherwise. + Allowed bool `json:"allowed"` + // Denied is optional. True if the action would be denied, otherwise + // false. + // +optional + Denied bool `json:"denied,omitempty"` + // Reason is optional. It indicates why a request was allowed or denied. + // +optional + Reason string `json:"reason,omitempty"` + // EvaluationError is an indication that some error occurred during the + // authorization check. + // +optional + EvaluationError string `json:"evaluationError,omitempty"` + + // AuthorizationDetails contains optional machine-readable metadata about + // the authorization decision provided by the authorizer. Webhook + // authorizers can use this to report which policy, rule, or engine + // produced the decision. + // + // Keys should use domain-prefixed naming to avoid collisions between + // different webhook implementations (e.g., "policy.example.com/name"). + // Values are string slices following the ExtraValue convention. + // + // The total serialized size of this field must not exceed 1KB. Entries + // that exceed this limit are dropped by the kube-apiserver. + // + // This field is only read by the kube-apiserver when the + // StructuredAuthorizationDecision feature gate is enabled. When the + // feature gate is disabled, the field is ignored. + // +optional + AuthorizationDetails map[string]ExtraValue `json:"authorizationDetails,omitempty"` +} +``` + +This allows webhook authorizers to report structured decision metadata. For +example, an OPA-based webhook could respond: + +```json +{ + "apiVersion": "authorization.k8s.io/v1", + "kind": "SubjectAccessReview", + "status": { + "allowed": true, + "reason": "allowed by policy allow-developers-readonly", + "authorizationDetails": { + "policy.example.com/name": ["allow-developers-readonly"], + "policy.example.com/engine": ["opa"], + "policy.example.com/version": ["v2.3"] + } + } +} +``` + +A Cedar-based webhook could respond: + +```json +{ + "apiVersion": "authorization.k8s.io/v1", + "kind": "SubjectAccessReview", + "status": { + "allowed": true, + "reason": "permitted by policy policy0", + "authorizationDetails": { + "cedar.example.com/effect": ["permit"], + "cedar.example.com/policyId": ["policy0"] + } + } +} +``` + +Webhooks that do not populate this field continue to work exactly as before. +When a webhook does not return `authorizationDetails`, the kube-apiserver still +records the webhook authorizer name in the audit event. + +The kube-apiserver reads `authorizationDetails` from the webhook response and +records them in the `WebhookDecision.Details` field on the audit event. The +values are passed through without additional validation beyond the size limit. + +### Implementation in the Authorization Filter + +The authorization filter in +`staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization.go` currently +calls `a.Authorize(ctx, attributes)` and records annotations. With this KEP, +when the `StructuredAuthorizationDecision` feature gate is enabled, the filter +also populates the `AuthorizationDecision` field on the audit event. + +The `Authorizer` interface returns `(Decision, string, error)` and is not +changed by this KEP. Instead, structured decision data is passed from +authorizers to the authorization filter through the request context. + +Each authorizer stores its structured decision in the request context using a +well-known context key when the feature gate is enabled. After `Authorize()` +returns, the authorization filter reads the structured decision from the +context and sets it on the audit event. + +The `InstrumentedAuthorizer` wrapper (in +`staging/src/k8s.io/apiserver/pkg/authorization/metrics/metrics.go`) already +wraps each authorizer with `authorizerType` and `authorizerName`. It is extended +to read the authorizer-specific decision details from the context and combine +them with the authorizer type, name, and decision outcome into the final +`AuthorizationDecision` struct before storing it back in the context. + +For built-in authorizers (RBAC, Node, ABAC, AlwaysAllow, AlwaysDeny), each +implementation stores its own decision details in the context: + +- the RBAC authorizer stores the matched binding, role, and subject (data + already available in the `authorizingVisitor`) +- the Node authorizer stores no additional details +- ABAC, AlwaysAllow, and AlwaysDeny store no additional details + +For the webhook authorizer, the approach is different and simpler. The webhook +returns structured data in the `SubjectAccessReviewStatus.AuthorizationDetails` +field of the SAR response. The webhook authorizer implementation reads this +field from the response and stores it in the context. No context-passing +complexity is needed on the webhook side - the data arrives in the HTTP response. + +When the feature gate is disabled, no context values are set and the audit event +field is not populated. The existing annotations continue to work as before +regardless of the feature gate. + +### Test Plan + +[x] I/we understand the owners of the involved components may require updates to +existing tests to make this code solid enough prior to committing the changes +necessary to implement this enhancement. + +##### Prerequisite testing updates + +None. + +##### Unit tests + +- `staging/src/k8s.io/apiserver/pkg/endpoints/filters/authorization_test.go`: + extend the existing `TestAuditAnnotation` test to verify that the audit event + contains the `authorizationDecision` field when the feature gate is enabled, + and does not contain it when the feature gate is disabled. The test already + uses a `fakeAuthorizer` and checks `audit.AuditEventFrom(req.Context())` for + annotation values - the same pattern is used to check the new structured + field. + +- `plugin/pkg/auth/authorizer/rbac/rbac_test.go`: extend `TestAuthorizer` to + verify that the RBAC authorizer stores structured decision data in the + context when the feature gate is enabled. Verify that the stored data + includes the correct role kind/name, binding kind/name, and matched subject + kind/name for ClusterRoleBinding, RoleBinding, ClusterRole, and Role + scenarios. + +- `staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook_test.go`: + add tests to verify that the webhook authorizer reads the + `authorizationDetails` field from the `SubjectAccessReviewStatus` response + and stores it in the context. Test with: webhook response that includes + `authorizationDetails`, webhook response without `authorizationDetails`, + webhook response with `authorizationDetails` exceeding the 1KB limit. + +- `staging/src/k8s.io/apiserver/pkg/authorization/union/union_test.go`: extend + `TestAuthorizationFirstPasses` and `TestAuthorizationSecondPasses` to verify + that the union authorizer preserves the structured decision from the + authorizer that short-circuited (returned Allow or Deny), and does not + include data from authorizers that returned NoOpinion. + +##### Integration tests + +The main integration test is in +`test/integration/controlplane/audit/audit_test.go` (`TestAudit`). This test +starts a real kube-apiserver with an audit policy, performs API operations, and +verifies the resulting audit log entries using `test/utils/audit.go` utilities. + +- extend the `AuditEvent` struct in `test/utils/audit.go` to include an + `AuthorizationDecision` field (the structured one, alongside the existing + `AuthorizeDecision` annotation field) and update `CheckAuditLines` to verify + it +- add test cases to `TestAudit` that verify the `authorizationDecision` field + is present and correct for RBAC-authorized requests (the default + authorization mode in the integration test server) +- add a test case with the feature gate disabled to verify the field is absent +- add an integration test in `test/integration/auth/authz_config_test.go` that + configures a webhook authorizer, sends a request, and verifies that the + webhook-returned `authorizationDetails` appear in the audit event's + `webhook.details` field + +##### e2e tests + +- verify that the `authorizationDecision` field appears in audit log output for + requests authorized by RBAC in a real cluster with the feature gate enabled + +### Graduation Criteria + +#### Alpha + +- feature implemented behind the `StructuredAuthorizationDecision` feature gate +- `authorizationDecision` field populated by all six authorizer types +- `SubjectAccessReviewStatus.AuthorizationDetails` field added to the + authorization API +- unit and integration tests in place + +#### Beta + +- address feedback from alpha users +- confirm no measurable performance regression in audit log throughput +- all integration tests stable in CI + +#### GA + +- at least two releases in beta +- confirmed adoption by at least one major audit log analysis tool or platform +- no outstanding issues or bug reports + +### Upgrade / Downgrade Strategy + +- **upgrade**: when the feature gate is enabled, audit events start containing + the `authorizationDecision` field. The `SubjectAccessReview` API gains the + optional `authorizationDetails` field in the status. No other behavior + changes. Existing audit consumers and webhook authorizers are not affected. +- **downgrade**: when the feature gate is disabled or the cluster is downgraded, + the `authorizationDecision` field stops appearing in audit events. The + `authorizationDetails` field in SAR status is ignored. No data migration is + needed. + +### Version Skew Strategy + +The `authorizationDecision` field in the audit event is a server-side addition. +No other components need to be aware of it. + +The `authorizationDetails` field in `SubjectAccessReviewStatus` is optional. +Older webhook authorizers that do not return this field continue to work. Newer +webhook authorizers that return this field against an older kube-apiserver will +simply have the field ignored (standard Kubernetes API behavior for unknown +fields). + +## Production Readiness Review Questionnaire + +### Feature Enablement and Rollback + +###### How can this feature be enabled / disabled in a live cluster? + +- [x] Feature gate (also fill in values in `kep.yaml`) + - Feature gate name: `StructuredAuthorizationDecision` + - Components depending on the feature gate: `kube-apiserver` + +###### Does enabling the feature change any default behavior? + +No. When the `StructuredAuthorizationDecision` feature gate is enabled, a new +field is added to audit events and an optional field on the SAR response type +is read. Existing annotations and behavior are unchanged. Audit log consumers +that do not read the new field are not affected. Webhook authorizers that do not +return the new field are not affected. When the feature gate is disabled, there +is no change in behavior at all. + +###### Can the feature be disabled once it has been enabled (i.e. can we roll back the enablement)? + +Yes. Disabling the feature gate stops the `authorizationDecision` field from +being populated in audit events and causes the `authorizationDetails` field in +SAR status to be ignored. No cleanup is needed. + +###### What happens if we reenable the feature if it was previously rolled back? + +The `authorizationDecision` field starts appearing in audit events again. There +is no state to reconcile. + +###### Are there any tests for feature enablement/disablement? + +Yes. Integration tests verify that the field is present when the feature gate is +enabled and absent when it is disabled. + +### Rollout, Upgrade and Rollback Planning + +###### How can a rollout or rollback fail? Can it impact already running workloads? + +This feature only affects audit log output and an optional SAR response field. +It cannot impact running workloads. A rollout failure would mean the field is +missing or malformed, which would be caught by integration tests. + +###### What specific metrics should inform a rollback? + +- a significant increase in audit event size causing storage pressure +- errors in the audit pipeline caused by unexpected fields + +###### Were upgrade and rollback tested? Was the upgrade->downgrade->upgrade path tested? + +Will be tested as part of the alpha implementation. + +###### Is the rollout accompanied by any deprecations and/or removals of features, APIs, fields of API types, flags, etc.? + +No. + +### Monitoring Requirements + +###### How can an operator determine if the feature is in use by workloads? + +The feature is not used by workloads directly. Operators can check for the +presence of the `authorizationDecision` field in their audit logs. + +###### How can someone using this feature know that it is working for their instance? + +- [x] Other (treat as last resort) + - Details: check audit logs for the presence of the `authorizationDecision` + field on requests. + +###### What are the reasonable SLOs (Service Level Objectives) for the enhancement? + +This feature does not introduce new SLOs. It should not measurably affect +existing request latency SLOs. + +###### What are the SLIs (Service Level Indicators) an operator can use to determine the health of the service? + +- [x] Other (treat as last resort) + - Details: this feature does not introduce new SLIs. The existing + `apiserver_authorization_decisions_total` metric tracks authorization + decisions. The structured data is an addition to audit log output, not a + runtime signal. + +###### Are there any missing metrics that would be useful to have to improve observability of this feature? + +No. This feature adds structured data to audit events. No additional metrics +are needed. + +### Dependencies + +###### Does this feature depend on any specific services running in the cluster? + +No. + +### Scalability + +###### Will enabling / using this feature result in any new API calls? + +No. + +###### Will enabling / using this feature result in introducing new API types? + +No new API types. An optional field is added to the existing +`SubjectAccessReviewStatus` type and to the audit `Event` type. + +###### Will enabling / using this feature result in any new calls to the cloud provider? + +No. + +###### Will enabling / using this feature result in increasing size or count of the existing API objects? + +No API objects are changed. Audit events grow by approximately 150-300 bytes +per event due to the new field. + +###### Will enabling / using this feature result in increasing time taken by any operations covered by existing SLIs/SLOs? + +No. Populating the structured field is negligible compared to the overall +request handling time. + +###### Will enabling / using this feature result in non-negligible increase of resource usage (CPU, RAM, disk, IO, ...) in any components? + +Minimal increase in disk usage for audit log storage due to the additional +field. The increase is approximately 150-300 bytes per audit event. + +###### Can enabling / using this feature result in resource exhaustion of some node resources (PIDs, sockets, inodes, etc.)? + +No. + +### Troubleshooting + +###### How does this feature react if the API server and/or etcd is unavailable? + +This feature does not interact with etcd. If the API server is unavailable, +no requests are processed and no audit events are generated. + +###### What are other known failure modes? + +None. If populating the structured field fails for any reason, the request +processing continues normally - only the `authorizationDecision` field would be +missing from the audit event. The existing annotations are unaffected. + +###### What steps should be taken if SLOs are not being met to determine the problem? + +Disable the `StructuredAuthorizationDecision` feature gate and restart the +kube-apiserver. + +## Implementation History + +- 2026-04-08: initial KEP draft + +## Drawbacks + +- increases audit event size, which may matter for clusters with very high + request volume and tight storage constraints +- adds a new field to the audit API, increasing the API surface +- extending `SubjectAccessReviewStatus` adds a new field to a GA API type + +## Alternatives + +- **use an audit annotation instead of a typed field**: encoding the structured + decision as a JSON string in the annotations map would avoid changing the + audit `Event` type. However, annotations are untyped strings, which defeats + the purpose of having a structured, validated format. A proper typed field + provides schema validation, better tooling support, and is consistent with how + other structured data is exposed in Kubernetes APIs. + +- **extend the existing `authorization.k8s.io/reason` annotation with a stable + format**: this was rejected because changing the format of an existing + annotation could break existing tooling that parses the current free-form + string. + +- **change the `Authorizer` interface return type**: returning a struct instead + of `(Decision, string, error)` would be cleaner but is a breaking change for + all out-of-tree authorizer implementations. The context-based approach avoids + this and can be revisited if the interface changes for other reasons. + +- **do not extend the webhook protocol**: the kube-apiserver could record only + the webhook name without allowing the webhook to provide its own decision + metadata. This was rejected because it limits the usefulness for + organizations using policy engines (OPA, Cedar, Kyverno) as authorization + webhooks. + +- **standardize the reason string format**: defining a stable format for the + reason string could make it parseable. However, this constrains all authorizer + implementations and does not support different structured data per authorizer + type. + +- **define specific webhook decision fields instead of a generic map**: fields + like `policyName` or `engine` are too opinionated toward specific policy + engines. The `authorizationDetails` map with domain-prefixed keys follows the + established Kubernetes convention (used in `UserInfo.Extra`, + `SubjectAccessReviewSpec.Extra`) and gives webhook authors full control over + what metadata to return. diff --git a/keps/sig-auth/6001-structured-authorization-decision/kep.yaml b/keps/sig-auth/6001-structured-authorization-decision/kep.yaml new file mode 100644 index 000000000000..56227c105060 --- /dev/null +++ b/keps/sig-auth/6001-structured-authorization-decision/kep.yaml @@ -0,0 +1,22 @@ +title: Structured Authorization Decision +kep-number: 6001 +authors: +- "@nabokihms" +owning-sig: sig-auth +participating-sigs: +status: provisional +creation-date: 2026-04-08 +reviewers: +- TBD +approvers: +- TBD +stage: alpha +latest-milestone: "v1.37" +milestone: + alpha: "v1.37" +feature-gates: + - name: StructuredAuthorizationDecision + components: + - kube-apiserver +disable-supported: true +metrics: []