Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Conditional Authorization, SIG Auth Deep Dive

Conditional Authorization, SIG Auth Deep Dive

Avatar for Lucas Käldström

Lucas Käldström

September 04, 2025
Tweet

More Decks by Lucas Käldström

Other Decks in Technology

Transcript

  1. Outline - Demo Setup/architecture - High level goals, end user

    policy authoring experience - Changes to Kubernetes for Conditional Authorization - Next areas to explore/evaluate - Open Questions
  2. Backwards-compability first I just want to emphasize that although I’ll

    show how some things could be done in another way, I am not proposing to replace anything. No changes to RBAC nor VAP; they continue working as-is, and using RBAC is probably the best option for 90% of the use-cases, where extra granularity is not needed. Everything here is additive. However, one goal is that the new expressions can emulate (all or most of) the existing RBAC+VAP use-cases.
  3. Proof-of-Concept setup/architecture Authorization RequestInfo UserInfo Admission Control Built-in CEL condition

    enforcer* RequestInfo UserInfo Body CEL Condition Body Kubernetes luxas/conditional_authz_2 branch github.com/upbound/kubernetes-cedar-authorizer Partial Evaluation Allow, Deny, or CEL conditions 403 403 Policies Authentication Storage @luxas.dev *Validating admission plugin if integrated into core, otherwise could use a webhook
  4. Multiple “frontends” can be used; ideally one IDL Kubernetes CEL

    (portion w/o loops) Kubernetes RBAC New Selector-based Authorization paradigm? New Multi-cluster Policies? CEL & Cedar intersection as the IDL? Any Mathematically-analyzable expression? SMT Solvers @luxas.dev
  5. Use CEL x Cedar intersection is this IDL? CEL Cedar

    str.contains str.startsWith str.endsWith uint int bool string bytes* null map** list double has size*** () . [] {} - (unary) ! * / % + - (binary) == != < > <= >= in && || ?: Cedar is analyzable, that is, reducible to SMT in open source; I make use of this as we’ll see. e.all e.exists e.exists_one e.map e.filter str.matches Entity DAG
  6. What are the benefits of analyzable policies? ⇒ Compare permissiveness

    of two policies (or sets of them) ⇒ Check for equality (help refactors) @luxas.dev
  7. What are the benefits of analyzable policies? ⇒ Compare permissiveness

    of two policies (or sets of them) ⇒ Check for equality (help refactors) ⇒ Prevent privilege escalation (in kube-api-server) @luxas.dev
  8. What are the benefits of analyzable policies? ⇒ Compare permissiveness

    of two policies (or sets of them) ⇒ Check for equality (help refactors) ⇒ Prevent privilege escalation (in kube-api-server) ⇒ Check for logical inconsistencies in a policy set (shadowing, no-ops) ← No effect ← Allow shadows allow ← Deny shadows allow Allow policy Deny policy @luxas.dev
  9. What are the benefits of analyzable policies? Understand what permissions

    a user has after any possible impersonation (even transitive). lucas foo SA secrets impersonate get
  10. What are the benefits of analyzable policies? Understand what permissions

    a user has after any possible impersonation (even transitive). Reason deeply about expressions: Can micah list pods unconditionally, if he can - list pods when namespace == “foo”, and (separately) - list pods, deployments when namespace != “foo” Yes, even though there is not a single policy that covers the whole set. lucas foo SA secrets impersonate get
  11. Maintains a decidable encoding into Satisfiability Modulo Theories Open Source

    Authorization Engine @luxas.dev Aims to be expressive, fast, safe, and analyzable
  12. Maintains a decidable encoding into Satisfiability Modulo Theories Open Source

    Authorization Engine @luxas.dev Aims to be expressive, fast, safe, and analyzable Supports RBAC, ReBAC and ABAC paradigms
  13. Maintains a decidable encoding into Satisfiability Modulo Theories Open Source

    Authorization Engine @luxas.dev Aims to be expressive, fast, safe, and analyzable AWS is donating Cedar to the CNCF Supports RBAC, ReBAC and ABAC paradigms
  14. High-level Goals Privilege Escalation Prevention bindingMatchesPrincipal(p) bindingMatchesNamespace(n) roleMatchesResource(r) vapCondMatchesPrincipal(p) vapCondMatchesObject(o1,

    o2) Privilege Escalation Prevention bindingMatchesPrincipal(p) bindingMatchesNamespace(n) roleMatchesResource(r) Write Requests Read Requests RBAC VAP
  15. High-level Goals Privilege Escalation Prevention bindingMatchesPrincipal(p) bindingMatchesNamespace(n) roleMatchesResource(r) vapCondMatchesPrincipal(p) vapCondMatchesObject(o1,

    o2) Privilege Escalation Prevention bindingMatchesPrincipal(p) bindingMatchesNamespace(n) roleMatchesResource(r) Write Requests Read Requests RBAC VAP No privilege escalation prevention No UX for field- & label selector authz yet Principal predicate duplicated; non-atomic
  16. High-level Goals Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional

    templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention
  17. High-level Goals Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional

    templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention Only field-selectable fields in o1 & o2
  18. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 1
  19. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 1 2
  20. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 1 2 3
  21. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 1 2 3 4
  22. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 1 2 3 4 5
  23. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 1
  24. 1. Arbitrary expression support User stories: - Needs to support

    everything that RBAC already does - Binding to UID or UserExtra, e.g. - “Admins must authenticate with a hardware key (OIDC amr claim) to delete” - “ServiceAccount’s node-name extra must match resource name on a ‘get nodes’” - ANDed principal constraints, e.g. - “User must be both in groups ‘ns-foo-admins’ AND ‘oncall’ to delete” - Matching strings by prefix, e.g. - “ServiceAccount kube-oidc-proxy can impersonate usernames that start with ‘oidc:’” - “User foo can do stuff in namespaces that start with ‘team-1-’” - NOTE: We can enable this while forbidding namespace subdivision if we have analyzability *In terms of expressitivity. If the job can be done with RBAC, use RBAC
  25. 1. Arbitrary expression support There have been concerns around the

    open-endedness of the expressions. I think most of these concerns can be alleviated with good, native tooling, that validates, lints, and requires explicit opt-in for potential footguns, e.g. - Syntax and type errors are already caught in the IDE - Tell whether an expression always evaluates to true or false - Require a “//nolint:wildcard-resource”-style marker to match resource=* - If the namespace field is accessed, disallow resource name prefix or selector matching
  26. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 2
  27. 2. (Optional) Template support The Role - Binding split in

    Kubernetes is definitely useful for a lot of cases. Cedar is adding support for typed (generalized) templates, which allows for “Kubernetes-style” roles, e.g. like follows: template(binding: Binding) => permit(principal, action in [k8s::Action::”get”, k8s::Action::”list”], resource is core::pods) when { principal in binding.subjects && binding has namespace => binding.namespace == resource.namespace } (The “=>” logical operator would be encoded using a Cedar if)
  28. 2. (Optional) Template support The Role - Binding split in

    Kubernetes is definitely useful for a lot of cases. Cedar is adding support for typed (generalized) templates, which allows for “Kubernetes-style” roles. There was previous discussion (in the Oct 2024 RBAC++ meeting) that we most likely want/need conditions on both the role (all available data) and binding side (principal data only). I’m mentioning this context, but this talk otherwise focuses on the “IDL” or “backend”; not what paradigm/model we recommend to users.
  29. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 3
  30. 3. Conditional Authorization for Writes Use-cases: - Only allow writing

    authorized .class/.type/.signerName/.adminAccess values, require some value to be unchanged on update, or other similar things you’d express in admission - Only allow someone to connect to nodes/proxy, only when the path starts with /pods (KEP-2862) - Restrict resource name on create - Allow certain operations in namespaces with certain labels - Constrained Impersonation
  31. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 4
  32. 4. Conditional Authorization for Reads In some previous meeting, David

    expressed interest in aligning field and label selectors across read and write verbs. One could add selectors to write verbs, but that expressivity is just a (very) small subset of what conditions users might write.
  33. 4. Conditional Authorization for Reads In some previous meeting, David

    expressed interest in aligning field and label selectors across read and write verbs. One could add selectors to write verbs, but that expressivity is just a (very) small subset of what conditions users might write. However, we should totally unify the experience across reads and writes. To achieve that goal, I went the opposite way, exposing per-object predicates (as used in writes) for reads as well, instead of “raw” selectors.
  34. 4. Conditional Authorization for Reads In some previous meeting, David

    expressed interest in aligning field and label selectors across read and write verbs. One could add selectors to write verbs, but that expressivity is just a (very) small subset of what conditions users might write. However, we should totally unify the experience across reads and writes. To achieve that goal, I went the opposite way, exposing per-object predicates (as used in writes) for reads as well, instead of “raw” selectors. Selector predicate: request.resourceAttributes.fieldSelector.requirements.exists(r, r.key == "type" && r.operator == "=" && sets.equivalent(r.values, ["mytype"]))
  35. 4. Conditional Authorization for Reads In some previous meeting, David

    expressed interest in aligning field and label selectors across read and write verbs. One could add selectors to write verbs, but that expressivity is just a (very) small subset of what conditions users might write. However, we should totally unify the experience across reads and writes. To achieve that goal, I went the opposite way, exposing per-object predicates (as used in writes) for reads as well, instead of “raw” selectors. Selector predicate: request.resourceAttributes.fieldSelector.requirements.exists(r, r.key == "type" && r.operator == "=" && sets.equivalent(r.values, ["mytype"])) Object predicate: object.type == "mytype"
  36. Selector dimensionality Visual of two ORed allow rule conditions: (labels.env

    != “prod” && labels.owner in [“team-1”, “team-2”]) || (labels.env == “test”) There are 22 possible label selectors that would be allowed by these policies. How can we check that every object that could be returned from storage is authorized? @luxas.dev
  37. Selector dimensionality The naive way would be to perform one

    check per object that could be matched. E.g. “owner in (‘team-1’, ‘team-2’), env in (‘test’, ‘dev’)” selectors Run authorization 4 times for all object “archetypes” In this case, authorized! @luxas.dev
  38. The naive way would be to perform one check per

    object that could be matched. E.g. “owner in (‘team-2’, ‘team-3’), env in (‘test’, ‘dev’)” selectors Run authorization 4 times for all object “archetypes” In this case, not authorized! Selector dimensionality @luxas.dev
  39. However, explicit enumeration doesn’t work with NotExists, !=, NotIn @luxas.dev

    Because then the amount of possibly selected objects is infinite
  40. Use the power of analyzable SMT ∀o: (objectSelected(o) ⇒ isAuthorized(o))

    == TRUE ⇔ (∃o: objectSelected(o) ∧ ¬isAuthorized(o)) == FALSE
  41. Use the power of analyzable SMT ∀o: (objectSelected(o) ⇒ isAuthorized(o))

    == TRUE ⇔ (∃o: objectSelected(o) ∧ ¬isAuthorized(o)) == FALSE isAuthorized(o) = (o.labels.env != “prod” && o.labels.owner in [“team-1”, “team-2”]) || (o.labels.env == “test”) objectSelected(o) = o.labels.env in [“test“, “dev”] && o.labels.owner in [“team-1”, “team-2”]
  42. Use the power of analyzable SMT ∀o: (objectSelected(o) ⇒ isAuthorized(o))

    == TRUE ⇔ (∃o: objectSelected(o) ∧ ¬isAuthorized(o)) == FALSE isAuthorized(o) = (o.labels.env != “prod” && o.labels.owner in [“team-1”, “team-2”]) || (o.labels.env == “test”) objectSelected(o) = o.labels.env in [“test“, “dev”] && o.labels.owner in [“team-2”, “team-3”] Concrete counterexample:
  43. High-level Goals: Going deeper, layer by layer Privilege Escalation Prevention

    attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Write Requests Read Requests Privilege Escalation Prevention attrsAuthorized(p, a, r, c) (Optional templating) objectAuthorized(o1, o2) Uniform expression Uniform expression Privilege Escalation Prevention Privilege Escalation Prevention 5
  44. 5. Privilege Escalation Prevention Can be done through the SMT

    analyzability through checking that: ∀o: (writtenPolicy ⇒ requestorPermissions) == TRUE ⇔ (∃o: writtenPolicy ∧ requestorPermissions) == FALSE In other words, that the written policy is a subset of the requestor’s permission
  45. Kubernetes patch needed is pretty small Authorization RequestInfo UserInfo Admission

    Control Built-in CEL condition enforcer* RequestInfo UserInfo Body CEL Condition Body Kubernetes luxas/conditional_authz_2 branch github.com/upbound/kubernetes-cedar-authorizer Partial Evaluation Allow, Deny, or CEL conditions 403 403 Policies Authentication Storage @luxas.dev *Validating admission plugin if integrated into core, otherwise could use a webhook