KEP-5681: Conditional Authorization
KEP-5681: Conditional Authorization
- Author: Lucas Käldström, Upbound
- Contributor: Micah Hausler, AWS
- Release Signoff Checklist
- Abstract
- Background and Major Considered Alternatives
- Proposal
- Technical Requirements
- Core interface changes
- Condition and ConditionSet data model
- Computing a concrete decision from a ConditionSet
- Computing a concrete decision from a conditional authorization chain
AuthorizationConditionsEnforceradmission controller- Changes to
(Self)SubjectAccessReview - Supporting webhooks through the
AuthorizationConditionsReviewAPI - Composite / Union Authorizer Support
- Built-in CEL conditions evaluator
- Feature availability and version skew
- Other Kubernetes authorization enforcement points, with and without conditions-awareness
- Authorizer requirements
- Production Readiness Review Questionnaire
- TODOs
- Alternatives Considered
- Expose all conditions in AdmissionReview, and have admission plugins “acknowledge” the conditions
- Propagate an API server-generated request UID to both authorization and admission
- Only one ConditionSet exposed as part of SubjectAccessReview status
- Require the client to annotate its write request with field or label selectors
- Extract label and field selectors from the request and current object in etcd, and supply that to the authorization process
- Do nothing, force implementers to implement all of this out of tree
- Drawbacks
- Appendix A: Further resources
- Appendix B: Future addition sketch: Conditional Reads
Release Signoff Checklist
- Enhancement issue in release milestone, which links to KEP dir in kubernetes/enhancements (not the initial KEP PR)
- KEP approvers have approved the KEP status as
implementable - 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
- (R) Minimum Two Week Window for GA e2e tests to prove flake free. Not yet applicable for this KEP
- (R) Graduation criteria is in place
- all GA Endpoints must be hit by Conformance Tests within one minor version of promotion to GA. Not yet applicable for this KEP
- Production readiness review completed
- 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
Abstract
This KEP proposes extending Kubernetes authorization to support conditions, where an authorization decision depends on resource data (labels and fields of object), rather than only metadata (apiGroup, resource, namespace, name). This enables more fine-grained, and most importantly, cohesive access control policies that span both authorization and admission phases, while maintaining backward compatibility with existing authorizers.
The goal of this proposal is to make authorizers able scope down their policies as if the authorizer had access to the resource data directly, through the use of two phases:
- compute
Allow,Deny,NoOpinionorConditionalresponse during the authorization phase. IfConditional, return the set of conditions to Kubernetes. - evaluate any conditions on the old/new object(s) during the validating admission
phase, and enforce the concrete
Allow,DenyorNoOpinionresult.
Through this KEP, Kubernetes guarantees to the authorizer that its scoped-down authorization policy will be enforced, without the authorizer having to rely on existence of other specific admission plugins.
Concretely, all SubjectAccessReview (SAR) APIs are extended such that a client
can ask for conditions and the authorizer can respond with conditions. The end
user thus becomes aware of what restrictions they are subject to through
kubectl auth can-i self-lookups. In addition, a new
AuthorizationConditionsReview API is added to let out-of-tree authorizers
evaluate conditions.
This KEP aims to provide generalized framework for multiple previous features, KEPs and issues:
- DRA AdminAccess
:
“Deny creates and updates to
ResourceClaims with.spec.devices[*].adminAccess=true, unlessnamespaceObject.metadata.labels["resource.kubernetes.io/admin-access"] == "true"” - Fine-grained Kubelet API Authorization
:
“Allow a node agent to proxy requests to nodes through the API server, but
only to scrape readonly information from a path starting with
/pods/, not to exec into pods” - Constrained Impersonation
:
“Allow node agent
csi-driver-footo only impersonate the node it is running on toget pods” - Requiring presence of certain labels or fields: #44703
- Empowering authorizers to restrict the names of created objects: #54080
- The tight coupling between the
Nodeauthorizer andNodeEnforcementadmission controller; with this KEP, the same logic could be modelled through only a conditional authorizer1. - Provide an alternative to hard-coded compound authorization (e.g. the secondary CSR SignerName authorization check) where needed (even though the two paradigms are complimentary)
- Hopefully, provide a feature that is helpful to unblocking the Referential Authorization KEP .
- “RBAC++” efforts, eventually.
Example Use Cases
A non-exhaustive list, in addition to the ones mentioned above:
- Allow user Alice to create, update, delete PersistentVolumes, but only whenever spec.storageClassName is “dev”
- Allow a principal to update an object, but only when a sensitive field is unchanged
- Allow a principal to create CertificateSigningRequests, but only when using a given signerName
- Allow a principal to update a resource, but only when a sensitive field is left unchanged
- Allow a principal to issue ServiceAccount tokens, but only with a given audience
- Allow a controller to update a resource, but only to add/remove its own finalizer name
- Allow a node agent to handle a resource, but only when
.spec.nodeName=<node-name> - Allow a user to
create subjectaccessreviews, but only to check permissions of certain other users
Goals
- Provide a way for an authorizer (and by extension, policy author) to only authorize certain write2 operations, when the payload or stored object satisfies some specific conditions
- Let users discover what conditions they are subject to through
(Self)SubjectAccessReview - Initially support enforcement of write and connect requests in the
k8s.io/apiserverWithAuthorizationHTTP filter, with options to expand coverage to read2 and impersonate verbs later.- However, conditions can be returned for any verb in
SubjectAccessReviewresponses, so extensions can be built arbitrarily on top. For instance, aggregated API servers can choose to enforce conditions on whatever custom verbs it wants, and authorizer can return conditions for any verb it likes.
- However, conditions can be returned for any verb in
- Allow any authorizer’s conditions to be expressed in both transparent,
analyzable forms (like Cedar or CEL), or opaque ones (like just
policy16). - Support expressing conditions with either “Allow” and “Deny” effects.
- Provide the foundational framework on top of which we can build other authorization features, such as Constrained Impersonation, RBAC++ and Referential Authorization.
- Ensure that a request is evaluated against an atomic set of policies in the authorizer, even in presence of cached authorization decisions
Non-goals
- Designing or mandating use of a specific policy authoring UX
- Designing or mandating use of a specific “official” condition syntax
- Expose the conditions to admission controllers
Background and Major Considered Alternatives
To make the proposal easier to read, context and rationale for commonly-asked-about alternatives is provided already at this stage. The reader deeply familiar with Kubernetes authorization may proceed to the proposal chapter .
Why not just give authorizers access to request and stored objects?
Kubernetes Authorizers today do not have access to the resource data for good reason:
- Not all requests have resource data attached to it
- The API server must be sure that the request can become authorized according to all data known at the time (even though to reach a final decision, the object must be decoded to check). It would be wasteful, and a DoS vector to use API server CPU and RAM resources to decode a request payload in a request that anyways cannot become authorized.
- Authorization decisions must be stateless, i.e. the same authorization query
must yield the exact same decision whenever the underlying policy store is
the same. The authorizer should in other words be a deterministic function
a: Metadata x PolicyStore → Decision. In other words, the initial authorization decision must not depend on the state of objects in etcd . - The request payload might be mutated many times by admission controllers before it reaches the state that can be checked into etcd. In addition, the old object is only available very late in the request handling process, right before an update is actually about to go through in the storage layer (and thus admission is invoked).
- Even if it was technically possible, providing the authorizer up-front with the objects could yield a significant hit, as some time would be spent on just propagating the objects to each authorizer, and the authorizer might spend more time examining the object earlier.
Why not just use ValidatingAdmissionPolicies?
The observant reader might notice that some of the
use cases
can already be achieved today with the
ValidatingAdmissionPolicy (VAP) API.
However, the solution is still in some regards sub-optimal:
- In the authorization phase, the policy author must “over-grant”, and then remember to (!) “remove” the permissions in the admission phase.
- The user needs to understand two different paradigms at once, and coordinate the policies between them.
- The principal-matching predicate needs to be duplicated between RBAC and VAP.
- The policy author needs permission to both author RBAC rules and VAP objects.
VAP objects do not have privilege escalation prevention, which means that
anyone that can create a VAP can author a static
falsecondition foroperation=*andresources=*, which blocks all writes to the cluster, until an admin removes the offending VAP. Thus should not “create VAP” permissions be handed out to lower-privileged principals, e.g. namespaced administrators, who otherwise legitimately would need to write conditionally authorized policies. - Strict ordering of creates and deletes: In order to not at any point leak data, must the VAP deny rule be authored and successfully activated before the RBAC allow rules are, and vice versa for deletes.
- The conditions do not show up in a
(Self)SubjectAccessReview, but are only noticed by a user subject to the policy upon performing a request. - Status quo with
ValidatingAdmissionPolicydoes not offer a tangible path forward for providing a unified experience for writing fine-grained authorization policies for reads. - The authorizer cannot let the policy author express conditional policies of the form “allow create persistentvolumes, only when storageClassName==‘foo’”, as the authorizer cannot mandate or control the admission plugins or cluster setup process of the cluster it serves authorization decisions to.
- If the policy could not be modelled as a
ValidatingAdmissionPolicy, but an admission webhook would be needed, that webhook might need to be sent for every request in the worst case, as opposed to only those requests that are conditionally authorized (as in this proposal).

This proposal solves all of these mentioned issues through a two-phase model:
- the authorizer partially evaluates its authorization policies into
Allow,Deny,NoOpinion, or a set of conditions on the at the authorization-stage unknown data: the request and stored object. - Kubernetes or the authorizer evaluates the conditions into a concrete response, during the admission phase.
What is partial evaluation?
Partial evaluation is the process of evaluating expressions as far as possible with incomplete data. A crucial consequence of this is that some unknown data might turn out to not be needed at all to assign a fixed value to the expression! This effective form of pruning can take place if some sub-expression is independent of the unknown data.
In the KEP context, the unknown data at authorization time is the request and stored objects, and the request options.
Consider a ValidatingAdmissionPolicy/CEL-like expression syntax, and how the following two policies would be partially evaluated for the two example users Alice and Bob:
Allow Policy 1:
request.apiGroup == "" && request.userInfo.username == "bob"Allow Policy 2:
request.apiGroup == "" && request.resource == "persistentvolumeclaims" && request.verb == "create" && request.userInfo.username == "alice" && object.spec.storageClassName == "dev"
Now, if Alice performs a create persistentvolumeclaims, what will the policies
partially evaluate to?
- Allow Policy 1:
true && false=>false - Allow Policy 2:
true && true && true && true && object.spec.storageClassName == "dev"=>object.spec.storageClassName == "dev"
In these examples, the result of each sub-expression is shown for clarity.
Policy 1 clearly evaluates to false, without knowing the value of object.
Policy 2 produces a residual expression. Without knowing the object, it is
impossible to assign a truth value to the residual, and thus is this our
condition in the authorizer response.
Next, let’s consider what happens if Bob performs create persistentvolumeclaims
in the same setting:
- Allow Policy 1:
true && true=>true - Allow Policy 2:
true && true && true && false && object.spec.storageClassName == "dev"=>false
Now, Policy 1 returned an unconditional (concrete) allow, and Policy 2 can never be true, no matter what the value of the object is. If the authorizer follows “at least one matching Allow policy yields Allow”, a concrete Allow can be returned instantly.
The authorizer might also do positive pruning, that is, if one Allow policy
evaluates to true, and another Allow policy to a residual, the authorizer
concludes that no matter what the object is, their union will yield Allow.
Finally, what would happen if user Eve (who is assigned no permissions) tries to
create persistentvolumeclaims:
- Allow Policy 1:
true && false=>false - Allow Policy 2:
true && true && true && false && object.spec.storageClassName == "dev"=>false
This gives us both properties we want: Eve is denied access immediately in the authorization stage (without ever decoding the body), while it being possible to express a policy that spans both authorization and admission (policy 2).
Three adjacent systems support partial evaluation: Cedar , CEL and OPA . In particular the Cedar RFC has good argumentation on why partial evaluation only works well in cases where all expressions have a concrete type.
Note that partial evaluation should substitute all known variables with
constants, even if the residual cannot be fully evaluated. For example, when a
sub-expression is initially object.name == request.userInfo.username and
request.userInfo.username is known to be "lucas", but object is unknown,
the resulting residual is object.name == "lucas". In other
words, the residual does not depend on any variables already known.
Why propagate the conditions with the request?
It was already concluded that the authorizer needs to be able to depend on the API server to “call the authorizer back” with the resource data, whenever a conditional decision is returned. However, instead of the authorizer returning the set of conditions to Kubernetes, one could imagine two other methods, as follows:
- The authorizer does not return a ConditionSet, but relies on Kubernetes to
send an
AdmissionReviewto the authorizer whenever a conditional decision was made. The authorizer then re-evaluates all policies against the AdmissionReview with complete data. This approach has many drawbacks:- Two full evaluations needed: During the authorization phase, the
worst-case runtime is
O(nk), where n is the number of policies, and k is the maximum policy evaluation time. The admission-time evaluation would also beO(nk)in this case.- With this proposal, only
O(k)time would be required in admission, given that the amount of conditions isO(1)for a typical request.
- With this proposal, only
- Non-atomicity: For a given authorizer, a request should be authorized
from exactly one policy store snapshot. If two full re-evaluations were
done, the latter (admission-time) policy store semantics would apply, if
the policy store changed between the request performed authorization and
admission.
- With this proposal, the conditions are computed at authorization time by partial evaluation and unmodified enforced at admission, exactly and only the authorization-time policy store semantics apply.
- Tight coupling between conditions authoring and evaluation: The
authorizer would be the only entity which would be able to evaluate the
conditional response in the admission stage, which forms a forced tight
coupling. Two webhooks per authorizer per request is necessary.
- With this proposal, builtin conditions enforcers might evaluate and enforce the conditions in-process, without a need for another webhook in admission. One such builtin enforcer is proposed to be CEL-based. This is faster and more reliable.
- Not observable through (Self)SubjectAccessReview: As for admission
today, a user subject to a policy would not know what policy they are
subject to before they execute a request that violates it (hopefully with
a nice error message).
- With this proposal, a user can see the conditions serialized in the
(Self)SubjectAccessReview. Some of the conditions might be opaque (likepolicy16), yes, but at least the user might know where to look next.
- With this proposal, a user can see the conditions serialized in the
- Two full evaluations needed: During the authorization phase, the
worst-case runtime is
- The authorizer does not return a ConditionSet, but instead caches the
conditions in a memory. The authorizer relies on Kubernetes to generate a
random “request ID”, which is passed to both
SubjectAccessReviewandAdmissionReviewwebhooks, so the authorizer can know which conditions to apply to which request.- This approach does not have the “Two full evaluations needed” and
“Non-atomicity” problems of the first alternative approach, as only the
conditions need to be atomically evaluated against the resource data.
However, this approach is subject to the “Tight coupling” and “Not
observable through
(Self)SubjectAccessReview” problems. In addition, the following problems arise: - A stateful authorizer is complex and hard to scale: The authorizer
must be way more complex, as it needs to keep a lookup table of request ID
to condition set internally. If the authorizer needs to be horizontally
scaled, the load balancer in front of the horizontally scaled authorizers
would somehow need to know which authorizer replica has what requests’
conditions stored in memory.
- With this proposal, the authorizer is allowed to be stateless and thus simpler. Therefore, also the horizontal scaling can be done in a straightforward manner, from this perspective.
- Unclear caching semantics: The authorizer would need to cache the conditions in memory for at least as long as SubjectAccessReview requests can be cached, for the above atomicity invariant to hold. However, the authorizer does not (generally) know the API server configuration, and thus does not know how long to cache the conditions, or if at all.
- This approach does not have the “Two full evaluations needed” and
“Non-atomicity” problems of the first alternative approach, as only the
conditions need to be atomically evaluated against the resource data.
However, this approach is subject to the “Tight coupling” and “Not
observable through
Glossary
- Concrete/Unconditional (authorization) decision: one of
Allow,Deny,NoOpinion. - Residual: Expression which is a deterministic function of data that was unknown during partial evaluation.
- Conditional Allow: A
Conditionaldecision with aConditionSetthat has at least oneeffect=Allowcondition. In other words, the conditional decision can turn into a concreteAllow,DenyorNoOpinionwhen evaluated. - Conditional Deny: A
Conditionaldecision with aConditionSetthat has noeffect=Allowcondition, in other words, justeffect=NoOpinionoreffect=Denyconditions. When evaluated, thisConditionSetcan thus only turn into concreteDenyorNoOpiniondecisions.
Proposal
To achieve the above mentioned goals, at a high level, the following changes are proposed:
- The
authorizer.Authorizerinterface andSubjectAccessReviewAPI are extended to support:- The client indicating it supports conditional authorization
- The authorizer returning, in addition to existing unconditional
Allow,DenyorNoOpiniondecisions, a set of conditions
- The
WithAuthorizationHTTP filter lets a supported request proceed in the request chain if the decision is:- A unconditional
Allow, just like before, or - A ConditionSet which can evaluate to
Allow
- A unconditional
- A new, always-on
k8s.io/apiserver-built-inAuthorizationConditionsEnforcervalidating admission plugin (ordered before other validating webhooks) which enforces that the set of conditions (if any) evaluate intoAllow, or denies the request.- To empower out-of-tree/webhook authorizers to evaluate their (opaque)
conditions, a new
AuthorizationConditionsReviewAPI is added. - Any conditional authorizer must serve this API, which means that also
kube-apiservermust serve it.3
- To empower out-of-tree/webhook authorizers to evaluate their (opaque)
conditions, a new
- A
k8s.io/apiserver-built-in CEL condition evaluator, which allows evaluating conditions expressed in CEL format to be evaluated in-process without another webhook.
Notably, this design achieves its goal of unified authorization expressions across authorization and admission, without the breaking the reasons why authorizers do not have direct access to the request body in the authorization stage:
- Conditional Authorization is only supported for certain requests, namely
whenever admission is invoked (verbs
create,update,patch,delete,deletecollectionand connect requests). - Any request that cannot become authorized, regardless of the value of the resource data, is rejected already at the authorization stage, thanks to partial evaluation.
- The conditions are part of the returned authorization decision, and partial evaluation is a deterministic function, i.e. the same output (which includes the conditions) is guaranteed for the same inputs (metadata and policy store content).
- The API server enforces the conditions in the validating admission stage, where access to the objects is available with the correct consistency guarantees.
- Authorizers process the object data only when really needed, which minimizes the performance hit.
The following picture summarizes how, with this feature, a webhook authorizer
can expose a unified policy authoring experience (e.g. through Cedar or CEL) by
returning conditions that are propagated with the request chain until validating
admission, where the AuthorizationConditionsEnforcer plugin “calls the
authorizer back” with the conditions it gave, and the rest of the data.

In function syntax, an authorizer is a deterministic function
authorize: Metadata x PolicyStore → Decision. A ConditionSet, returned by
some authorizer, is a map from an authorizer-scoped identifier to a condition.
With this proposal, the Decision logical enum gets a new Conditional
variant, that has a ConditionSet associated with it.
Let ConditionData be the term for the data unknown at authorization time
(request, stored object and request options). A condition is a deterministic
function condition: ConditionData → Boolean. Note that the condition is only
a function of the unknown data; already-known data should be constants of the
condition (see the partial evaluation section
for
an example). A condition also has an effect, which controls if evaluation to
true should be treated as producing a Allow, Deny, or NoOpinion
decision.
Note that even though the “full” new and old objects are given as inputs to the condition in this model, the authorizer is free to choose how much of that API surface is exposed to policy authors. Some authorizer might decide to e.g. only expose field-selectable fields in the expression model given to the policy author.
Evaluating a ConditionSet is a deterministic function
evaluate: ConditionSet x ConditionData → (Decision - {Conditional}). Note
that conditions evaluation should not have access to the policy store; this is
by design, as it makes this two-stage mechanism atomic, just like it would
have been if it could have been evaluated directly.
Technical Requirements
- The final decision must always be the same in this two-phase model as in a one-phase model (that is, if the request / stored object were given directly to the authorizer). This for example implies that the order of the authorizers must be preserved.
- Capabilities and decision logic must be exactly the same for both in-tree and out-of-tree authorizers.
- Only proceed to decode the object if the request can become authorized, to avoid Denial-of-Service attack vectors.
- Must work for connectible resources (see this section for more details)
- Keep backwards compatibility within supported version skew, as always.
- Consider that a
patchorupdatein authorization can turn into acreatein admission,patchin authorization can turn into anupdatein admission, anddeletecollectionin authorization turns into adeletein admission. - Must work with aggregated API servers.
- Must work with any authorizer chain that is formed as a DAG.
Core interface changes
This KEP proposes to change authorizer.Decision to be a struct (as Go does not
support enums with attached data :/), change the authorizer.Authorizer interface
accordingly, and let the caller signal its conditions-readiness:
package authorizer // k8s.io/apiserver/pkg/authorization/authorizer
// Authorizer makes an authorization decision based on information gained by making
// zero or more calls to methods of the Attributes interface. It might return
// an error together with any decision. It is then up to the caller to decide
// whether that error is critical or not.
type Authorizer interface {
Authorize(ctx context.Context, a Attributes) (Decision, error)
// All authorizers are required to implement the conditions evaluation method,
// even though they just return an error saying "unsupported".
// This ensures composite authorizers do not "forget" to implement the
// EvaluateConditions method, even if it'd wrap a conditions-aware authorizer
ConditionSetEvaluator
}
type ConditionSetEvaluator interface {
// EvaluateConditions evaluates a condition set into
// a concrete decision (Allow, Deny, NoOpinion), given full information
// about the request (ConditionData, which includes e.g. the old and new objects).
// The returned Decision must be concrete.
EvaluateConditions(ctx context.Context, conditionSet *ConditionSet, data ConditionData) (Decision, error)
}
type Attributes interface {
// ... everything else as before, plus:
// ConditionsMode indicates how, if any, the client wants conditions to be returned.
ConditionsMode() ConditionsMode
}
// ConditionsMode specifies the client's request of how conditions should be returned
// by the authorizer (or not at all). ConditionsMode name TBD.
type ConditionsMode string
const (
// ConditionsModeNone indicates that the client does not support conditions.
ConditionsModeNone ConditionsMode = ""
// ConditionsModeHumanReadable indicates that the client wants a
// human-readable condition and description, if possible.
ConditionsModeHumanReadable ConditionsMode = "HumanReadable"
// ConditionsModeOptimized indicates that the client wants an
// optimized conditions encoding without description, if possible.
ConditionsModeOptimized ConditionsMode = "Optimized"
)
// Decision constructors
func DecisionAllow(reason string) Decision {}
func DecisionDeny(reason string) Decision {}
func DecisionNoOpinion(reason string) Decision {}
func DecisionConditional(conditionSets *ConditionSet) Decision {}
// ChainedDecisions combines an ordered list of computed decisions of an authorizer
// chain, and registers a set of lazily-evaluated authorizers in the chain to call
// in AllConditionSets() or if needed in Evaluate()
// In case of nested unioned Decisions, the hierarchy is flattened.
func ChainedDecisions(decision []Decision, authorizerChain ...Authorizer) Decision
// Decision models an enum that can be Allow, Deny, NoOpinion or
// Conditional([]*ConditionSet, []Authorizer)
// The Decision struct is passed by value, and the empty struct == Deny.
type Decision struct {
// only private fields
}
// Replaces the old enum d == authorizer.DecisionAllowed, etc.
func (d Decision) IsAllowed() bool {}
func (d Decision) IsDenied() bool {}
func (d Decision) IsNoOpinion() bool {}
func (d Decision) IsConditional() bool {}
func (d Decision) IsConcrete() bool {return d.IsAllowed() || d.IsDenied() || d.IsNoOpinion()}
func (d Decision) Reason() string {}
// CanBecomeAllowed returns true if d.IsAllowed() or any ConditionSet can become
// allowed.
func (d Decision) CanBecomeAllowed() bool {}
// Evaluate evaluates all ordered ConditionSets of d into a concrete decision.
//
// Might be an expensive operation, as authorizers might webhook to evaluate
// its conditions.
func (d Decision) Evaluate(ctx context.Context, data ConditionData, builtinConditionSetEvaluators ...BuiltinConditionSetEvaluator) (Decision, error)
// AllConditionSets returns an ordered list of all (including lazily-evaluated)
// authorizers' decisions, until a concrete decision is reached.
//
// Might be an expensive operation, as webhook authorizers might be called.
func (d Decision) AllConditionSets(ctx context.Context) ([]*ConditionSet, error)
Instead of return authorizer.DecisionAllow, "some reason", nil, all in-tree
authorizers’ return statements need to be rewritten to
return authorizer.DecisionAllow("some reason"), nil. The change is not huge,
but could be tedious. One good side-effect is that before it was possible to use
values beyond the enum, e.g. like return -100, "", nil, but not anymore.
The behavior of the zero value was previously 0 == DecisionDeny, and is
preserved as Decision{} == DecisionDeny("").
The authorizer.Attributes package is augmented with ConditionsMode, to let
the client indicate whether slower-to-execute, human-readable conditions with
description or optimized (e.g. binary AST) conditions are more important. The
default is an empty string, which is treated as “the client does not support
conditions”. In this case, an authorizer is advised to return Deny if it would
have returned one or more effect=Deny conditions, otherwise the authorizer
should NoOpinion.
Evaluating a conditional decision into a concrete one is done using the
Evaluate() method, and this function requires supplying at least all the data
that was not present earlier, but which authorization policies are allowed to
use. If the partial evaluation process was done correctly, the condition should
be a pure function of this data:
package authorizer // k8s.io/apiserver/pkg/authorization/authorizer
// TODO: This interface might need to change to something more generic,
// as e.g. constrained impersonation might use other contextual data
// (or we bake that data into GetObject(), or add another field)
type ConditionData interface {
// GetOperation is the operation being performed
// TODO: For impersonation requests, we might make this IMPERSONATE, if we want/need.
GetOperation() string
// GetOperationOptions is the options for the operation being performed
GetOperationOptions() runtime.Object
// GetObject is the object from the incoming request prior to default values
// being applied.
// Only populated for CREATE and UPDATE requests.
GetObject() runtime.Object
// GetOldObject is the existing object.
// Only populated for UPDATE and DELETE requests.
GetOldObject() runtime.Object
}
Condition and ConditionSet data model
Next, the observant reader noticed that the ConditionSet struct was referred
to above. Next, let’s walk through the detailed model of a condition and its
set:
package authorizer
// ConditionEffect specifies how a condition evaluating to true should be handled.
type ConditionEffect string
const (
// If any Deny condition evaluates to true, the ConditionSet
// necessarily evaluates to Deny. No further authorizers
// are consulted.
ConditionEffectDeny ConditionEffect = "Deny"
// If a NoOpinion condition evaluates to true, the given
// authorizer's ConditionSet cannot evaluate to Allow anymore, but
// necessarily Deny or NoOpinion, depending on whether there are any
// true EffectDeny conditions.
// However, later authorizers in the chain can still Allow or Deny.
// It is effectively a softer deny that just overrides the
// authorizer's own allow policies. It can be used if an authorizer
// does not consider itself or the principal authoritative for a given request.
// TODO: Talk about error handling; what happens if any of these
// conditions fail to evaluate.
ConditionEffectNoOpinion ConditionEffect = "NoOpinion"
// If an Allow condition evaluates to true, the ConditionSet evaluates
// to Allow, unless any Deny/NoOpinion condition also evaluates to
// true (in which case the Deny/NoOpinion conditions have precedence).
ConditionEffectAllow ConditionEffect = "Allow"
)
// A condition to be evaluated
type Condition struct {
// An alphanumeric string, validated as a Kubernetes label, that is
// (<DNS1123 subdomain>/)[-A-Za-z0-9_.]{1,63}.
// Users must not use IDs with the 'k8s.io/' prefix.
// Uniquely identifies the condition within the scope of the
// authorizer that authored the condition. Acts as a key for a
// slice of conditions, such that it can be used as a map.
// The FailureMode of the ConditionalAuthorizer determines how to
// handle invalid ID values.
// Used for error messages, e.g.
// "condition 'company.com/no-pod-exec' denied the request"
ID string
// An opaque string that represents the condition that should be
// evaluated. A condition is evaluated after mutation.
// A pure, deterministic function from ConditionData to a Boolean.
// Might or might not be human-readable (could e.g. be
// base64-encoded), but max 1024 bytes.
// The FailureMode of the ConditionalAuthorizer determines how to
// handle too long Condition values.
// TODO: we could consider supporting also byte-encoded data using a
// mutually-exclusive ConditionBytes field, for more efficient
// AST encoding and execution of e.g. CEL conditions.
Condition string
// How should the condition evaluating to "true" be treated.
// The FailureMode of the ConditionalAuthorizer determines how to
// handle unknown Effect values.
Effect ConditionEffect
// Optional human-friendly description that can be shown as an error
// message or for debugging.
Description string
// TODO: Do we need per-condition failure modes? Most likely not initially.
}
// ConditionSet represents a conditional response from an authorizer.
// TODO: Decide on a maximum amount of conditions?
type ConditionSet struct {
// private fields only, must be constructed through a constructor
// Some authorizers that are later in the chain than an authorizer that
// returned a conditional response, might return unconditional responses.
// Capture this in the ConditionSet.
// Mutually exclusive with set
unconditionalDecision *Decision
// Private field so constructor function can validate the conditions before
// adding them to the set.
set []Condition
}
// The format/encoding/language the conditions in this set.
// Any type starting with `k8s.io/` is reserved for Kubernetes
// condition types to be added in the future.
// An authorizer must be able to evaluate any conditions it authors.
// Validated as a label key, i.e. an alphanumeric string with an
// optional DNS1123 subdomain prefix, and a key name of max 63 chars.
// The FailureMode of the ConditionalAuthorizer determines how to
// handle invalid Type values.
func (c *ConditionSet) Type() string {}
Computing a concrete decision from a ConditionSet
How should a ConditionSetEvaluator evaluate the conditions in the given set? The
process is two-fold:
- Evaluate each condition function to a boolean value, or error
- Compute the individual truth values of the conditions, along with their
desired effect into an aggregate, concrete decision
(
Allow/Deny/NoOpinion) at the authorizer level, according to the following logic:
If there is at least one condition with effect=Deny that evaluates to true,
return Deny.
If there is at least one condition with effect=Deny that evaluates to an
error, return an error. The FailureMode of the ConditionalAuthorizer controls
whether to treat the error as decision Deny or NoOpinion.
Otherwise, it is known that all effect=Deny conditions evaluate to false.
Then, if there is at least one condition with effect=NoOpinion that evaluates
to true, return NoOpinion.
If there is at least one condition with effect=NoOpinion that evaluates to an
error, return NoOpinion to fail closed (as if the condition evaluated to true)
along with the error for logging/diagnostics.
Otherwise, it is known that all effect=NoOpinion conditions evaluate to false.
Then, if there is at least one condition with effect=Allow that evaluates to
true, return Allow.
Any effect=Allow condition that evaluates to an error is ignored. If no
effect=Allow condition evaluates to true, return NoOpinion.

One quite tricky technical detail about partial evaluation is the
short-circuiting of e.g. the common && and || operators, especially with
regards to errors. Clearly, false && <residual> can be simplified to false.
However, <residual> && false can either be false or <error>, if evaluating
<residual> can produce an error. Thus are the && and || operators not
commutative.
The authorizer contract is such that the authorizer should only return a set
of conditions that could evaluate to Allow. Returning a set of conditions
that always evaluate to NoOpinion or Deny is a waste of resources.
Concretely, the authorizer should not return conditions of form
<residual> && false with Allow effect, as such conditions are either false
or <error> and thus never contribute to an Allow decision. However, the same
pruning cannot be done for effect=Deny or effect=NoOpinion conditions, as an
evaluation error would trigger fail-closed short circuiting to Deny or NoOpinion.
Computing a concrete decision from a conditional authorization chain
It is now known how to evaluate a single ConditionSet together with the
ConditionData into a single, aggregate concrete decision, the
same decision that the authorizer would have immediately returned, if it had
direct access to the ConditionData. Next, we discuss the semantics of
multiple authorizers chained after each other (i.e. the
union
authorizer), in the light of conditional authorization.
To begin with, it is good to state that the semantics of the existing modes
Allow, Deny and NoOpinion do not change. Whenever a NoOpinion is
returned by an authorizer, that decision is ignored (even if an error is
returned), and the next authorizer in the chain is consulted. Thus must any
safety-critical errors be turned into Deny decisions if failing closed is
needed. A chain with the decision prefix NoOpinion, …, NoOpinion, Allow still
short-circuits and returns a concrete Allow. Vice versa for a chain with the
prefix NoOpinion, …, NoOpinion, Deny => Deny.
A ConditionSet with at least one effect=Allow condition is considered a
“conditional allow”. The union authorizer short-circuits when seeing such a
decision in a “lazy” way, as now the request can become allowed. Crucially,
however, the rest of the authorizer chain (that was not yet considered) must be
saved in the Decision for later, lazy evaluation, in case the conditional allow
would evaluate into a NoOpinion.
The WithAuthorization HTTP filter makes sure that the current request supports
conditional authorization, and that the decision can become allowed (that is,
the decision is a concrete or conditional allow) before proceeding. The returned
Decision is propagated using the context to the validating admission phase,
just like UserInfo and RequestInfo are today. The HTTP filter signature is
augmented with a function that determines whether the request supports
conditions or not:
func WithAuthorization(hhandler http.Handler, auth authorizer.Authorizer, s runtime.NegotiatedSerializer, supportsAuthorizationsConditions func(ctx context.Context))
If an authorizer returns a conditional response for a request that does not
support conditions (such as list requests, for now), WithAuthorization fails
closed. The function ensures that there is always a “safety net” behind the
authorization filter, if the request is let to proceed. Aggregated API servers
that use the WithAuthorization function can themselves choose when conditions
are applicable. Initially, the kube-apiserver supports conditional responses
for the following classes of requests:
- When verb is
create,update,deleteordeletecollection, the API object is served by the same API server, and the GVR doesn’t contain wildcards. - When the request maps to a
Connecthandler instead of normal CRUD.- Without the
supportsAuthorizationsConditionsfunction,WithAuthorizationhas no way to know thatget pods/execis actually covered by admission, and thus safe to authorize conditionally. - Note that other
getrequests are not necessarily covered by admission (get pods/logis a counterexample)
- Without the
- When the request belongs to an API group that is served by an aggregated API server.
- Warning: Any aggregated API server MUST use
kube-apiserveras its first authorizer; any other behavior is unsafe and with undefined behavior. - When the aggregated API server uses the
kube-apiserver(acting as an authenticating front proxy) as its first webhook authorizer, thekube-apiserverwill return the applicable conditions (if any) to the aggregated API server.
- Warning: Any aggregated API server MUST use
However, if no effect=Allow condition is present in a returned ConditionSet,
the decision is considered like a “conditional deny”. In this case, later
authorizers need to be consulted to find out if this request can become
authorized. If a later authorizer returns a concrete Deny, clearly the request
cannot become allowed; it is either conditionally or concretely denied.4 However,
if a later authorizer returns a concrete Allow, the request is conditionally
allowed; if the deny conditions in the beginning all evaluate to false, that
first authorizer would have returned NoOpinion, and the next authorizer then
returns a concrete Allow.
The DRA AdminAccess feature is a good example of a feature that could be
modelled as an authorizer in the beginning of the chain that returns NoOpinion
for most requests, but conditional denies for some requests (namely, creates and
updates of ResourceClaim(Template)s). In contrast to using
ValidatingAdmissionPolicy for that purpose, an authorizer does not need to
allow for its policies to be deleted. In contrast to the existing DRA
AdminAccess implementation at the storage layer, the condition shows up in
SubjectAccessReviews.
What is proposed in this KEP is thus lazy evaluation, that allows a request
to proceed to admission whenever a conditional allow is seen at authorization
time, and the rest of the chain is lazily evaluated only if needed (if the
previous authorizer evaluated to a concrete NoOpinion).
Another considered alternative is the eager variant, that would call each
authorizer in the chain already in the authorization stage, until a concrete
Allow or Deny is reached. However, this approach might be wasteful and call
later authorizers, whose response is never considered in the evaluation phase in
admission. Thus is the lazy approach proposed.

A high-level picture of the request flow with conditional authorization. The
chain of authorizer decisions can be lazily evaluated, such that the third
authorizer in the picture is not evaluated directly in the authorization stage,
as already the second one might yield an Allow. However, in admission, if the
second authorizer ends up evaluating to NoOpinion, the third authorizer is
evaluated (and in this example evaluates first to a conditional allow, then
concrete Allow).
A diagram to summarize what the request chain looks like:

AuthorizationConditionsEnforcer admission controller
Whenever the ConditionalAuthorization feature gate is enabled in the API
server, there is an AuthorizationConditionsEnforcer validating admission
controller whose job it is to evaluate the conditions, and enforce the decision
that the condition set evaluated to. If the ConditionalAuthorization feature
gate is enabled, but the user disables the AuthorizationConditionsEnforcer
admission controller, k8s.io/apiserver options validation errors, and thus
won’t the API server start in this setting. This is critical, as there must not
be a case where the feature would be enabled, but there would be no enforcement.
The validating admission controller operates on a fully-mutated request object just like other validating admission controllers, by design.
It is proposed that the AuthorizationConditionsEnforcer is the first
validating admission plugin to run; such that e.g. no validating webhooks need
to execute unnecessarily.
Changes to (Self)SubjectAccessReview
One of the core goals of this KEP is to make it easier also for users subject to
authorization policies that span authorization and admission understand what
policies they are subject to. This in practice means that the conditions should
be shown in (Self)SubjectAccessReview (SAR) responses, as is logical when the
authorizer response area grows. However, there are some details to pay attention
to:
- The same request might be subject to multiple conditional authorizers in the
authorizer chain. Consider a chain of two authorizers both returning a
Conditionaldecision. The first authorizer’s returnedConditionSetwill have precedence over the second, and thus cannot be merged into one. Instead, theSubjectAccessReviewresponse must retain the ordering of the twoConditionSets, so the user can reason about them. - Consider a two-authorizer chain, where the first returns a
Conditionaldecision, and the secondAllow. As theConditionalresponse could evaluate toDeny(if that there areeffect=Denyconditions), the structure must be able to model both conditional and concrete decisions.
The SubjectAccessReviewStatus API is thus augmented with the following field and
types:
type SubjectAccessReviewStatus struct {
// ... Allowed, Denied, Reason and EvaluationError here as normal
// ConditionSetChain is an ordered list of condition sets, where every item
// of the list represents one authorizer's ConditionSet response.
// When evaluating the conditions, the first condition set must be evaluated
// as a whole first, and only if that condition set
// evaluates to NoOpinion, can the subsequent condition sets be evaluated.
//
// When ConditionSetChain is non-null, Allowed and Denied must be false.
//
// +optional
// +listType=atomic
ConditionSetChain []SubjectAccessReviewConditionSet `json:"conditionSetChain,omitempty"`
}
type SubjectAccessReviewConditionSet struct {
// Allowed specifies whether this condition set is unconditionally allowed.
// Mutually exclusive with Denied, Conditions, and ConditionSetChain.
Allowed bool `json:"allowed,omitempty"`
// Denied specifies whether this condition set is unconditionally denied.
// Mutually exclusive with Allowed, Conditions, and ConditionSetChain.
Denied bool `json:"denied,omitempty"`
// FailureMode specifies the failure mode for this condition set.
// Only relevant if the conditions are non-null.
FailureMode string `json:"failureMode,omitempty"`
// AuthorizerName specifies the authorizer name, unique within the server,
// that authored these conditions. This is used by kube-apiserver to correlate
// conditions that need to be evaluated through the AuthorizationConditionsReview API.
AuthorizerName string `json:"authorizerName"`
// ConditionsType describes the type of all conditions in the Conditions slice.
// It does not apply at all to nested conditions in ConditionSetChain.
//
// Mutually exclusive with Allowed, Denied, and ConditionSetChain.
ConditionsType string `json:"authorizerName,omitempty"`
// Conditions is an unordered set of conditions that should be evaluated
// against admission attributes, to determine
// whether this authorizer allows the request.
//
// Mutually exclusive with Allowed, Denied, and ConditionSetChain.
//
// +listType=map
// +listMapKey=id
// +optional
Conditions []SubjectAccessReviewCondition `json:"conditions,omitempty"`
// ConditionSetChain is an ordered list of condition sets, where every item
// of the list represents one authorizer's ConditionSet response.
// When evaluating the conditions, the first condition set must be evaluated
// as a whole first, and only if that condition set
// evaluates to NoOpinion, can the subsequent condition sets be evaluated.
//
// This field is used by composite authorizers, such as the kube-apiserver,
// that in turn delegate their decisions to other sub-authorizers.
//
// Mutually exclusive with Allowed, Denied, and Conditions.
//
// +optional
// +listType=atomic
ConditionSetChain []SubjectAccessReviewConditionSet `json:"conditionSetChain,omitempty"`
}
type SubjectAccessReviewCondition struct {
ID string `json:"id"`
Effect SubjectAccessReviewConditionEffect `json:"effect"`
Condition string `json:"condition"`
Description string `json:"description,omitempty"`
}
type SubjectAccessReviewConditionEffect string
const (
SubjectAccessReviewConditionEffectAllow SubjectAccessReviewConditionEffect = "Allow"
SubjectAccessReviewConditionEffectDeny SubjectAccessReviewConditionEffect = "Deny"
SubjectAccessReviewConditionEffectNoOpinion SubjectAccessReviewConditionEffect = "NoOpinion"
)
Status.ConditionSetChain is mutually exclusive with Status.Allowed and
Status.Denied. A conditional response is characterized by
Status.ConditionSetChain != null. Old implementers that do not recognize
Status.ConditionSetChain will just safely assume it was a NoOpinion.
The spec field is augmented to add the ConditionsMode, as described above:
type SubjectAccessReviewSpec struct {
// ConditionalAuthorization specifies caller-specified configuration related
// to conditional authorization. If unset, conditions are not supported.
ConditionalAuthorization *ConditionalAuthorizationConfiguration `json:"conditionalAuthorization,omitempty"`
// ... other field as usual.
}
// ConditionalAuthorizationConfiguration is its own struct/field, to allow
// possible future expansion of caller-provided knobs (e.g. for version skew).
type ConditionalAuthorizationConfiguration struct {
// Mode describes
// a) if the caller supports or wants conditions to be returned, and
// b) if supported, how (preferably) conditions should be returned.
// To indicate no support for conditional authorization, leave this field empty.
// An authorizer must never return conditions when this field is empty.
// However, respecting the caller's wish of presentation mode="HumanReadable"
// or "Optimized" is voluntary for the authorizer.
Mode ConditionsMode `json:"mode,omitempty"`
}
Supporting webhooks through the AuthorizationConditionsReview API
The webhook authorizer is augmented to support webhooks returning an ordered
list (chain) of authorizer decisions, not just one decision. Supporting multiple
returned decisions is required e.g. by aggregated API servers, that consult
kube-apiserver as a webhook, which in turn can return more than one
conditional decision. Note that aggregate API servers’ evaluation is thus always
in practice eager (not lazy). This is considered acceptable though.
How should the webhook authorizer evaluate potentially opaque conditions? Unless
the API server can evaluate the conditions returned by the webhook natively,
another webhook needs to be made. To facilitate this, a new
AuthorizationConditionsReview API, very similar to AdmissionReview is added.
Because kube-apiserver acts as a webhook server, kube-apiserver must also
serve this API. The AuthorizationConditionsReview API implementation is not
subject to admission in kube-apiserver. A sketch of the new API is as follows:
// AuthorizationConditionsReview describes a request to evaluate authorization conditions.
type AuthorizationConditionsReview struct {
metav1.TypeMeta `json:",inline"`
// Request describes the attributes for the authorization conditions request.
// +optional
Request *AuthorizationConditionsRequest `json:"request,omitempty"`
// Response describes the attributes for the authorization conditions response.
// +optional
Response *AuthorizationConditionsResponse `json:"response,omitempty"`
}
// AuthorizationConditionsRequest describes the authorization conditions request.
type AuthorizationConditionsRequest struct {
// TODO: Do we want UID like AdmissionReview here? I guess we don't need it.
// ConditionSetChain is an ordered list of condition sets, where every item
// of the list represents one authorizer's ConditionSet response.
// When evaluating the conditions, the first condition set must be evaluated
// as a whole first, and only if that condition set
// evaluates to NoOpinion, can the subsequent condition sets be evaluated.
//
// Composite authorizers (such as the kube-apiserver), which delegate their
// decisions to other sub-authorizers, might return a ConditionSetChain in
// SubjectAccessReview. That ConditionSetChain must be sent back unmodified
// by the client in this field.
//
// This list contains exactly one element for non-composite authorizers,
// which returned a simple list of their own conditions in SubjectAccessReview.
//
// +optional
// +listType=atomic
ConditionSetChain []SubjectAccessReviewConditionSet `json:"conditionSetChain,omitempty"`
// All fields present in the ConditionData interface, not exhaustively listed
// in this KEP for brevity.
}
// AuthorizationConditionsResponse describes an authorization conditions response.
type AuthorizationConditionsResponse struct {
// TODO: Do we want UID like AdmissionReview here? I guess we don't need it.
// Allowed indicates whether or not the request is authorized according to
// the authorization conditions.
// Mutually exclusive with Denied.
// Allowed=false and Denied=false means that the authorizer has no NoOpinion on the request.
Allowed bool `json:"allowed"`
// Denied indicates whether or not the request is denied according to the authorization conditions.
// Mutually exclusive with Allowed.
// Allowed=false and Denied=false means that the authorizer has no NoOpinion on the request.
Denied bool `json:"denied,omitempty"`
// Reason describes a reason for the concrete decision
Reason string `json:"reason,omitempty"`
// EvaluationError describes a possible error that happened during evaluation.
EvaluationError string `json:"evaluationError,omitempty"`
// ConditionSetChain is an ordered list of condition sets, where every item
// of the list represents one authorizer's ConditionSet response.
// When evaluating the conditions, the first condition set must be evaluated
// as a whole first, and only if that condition set
// evaluates to NoOpinion, can the subsequent condition sets be evaluated.
//
// In order to support constrained impersonation that is also conditional
// on the object, evaluating a ConditionSet might yield another ConditionSet.
//
// When ConditionSetChain is non-null, Allowed and Denied must be false.
//
// +optional
// +listType=atomic
ConditionSetChain []SubjectAccessReviewConditionSet `json:"conditionSetChain,omitempty"`
// TODO: Add AuditAnnotations and/or Warnings as in AdmissionReview?
}
In the aggregated API server case, there is automatic configuration to evaluate
the conditions through a
POST /apis/authorization.k8s.io/v1alpha1/admissionconditionsreviews.
User-configured webhooks supply the URL to call the evaluation endpoint through
a dedicated context in the supplied kubeconfig:
apiVersion: apiserver.config.k8s.io/v1
kind: AuthorizationConfiguration
authorizers:
- type: Webhook
name: webhook
webhook:
# New: Encode the endpoint for resolving the conditions as a KubeConfig
# context. If unset, conditional authorization is not supported.
# The authentication info and service hostname can be the same, but most
# likely the HTTP endpoint of the authorizer service is different.
# The authorizer MUST support evaluating any condition type it returns
# in the SubjectAccessReview.
conditionsEndpointKubeConfigContext: authorization-conditions
# New: What version of the AuthorizationConditionsReview to use.
# This field has no default.
authorizationConditionsReviewVersion: v1alpha1
# Existing struct, pointer to KubeConfig file where the context exists
connectionInfo:
type: KubeConfigFile
kubeConfigFile: /kube-system-authz-webhook.yaml
Finally, recall that the webhook authorizer by default caches requests. Any
authorizer that utilizes caching, must also cache all conditions of the
Conditional decision. If that advice is followed, evaluation is always done
against a specific revision of the authorizers’ underlying policy store, without
the authorizer needing to implement snapshot reads.
If Kubernetes supports evaluating the conditions in-process with a builtin
ConditionsEvaluator, e.g. the proposed CEL one, a
AuthorizationConditionsReview webhook is not needed, as per the following
table:
| Webhooks during phase: | Authorization response not cached | Authorization response cached |
|---|---|---|
| Condition Type Not Supported by Builtin Condition Evaluators | Authorize() + EvaluateConditions() | EvaluateConditions() |
| Condition Type Supported | Authorize() | Neither |
Composite / Union Authorizer Support
Some authorizers, like kube-apiserver itself, do not perform authorization logic themselves, but instead delegates actual authorization decisions to a set of ordered sub-authorizers. As long as there are no clearly circular dependencies in the authorizer call chain, this is supported. Consider the following example:

A user sends a conditionally-authorized request (e.g.
create) to an aggregated API server.The aggregated API server, as per our contract, must be configured with the
kube-apiserveras its first webhook authorizer, and thus sends aSubjectAccessReviewto it.kube-apiserverin turn is configured with a webhook authorizerfoo, to which it sends anotherSubjectAccessReview.fooresponds with two ConditionSets (each ConditionSet which maps to an authorizer-internal concept of “tiers”, modelled as a composite authorizer of two smaller authorizers:systemanduser). The response is:kind: SubjectAccessReview status: allowed: false conditionSetChain: - authorizerName: system # Supported by both the kube-apiserver and the aggregated API server conditionsType: k8s.io/cel conditions: - id: foo-system-1 effect: Deny condition: <something> - authorizerName: user conditionsType: foo-opaque-type conditions: - id: foo-user-1 effect: NoOpinion condition: <something> - id: foo-user-2 effect: Allow condition: <something>Although it is at this stage known that the request can be authorized (if the
DenyandNoOpinionconditions are false, and theAllowcondition is true), the evaluation of the authorizer chain proceeds eagerly until an unconditional response is found, or the end of the chain is reached. (See this section for a comparison of eager and lazy evaluation, lazy evaluation is only available when the authorizer resides in the same process as the request)Thus,
kube-apiserverperforms a webhook to authorizerbar, which responds with one ConditionSet of oneeffect=Allowcondition.kind: SubjectAccessReview status: allowed: false conditionSetChain: - authorizerName: whatever # Supported by both the kube-apiserver and the aggregated API server conditionsType: k8s.io/cel conditions: - id: bar-1 effect: Allow condition: <something>As
baralso responded with a conditional allow, the authorizer queries next the Node authorizer, which responds withNoOpinion, and finally, the RBAC authorizer responds withNoOpinion.If an unconditional response would have been found,
kube-apiserverwould have been able to soundly short-circuit the evaluation. Now it reached the end of the authorizer chain, and thus returned the following aggregated response to the aggregated API server is:kind: SubjectAccessReview status: allowed: false conditionSetChain: - authorizerName: foo conditionSetChain: # nested composite authorizer - authorizerName: system conditionsType: k8s.io/cel conditions: - id: foo-system-1 effect: Deny condition: <something> - authorizerName: user conditionsType: foo-opaque-type conditions: - id: foo-user-1 effect: NoOpinion condition: <something> - id: foo-user-2 effect: Allow condition: <something> - authorizerName: bar # authorizer name in kube-apiserver, "whatever" was ignored. conditionsType: k8s.io/cel conditions: - id: bar-1 effect: Allow condition: <something> # node authorizer ignored, as it responded NoOpinion # - authorizerName: node # denied: true # unconditional deny # rbac authorizer ignored, as it responded NoOpinion # if it would have answered Allow, it would have been communicated as: # - authorizerName: rbac # allowed: true # unconditional allowThe aggregated API server sees that given this aggregate conditional response from
kube-apiserver, the request can become authorized, if certain conditions are met, so it saves the conditions in the request context, and proceeds with the request.Next, the
AuthorizationConditionsEnforceradmission controller enforces that the conditions hold. It walks the ConditionSets from top to bottom. First up is thefooauthorizer’ssystemConditionSet, which uses the condition typek8s.io/celwhich is supported by the aggregated API server. Evaluation of thefoo-system-1condition is thus directly done against the object, which yieldsfalse, in other words, the Deny condition does not apply, and thus can the evaluation proceed.The
fooauthorizer’suserConditionSet uses the opaque condition typefoo-opaque-typewhich cannot readily be evaluated by the aggregated API server, and thus does the aggregated API server send the following request tokube-apiserver:kind: AuthorizationConditionsReview spec: conditionSetChain: - authorizerName: foo conditionSetChain: # The "system" ConditionSet was already evaluated to NoOpinion, # and is thus omitted - authorizerName: user conditionsType: foo-opaque-type conditions: - id: foo-user-1 effect: NoOpinion condition: <something> - id: foo-user-2 effect: Allow condition: <something> - authorizerName: bar conditionsType: k8s.io/cel conditions: - id: bar-1 effect: Allow condition: <something> object: {} # request object # ... + other metadatakube-apiservercan correlate what authorizer authored what condition through theauthorizerNamefield, and thus calls thefooauthorizer’sEvaluateConditionsmethod. In the webhook authorizer case, this yields another webhook to the foo authorizer:kind: AuthorizationConditionsReview spec: conditionSetChain: # The "system" ConditionSet was already evaluated to NoOpinion, # and is thus omitted - authorizerName: user conditionsType: foo-opaque-type conditions: - id: foo-user-1 effect: NoOpinion condition: <something> - id: foo-user-2 effect: Allow condition: <something> object: {} # request object # ... + other metadataAuthorizer
fooauthorizer responds with aNoOpinion, as both thefoo-user-1andfoo-user-2conditions evaluated tofalse:kind: AuthorizationConditionsReview spec: {} status: allowed: false denied: falseNext,
kube-apiserverevaluates the ConditionSet frombar. These conditions are of the built-in CEL condition type, so thekube-apiservertries to directly evaluate them. However, the condition used a CEL functioncidr, which was introduced in, say v1.37, andkube-apiserveris of v1.36. As the “fast-path” of in-tree evaluation failed, thekube-apiserverfalls back to webhook evaluation and sends this to authorizerbar:kind: AuthorizationConditionsReview spec: conditionSetChain: - authorizerName: bar conditionsType: k8s.io/cel conditions: - id: bar-1 effect: Allow condition: <something> object: {} # request object # ... + other metadataThe
barauthorizer who built a CEL condition using thecidrfunction, naturally also has a CEL environment that is capable of evaluating it, which means that evaluation succeeds. The result is againfalse(so that we can illustrate the whole end-to-end flow), which means the ConditionSet as a whole evaluates toNoOpinion, in the same way asfoo.As both the ConditionSet of
fooandbarevaluated toNoOpinion, does thekube-apiserveralso returnNoOpinionto the aggregated API server.As evaluation of the aggregated API server’s first authorizer’s conditional allow turned out to be
NoOpinion, evaluation of the other authorizers in the chain is lazily resumed.Thus, is the
Authorizemethod on the RBAC authorizer called. Say it also returnsNoOpinion.Finally,
Authorizeon webhookbazis called, which sends aSubjectAccessReviewtobaz. Say the response is the following:kind: SubjectAccessReview status: allowed: false conditionSetChain: - authorizerName: baz conditionsType: k8s.io/cel conditions: - id: baz-1 effect: Allow condition: <something> - id: baz-2 effect: Deny condition: <something> - id: baz-3 effect: Allow condition: <something>Because the aggregated API server supports evaluating
k8s.io/cel, it evaluates the conditions in order of importance, that is, Deny conditions (baz-2) first. Asbaz-2returnsfalse, Allow conditionsbaz-1andbaz-3are evaluated tofalseandtruerespectively. As either of them returnedtrue(order of conditions within a set does not matter, only ordering of the sets themselves), thebazConditionSet evaluates to anAllow. Thus is the request allowed, and can proceed to other validating admission controllers.
Built-in CEL conditions evaluator
The most logical alternative for Kubernetes to provide as a builtin primitive is
a CEL conditions evaluator. Such a conditions evaluator could re-use most of the
CEL infrastructure that Kubernetes already has, and provide a unified model for
those that already are familiar with ValidatingAdmissionPolicies. This means
that a wide variety of authorizers could author CEL-typed conditions, and let
the API server evaluate them without a need for a second webhook. RBAC++ could
use this as well.
However, this evaluator could evolve with distinct maturity guarantees than the core conditional authorization feature.
The observant reader noticed that Decision.Evaluate takes a list of
BuiltinConditionSetEvaluator as input, which allow evaluating the conditions
in-process, without potentially sending webhooks back to the authorizer. A
BuiltinConditionSetEvaluator is just a normal ConditionSetEvaluator, but
scoped to just a set of supported types:
package authorizer
type BuiltinConditionSetEvaluator interface {
ConditionSetEvaluator
// SupportedConditionTypes defines the condition types that the builtin
// evaluator can assign truth values to in-process.
SupportedConditionTypes() sets.Set[ConditionType]
}
To avoid having to parse the AST from a string (which is relatively expensive),
there could be an optimized mode in which the CEL evaluator can evaluate a
binary-encoded AST directly, to get performance on par with e.g.
ValidatingAdmissionPolicy, which also executes pre-compiled CEL programs.
The built-in CEL condition environment would be similar to that of
ValidatingAdmissionPolicy, including the ability to perform secondary
authorization checks through the builtin authorizer function. This allows an
authorizer at any point in the authorizer chain to respect other authorizers in
their configured order for secondary authorization checks. This also makes the
authorization layer aware of API author-designated secondary checks, e.g. the
designer of the CertificateSigningRequest API can require any writer of its
objects to also have the sign permission of some signer resource.
One important point of note is that the authorizer returning conditions might
not know what the caller’s (enforcement point’s) CEL capabilities are. Consider
that the authorizer that wants to return a condition, which can be encoded in
CEL form. However, if there would be two k8s-supported CEL condition types
k8s.io/authorization-cel-v1 and k8s.io/authorization-cel-v2, the authorizer
needs to naturally choose to encode its condition in either form. However, if
the API server supports only v1, and the authorizer returned v2, or vice
versa, the API server cannot necessarily evaluate those conditions in-process
(if the formats do not round-trip between each other), but might have to “call
out” to the authorizer again (which can be either a webhook or simple function
call). This means that even in this case, the evaluation won’t fail, it might
just be slightly slower. If we decide it is worth it, we can add other
caller-provided knobs to SubjectAccessReview.spec.conditionalAuthorization in
the future.
However, if there was a change in the semantics of evaluating a certain
condition type that did not lead to a “major version bump”, that is, change of
condition type entirely, there might be risk that an authorizer returns a
condition that cannot be evaluated in-process. For example, in Kubernetes v1.36,
say Kubernetes would support evaluating conditions of form
k8s.io/authorization-cel in-process. If in v1.37, a new function was added to
the CEL environment (say, datetime or similar), a new v1.37 authorizer could
return a CEL condition referencing the new datetime function to an old v1.36
API server, which would error upon evaluation with “no such function exists”. If
this happens, the in-process optimized evaluation is ignored, and the API server
asks the authorizer to evaluate the conditions directly instead, which will lead
to the correct result, as the authorizer is new.
Feature availability and version skew
Conditional authorization is available when all of the following criteria are met:
- The authorizer implementation supports conditions, which can be done in two ways:
- In-tree authorizer: through implementing the
authorizer.ConditionSetEvaluatorinterface, and - Webhook authorizer: when needed, responds with a non-null
.status.conditionSetChainarray, along with.status.allowed=falseand.status.denied=false.
- In-tree authorizer: through implementing the
- The
ConditionalAuthorizationfeature gate is enabled AND theAuthorizationConditionsEnforceradmission plugin is enabled- The
AuthorizationConditionsEnforcerplugin could be enabled by default, as it returnsHandles=<featureGateEnabled>. - However, to avoid the problematic configuration of a server being set up with the feature gate enabled, but not the admission plugin, the proposal is that AdmissionOptions.Validate will error, such that the API server can never start up in such a misconfigured state.
- The
- The SubjectAccessReview’s
apiGroup,resourceandapiVersionselects exactly one GVR (no wildcards allowed), which is served by the current API server, and the verb is one ofcreate,update,patch,delete, ordeletecollection. In the future, one could consider conditional authorization for reads as well (see below).
| Version skew matrix | Old API server | New API server |
|---|---|---|
| Old webhook | Conditions never returned | Conditions never returned from webhooks |
| New webhook | Webhook respects ConditionsModeNone and never returns a conditional response | Conditions respected if asked for |
Other Kubernetes authorization enforcement points, with and without conditions-awareness
In the following section, relevant applications of the conditional authorization
feature are listed. Existing Authorize calls that not mentioned here to
specifically support conditional authorization, do not support it, and will fail
closed upon seeing on any conditions.
One thing that needs to be taken into account for secondary authorization
checks: today some of the checks set APIVersion="*" (for unknown reason) when
there is no logical API version at hand. If such checks would need to start
supporting conditional authorization, we’d need to propagate a concrete, logical
API version instead, as conditional authorization requires API version to be
concrete (not a wildcard).
Compound Authorization for Connectible Resources
After the move to WebSockets
(KEP 4006
),
connect requests are initially authorized as e.g. get pods/exec, which can
lead someone thinking that giving get * gives only read-only access, and not
also write access. To mitigate this privilege-escalation vector, when the
AuthorizePodWebsocketUpgradeCreatePermission feature gate is enabled (beta and
on by default in 1.35), currently pods/attach, pods/exec and
pods/portforward are subject to compound authorization, where effectively it
is made sure that the requestor also is authorized to create the corresponding
connectible resource. However, this check is not added (yet at least) for
pods/proxy, services/proxy and nodes/proxy.
In relation to these two workstreams, it is proposed that we uniformly and
generally require the requestor to have the create verb using compound
authorization in the ConnectResource handler, whenever the feature gate (or a
new one) is enabled. Both the initial (get) and compound (create) check
would support conditional authorization, with operation == CONNECT,
object == <connect-data> (e.g. PodExecOptions), oldobject == null, and
options == null, just like connect admission today.
Such a check thus becomes a generalization of KEP-2862: Fine-grained Kubelet
API
Authorization
,
as now an authorizer can say “allow lucas to create nodes/proxy, but only when
options.path == "/configz"”, or any other such policy that the administrator
might fancy.
Compound Authorization for update/patch → create
If an update or patch turns into a create, the API server performs compound
authorization to make sure the requestor also has the privilege to create the
resource. This KEP also applies conditional authorization support for this
compound authorization check.
Constrained Impersonation through Conditional Authorization
KEP-5284: Constrained Impersonation
proposes a way to restrict impersonation such that the requestor both needs the
permission to impersonate the specified user, but the permission to impersonate
certain types of requests, e.g.
lucas can only impersonate node foo, but only to get pods. This is a perfect
example of where conditional authorization shines; the request that is being
performed is the initially-unknown data that an authorizer might want to specify
conditions on.
Consider the example of
lucas can only impersonate node foo, but only to get pods. The authorizer
policy (in pseudo-code) is of form:
request.userInfo.username == "lucas" &&
request.verb == "impersonate" &&
request.resource == "nodes" &&
request.apiGroup == "authentication.k8s.io" &&
request.name == "foo" &&
impersonatedRequest.apiGroup == "" &&
impersonatedRequest.resource == "pods" &&
impersonatedRequest.verb == "get"
The first five ANDed expressions can be evaluated to true directly, just based
on the data that is available in the normal impersonation SubjectAccessReview.
However, impersonatedRequest is unknown, and thus does the residual expression
yield conditions in the SubjectAccessReview response, e.g. as follows:
apiVersion: authorization.k8s.io/v1
kind: SubjectAccessReview
status:
allowed: false
conditionSetChain:
- conditionsSet:
- type: k8s.io/authorization-cel
id: "lucas-only-impersonate-node-get-pods"
condition: |
impersonatedRequest.apiGroup == "" &&
impersonatedRequest.resource == "pods" &&
impersonatedRequest.verb == "get"
effect: Allow
Now, the impersonation filter can evaluate the condition, either through the
builtin CEL evaluator (if applicable), or by calling the Authorizer’s
EvaluateConditions endpoint with the missing data (the information about the
impersonated request).
This approach supports the use-cases of the existing Constrained Impersonation KEP, but also other types of expressions, for instance:
- “A ServiceAccount
ai-agent-foocan only impersonate userlucasif it also at the same time impersonates groupai-agent-foo”- This allows attenuating what an impersonator can do through generic deny rules for the given additional group.
- “ServiceAccount
ai-agent-foocan impersonatealiceonly for read requests, but the same ServiceAccount can impersonatebobfor any action”- The current Constrained Impersonation KEP does not allow distinguishing what the impersonator can do for what target user.
In the future, it would be possible to even restrict impersonation based on the
object and oldObject. For example, consider the following abstract policy
which allows lucas to impersonate any user for create and update requests,
but only if lucas annotates changed resources with the lucas-impersonated=true
label:
request.userInfo.username == "lucas" &&
request.verb == "impersonate" &&
request.resource == "users" &&
request.apiGroup == "" &&
impersonatedRequest.verb in ["create", "update"] &&
has(object.metadata.labels["lucas-impersonated"]) &&
object.metadata.labels["lucas-impersonated"] == "true"
For fully evaluating this request, three stages are needed.
First, in the normal
SubjectAccessReview, only metadata about the requesting user, and the user to be impersonated can be populated in the SAR fields. The user to be impersonated is modelled as the “resource”, and the verb isimpersonate. In the CEL environment, this data corresponds to therequestvariable. For an applicable request for the policy above, the condition produced look like:impersonatedRequest.verb in ["create", "update"] && has(object.metadata.labels["lucas-impersonated"]) && object.metadata.labels["lucas-impersonated"] == "true"The impersonation filter does know up front, however, the metadata of the request that is being performed, and if some authorizer returned a conditional response in the first stage, the impersonation filter could directly afterwards run
EvaluateConditions()/AdmissionConditionsReviewwith more information: namely theimpersonatedRequestmetadata. After this step, the condition produced by partial evaluation with this additional information yields:has(object.metadata.labels["lucas-impersonated"]) && object.metadata.labels["lucas-impersonated"] == "true"As impersonation might happen in an authenticating front proxy (e.g.
kube-apiserver), but object decoding and admission run in another process (e.g. an aggregated API server), the impersonation filter running in the former process does not have access to the request object. Thus, if we allowed impersonation expressing conditions on the request/stored object, the condition residual shown in 2. needs to be propagated from the front proxy to the aggregated API server.- One backwards-compatible way to do this in practice, is to reserve one
userinfo extra key (e.g.
k8s.io/impersonation-conditions) to propagate the conditions. As we already require and assume that the aggregated API server uses the front proxykube-apiserveras its first webhook authorizer,kube-apiservercan treat the conditions found in the userextra as the first deny-only conditional authorizer, and thus return theseobject-scoped conditions to the aggregated API server like usual. - The conditions-aware impersonation authorizer would need to either cache the conditions internally using a context key, or pass the conditions transparently to the client, so that it can evaluate those later itself when passed back.
- One backwards-compatible way to do this in practice, is to reserve one
userinfo extra key (e.g.
Initially, however, we do not need to go this far as to implement object-level constraints during impersonation, but this design should be future-proof as keeps the option open.
Node authorizer
The Node authorizer was the first conditional authorizer in that it had both an
authorization and admission part that always were designed and evolved in
tandem. This proposal generalizes this; now the Node authorizer could return
conditional responses with type e.g. k8s.io/node-authorizer and either
transparent conditions written in CEL, if possible, or opaque ones, e.g.
condition: '{"condition": "require-pod-node-name", "nodeName": "foo"}'.
In the opaque condition case, the Node authorizer will get a callback on its
then-added EvaluateConditions() function to, even in native code, enforce e.g.
a Pod’s spec.nodeName actually matches what it should be. If this were the case,
all logic is centralized in the authorizer, instead of being split between two
components, and SubjectAccessReview shows what policies apply.
ValidatingAdmissionPolicies
ValidatingAdmissionPolicies support secondary authorization checks through the
authorizer function in the CEL environment. This could be used, for example, to
check that the requestor also has the
sign certificates.k8s.io signers <signerName> permission, for the signer
specified in the CertificateSigningRequest API.
Secondary checks in VAP could support conditionally-authorized requests too,
given that the secondary check using the authorizer also supplies the relevant
object and/or oldobject against which conditions could be written. However,
this does not seem to be a major use-case, as most secondary checks that have
been seen in the wild check permissions against some resource, subresource or
verb that is not served by the API server.
The loss of power of not supporting conditions in secondary VAP checks is minor.
The VAP authorizer can perform secondary permission checks for permissions
independently from each other, e.g. “this write request of the VM resource is
only allowed if the requestor also has the backup privilege on the referenced
storage bucket (when backups are enabled) and when the requestor can use the
referenced network”. Without conditional authorization in this context, the
authorizer cannot express an intersection of the two, e.g. “the requestor can
only backup VM objects using this storage bucket when the VM is using this
given network”.
If conditions were supported in VAP, it would be possible to decouple the authorization policy (in the authorizer) from the enforcement code (in VAP), as long as the data exchanged between them is consistent. Without conditions supported in VAP, the enforcement code there needs to choose based on what attributes to perform secondary checks.
To give users more information about what they can do (for debugging), the
SelfSubjectAccessReview endpoint could also show partially-evaluated CEL from
ValidatingAdmissionPolicy objects. However, if this is done, the user could
know better how to create an object that passes authorization and admission, for
better and worse.
deletecollection support
Although not immediately obvious, conditional authorization would also work for
verb=deletecollection requests. In this case, the condition is written just as
it would for verb=delete, the same admission chain (which has the conditions
enforcement as the first validating admission plugin) is run once for all
objects.
Complete list of all Authorize calls in kube-apiserver
k8s.io/kubernetes/pkg/certauthorization.IsAuthorizedForSignerName: Secondary authorization check for thesign/approve/attestvirtual verbs on thecertificates.k8s.io signersresource.- Does not support conditions (for now at least), for the same reasoning as VAP.
k8s.io/kubernetes/pkg/kubelet/server.InstallAuthFilter: Primary authorization for the kubelet server.- Would support conditions, so that conditions can apply to the path called of
the
nodes/proxyresource.
- Would support conditions, so that conditions can apply to the path called of
the
k8s.io/kubernetes/pkg/registry/admissionregistration/{validating,mutating}admissionpolicy{,binding}Performs secondary checks of the requestor being able togetthe referenced parameter resource object or all objects.- Does not support conditions, verb is
get
- Does not support conditions, verb is
k8s.io/kubernetes/pkg/registry/authorization/{local,self,}subjectaccessreview: Serving the SAR endpoints- Would be conditions-aware, so conditions can be propagated further (e.g. to aggregated API servers)
k8s.io/kubernetes/pkg/registry/core/pod/rest.ensureAuthorizedForVerb: Ensures that the requestor also has thecreateverb on certain connectible subresources for pods, as discussed above.- Would be conditions-aware, as discussed above .
k8s.io/kubernetes/pkg/registry/rbac: Verifies that a user cannot privilege-escalate their permissions when creating roles and bindings. If the user indeed would try to privilege-escalate, allow them to do so if they have theescalateorbindverbs. This check would not support conditions.k8s.io/kubernetes/plugin/pkg/admission/gc: Verifies that ownerReferences are correctly set. Someone that updates an object’s ownerReferences, need to be able to delete that object. In addition, if an owner reference of an create or update is blocking, the requestor either needs permission toupdate */finalizers, or on the owner resource. These checks would not support conditions.k8s.io/kubernetes/plugin/pkg/admission/noderestriction: Ensures that a node can only issue ServiceAccount tokens for allowed audiences. If the requested audiences are not found in the PodSpec, a secondary check torequest-serviceaccounts-token-audience <audience>of the ServiceAccount’s name/ns is issued. These checks would not support conditions.k8s.io/apiserver/plugin/pkg/admission/plugin/authorizer/caching_authorizer.go: This authorizer caches responses from SAR requests issued from e.g.ValidatingAdmissionPolicysecondary checks. This could eventually support caching conditional checks, but initially, it is not needed, as long as all such secondary checks do not support conditions.k8s.io/apiserver/pkg/cel/library: Implements the CEL functions for VAP secondary checks. These do not support conditions initially at least, but this could be expanded in the future as discussed above.k8s.io/apiserver/pkg/endpoints/filters: This is the mainWithAuthorizationentrypoint to make conditions-aware as part of this proposal. It also contains the impersonation code that could be made conditional.k8s.io/apiserver/pkg/endpoints/handlers/delete.go: Authorizes use of unsafe deletion without reading the object, as a special case. This would not support conditions.k8s.io/apiserver/pkg/endpoints/handlers/update.go: Secondary check forupdaterequests turning into acreaterequest.
Authorizer requirements
To recap, the authorizer must adhere to the following requirements to be considered functional:
If the
ConditionsModeisNone(that is, unset), no conditions must be returned, and it is up to the authorizer if it should fold a response it wanted to be conditional to eitherNoOpinionorDeny.If any of the conditions that the authorizer would have returned would have been of
effect=Deny, it is recommended for the authorizer to fold todecision=Deny.Only ever produce a conditional response if producing an unconditional response is not possible.
- The effect of authorizer-internal policies determines this. If there is an
authorizer which has
effect=Allow|NoOpinion (soft deny)|Deny (hard deny)policies, then the strength of policies are ordered asDeny(unconditional) >Deny(conditional) >NoOpinion(unconditional) >NoOpinion(conditional) >Allow(unconditional) >Allow(conditional). - For example, if an unconditional
Denypolicy matches, the output is always an unconditionalDeny, regardless of other matches. - If no
DenyorNoOpinionpolicies match, only a conditional and unconditionalAllow, the unconditionalAllowtakes precedence. - However, if a conditional
Denypolicy matches together with an unconditionalAllow, the response needs to be conditional, as before producing a final response, one needs to know whether the conditionalDenywill override the unconditionalAllow.
- The effect of authorizer-internal policies determines this. If there is an
authorizer which has
The authorizer can only return a conditional response with an
effect=Allowif there is a path for the request to become authorized. All pruning that is possible to do with the initial authorizer Attributes MUST be used.- For example, a policy of form
object.metadata.labels.foo == "bar" && request.verb == "create"MUST not yield a conditional response for averb="update"request, as the LHS of&&is then alwaysfalse.
- For example, a policy of form
An authorizer must be API version-aware, and should only let a policy author refer to a field in a version-dependent manner, and/or validate that the policy applies successfully to all known versions.
- The request object version might not equal the storage version, and the API
server cannot necessarily convert between the versions (due to CRD
conversion webhooks failing). For in-tree, one can always convert without
errors and reasonably fast. The new (request) object is always the request
API version. The authorizer could ask the API server to convert, if we want,
but this is not necessarily error-free.
- TODO(Lucas): See what happens for a CRD + VAP, if the request version != storage version, or if the CRD schema changes.
- For example, VAP policies today use the latter technique, which rejects expressions that do not compile under all possible API version-specific CEL environments.
- Another technique could be to expose the object through a version-specific
fieldpath, e.g.
v1.spec.fooandv2.spec.barcould refer to logically the same value of a field that was renamed fromfooinv1tobarinv2.
- The request object version might not equal the storage version, and the API
server cannot necessarily convert between the versions (due to CRD
conversion webhooks failing). For in-tree, one can always convert without
errors and reasonably fast. The new (request) object is always the request
API version. The authorizer could ask the API server to convert, if we want,
but this is not necessarily error-free.
To fail closed when new API versions are added, the authorizer could automatically insert restrictions that only API versions that are referenced in the policy can yield an allowed response. In the following example, write requests fail closed for API version
v3until the policy author has had time to add the restriction specific to that version, as follows:request.verb in ["create", "update"] && if has(v1) then v1.spec.foo == "baz" else if has(v2) then v2.spec.bar == "baz" else falseAn authorizer must be able to evalute any condition they authored, such that the API server always can call the authorizer to evaluate the condition (regardless of in-process evaluation capabilities). An authorizer only ever evaluates its own conditions.
The authorizer must make sure their conditions are safe and performant to execute. In particular, any CEL condition that is returned to Kubernetes must be within reasonable CEL cost limits. The authorizer should reject policies that would be too costly to execute in the request path.
- Towards this end, it is highly recommended that the authorizer uses a non-Turing complete language, to avoid e.g. the [Halting problem] and remote code execution.
- As a worst-case example, if an authorizer accepted Python code as
conditions, then any Kubernetes user with
create authorizationconditionsreviewwould be able to send malicious conditions that remotely executes arbitrary code through a “condition” of formimport os; os.system("sudo nmap ...")
Risks and Mitigations
Showing the users what authorization conditions that they are subject to through
SelfSubjectAccessReview means that badly-written access control policies which
follow the pattern of [Security Through Obscurity] would be “broken”. For
example, if a user can only create pods conditioned on metadata.labels.foo == "42", the latter condition would be returned in the SelfSAR, whereas without
this information the user would need to “blindly” guess the number. However,
these kind of practices are highly discouraged, as they rarely are effective in
practice. For example, if the user could list pods otherwise (which in that
case would be a reasonable permission to have), they could most likely
reverse-engineer the conditions they are subject to, by seeing data that “got
through”.
This risk could be mitigated by the authorizer returning opaque conditions (which could not be efficiently evaluated in the API server), instead of transparent (e.g. CEL) ones. It is up to the authorizer’s discretion to know whether conditions are “secret” or can be shown to the user. For clarity to the user, it is recommended that conditions are shown when possible and reasonable.
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
The update of the authorizer interface will need to update any test which uses the authorizer, which could be quite many.
Unit tests
Existing code that will be affected by the change (along with existing unit test coverage):
k8s.io/apiserver/pkg/authorization/union:2026-02-10-88k8s.io/apiserver/plugin/pkg/authorizer/webhook:2026-02-10-86k8s.io/apiserver/pkg/endpoints/filters/authorization.go:2026-02-10-91
Testing will in addition be added as appropriate to new packages.
Integration tests
An integration test will be added for this feature in
k8s.io/kubernetes/test/integration/apiserver/cel/conditionalauthz, both when
the feature is enabled and disabled.
e2e tests
When the feature is enabled, tests will be added for user-facing functionality
such as the SubjectAccessReview API, and sending request objects which are
allowed and denied. In addition, one end to end test should make sure that the
feature also works as expected in a scenario with aggregated API servers.
Graduation Criteria
Alpha
- Refactoring of the authorizer interface is completed
- Feature implemented behind a feature flag
- Initial integration tests completed and enabled
Beta
- There is in-core use of this feature, e.g. the node authorizer and/or constrained impersonation.
- Gather feedback from developers and surveys
- Additional tests are in Testgrid and linked in KEP
- More rigorous forms of testing—e.g., downgrade tests and scalability tests
- All functionality completed
- All security enforcement completed
- All monitoring requirements completed
- All testing requirements completed
- All known pre-release issues and gaps resolved
Note: Beta criteria must include all functional, security, monitoring, and testing requirements along with resolving all issues and gaps identified
GA
- N (to be determined later) examples of real-world usage
- N installs
- Allowing time for feedback
- All issues and gaps identified as feedback during beta are resolved
Note: GA criteria must not include any functional, security, monitoring, or testing requirements. Those must be beta requirements.
Note: Generally we also wait at least two releases between beta and GA/stable, because there’s no opportunity for user feedback, or even bug reports, in back-to-back releases.
For non-optional features moving to GA, the graduation criteria must include conformance tests .
Version Skew Strategy
In general, old clients can safely talk to new conditional authorizers, as the
authorizer will notice that the old client did not explicitly opt into the new
behavior, and thus will the authorizer fold to NoOpinion or Deny as
appropriate.
However, rollouts of new AuthorizationConfig configurations in multi-API server
scenarios (or similar) such as shown in this picture might need some special care.

For this reason, it is recommended that conditions-aware authorizers with a loadbalancer in between itself and the client either:
- (preferred) configure the load balancer to perform a blue-green rollout of the
API server configuration, initially only sending requests to old API servers
(without the API server configured), and then, at once, sending requests to
new API servers.
- If the rollout is such that a conditional authorizer is removed in the new
configuration, the authorizer should send only
NoOpiniondecisions before the blue-green rollout, to make sure there are noAuthorizationConfigReviewrequests pending to be sent to it.
- If the rollout is such that a conditional authorizer is removed in the new
configuration, the authorizer should send only
- make the authorizer respond with
NoOpinionuntil it is known that the rollout of all intermediate kube-apiservers to the new configuration has completed.
Production Readiness Review Questionnaire
Feature Enablement and Rollback
How can this feature be enabled / disabled in a live cluster?
- Feature gate
- Feature gate name:
ConditionalAuthorization - Components depending on the feature gate:
kube-apiserver
- Feature gate name:
- Opt-in on an authorizer basis for webhook authorizers in
StructuredAuthorizationConfig- This can be done with no API server downtime.
Does enabling the feature change any default behavior?
No.
Can the feature be disabled once it has been enabled (i.e. can we roll back the enablement)?
Yes.
What happens if we reenable the feature if it was previously rolled back?
Nothing special, feature enablement is stateless.
Are there any tests for feature enablement/disablement?
Yes, there will be integration and/or end to end tests covering both the feature when enabled and disabled.
Rollout, Upgrade and Rollback Planning
How can a rollout or rollback fail? Can it impact already running workloads?
- If an administrator configures a webhook authorizer to support conditions on
the endpoint
/conditions, but the authorizer server for some reason does not serve this endpoint (correctly), this misconfiguration can fail conditionally authorized requests that otherwise could succeed. Before pointing the API server at a conditional endpoint, all replicas of the authorizer must support conditions.
What specific metrics should inform a rollback?
- High p50 and p99 latency of authorizer-handled and in-process conditions evaluation
- We could add a counter for how many authorizer requests (out of a total count) that produced an error, and then users can make sure this metric is not growing at a higher pace
Were upgrade and rollback tested? Was the upgrade->downgrade->upgrade path tested?
This has not been tested yet, but should not be a problem as all code is feature-gated.
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?
Audit annotations can surface information that a request was initially conditionally authorized.
How can someone using this feature know that it is working for their instance?
Submit a (Self)SubjectAccessReview whose response is known to be conditional,
and check if conditions are returned.
What are the reasonable SLOs (Service Level Objectives) for the enhancement?
Evaluation time of authorization builtin CEL conditions is of the same magnitude
as for ValidatingAdmissionPolicy.
What are the SLIs (Service Level Indicators) an operator can use to determine the health of the service?
- Metrics
- Metric name: Authorization latency and error rate (both should be low and steady)
- Components exposing the metric: kube-apiserver
- Instrumentation of conditional authorizer implementations external to Kubernetes
Are there any missing metrics that would be useful to have to improve observability of this feature?
N/A
Dependencies
Existing CEL libraries of Kubernetes.
Does this feature depend on any specific services running in the cluster?
Not in particular, but until there is a built-in authorizer that is conditional, the cluster administrator needs to make use of some webhook authorizer to enable conditionally-authorized responses.
Scalability
Will enabling / using this feature result in any new API calls?
There might be a slight increase in traffic between aggregated API servers and the kube-apiserver, due to aggregated API servers potentially asking the kube-apiserver to resolve authorization conditions.
Will enabling / using this feature result in introducing new API types?
A new API type AuthorizationConditionsReview is introduced, but it is not
stored in etcd.
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.
Will enabling / using this feature result in increasing time taken by any operations covered by existing SLIs/SLOs?
Authorization latency could increase.
Will enabling / using this feature result in non-negligible increase of resource usage (CPU, RAM, disk, IO, …) in any components?
No.
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?
Not applicable, the feature resides completely within the API server. However, as before, if the kube-apiserver is down, so are all aggregated API servers (unable to properly authorize requests).
What are other known failure modes?
Not known.
What steps should be taken if SLOs are not being met to determine the problem?
- Check the
AuthorizationConfiguration - Troubleshoot any webhook authorizers’ responses
- Check audit logs and metrics
TODOs
- TODO: Expand on this point of conditional vs composite authorization
- TODO: Add more wording on ReferenceGrants
- TODO: One might be able to infer the admission-time operation through whether only the request object is available (create), or both the stored and request object is (update)?
Alternatives Considered
Expose all conditions in AdmissionReview, and have admission plugins “acknowledge” the conditions
The SIG Auth meeting of September 4, 2025 concluded that this feature should support also condition types that are not built into Kubernetes. Thus does there need to be some way to evaluate the not-natively-supported conditions in the admission phase. The most logical way, would be to add some fields to AdmissionReview, and thus let admission webhooks let the API server know (besides the AdmissionReview’s primary response.allowed field) what the conditions evaluated to.
However, this turned out to be unnecessarily complicated in practice, when taking the idea further. Should all conditions from potentially every authorizer in the chain be sent to every admission webhook? Probably not.
Can an admission webhook choose to evaluate individual conditions of some specific authorizer, or does the admission webhook need to evaluate all conditions produced by a certain authorizer at once, returning the result of the whole condition set according to the defined semantics? The latter solution is much simpler for both users and implementers to understand, so probably the latter.
However, then, how can one know that a certain admission webhook has the right to acknowledge a certain authorizer’s conditions? What if the conditional authorizer is controlled by the cloud provider or infrastructure team, but a (malicious) user dynamically registers its own admission webhook that wants to acknowledge the conditions from the cloud provider’s authorizer? What happens if there are multiple (dynamically registered) admission webhooks that evaluated the same input data (conditions+request body) to two different outputs?
These questions led us to realize that the safest initial plan is to require a
1:1 mapping between the authorizer (registered through
AuthorizationConfiguration) and the authorizer’s condition enforcer. As normal
users anyways cannot dynamically register authorizers, there is no need to
dynamically register authorizer condition enforcers either for normal users.
Thus is the most logical place to register the authorizer’s condition enforcer,
in the same place the authorizer is defined in AuthorizationConfiguration.
In other words, only the authorizer itself can evaluate its own conditions in the admission phase, and all at once only (as a set), not partially.
Propagate an API server-generated request UID to both authorization and admission
This would have helped solve the atomicity concern, but it is not a full long-term solution, as it still relies on people setting up webhooks.
Only one ConditionSet exposed as part of SubjectAccessReview status
However, if only one condition set is exposed, it might be impossible for a user to understand what conditions it is subject to for a given request through a (Self/Local/Standard) SubjectAccessReview, as the first conditional response might be just a “deny dangerous operations”-type of conditional response.
The user should thus see all conditional allows and denies until there is an unconditional response.
Require the client to annotate its write request with field or label selectors
This would be a breaking change for clients, as whenever conditional authorizers would hit production usage, every client would need to annotate its every request with all selectors “just in case” some authorizer would make use of it, to higher the chances of getting authorized. This could duplicate a fair amount of the request data.
The other problem is updates: would the selector apply only to the request object, only to the stored one, or both at once.
Extract label and field selectors from the request and current object in etcd, and supply that to the authorization process
If the client was not required to send all this data, but the API server would decode the object to extract “just” label and field selectors, the DoS vector occurs, where a malicious party could send huge requests with bogus data, that the API server would decode before authorization takes place. In addition, would this make the authorization process state-dependent (if the selector would need to apply to both the request and stored object), something which is considered an explicit anti-pattern.
Do nothing, force implementers to implement all of this out of tree
Pros:
- No extra code is added to Kubernetes.
Drawbacks:
- Authorizers would need to fold a conditional allow into a concrete
Allowin authorization responses. This is confusing, and could easily be misunderstood, for example composite authorization ofupdaterequests turning intocreates would not respect conditions, which could be unsafe and unexpected. - Likewise, Constrained Impersonation could not be made more expressive.
- Requires the cluster administrator to install a validating webhook that never can become deleted (which normal admission webhooks can).
- Users would not see their conditions in the
SelfSAR, and e.g. conditional read policies (see below) would be more or less impossible to discover. - Two webhooks are always needed for every request, even if the condition could be expressed in CEL, which in theory would not require a webhook.
- Evaluation of policies is not atomic across authorization and admission phases. A full re-evaluation of all policies need to be done twice.
- Only one conditional authorizer could effectively be supported, instead of many in this framework.
- Without a clear framework on how to do this (with reference implementations), the risk of wrong or incompatible implementations is higher.
- Kubernetes could not use this itself, even though it could be useful.
Drawbacks
- Added complexity to core (complexity which today is either at the cost of expressiveness, or for users to deal with)
Appendix A: Further resources
- SIG Auth meeting June 4, 2025: meeting notes , video , slides
- SIG Auth Deep Dive on Conditional Authorization Sept 4, 2025: meeting notes , video , slides
- KubeCon Atlanta talk Nov 13, 2025: slides , video
- Proof of Concept Policy Author Interface implementation: upbound/kubernetes-cedar-authorizer
- Proof of Concept Kubernetes implementation: luxas/conditional_authz_4 branch
- Lucas Master’s thesis with detailed design information
Appendix B: Future addition sketch: Conditional Reads
Together with the Authorize with Selectors KEP , it becomes possible to provide policy authors to write unified policies across both authorization and admission, and reads and writes, at least whenever operating on field-selectable fields. A practical example would be “allow user Alice to perform any verb on PersistentVolumes, but only when spec.storageClassName is “dev”” (assuming storageClassName is/would become field-selectable).
Consider that before this KEP, a user might need to use two or even three different paradigms to protect both data and metadata across reads and writes:

But after this KEP, it is possible to use a unified paradigm for all types:

For a practical example of what this unified interface can look like, take a
look at Lucas’ proof of concept at
upbound/kubernetes-cedar-authorizer
,
in particular the
getting started guide
.
If this project proves generally useful, it can be donated to a fitting place in
the CNCF ecosystem (e.g. kubernetes-sigs or Cedar, which is now a CNCF Sandbox
project). For more detailed information about the project and the philosophy
behind it, take a look at
Lucas’ Master’s thesis
(written at Aalto University).
A concrete example of how a future version of Kubernetes could integrate this would be that authorizers are allowed to return conditions also on read requests. The syntax of the condition must be a generalized selector (most likely a subset of CEL) of a well-known condition type. Note that extraction of values from an object does not need to change, we can limit expressiveness to labels and existing simple JSONpath-based extractors.
Consider the following fictional authorizer chain decisions:
- Authorizer 1:
effect=Denycondition:metadata.labels.owner != "lucas"effect=NoOpinioncondition:metadata.labels.visible != "true"effect=Allowcondition:object.type == "k8s.io/basic-auth"effect=Allowcondition:metadata.labels.public == "true"
- Authorizer 2:
effect=Allowcondition:metadata.labels.env == "dev"
These conditions turn into the following boolean predicate:
isAuthorized(object) = !(object.metadata.labels.owner != "lucas") AND (
(
!(object.metadata.labels.visible != "true") AND
(
(object.type == "k8s.io/basic-auth") OR
(object.metadata.labels.public == "true")
)
) OR
(
(object.metadata.labels.env == "dev")
)
)
which could also be written in Disjunctive Normal Form (DNF) as follows:
isAuthorized(object) = (
(object.metadata.labels.owner == "lucas") AND
(object.metadata.labels.visible == "true") AND
(object.type == "k8s.io/basic-auth")
) OR
(
(object.metadata.labels.owner == "lucas") AND
(object.metadata.labels.visible == "true") AND
(object.metadata.labels.public == "true")
) OR
(
(object.metadata.labels.owner == "lucas") AND
(object.metadata.labels.env == "dev")
)
Note that the authorizer 1’s effect=Deny condition must evaluate to false for
an object to be matched. However, the effect=NoOpinion is scoped only to
authorizer 1, if an object was such that metadata.labels.owner == "lucas" and
metadata.labels.env == "dev", it is authorized by authorizer 2, even though
metadata.labels.visible == "false" (which yields a NoOpinion response from
authorizer 1).
The API server must make sure that every object that is returned from storage is
authorized. The API server cannot know what objects are in storage (as one of
the authorization requirements is to be stateless with regards to the data
store), but it can prove something stronger: for every possible object that
could be constructed, that matches the given objectSelected(object) selector,
isAuthorized(object) is true.
This equation can be resolved with a SAT/SMT solver as follows:
(forall object: objectSelected(object) => isAuthorized(object)) == TRUE
=== (forall object: (not objectSelected(object)) OR isAuthorized(object)) == TRUE
=== (exists object: objectSelected(object) AND (not isAuthorized(object))) == FALSE
A client who wants to ask “show me all instances of resource X that I can see”
can thus perform a SelfSAR, construct a selector objectSelected which is equal
to isAuthorized (and thus correct-by-construction), and thus see all objects
that it can, without having to know its permissions up front, or issue n
different requests (e.g. for each namespace). This would work for
controllers/watches as well. Even more conveniently, the API server could
provide the client with a mode that “downgrades” an unconstrained request (e.g.
GET /api/v1/pods) by the server adding the selector that the client is
authorized to see.
However, note that Conditional Reads are NOT part of this proposal right now, another KEP is expected for that eventually (if people like the idea), but I felt it is good to mention the sketch up-front here so that reviewers have an idea how conditional authorization can become usable for both reads and writes, eventually.
Note that this proposal does not directly propose any relation/graph-based authorization mechanism, but that a request might be conditionally allowed on relation-based conditions. ↩︎
This KEP focuses on write requests, but another KEP that would add a generalized selector syntax is anticipated. That KEP would add a CEL-based, Kubernetes-specific, selector syntax that form conditions for read requests. Thus could a generic, conditions-aware “list me what I can see”-client be written that first issues a SelfSAR, and then adds the returned (authorized) conditions to the list/watch request, such that the request is by definition authorized. ↩︎ ↩︎
As
kube-apiserverserves as a webhook authorizer for aggregated API servers. ↩︎Note: As we fold
ConditionalDeny + Denyinto Deny directly, the audit log just tells that one of the authorizers (in this case, the latter) denied it, not necessarily the first one. ↩︎