1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-28 02:18:15 +00:00

feat: support cel expression in validate rules (#7070)

* feat: support cel expression in validate rules

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Adding CEL preconditions in kyverno policies

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Support parameter resources in validate.cel subrule

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* fix

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Adding CEL preconditions in kyverno policies

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Add kuttl tests for validate.cel subrule

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* fix

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Fix disallow-host-path kuttl test

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Add kuttl test for cel preconditions

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Fix kuttl tests for validate.cel

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Use K8S API Validation and AuditAnnotation

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

* Use K8S API ParamKind and ParamRef

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>

---------

Signed-off-by: Mariam Fahmy <mariamfahmy66@gmail.com>
Co-authored-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
Mariam Fahmy 2023-06-01 00:30:55 +03:00 committed by GitHub
parent 194bfacc71
commit 7f6fb24057
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 6656 additions and 6 deletions

View file

@ -7,6 +7,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine/variables/regex"
"github.com/sigstore/k8s-manifest-sigstore/pkg/k8smanifest"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -387,6 +388,10 @@ type Validation struct {
// by specifying exclusions for Pod Security Standards controls.
// +optional
PodSecurity *PodSecurity `json:"podSecurity,omitempty" yaml:"podSecurity,omitempty"`
// CEL allows validation checks using the Common Expression Language (https://kubernetes.io/docs/reference/using-api/cel/).
// +optional
CEL *CEL `json:"cel,omitempty" yaml:"cel,omitempty"`
}
// PodSecurity applies exemptions for Kubernetes Pod Security admission
@ -422,6 +427,36 @@ type PodSecurityStandard struct {
Images []string `json:"images,omitempty" yaml:"images,omitempty"`
}
// CEL allows validation checks using the Common Expression Language (https://kubernetes.io/docs/reference/using-api/cel/).
type CEL struct {
// Expressions is a list of CELExpression types.
Expressions []v1alpha1.Validation `json:"expressions,omitempty" yaml:"expressions,omitempty"`
// ParamKind is a tuple of Group Kind and Version.
// +optional
ParamKind *v1alpha1.ParamKind `json:"paramKind,omitempty" yaml:"paramKind,omitempty"`
// ParamRef references a parameter resource.
// +optional
ParamRef *v1alpha1.ParamRef `json:"paramRef,omitempty" yaml:"paramRef,omitempty"`
// AuditAnnotations contains CEL expressions which are used to produce audit annotations for the audit event of the API request.
// +optional
AuditAnnotations []v1alpha1.AuditAnnotation `json:"auditAnnotations,omitempty" yaml:"auditAnnotations,omitempty"`
}
func (c *CEL) HasParam() bool {
return c.ParamKind != nil && c.ParamRef != nil
}
func (c *CEL) GetParamKind() v1alpha1.ParamKind {
return *c.ParamKind
}
func (c *CEL) GetParamRef() v1alpha1.ParamRef {
return *c.ParamRef
}
// DeserializeAnyPattern deserialize apiextensions.JSON to []interface{}
func (in *Validation) DeserializeAnyPattern() ([]interface{}, error) {
anyPattern := in.GetAnyPattern()

View file

@ -7,6 +7,7 @@ import (
"github.com/kyverno/kyverno/pkg/pss/utils"
datautils "github.com/kyverno/kyverno/pkg/utils/data"
wildcard "github.com/kyverno/kyverno/pkg/utils/wildcard"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/util/sets"
@ -77,6 +78,11 @@ type Rule struct {
// +optional
RawAnyAllConditions *apiextv1.JSON `json:"preconditions,omitempty" yaml:"preconditions,omitempty"`
// CELPreconditions are used to determine if a policy rule should be applied by evaluating a
// set of CEL conditions. It can only be used with the validate.cel subrule
// +optional
CELPreconditions []admissionregistrationv1.MatchCondition `json:"celPreconditions,omitempty" yaml:"celPreconditions,omitempty"`
// Mutation is used to modify matching resources.
// +optional
Mutation Mutation `json:"mutate,omitempty" yaml:"mutate,omitempty"`
@ -129,6 +135,11 @@ func (r Rule) HasValidatePodSecurity() bool {
return r.Validation.PodSecurity != nil && !datautils.DeepEqual(r.Validation.PodSecurity, &PodSecurity{})
}
// HasValidateCEL checks for validate.cel rule
func (r *Rule) HasValidateCEL() bool {
return r.Validation.CEL != nil && !datautils.DeepEqual(r.Validation.CEL, &CEL{})
}
// HasValidate checks for validate rule
func (r *Rule) HasValidate() bool {
return !datautils.DeepEqual(r.Validation, Validation{})

View file

@ -23,6 +23,8 @@ package v1
import (
"github.com/sigstore/k8s-manifest-sigstore/pkg/k8smanifest"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -205,6 +207,43 @@ func (in *AutogenStatus) DeepCopy() *AutogenStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CEL) DeepCopyInto(out *CEL) {
*out = *in
if in.Expressions != nil {
in, out := &in.Expressions, &out.Expressions
*out = make([]v1alpha1.Validation, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ParamKind != nil {
in, out := &in.ParamKind, &out.ParamKind
*out = new(v1alpha1.ParamKind)
**out = **in
}
if in.ParamRef != nil {
in, out := &in.ParamRef, &out.ParamRef
*out = new(v1alpha1.ParamRef)
**out = **in
}
if in.AuditAnnotations != nil {
in, out := &in.AuditAnnotations, &out.AuditAnnotations
*out = make([]v1alpha1.AuditAnnotation, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CEL.
func (in *CEL) DeepCopy() *CEL {
if in == nil {
return nil
}
out := new(CEL)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CTLog) DeepCopyInto(out *CTLog) {
*out = *in
@ -1123,6 +1162,11 @@ func (in *Rule) DeepCopyInto(out *Rule) {
*out = new(apiextensionsv1.JSON)
(*in).DeepCopyInto(*out)
}
if in.CELPreconditions != nil {
in, out := &in.CELPreconditions, &out.CELPreconditions
*out = make([]admissionregistrationv1.MatchCondition, len(*in))
copy(*out, *in)
}
in.Mutation.DeepCopyInto(&out.Mutation)
in.Validation.DeepCopyInto(&out.Validation)
in.Generation.DeepCopyInto(&out.Generation)
@ -1367,6 +1411,11 @@ func (in *Validation) DeepCopyInto(out *Validation) {
*out = new(PodSecurity)
(*in).DeepCopyInto(*out)
}
if in.CEL != nil {
in, out := &in.CEL, &out.CEL
*out = new(CEL)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Validation.

View file

@ -37,6 +37,10 @@ type Validation struct {
// by specifying exclusions for Pod Security Standards controls.
// +optional
PodSecurity *kyvernov1.PodSecurity `json:"podSecurity,omitempty" yaml:"podSecurity,omitempty"`
// CEL allows validation checks using the Common Expression Language (https://kubernetes.io/docs/reference/using-api/cel/).
// +optional
CEL *kyvernov1.CEL `json:"cel,omitempty" yaml:"cel,omitempty"`
}
// ConditionOperator is the operation performed on condition key and value.

View file

@ -5,6 +5,7 @@ import (
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
datautils "github.com/kyverno/kyverno/pkg/utils/data"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
@ -45,6 +46,11 @@ type Rule struct {
// +optional
RawAnyAllConditions *AnyAllConditions `json:"preconditions,omitempty" yaml:"preconditions,omitempty"`
// CELPreconditions are used to determine if a policy rule should be applied by evaluating a
// set of CEL conditions. It can only be used with the validate.cel subrule
// +optional
CELPreconditions []admissionregistrationv1.MatchCondition `json:"celPreconditions,omitempty" yaml:"celPreconditions,omitempty"`
// Mutation is used to modify matching resources.
// +optional
Mutation kyvernov1.Mutation `json:"mutate,omitempty" yaml:"mutate,omitempty"`
@ -97,6 +103,11 @@ func (r Rule) HasValidatePodSecurity() bool {
return r.Validation.PodSecurity != nil && !datautils.DeepEqual(r.Validation.PodSecurity, &kyvernov1.PodSecurity{})
}
// HasValidateCEL checks for validate.cel rule
func (r *Rule) HasValidateCEL() bool {
return r.Validation.CEL != nil && !datautils.DeepEqual(r.Validation.CEL, &kyvernov1.CEL{})
}
// HasValidate checks for validate rule
func (r *Rule) HasValidate() bool {
return !datautils.DeepEqual(r.Validation, Validation{})

View file

@ -23,6 +23,7 @@ package v2beta1
import (
v1 "github.com/kyverno/kyverno/api/kyverno/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@ -405,6 +406,11 @@ func (in *Rule) DeepCopyInto(out *Rule) {
*out = new(AnyAllConditions)
(*in).DeepCopyInto(*out)
}
if in.CELPreconditions != nil {
in, out := &in.CELPreconditions, &out.CELPreconditions
*out = make([]admissionregistrationv1.MatchCondition, len(*in))
copy(*out, *in)
}
in.Mutation.DeepCopyInto(&out.Mutation)
in.Validation.DeepCopyInto(&out.Validation)
in.Generation.DeepCopyInto(&out.Generation)
@ -521,6 +527,11 @@ func (in *Validation) DeepCopyInto(out *Validation) {
*out = new(v1.PodSecurity)
(*in).DeepCopyInto(*out)
}
if in.CEL != nil {
in, out := &in.CEL, &out.CEL
*out = new(v1.CEL)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Validation.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -927,6 +927,82 @@ attributes for keyless verification, or a nested attestor declaration.</p>
</tbody>
</table>
<hr />
<h3 id="kyverno.io/v1.CEL">CEL
</h3>
<p>
(<em>Appears on:</em>
<a href="#kyverno.io/v1.Validation">Validation</a>,
<a href="#kyverno.io/v2beta1.Validation">Validation</a>)
</p>
<p>
<p>CEL allows validation checks using the Common Expression Language (<a href="https://kubernetes.io/docs/reference/using-api/cel/">https://kubernetes.io/docs/reference/using-api/cel/</a>).</p>
</p>
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>expressions</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#validation-v1alpha1-admissionregistration">
[]Kubernetes admissionregistration/v1alpha1.Validation
</a>
</em>
</td>
<td>
<p>Expressions is a list of CELExpression types.</p>
</td>
</tr>
<tr>
<td>
<code>paramKind</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#paramkind-v1alpha1-admissionregistration">
Kubernetes admissionregistration/v1alpha1.ParamKind
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ParamKind is a tuple of Group Kind and Version.</p>
</td>
</tr>
<tr>
<td>
<code>paramRef</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#paramref-v1alpha1-admissionregistration">
Kubernetes admissionregistration/v1alpha1.ParamRef
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ParamRef references a parameter resource.</p>
</td>
</tr>
<tr>
<td>
<code>auditAnnotations</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#auditannotation-v1alpha1-admissionregistration">
[]Kubernetes admissionregistration/v1alpha1.AuditAnnotation
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>AuditAnnotations contains CEL expressions which are used to produce audit annotations for the audit event of the API request.</p>
</td>
</tr>
</tbody>
</table>
<hr />
<h3 id="kyverno.io/v1.CTLog">CTLog
</h3>
<p>
@ -3060,6 +3136,21 @@ See: <a href="https://kyverno.io/docs/writing-policies/preconditions/">https://k
</tr>
<tr>
<td>
<code>celPreconditions</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#matchcondition-v1-admissionregistration">
[]Kubernetes admissionregistration/v1.MatchCondition
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>CELPreconditions are used to determine if a policy rule should be applied by evaluating a
set of CEL conditions. It can only be used with the validate.cel subrule</p>
</td>
</tr>
<tr>
<td>
<code>mutate</code><br/>
<em>
<a href="#kyverno.io/v1.Mutation">
@ -3774,6 +3865,20 @@ PodSecurity
by specifying exclusions for Pod Security Standards controls.</p>
</td>
</tr>
<tr>
<td>
<code>cel</code><br/>
<em>
<a href="#kyverno.io/v1.CEL">
CEL
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>CEL allows validation checks using the Common Expression Language (<a href="https://kubernetes.io/docs/reference/using-api/cel/">https://kubernetes.io/docs/reference/using-api/cel/</a>).</p>
</td>
</tr>
</tbody>
</table>
<hr />
@ -6701,6 +6806,21 @@ See: <a href="https://kyverno.io/docs/writing-policies/preconditions/">https://k
</tr>
<tr>
<td>
<code>celPreconditions</code><br/>
<em>
<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#matchcondition-v1-admissionregistration">
[]Kubernetes admissionregistration/v1.MatchCondition
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>CELPreconditions are used to determine if a policy rule should be applied by evaluating a
set of CEL conditions. It can only be used with the validate.cel subrule</p>
</td>
</tr>
<tr>
<td>
<code>mutate</code><br/>
<em>
<a href="#kyverno.io/v1.Mutation">
@ -7052,6 +7172,20 @@ PodSecurity
by specifying exclusions for Pod Security Standards controls.</p>
</td>
</tr>
<tr>
<td>
<code>cel</code><br/>
<em>
<a href="#kyverno.io/v1.CEL">
CEL
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>CEL allows validation checks using the Common Expression Language (<a href="https://kubernetes.io/docs/reference/using-api/cel/">https://kubernetes.io/docs/reference/using-api/cel/</a>).</p>
</td>
</tr>
</tbody>
</table>
<hr />

9
go.mod
View file

@ -83,6 +83,12 @@ require (
sigs.k8s.io/yaml v1.3.0
)
require (
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/google/cel-go v0.12.6 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
)
require (
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
@ -119,7 +125,6 @@ require (
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/credentials-go v1.2.7 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2 v1.17.7 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.18 // indirect
@ -192,7 +197,6 @@ require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/cel-go v0.12.6 // indirect
github.com/google/certificate-transparency-go v1.1.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-github/v45 v45.2.0 // indirect
@ -280,7 +284,6 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.15.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.1.3 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect

View file

@ -0,0 +1,163 @@
package validation
import (
"context"
"fmt"
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/clients/dclient"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/handlers"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
type validateCELHandler struct {
client dclient.Interface
}
func NewValidateCELHandler(client dclient.Interface) (handlers.Handler, error) {
return validateCELHandler{
client: client,
}, nil
}
func (h validateCELHandler) Process(
ctx context.Context,
logger logr.Logger,
policyContext engineapi.PolicyContext,
resource unstructured.Unstructured,
rule kyvernov1.Rule,
_ engineapi.EngineContextLoader,
) (unstructured.Unstructured, []engineapi.RuleResponse) {
if engineutils.IsDeleteRequest(policyContext) {
logger.V(3).Info("skipping CEL validation on deleted resource")
return resource, nil
}
oldResource := policyContext.OldResource()
var object, oldObject, versionedParams runtime.Object
object = resource.DeepCopyObject()
if oldResource.Object == nil {
oldObject = nil
} else {
oldObject = oldResource.DeepCopyObject()
}
var expressions, messageExpressions, matchExpressions, auditExpressions []cel.ExpressionAccessor
validations := rule.Validation.CEL.Expressions
auditAnnotations := rule.Validation.CEL.AuditAnnotations
// Get the parameter resource
hasParam := rule.Validation.CEL.HasParam()
if hasParam {
paramKind := rule.Validation.CEL.GetParamKind()
paramRef := rule.Validation.CEL.GetParamRef()
apiVersion := paramKind.APIVersion
kind := paramKind.Kind
name := paramRef.Name
namespace := paramRef.Namespace
if namespace == "" {
namespace = "default"
}
paramResource, err := h.client.GetResource(ctx, apiVersion, kind, namespace, name, "")
if err != nil {
return resource, handlers.WithError(rule, engineapi.Validation, "Error while getting the parameterized resource", err)
}
versionedParams = paramResource.DeepCopyObject()
}
for _, cel := range validations {
condition := &validatingadmissionpolicy.ValidationCondition{
Expression: cel.Expression,
Message: cel.Message,
}
messageCondition := &validatingadmissionpolicy.MessageExpressionCondition{
MessageExpression: cel.MessageExpression,
}
expressions = append(expressions, condition)
messageExpressions = append(messageExpressions, messageCondition)
}
for _, condition := range rule.CELPreconditions {
matchCondition := &matchconditions.MatchCondition{
Name: condition.Name,
Expression: condition.Expression,
}
matchExpressions = append(matchExpressions, matchCondition)
}
for _, auditAnnotation := range auditAnnotations {
auditCondition := &validatingadmissionpolicy.AuditAnnotationCondition{
Key: auditAnnotation.Key,
ValueExpression: auditAnnotation.ValueExpression,
}
auditExpressions = append(auditExpressions, auditCondition)
}
filterCompiler := cel.NewFilterCompiler()
filter := filterCompiler.Compile(expressions, cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}, celconfig.PerCallLimit)
messageExpressionfilter := filterCompiler.Compile(messageExpressions, cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}, celconfig.PerCallLimit)
auditAnnotationFilter := filterCompiler.Compile(auditExpressions, cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}, celconfig.PerCallLimit)
matchConditionFilter := filterCompiler.Compile(matchExpressions, cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}, celconfig.PerCallLimit)
newMatcher := matchconditions.NewMatcher(matchConditionFilter, nil, nil, "", "")
validator := validatingadmissionpolicy.NewValidator(filter, newMatcher, auditAnnotationFilter, messageExpressionfilter, nil, nil)
admissionAttributes := admission.NewAttributesRecord(
object,
oldObject,
resource.GroupVersionKind(),
resource.GetNamespace(),
resource.GetName(),
schema.GroupVersionResource{},
"",
admission.Operation(policyContext.Operation()),
nil,
false,
nil,
)
versionedAttr, _ := admission.NewVersionedAttributes(admissionAttributes, admissionAttributes.GetKind(), nil)
validateResult := validator.Validate(ctx, versionedAttr, versionedParams, celconfig.RuntimeCELCostBudget)
for _, decision := range validateResult.Decisions {
switch decision.Action {
case validatingadmissionpolicy.ActionAdmit:
if decision.Evaluation == validatingadmissionpolicy.EvalError {
return resource, handlers.WithResponses(
engineapi.RuleError(rule.Name, engineapi.Validation, decision.Message, nil),
)
}
case validatingadmissionpolicy.ActionDeny:
return resource, handlers.WithResponses(
engineapi.RuleFail(rule.Name, engineapi.Validation, decision.Message),
)
}
}
msg := fmt.Sprintf("Validation rule '%s' passed.", rule.Name)
return resource, handlers.WithResponses(
engineapi.RulePass(rule.Name, engineapi.Validation, msg),
)
}

View file

@ -129,7 +129,7 @@ func (v *validator) validate(ctx context.Context) *engineapi.RuleResponse {
return ruleResponse
}
v.log.V(2).Info("invalid validation rule: podSecurity, patterns, or deny expected")
v.log.V(2).Info("invalid validation rule: podSecurity, cel, patterns, or deny expected")
return nil
}

View file

@ -38,6 +38,7 @@ func (e *engine) validate(
if hasValidate {
hasVerifyManifest := rule.HasVerifyManifests()
hasValidatePss := rule.HasValidatePodSecurity()
hasValidateCEL := rule.HasValidateCEL()
if hasVerifyManifest {
return validation.NewValidateManifestHandler(
policyContext,
@ -45,6 +46,8 @@ func (e *engine) validate(
)
} else if hasValidatePss {
return validation.NewValidatePssHandler()
} else if hasValidateCEL {
return validation.NewValidateCELHandler(e.client)
} else {
return validation.NewValidateResourceHandler()
}

View file

@ -68,17 +68,61 @@ func (v *Validate) Validate(ctx context.Context) (string, error) {
}
}
if v.rule.CEL != nil {
for _, expression := range v.rule.CEL.Expressions {
if expression.Expression == "" {
return "", fmt.Errorf("cel.expressions.expression is required")
}
}
if v.rule.CEL.ParamKind != nil {
if v.rule.CEL.ParamKind.APIVersion == "" {
return "", fmt.Errorf("cel.paramKind.apiVersion is required")
}
if v.rule.CEL.ParamKind.Kind == "" {
return "", fmt.Errorf("cel.paramKind.kind is required")
}
if v.rule.CEL.ParamRef == nil {
return "", fmt.Errorf("cel.paramRef is required")
}
}
if v.rule.CEL.ParamRef != nil {
if v.rule.CEL.ParamRef.Name == "" {
return "", fmt.Errorf("cel.paramRef.name is required")
}
if v.rule.CEL.ParamKind == nil {
return "", fmt.Errorf("cel.paramKind is required")
}
}
if v.rule.CEL.AuditAnnotations != nil {
for _, auditAnnotation := range v.rule.CEL.AuditAnnotations {
if auditAnnotation.Key == "" {
return "", fmt.Errorf("cel.auditAnnotation.key is required")
}
if auditAnnotation.ValueExpression == "" {
return "", fmt.Errorf("cel.auditAnnotation.valueExpression is required")
}
}
}
}
return "", nil
}
func (v *Validate) validateElements() error {
count := validationElemCount(v.rule)
if count == 0 {
return fmt.Errorf("one of pattern, anyPattern, deny, foreach must be specified")
return fmt.Errorf("one of pattern, anyPattern, deny, foreach, cel must be specified")
}
if count > 1 {
return fmt.Errorf("only one of pattern, anyPattern, deny, foreach can be specified")
return fmt.Errorf("only one of pattern, anyPattern, deny, foreach, cel can be specified")
}
return nil
@ -110,6 +154,10 @@ func validationElemCount(v *kyvernov1.Validation) int {
count++
}
if v.CEL != nil {
count++
}
if v.Manifests != nil && len(v.Manifests.Attestors) != 0 {
count++
}

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- policy.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: pod-pass.yaml
shouldFail: false
- file: pod-fail.yaml
shouldFail: true

View file

@ -0,0 +1,9 @@
## Description
This test validates the use of `rule.celPreconditions`.
The policy will be applied on resources that matches the CEL Preconditions.
## Expected Behavior
The policy will be applied on `pod-fail` and since it violates the rule, it will be blocked.
The policy won't be applied on `pod-pass` because it doesn't match the CEL precondition. Therefore it will be created.

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: webserver
image: nginx:latest
ports:
- containerPort: 8080
hostPort: 80

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: Pod
metadata:
name: pod
spec:
containers:
- name: webserver
image: nginx:latest
ports:
- containerPort: 8080
hostPort: 80

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-host-port-range
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,22 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-host-port-range
spec:
validationFailureAction: Enforce
background: false
rules:
- name: host-port-range
match:
any:
- resources:
kinds:
- Pod
celPreconditions:
- name: "first match condition in CEL"
expression: "object.metadata.name.matches('nginx-pod')"
validate:
cel:
expressions:
- expression: "object.spec.containers.all(container, !has(container.ports) || container.ports.all(port, !has(port.hostPort) || (port.hostPort >= 5000 && port.hostPort <= 6000)))"
message: "The only permitted hostPorts are in the range 5000-6000."

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- ns.yaml
assert:
- ns.yaml

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- crd.yaml
assert:
- crd-assert.yaml

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- replicaLimit.yaml
assert:
- replicaLimit.yaml

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- policy.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: deployment-pass.yaml
shouldFail: false
- file: deployment-fail.yaml
shouldFail: true

View file

@ -0,0 +1,13 @@
## Description
This test validates the use of parameter resources in validate.cel subrule.
This test creates the following:
1. A namespace `test-params`
2. A custom resource definition `ReplicaLimit`
3. A policy that checks the deployment replicas using the parameter resource.
4. Two deployments.
## Expected Behavior
The deployment `deployment-fail` is blocked, and the deployment `deployment-pass` is created.

View file

@ -0,0 +1,4 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: replicalimits.rules.example.com

View file

@ -0,0 +1,26 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: replicalimits.rules.example.com
spec:
group: rules.example.com
names:
kind: ReplicaLimit
plural: replicalimits
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
maxReplicas:
type: integer

View file

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-fail
spec:
replicas: 4
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: container2
image: nginx

View file

@ -0,0 +1,17 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-pass
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: container2
image: nginx

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: test-params

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-deployment-replicas
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,25 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-deployment-replicas
spec:
validationFailureAction: Enforce
background: false
rules:
- name: deployment-replicas
match:
any:
- resources:
kinds:
- Deployment
validate:
cel:
paramKind:
apiVersion: rules.example.com/v1
kind: ReplicaLimit
paramRef:
name: "replica-limit-test.example.com"
namespace: "test-params"
expressions:
- expression: "object.spec.replicas <= params.maxReplicas"
messageExpression: "'Deployment spec.replicas must be less than ' + string(params.maxReplicas)"

View file

@ -0,0 +1,6 @@
apiVersion: rules.example.com/v1
kind: ReplicaLimit
metadata:
name: "replica-limit-test.example.com"
namespace: test-params
maxReplicas: 3

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- policy.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: pod-pass.yaml
shouldFail: false
- file: pod-fail.yaml
shouldFail: true

View file

@ -0,0 +1,7 @@
## Description
This test creates a policy that uses CEL expressions to disallow host ports in pods.
## Expected Behavior
The pod `pod-fail` is blocked, and the pod `pod-pass` is created.

View file

@ -0,0 +1,11 @@
apiVersion: v1
kind: Pod
metadata:
name: webserver
spec:
containers:
- name: webserver
image: nginx:latest
ports:
- hostPort: 80

View file

@ -0,0 +1,10 @@
apiVersion: v1
kind: Pod
metadata:
name: webserver
spec:
containers:
- name: webserver
image: nginx:latest
ports:
- containerPort: 80

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-host-port
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,19 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-host-port
spec:
validationFailureAction: Enforce
background: false
rules:
- name: host-port
match:
any:
- resources:
kinds:
- Pod
validate:
cel:
expressions:
- expression: "object.spec.containers.all(container, !has(container.ports) || container.ports.all(port, !has(port.hostPort) || port.hostPort == 0))"
message: "The fields spec.containers[*].ports[*].hostPort must either be unset or set to `0`"