Skip to content

Commit 2d12956

Browse files
committed
Clean up unresolved issues and apply feedback
1 parent fdf74c6 commit 2d12956

File tree

2 files changed

+98
-69
lines changed

2 files changed

+98
-69
lines changed

keps/sig-api-machinery/3488-cel-admission-control/README.md

Lines changed: 98 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,31 @@ can be completed in a single Kubernetes release cycle.
350350
Before getting into all the individual fields and capabilities, let's look at the
351351
general "shape" of the API.
352352

353-
This enhancement introduces a new `ValidatingAdmissionPolicy` kind.
353+
This API separates policy _definition_ from policy _configuration_ by splitting
354+
responsibilities across resources. The resources involved are:
355+
356+
- Policy definitions (ValidatingAdmissionPolicy)
357+
- Policy bindings (PolicyBinding)
358+
- Policy param resources (custom resources)
359+
360+
![Relatinships between policy resources](erd.png)
361+
362+
This allows for a N:N relationship between policy definitions and the configuration of those policies. This separation has already been
363+
demonstrated successfully by multiple policy frameworks (see the survey further down in this KEP). It has a few key properties:
364+
365+
- Reduces total amount of resource data needed to manage policies:
366+
- Params can be shared across multiple policies instead of copied. E.g.
367+
multiple policies can be enforce different aspects of a "no external
368+
connections", for example, but can all share the configuration.
369+
- Policies can be configured in different ways for different use cases without
370+
having to copy the policy.
371+
- Rollouts and canary-ing can be managed largely via bindings without having
372+
to copy policies or params.
373+
- Ownership of resources aligns well with typical separation of roles for policy
374+
management.
375+
- Existing policy frameworks can leverage this design far more easily because it
376+
aligns with how separation of concerns is expressed in most major policy
377+
frameworks.
354378

355379
Each `ValidatingAdmissionPolicy` resource defines a admission control policy.
356380
The resource contains the CEL expressions to validate the admission policy and
@@ -379,8 +403,7 @@ spec:
379403
validations:
380404
- name: max-replicas
381405
expression: "object.spec.replicas <= params.maxReplicas"
382-
message: "object.spec.replicas must be no greater than {1}"
383-
messageArgs: ["params.maxReplicas"]
406+
messageExpression: "'object.spec.replicas must be no greater than ' + string(params.maxReplicas)"
384407
# ...other rule related fields here...
385408
```
386409

@@ -407,19 +430,6 @@ responsible for providing the `ReplicaLimit` parameter CRD.
407430
To configure an admission policy for use in a cluster, a binding and parameter
408431
resource are created. For example:
409432

410-
<<[UNRESOLVED jpbetz ]>>
411-
Clean up required:
412-
413-
Use `ClusterPolicyBinding` so we can add `PolicyBinding` (namespace scoped) in
414-
the future?
415-
416-
Use a verb for secondary authz check that policy binding editor has policy
417-
parameter edit roles? (tallclair suggested this, it has nice properties).
418-
419-
kubectl support for this feature could show information about a policy and how
420-
it is applied? could be really useful pre-GA to help users
421-
<<[/UNRESOLVED]>>
422-
423433
```yaml
424434
# Policy binding
425435
apiVersion: admissionregistration.k8s.io/v1
@@ -499,15 +509,6 @@ With this binding, the test and global policy bindings overlap. Resources
499509
admitted to test environment would then be checked against both policy
500510
configurations.
501511

502-
To summarize, this "API Shape" separates admission policy _definition_ from
503-
_configuration_. This has a couple advantages:
504-
505-
- Access to, and delegation of, policy configuration is more manageable. In
506-
particular, Kubernetes RBAC works well with this design.
507-
- Without the separation, the next most obvious API shape would be to encode
508-
everything into a single resource, which could easily become very large and
509-
run into resource size limits.
510-
511512
##### Policy Definitions
512513

513514
Policy definitions are responsible for:
@@ -607,7 +608,8 @@ needed.
607608
See "Alternatives considered" section for rejected alternatives. This design was
608609
selected because:
609610

610-
- Matching criteria is fully defined and validated in a builtin type.
611+
- The param CRD schema is owned entirely by the policy author.
612+
- Matching criteria is fully defined and validated in the builtin `PolicyBinding` type.
611613
- Type checking is straight forward.
612614
- Policy parameterization is separated from the policy binding, allowing for
613615
well abstracted parameterization types to be used by applied in different ways
@@ -637,6 +639,14 @@ Details:
637639
- To address this, bindings resource will have extra auth check to verify that
638640
anyone modifying the binding is also permitted to modify the parameters
639641
resource.
642+
- We should consider using a verb for secondary authz check that policy
643+
binding editor has policy parameter edit roles? (tallclair suggested this,
644+
it has nice properties).
645+
646+
API details:
647+
648+
- Name this `ClusterPolicyBinding` so we can add `PolicyBinding` (namespace
649+
scoped) in the future?
640650

641651
##### Match Criteria
642652

@@ -658,6 +668,14 @@ For CEL expressions, the primary benefits of match criteria are:
658668
- Match criteria is available on policy bindings and allows the binding author
659669
to further constrain what resources the particular binding applies to.
660670

671+
We did consider not having any "YAML matching" for this feature and instead pushing all matching into CEL. The main deciders for me were:
672+
673+
- Kubernetes already has resource matching as a well established concept
674+
- Match criteria can be built indexed/accelerated/built into decision trees
675+
- Match criteria can evaluate only to true/false. There is no 'error' case to consider
676+
- Match criteria can be used to guide static typing. If the match is for
677+
v1.Deployment, we know ahead of runtime what type the object variable is
678+
661679
Matching is performed in quite a few systems across Kubernetes:
662680

663681
| Match type | Usages in existing matchers | Support? |
@@ -814,10 +832,6 @@ xref:
814832

815833
#### Reporting violations to Clients
816834

817-
<<[UNRESOLVED jpbetz, TristonianJones ]>>
818-
Use CEL expressions for messages instead of for message args?
819-
<<[/UNRESOLVED]>>
820-
821835
This section focuses on how information is reported back to clients in
822836
when validations fail.
823837

@@ -832,12 +846,12 @@ High level proposal:
832846

833847
- Each policy may set a `denyReason` and/or `denyCode`
834848
- Each validation may define a message:
835-
- `message` - may contain `{1}` format args that are supplied by
836-
`messageArgs: [<cel expression>, <cel expression>, ...]`
837-
- If `message` is absent, `expression` and `name` will be included in the
838-
failure message
839-
- If any of the arg CEL expressions fail: `expression` and `name` will be
840-
included in the failure message plus the arg evaluation failure
849+
- `message` - plain string message
850+
- `messageExpression: "<cel expression>"` (mutually exclusive with `message`)
851+
- If `message` and `messageExpression` are absent, `expression` and `name`
852+
will be included in the failure message
853+
- If `messageExpression` results in an error: `expression` and `name` will be
854+
included in the failure message plus the arg evaluation failure
841855
- allow/deny, warnings, audit annotations will be collectively referred to as
842856
`enforcement`, which will have the following options:
843857
- `Deny`,
@@ -866,15 +880,13 @@ spec:
866880
validations:
867881
- expression: "self.name.startsWith('xyz-')"
868882
name: name-prefix
869-
message: "{1} must start with 'xyz-'"
870-
messageArgs: ["self.name"]
883+
messageExpression: "self.name + ' must start with \'xyz-\''"
871884
- expression: "self.name.contains('bad')"
872885
name: bad-name
873886
message: "name contains 'bad' which is discouraged due to ..."
874887
- expression: "self.name.contains('suspicious')"
875888
name: suspicious-name
876-
message: "{1} contains 'suspicious'"
877-
messageArgs: ["self.name"]
889+
messageExpression: "self.name + ' contains \'suspicious\''"
878890
```
879891

880892
corresponding policy configuration:
@@ -993,24 +1005,25 @@ apiVersion: admissionregistration.k8s.io/v1
9931005
kind: ValidatingAdmissionPolicy
9941006
...
9951007
spec:
996-
matchResources: ...
1008+
matchConstraints: ...
9971009
validations:
9981010
- expression: "object.spec.replicas < 100"
999-
enforcement: [Deny]
1011+
singletonBinding:
1012+
matchResources: ...
1013+
enforcement: [Deny]
10001014
```
10011015

10021016
Note that:
10031017

1004-
- `spec.paramSource` is absent
1005-
- validations do not reference `params` (since there are none)
1006-
- `matchResources` is used instead of `matchConstraints`. This also disables
1007-
policy binding support.
1008-
- `enforcement` is set. This disabbles policy binding support and must
1009-
be used in conjunction with `matchConstraints`
1018+
- `spec.paramSource` must be absent and validations may not reference `params`
1019+
- If `spec.singletonBinding` is present policy binding support is disabled.
10101020

1011-
<<[UNRESOLVED jpbetz, ?? ]>>
1012-
Is there a better way to indicate a "singleton" policy than `matchResources` + `enforcement`? Neither seem like sufficiently obvious indicators of intent.
1013-
<<[/UNRESOLVED]>>
1021+
Safety features:
1022+
1023+
- This field may only be set when the policy is created. it may not be set on
1024+
existing policies.
1025+
- Any bindings assigned to a singleton policy are considered "misconfigured" and
1026+
apply the `FailurePolicy`.
10141027

10151028
Reporting/debugging/analysis implications:
10161029

@@ -1031,9 +1044,9 @@ We will put limits on:
10311044

10321045
### Phase 2
10331046

1034-
All these capabilities are required before Beta, but will not be implemented
1035-
in the first alpha release of this enhancement due to the size and complexity
1036-
of this enhancement.
1047+
All these capabilities are required before Beta, but will not be implemented in
1048+
the first alpha release of this enhancement due to the size and complexity of
1049+
this enhancement.
10371050

10381051
#### Namespace scoped policy binding
10391052

@@ -1220,6 +1233,7 @@ Plan:
12201233
To consider:
12211234

12221235
- labelSelector evaluation functions or other match evaluator functions ([original comment thread](https://github.com/kubernetes/enhancements/pull/3492#discussion_r981747317))
1236+
- `string.format(string, list(dyn))` to make `messageExpression` more convenient.
12231237

12241238
#### Metrics
12251239

@@ -1522,11 +1536,25 @@ xref: https://kyverno.io/docs/writing-policies/autogen/
15221536

15231537
#### Use Case: Rollout of a new validation expression to an existing policy
15241538

1525-
TODO
1539+
1. Policy definition A exists in cluster, policy bindings X1..Xn exist
1540+
1. "temporary" policy definition B is created with the new validation, it has
1541+
the same settings as policy definition A otherwise (e.g. it uses the same
1542+
param CR)
1543+
1. Policy bindings X1..Xn are replicated as Y1..Yn but modified to use policy
1544+
definition B and `enforcement: [Audit, Warn]`
1545+
1. Cluster administrators observe violations (via metrics, audit logs or logged warnings)
1546+
1. Cluster administrator determines new validation is safe
1547+
1. Policy definition A is updated to include the new validation
1548+
1. Policy definition B and policy bindings Y1..Yn are deleted
15261549

15271550
#### Use Case: Canary-ing a policy
15281551

1529-
TODO
1552+
1. New policy definition is created
1553+
1. Any needed param CRs are created
1554+
1. policy bindings are created and set to `enforcement: [Audit, Warn]`
1555+
1. Cluster administrators observe violations (via metrics, audit logs or logged warnings)
1556+
1. Cluster administrator determines new policy is safe
1557+
1. policy bindings are set to `enforcement: [Deny, Audit, Warn]`
15301558

15311559
### Potential Applications
15321560

@@ -2260,6 +2288,10 @@ Why should this KEP _not_ be implemented?
22602288

22612289
- cel-policy-template [`range`](https://github.com/google/cel-policy-templates-go/blob/master/test/testdata/map_ranges/template.yaml) or equivalent.
22622290
- Default validations?
2291+
- Short circuiting of validation (right now all are always evaluated)?
2292+
- CEL based matching support?
2293+
- kubectl support for this feature could show information about a policy and how
2294+
it is applied? could be really useful pre-GA to help users
22632295

22642296
## Alternatives
22652297

@@ -2759,8 +2791,7 @@ For example, to validate all containers:
27592791
validations:
27602792
- scope: "spec.containers[*]"
27612793
expression: "scope.name.startsWith('xyz-')"
2762-
message: "{1} does not start with 'xyz'"
2763-
messageArgs: ["scope.name"]
2794+
messageExpression: "scope.name + 'does not start with \'xyz\''"
27642795
```
27652796

27662797
To make it possible to access the path information in the scope, we can offer a
@@ -2774,8 +2805,7 @@ spec.x[xKey].y[yIndex].field
27742805
validations:
27752806
- scope: "x[xKey].y[yIndex].field"
27762807
expression: "scope.startsWith('xyz-')"
2777-
message: "{1}, {2}: some problem"
2778-
messageArgs: ["scopePath.xKey", "scopePath.yIndex"]
2808+
messageExpression: "scopePath.xKey + ', ' + scopePath.yIndex + ': some problem'"
27792809
```
27802810

27812811
Prior art:
@@ -2796,27 +2826,26 @@ Note: We considered extending to a list of scopes, e.g.:
27962826
validations:
27972827
- scopes: ["spec.containers[*]", "initContainers[*]", "spec.ephemeralContainers[*]"]
27982828
expression: "scope.name.startsWith('xyz-')"
2799-
message: "{1} does not start with 'xyz'"
2800-
messageArgs: ["scope.name"]
2829+
messageExpression: "scope.name + ' does not start with \'xyz\''"
28012830
```
28022831

28032832
But feedback was this is signficantly more difficult to understand.
28042833

28052834
### Message formatting alternatives
28062835

2807-
Alternative: offer a CEL expression
2836+
Alternative: CEL args
28082837

28092838
```yaml
28102839
- expression: "..."
2811-
messageExpression: "string(object.int1) + ' is less than ' + string(object.int2)"
2840+
message: "{1} is less than {2}"
2841+
messageArgs: ["spec.value", "spec.max"]
28122842
```
28132843

28142844
Cons:
28152845

2816-
- CEL requires explicit casts to string
2817-
- Plain string message support is a bit messy. Options:
2818-
- Use CEL always: `"'this is a simple message'"`
2819-
- Offer both plain string (`message`) and CEL (`messageExpression`)
2846+
- How all types are converted to string becomes the responsibility of this API.
2847+
Hard to please everyone and may end up needing to reimplementing `fmt.Sprintf`.
2848+
In which case this is probably best handled from within CEL.
28202849

28212850
Alternative: Inline CEL expressions
28222851

@@ -2848,7 +2877,7 @@ Alternative: CEL expressions, separate args from format string
28482877
```yaml
28492878
- expression: "..."
28502879
message: "{1} is less than {2}"
2851-
messageArgs: ["object.int1", "object.int2"]
2880+
messageArgs: ["", "object.int2"]
28522881
```
28532882

28542883
Note "%s is less than %s" is also viable, but CEL can always preformat and emit
15.4 KB
Loading

0 commit comments

Comments
 (0)