package policy import ( "context" "encoding/json" "errors" "fmt" "reflect" "regexp" "sort" "strings" "github.com/distribution/distribution/reference" jsonpatch "github.com/evanphx/json-patch/v5" "github.com/jmoiron/jsonq" "github.com/kyverno/go-jmespath" "github.com/kyverno/kyverno/api/kyverno" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/common" "github.com/kyverno/kyverno/pkg/autogen" "github.com/kyverno/kyverno/pkg/clients/dclient" openapicontroller "github.com/kyverno/kyverno/pkg/controllers/openapi" enginecontext "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/variables" "github.com/kyverno/kyverno/pkg/engine/variables/regex" "github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/openapi" apiutils "github.com/kyverno/kyverno/pkg/utils/api" datautils "github.com/kyverno/kyverno/pkg/utils/data" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" "github.com/kyverno/kyverno/pkg/utils/wildcard" "golang.org/x/exp/slices" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/discovery" ) var ( allowedVariables = regexp.MustCompile(`request\.|serviceAccountName|serviceAccountNamespace|element|elementIndex|@|images\.|image\.|([a-z_0-9]+\()[^{}]`) allowedVariablesBackground = regexp.MustCompile(`request\.|element|elementIndex|@|images\.|image\.|([a-z_0-9]+\()[^{}]`) allowedVariablesInTarget = regexp.MustCompile(`request\.|serviceAccountName|serviceAccountNamespace|element|elementIndex|@|images\.|image\.|target\.|([a-z_0-9]+\()[^{}]`) allowedVariablesBackgroundInTarget = regexp.MustCompile(`request\.|element|elementIndex|@|images\.|image\.|target\.|([a-z_0-9]+\()[^{}]`) // wildCardAllowedVariables represents regex for the allowed fields in wildcards wildCardAllowedVariables = regexp.MustCompile(`\{\{\s*(request\.|serviceAccountName|serviceAccountNamespace)[^{}]*\}\}`) errOperationForbidden = errors.New("variables are forbidden in the path of a JSONPatch") ) var allowedJsonPatch = regexp.MustCompile("^/") // validateJSONPatchPathForForwardSlash checks for forward slash func validateJSONPatchPathForForwardSlash(patch string) error { // Replace all variables in PatchesJSON6902, all variable checks should have happened already. // This prevents further checks from failing unexpectedly. patch = variables.ReplaceAllVars(patch, func(s string) string { return "kyvernojsonpatchvariable" }) jsonPatch, err := yaml.ToJSON([]byte(patch)) if err != nil { return err } decodedPatch, err := jsonpatch.DecodePatch(jsonPatch) if err != nil { return err } for _, operation := range decodedPatch { path, err := operation.Path() if err != nil { return err } val := allowedJsonPatch.MatchString(path) if !val { return fmt.Errorf("%s", path) } } return nil } func validateJSONPatch(patch string, ruleIdx int) error { patch = variables.ReplaceAllVars(patch, func(s string) string { return "kyvernojsonpatchvariable" }) jsonPatch, err := yaml.ToJSON([]byte(patch)) if err != nil { return err } decodedPatch, err := jsonpatch.DecodePatch(jsonPatch) if err != nil { return err } for _, operation := range decodedPatch { op := operation.Kind() if op != "add" && op != "remove" && op != "replace" { return fmt.Errorf("unexpected kind: spec.rules[%d]: %s", ruleIdx, op) } if op != "remove" { if _, err := operation.ValueInterface(); err != nil { return fmt.Errorf("invalid value: spec.rules[%d]: %s", ruleIdx, err) } } } return nil } func checkValidationFailureAction(spec *kyvernov1.Spec) []string { msg := "Validation failure actions enforce/audit are deprecated, use Enforce/Audit instead." if spec.ValidationFailureAction == "enforce" || spec.ValidationFailureAction == "audit" { return []string{msg} } for _, override := range spec.ValidationFailureActionOverrides { if override.Action == "enforce" || override.Action == "audit" { return []string{msg} } } return nil } // Validate checks the policy and rules declarations for required configurations func Validate(policy, oldPolicy kyvernov1.PolicyInterface, client dclient.Interface, mock bool, openApiManager openapi.Manager, username string) ([]string, error) { var warnings []string spec := policy.GetSpec() background := spec.BackgroundProcessingEnabled() mutateExistingOnPolicyUpdate := spec.GetMutateExistingOnPolicyUpdate() if !mock { openapicontroller.NewController(client, openApiManager).CheckSync(context.TODO()) } warnings = append(warnings, checkValidationFailureAction(spec)...) var errs field.ErrorList specPath := field.NewPath("spec") err := ValidateVariables(policy, background) if err != nil { return warnings, err } if mutateExistingOnPolicyUpdate { err := ValidateOnPolicyUpdate(policy, mutateExistingOnPolicyUpdate) if err != nil { return warnings, err } } var res []*metav1.APIResourceList clusterResources := sets.New[string]() if !mock { // Get all the cluster type kind supported by cluster res, err = discovery.ServerPreferredResources(client.Discovery().DiscoveryInterface()) if err != nil { if discovery.IsGroupDiscoveryFailedError(err) { err := err.(*discovery.ErrGroupDiscoveryFailed) for gv, err := range err.Groups { logging.Error(err, "failed to list api resources", "group", gv) } } else { return warnings, err } } for _, resList := range res { for _, r := range resList.APIResources { if !r.Namespaced { clusterResources.Insert(resList.GroupVersion + "/" + r.Kind) clusterResources.Insert(r.Kind) } } } } if errs := policy.Validate(clusterResources); len(errs) != 0 { return warnings, errs.ToAggregate() } if !policy.IsNamespaced() { err := validateNamespaces(spec, specPath.Child("validationFailureActionOverrides")) if err != nil { return warnings, err } } if !policy.AdmissionProcessingEnabled() && !policy.BackgroundProcessingEnabled() { return warnings, fmt.Errorf("disabling both admission and background processing is not allowed") } if !policy.AdmissionProcessingEnabled() { if spec.HasMutate() || spec.HasGenerate() || spec.HasVerifyImages() { return warnings, fmt.Errorf("disabling admission processing is only allowed with validation policies") } } if err := immutableGenerateFields(policy, oldPolicy); err != nil { return warnings, err } rules := autogen.ComputeRules(policy) rulesPath := specPath.Child("rules") for i, rule := range rules { match := rule.MatchResources exclude := rule.ExcludeResources for j, value := range match.Any { if err := validateKinds(value.ResourceDescription.Kinds, rule, mock, background, client); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d].match.any[%d].kinds: %v", i, j, err) } } for j, value := range match.All { if err := validateKinds(value.ResourceDescription.Kinds, rule, mock, background, client); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d].match.all[%d].kinds: %v", i, j, err) } } for j, value := range exclude.Any { if err := validateKinds(value.ResourceDescription.Kinds, rule, mock, background, client); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d].exclude.any[%d].kinds: %v", i, j, err) } } for j, value := range exclude.All { if err := validateKinds(value.ResourceDescription.Kinds, rule, mock, background, client); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d].exclude.all[%d].kinds: %v", i, j, err) } } if err := validateKinds(rule.MatchResources.Kinds, rule, mock, background, client); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d].match.kinds: %v", i, err) } if err := validateKinds(rule.ExcludeResources.Kinds, rule, mock, background, client); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d].exclude.kinds: %v", i, err) } } for i, rule := range rules { rulePath := rulesPath.Index(i) // check for forward slash if err := validateJSONPatchPathForForwardSlash(rule.Mutation.PatchesJSON6902); err != nil { return warnings, fmt.Errorf("path must begin with a forward slash: spec.rules[%d]: %s", i, err) } if err := validateJSONPatch(rule.Mutation.PatchesJSON6902, i); err != nil { return warnings, fmt.Errorf("%s", err) } if jsonPatchOnPod(rule) { msg := "Pods managed by workload controllers should not be directly mutated using policies. " + "Use the autogen feature or write policies that match Pod controllers." logging.V(1).Info(msg) warnings = append(warnings, msg) } // validate resource description if path, err := validateResources(rulePath, rule); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d].%s: %v", i, path, err) } err := validateElementInForEach(rule) if err != nil { return warnings, err } if err := validateRuleContext(rule); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d]: %v", i, err) } if err := validateRuleImageExtractorsJMESPath(rule); err != nil { return warnings, fmt.Errorf("path: spec.rules[%d]: %v", i, err) } // If a rule's match block does not match any kind, // we should only allow it to have metadata in its overlay if len(rule.MatchResources.Any) > 0 { for _, rmr := range rule.MatchResources.Any { if len(rmr.Kinds) == 0 { return warnings, validateMatchKindHelper(rule) } } } else if len(rule.MatchResources.All) > 0 { for _, rmr := range rule.MatchResources.All { if len(rmr.Kinds) == 0 { return warnings, validateMatchKindHelper(rule) } } } else { if len(rule.MatchResources.Kinds) == 0 { return warnings, validateMatchKindHelper(rule) } } if err := validateActions(i, &rules[i], client, mock, username); err != nil { return warnings, err } if rule.HasVerifyImages() { isAuditFailureAction := false if spec.ValidationFailureAction == kyvernov1.Audit { isAuditFailureAction = true } verifyImagePath := rulePath.Child("verifyImages") for index, i := range rule.VerifyImages { errs = append(errs, i.Validate(isAuditFailureAction, verifyImagePath.Index(index))...) } if len(errs) != 0 { return warnings, errs.ToAggregate() } } kindsFromRule := rule.MatchResources.GetKinds() resourceTypesMap := make(map[string]bool) for _, kind := range kindsFromRule { _, k := kubeutils.GetKindFromGVK(kind) k, _ = kubeutils.SplitSubresource(k) resourceTypesMap[k] = true } if len(resourceTypesMap) == 1 { for k := range resourceTypesMap { if k == "Pod" && podControllerAutoGenExclusion(policy) { msg := "Policies that match Pods apply to all Pods including those created and managed by controllers " + "excluded from autogen. Use preconditions to exclude the Pods managed by controllers which are " + "excluded from autogen. Refer to https://kyverno.io/docs/writing-policies/autogen/ for details." warnings = append(warnings, msg) } } } // Validate string values in labels if !isLabelAndAnnotationsString(rule) { return warnings, fmt.Errorf("labels and annotations supports only string values, \"use double quotes around the non string values\"") } match := rule.MatchResources exclude := rule.ExcludeResources matchKinds := match.GetKinds() excludeKinds := exclude.GetKinds() allKinds := make([]string, 0, len(matchKinds)+len(excludeKinds)) allKinds = append(allKinds, matchKinds...) allKinds = append(allKinds, excludeKinds...) if rule.HasValidate() { validationElem := rule.Validation.DeepCopy() if validationElem.Deny != nil { validationElem.Deny.RawAnyAllConditions = nil } validationJson, err := json.Marshal(validationElem) if err != nil { return nil, err } checkForScaleSubresource(validationJson, allKinds, &warnings) checkForStatusSubresource(validationJson, allKinds, &warnings) } if rule.HasMutate() { mutationJson, err := json.Marshal(rule.Mutation) targets := rule.Mutation.Targets for _, target := range targets { allKinds = append(allKinds, target.GetKind()) } if err != nil { return nil, err } checkForScaleSubresource(mutationJson, allKinds, &warnings) checkForStatusSubresource(mutationJson, allKinds, &warnings) } if rule.HasVerifyImages() { checkForDeprecatedFieldsInVerifyImages(rule, &warnings) } } if !mock && (spec.SchemaValidation == nil || *spec.SchemaValidation) { if err := openApiManager.ValidatePolicyMutation(policy); err != nil { return warnings, fmt.Errorf("%s (you can bypass schema validation by setting `spec.schemaValidation: false`)", err) } } return warnings, nil } func ValidateVariables(p kyvernov1.PolicyInterface, backgroundMode bool) error { vars, err := hasVariables(p) if err != nil { return err } if backgroundMode { if err := containsUserVariables(p, vars); err != nil { return fmt.Errorf("only select variables are allowed in background mode. Set spec.background=false to disable background mode for this policy rule: %s ", err) } } if err := hasInvalidVariables(p, backgroundMode); err != nil { return fmt.Errorf("policy contains invalid variables: %s", err.Error()) } return nil } // hasInvalidVariables - checks for unexpected variables in the policy func hasInvalidVariables(policy kyvernov1.PolicyInterface, background bool) error { for _, r := range autogen.ComputeRules(policy) { ruleCopy := r.DeepCopy() if err := ruleForbiddenSectionsHaveVariables(ruleCopy); err != nil { return err } // skip variable checks on verifyImages.attestations, as variables in attestations are dynamic for i, vi := range ruleCopy.VerifyImages { for j := range vi.Attestations { ruleCopy.VerifyImages[i].Attestations[j].Conditions = nil } } mutateTarget := false if ruleCopy.Mutation.Targets != nil { mutateTarget = true withTargetOnly := ruleWithoutPattern(ruleCopy) for i := range ruleCopy.Mutation.Targets { withTargetOnly.Mutation.Targets[i].ResourceSpec = ruleCopy.Mutation.Targets[i].ResourceSpec ctx := buildContext(withTargetOnly, background, false) if _, err := variables.SubstituteAllInRule(logging.GlobalLogger(), ctx, *withTargetOnly); !variables.CheckNotFoundErr(err) { return fmt.Errorf("invalid variables defined at mutate.targets[%d]: %s", i, err.Error()) } } } ctx := buildContext(ruleCopy, background, mutateTarget) if _, err := variables.SubstituteAllInRule(logging.GlobalLogger(), ctx, *ruleCopy); !variables.CheckNotFoundErr(err) { return fmt.Errorf("variable substitution failed for rule %s: %s", ruleCopy.Name, err.Error()) } } return nil } func ValidateOnPolicyUpdate(p kyvernov1.PolicyInterface, onPolicyUpdate bool) error { vars, err := hasVariables(p) if err != nil { return err } if len(vars) == 0 { return nil } if err := hasInvalidVariables(p, onPolicyUpdate); err != nil { return fmt.Errorf("update event, policy contains invalid variables: %s", err.Error()) } if err := containsUserVariables(p, vars); err != nil { return fmt.Errorf("only select variables are allowed in on policy update. Set spec.mutateExistingOnPolicyUpdate=false to disable update policy mode for this policy rule: %s ", err) } return nil } // for now forbidden sections are match, exclude and func ruleForbiddenSectionsHaveVariables(rule *kyvernov1.Rule) error { var err error err = jsonPatchPathHasVariables(rule.Mutation.PatchesJSON6902) if err != nil && errors.Is(errOperationForbidden, err) { return fmt.Errorf("rule \"%s\" should not have variables in patchesJSON6902 path section", rule.Name) } err = objectHasVariables(rule.ExcludeResources) if err != nil { return fmt.Errorf("rule \"%s\" should not have variables in exclude section", rule.Name) } err = objectHasVariables(rule.MatchResources) if err != nil { return fmt.Errorf("rule \"%s\" should not have variables in match section", rule.Name) } err = imageRefHasVariables(rule.VerifyImages) if err != nil { return fmt.Errorf("rule \"%s\" should not have variables in image reference section", rule.Name) } return nil } // hasVariables - check for variables in the policy func hasVariables(policy kyvernov1.PolicyInterface) ([][]string, error) { polCopy := cleanup(policy.CreateDeepCopy()) policyRaw, err := json.Marshal(polCopy) if err != nil { return nil, fmt.Errorf("failed to serialize the policy: %v", err) } matches := regex.RegexVariables.FindAllStringSubmatch(string(policyRaw), -1) return matches, nil } func cleanup(policy kyvernov1.PolicyInterface) kyvernov1.PolicyInterface { ann := policy.GetAnnotations() if ann != nil { ann["kubectl.kubernetes.io/last-applied-configuration"] = "" policy.SetAnnotations(ann) } if policy.GetNamespace() == "" { pol := policy.(*kyvernov1.ClusterPolicy) pol.Status.Autogen.Rules = nil return pol } else { pol := policy.(*kyvernov1.Policy) pol.Status.Autogen.Rules = nil return pol } } func jsonPatchPathHasVariables(patch string) error { jsonPatch, err := yaml.ToJSON([]byte(patch)) if err != nil { return err } decodedPatch, err := jsonpatch.DecodePatch(jsonPatch) if err != nil { return err } for _, operation := range decodedPatch { path, err := operation.Path() if err != nil { return err } vars := regex.RegexVariables.FindAllString(path, -1) if len(vars) > 0 { return errOperationForbidden } } return nil } func objectHasVariables(object interface{}) error { var err error objectJSON, err := json.Marshal(object) if err != nil { return err } if len(common.RegexVariables.FindAllStringSubmatch(string(objectJSON), -1)) > 0 { return fmt.Errorf("invalid variables") } return nil } func imageRefHasVariables(verifyImages []kyvernov1.ImageVerification) error { for _, verifyImage := range verifyImages { verifyImage = *verifyImage.Convert() for _, imageRef := range verifyImage.ImageReferences { matches := regex.RegexVariables.FindAllString(imageRef, -1) if len(matches) > 0 { return fmt.Errorf("variables are not allowed in image reference") } } } return nil } func ruleWithoutPattern(ruleCopy *kyvernov1.Rule) *kyvernov1.Rule { withTargetOnly := new(kyvernov1.Rule) withTargetOnly.Mutation.Targets = make([]kyvernov1.TargetResourceSpec, len(ruleCopy.Mutation.Targets)) withTargetOnly.Context = ruleCopy.Context withTargetOnly.RawAnyAllConditions = ruleCopy.RawAnyAllConditions return withTargetOnly } func buildContext(rule *kyvernov1.Rule, background bool, target bool) *enginecontext.MockContext { re := getAllowedVariables(background, target) ctx := enginecontext.NewMockContext(re) addContextVariables(rule.Context, ctx) for _, fe := range rule.Validation.ForEachValidation { addContextVariables(fe.Context, ctx) } for _, fe := range rule.Mutation.ForEachMutation { addContextVariables(fe.Context, ctx) } for _, fe := range rule.Mutation.Targets { addContextVariables(fe.Context, ctx) } return ctx } func getAllowedVariables(background bool, target bool) *regexp.Regexp { if target { if background { return allowedVariablesBackgroundInTarget } return allowedVariablesInTarget } else { if background { return allowedVariablesBackground } return allowedVariables } } func addContextVariables(entries []kyvernov1.ContextEntry, ctx *enginecontext.MockContext) { for _, contextEntry := range entries { if contextEntry.APICall != nil || contextEntry.ImageRegistry != nil || contextEntry.Variable != nil { ctx.AddVariable(contextEntry.Name + "*") } if contextEntry.ConfigMap != nil { ctx.AddVariable(contextEntry.Name + ".data") ctx.AddVariable(contextEntry.Name + ".metadata") ctx.AddVariable(contextEntry.Name + ".data.*") ctx.AddVariable(contextEntry.Name + ".metadata.*") } } } func validateElementInForEach(document apiextensions.JSON) error { jsonByte, err := json.Marshal(document) if err != nil { return err } var jsonInterface interface{} err = json.Unmarshal(jsonByte, &jsonInterface) if err != nil { return err } _, err = variables.ValidateElementInForEach(logging.GlobalLogger(), jsonInterface) return err } func validateMatchKindHelper(rule kyvernov1.Rule) error { if !ruleOnlyDealsWithResourceMetaData(rule) { return fmt.Errorf("policy can only deal with the metadata field of the resource if" + " the rule does not match any kind") } return fmt.Errorf("at least one element must be specified in a kind block, the kind attribute is mandatory when working with the resources element") } // isLabelAndAnnotationsString :- Validate if labels and annotations contains only string values func isLabelAndAnnotationsString(rule kyvernov1.Rule) bool { checkLabelAnnotation := func(metaKey map[string]interface{}) bool { for mk := range metaKey { if mk == "labels" { labelKey, ok := metaKey[mk].(map[string]interface{}) if ok { // range over labels for _, val := range labelKey { if reflect.TypeOf(val).String() != "string" { return false } } } } else if mk == "annotations" { annotationKey, ok := metaKey[mk].(map[string]interface{}) if ok { // range over annotations for _, val := range annotationKey { if reflect.TypeOf(val).String() != "string" { return false } } } } } return true } // checkMetadata - Verify if the labels and annotations contains string value inside metadata checkMetadata := func(patternMap map[string]interface{}) bool { for k := range patternMap { if k == "metadata" { metaKey, ok := patternMap[k].(map[string]interface{}) if ok { // range over metadata return checkLabelAnnotation(metaKey) } } if k == "spec" { metadata, _ := jsonq.NewQuery(patternMap).Object("spec", "template", "metadata") return checkLabelAnnotation(metadata) } } return true } if rule.HasValidate() { if rule.Validation.ForEachValidation != nil { for _, foreach := range rule.Validation.ForEachValidation { patternMap, ok := foreach.GetPattern().(map[string]interface{}) if ok { return checkMetadata(patternMap) } } } else { patternMap, ok := rule.Validation.GetPattern().(map[string]interface{}) if ok { return checkMetadata(patternMap) } else if rule.Validation.GetAnyPattern() != nil { anyPatterns, err := rule.Validation.DeserializeAnyPattern() if err != nil { logging.Error(err, "failed to deserialize anyPattern, expect type array") return false } for _, pattern := range anyPatterns { patternMap, ok := pattern.(map[string]interface{}) if ok { ret := checkMetadata(patternMap) if !ret { return ret } } } } } } if rule.HasMutate() { if rule.Mutation.ForEachMutation != nil { for _, foreach := range rule.Mutation.ForEachMutation { forEachStrategicMergeMap, ok := foreach.GetPatchStrategicMerge().(map[string]interface{}) if ok { return checkMetadata(forEachStrategicMergeMap) } } } else { strategicMergeMap, ok := rule.Mutation.GetPatchStrategicMerge().(map[string]interface{}) if ok { return checkMetadata(strategicMergeMap) } } } return true } func ruleOnlyDealsWithResourceMetaData(rule kyvernov1.Rule) bool { patches, _ := rule.Mutation.GetPatchStrategicMerge().(map[string]interface{}) for k := range patches { if k != "metadata" { return false } } if rule.Mutation.PatchesJSON6902 != "" { bytes := []byte(rule.Mutation.PatchesJSON6902) jp, _ := jsonpatch.DecodePatch(bytes) for _, o := range jp { path, _ := o.Path() if !strings.HasPrefix(path, "/metadata") { return false } } } patternMap, _ := rule.Validation.GetPattern().(map[string]interface{}) for k := range patternMap { if k != "metadata" { return false } } anyPatterns, err := rule.Validation.DeserializeAnyPattern() if err != nil { logging.Error(err, "failed to deserialize anyPattern, expect type array") return false } for _, pattern := range anyPatterns { patternMap, _ := pattern.(map[string]interface{}) for k := range patternMap { if k != "metadata" { return false } } } return true } func validateResources(path *field.Path, rule kyvernov1.Rule) (string, error) { // validate userInfo in match and exclude if errs := rule.ExcludeResources.UserInfo.Validate(path.Child("exclude")); len(errs) != 0 { return "exclude", errs.ToAggregate() } if (len(rule.MatchResources.Any) > 0 || len(rule.MatchResources.All) > 0) && !datautils.DeepEqual(rule.MatchResources.ResourceDescription, kyvernov1.ResourceDescription{}) { return "match.", fmt.Errorf("can't specify any/all together with match resources") } if (len(rule.ExcludeResources.Any) > 0 || len(rule.ExcludeResources.All) > 0) && !datautils.DeepEqual(rule.ExcludeResources.ResourceDescription, kyvernov1.ResourceDescription{}) { return "exclude.", fmt.Errorf("can't specify any/all together with exclude resources") } if len(rule.ExcludeResources.Any) > 0 && len(rule.ExcludeResources.All) > 0 { return "match.", fmt.Errorf("can't specify any and all together") } if len(rule.MatchResources.Any) > 0 { for _, rmr := range rule.MatchResources.Any { // matched resources if path, err := validateMatchedResourceDescription(rmr.ResourceDescription); err != nil { return fmt.Sprintf("match.resources.%s", path), err } } } else if len(rule.MatchResources.All) > 0 { for _, rmr := range rule.MatchResources.All { // matched resources if path, err := validateMatchedResourceDescription(rmr.ResourceDescription); err != nil { return fmt.Sprintf("match.resources.%s", path), err } } } else { // matched resources if path, err := validateMatchedResourceDescription(rule.MatchResources.ResourceDescription); err != nil { return fmt.Sprintf("match.resources.%s", path), err } } // validating the values present under validate.preconditions, if they exist if target := rule.GetAnyAllConditions(); target != nil { if path, err := validateConditions(target, "preconditions"); err != nil { return fmt.Sprintf("validate.%s", path), err } } // validating the values present under validate.conditions, if they exist if rule.Validation.Deny != nil { if target := rule.Validation.Deny.GetAnyAllConditions(); target != nil { if path, err := validateConditions(target, "conditions"); err != nil { return fmt.Sprintf("validate.deny.%s", path), err } } } return "", nil } // validateConditions validates all the 'conditions' or 'preconditions' of a rule depending on the corresponding 'condition.key'. // As of now, it is validating the 'value' field whether it contains the only allowed set of values or not when 'condition.key' is {{request.operation}} // this is backwards compatible i.e. conditions can be provided in the old manner as well i.e. without 'any' or 'all' func validateConditions(conditions apiextensions.JSON, schemaKey string) (string, error) { // Conditions can only exist under some specific keys of the policy schema allowedSchemaKeys := map[string]bool{ "preconditions": true, "conditions": true, } if !allowedSchemaKeys[schemaKey] { return schemaKey, fmt.Errorf("wrong schema key found for validating the conditions. Conditions can only occur under one of ['preconditions', 'conditions'] keys in the policy schema") } // conditions are currently in the form of []interface{} kyvernoConditions, err := apiutils.ApiextensionsJsonToKyvernoConditions(conditions) if err != nil { return schemaKey, err } switch typedConditions := kyvernoConditions.(type) { case kyvernov1.AnyAllConditions: // validating the conditions under 'any', if there are any if !datautils.DeepEqual(typedConditions, kyvernov1.AnyAllConditions{}) && typedConditions.AnyConditions != nil { for i, condition := range typedConditions.AnyConditions { if path, err := validateConditionValues(condition); err != nil { return fmt.Sprintf("%s.any[%d].%s", schemaKey, i, path), err } } } // validating the conditions under 'all', if there are any if !datautils.DeepEqual(typedConditions, kyvernov1.AnyAllConditions{}) && typedConditions.AllConditions != nil { for i, condition := range typedConditions.AllConditions { if path, err := validateConditionValues(condition); err != nil { return fmt.Sprintf("%s.all[%d].%s", schemaKey, i, path), err } } } case []kyvernov1.Condition: // backwards compatibility for i, condition := range typedConditions { if path, err := validateConditionValues(condition); err != nil { return fmt.Sprintf("%s[%d].%s", schemaKey, i, path), err } } } return "", nil } // validateConditionValues validates whether all the values under the 'value' field of a 'conditions' field // are apt with respect to the provided 'condition.key' func validateConditionValues(c kyvernov1.Condition) (string, error) { k := c.GetKey() v := c.GetValue() if k == nil || v == nil || c.Operator == "" { return "", fmt.Errorf("entered value of `key`, `value` or `operator` is missing or misspelled") } switch reflect.TypeOf(k).Kind() { case reflect.String: value, err := validateValuesKeyRequest(c) return value, err default: return "", nil } } func validateValuesKeyRequest(c kyvernov1.Condition) (string, error) { k := c.GetKey() switch strings.ReplaceAll(k.(string), " ", "") { case "{{request.operation}}": return validateConditionValuesKeyRequestOperation(c) default: return "", nil } } // validateConditionValuesKeyRequestOperation validates whether all the values under the 'value' field of a 'conditions' field // are one of ["CREATE", "UPDATE", "DELETE", "CONNECT"] when 'condition.key' is {{request.operation}} func validateConditionValuesKeyRequestOperation(c kyvernov1.Condition) (string, error) { valuesAllowed := map[string]bool{ "CREATE": true, "UPDATE": true, "DELETE": true, "CONNECT": true, } v := c.GetValue() switch reflect.TypeOf(v).Kind() { case reflect.String: valueStr := v.(string) // allow templatized values like {{ config-map.data.sample-key }} // because they might be actually pointing to a rightful value in the provided config-map if len(valueStr) >= 4 && valueStr[:2] == "{{" && valueStr[len(valueStr)-2:] == "}}" { return "", nil } if !valuesAllowed[valueStr] { return fmt.Sprintf("value: %s", v.(string)), fmt.Errorf("unknown value '%s' found under the 'value' field. Only the following values are allowed: [CREATE, UPDATE, DELETE, CONNECT]", v.(string)) } case reflect.Slice: values := reflect.ValueOf(v) for i := 0; i < values.Len(); i++ { value := values.Index(i).Interface().(string) if !valuesAllowed[value] { return fmt.Sprintf("value[%d]", i), fmt.Errorf("unknown value '%s' found under the 'value' field. Only the following values are allowed: [CREATE, UPDATE, DELETE, CONNECT]", value) } } default: return "value", fmt.Errorf("'value' field found to be of the type %v. The provided value/values are expected to be either in the form of a string or list", reflect.TypeOf(v).Kind()) } return "", nil } func validateRuleContext(rule kyvernov1.Rule) error { if rule.Context == nil || len(rule.Context) == 0 { return nil } for _, entry := range rule.Context { if entry.Name == "" { return fmt.Errorf("a name is required for context entries") } for _, v := range []string{"images", "request", "serviceAccountName", "serviceAccountNamespace", "element", "elementIndex"} { if entry.Name == v || strings.HasPrefix(entry.Name, v+".") { return fmt.Errorf("entry name %s is invalid as it conflicts with a pre-defined variable %s", entry.Name, v) } } var err error if entry.ConfigMap != nil && entry.APICall == nil && entry.ImageRegistry == nil && entry.Variable == nil { err = validateConfigMap(entry) } else if entry.ConfigMap == nil && entry.APICall != nil && entry.ImageRegistry == nil && entry.Variable == nil { err = validateAPICall(entry) } else if entry.ConfigMap == nil && entry.APICall == nil && entry.ImageRegistry != nil && entry.Variable == nil { err = validateImageRegistry(entry) } else if entry.ConfigMap == nil && entry.APICall == nil && entry.ImageRegistry == nil && entry.Variable != nil { err = validateVariable(entry) } else { return fmt.Errorf("exactly one of configMap or apiCall or imageRegistry or variable is required for context entries") } if err != nil { return err } } return nil } // validateRuleImageExtractorsJMESPath ensures that the rule does not // mutate image digests if it has an image extractor that uses a JMESPath. func validateRuleImageExtractorsJMESPath(rule kyvernov1.Rule) error { imageExtractorConfigs := rule.ImageExtractors imageVerifications := rule.VerifyImages if imageExtractorConfigs == nil || imageVerifications == nil { return nil } anyMutateDigest := false for _, imageVerification := range imageVerifications { if imageVerification.MutateDigest { anyMutateDigest = true break } } if !anyMutateDigest { return nil } anyJMESPath := false for _, imageExtractors := range imageExtractorConfigs { for _, imageExtractor := range imageExtractors { if imageExtractor.JMESPath != "" { anyJMESPath = true break } } } if anyJMESPath { return fmt.Errorf("jmespath may not be used in an image extractor when mutating digests with verify images") } return nil } func validateVariable(entry kyvernov1.ContextEntry) error { // If JMESPath contains variables, the validation will fail because it's not possible to infer which value // will be inserted by the variable // Skip validation if a variable is detected jmesPath := variables.ReplaceAllVars(entry.Variable.JMESPath, func(s string) string { return "kyvernojmespathvariable" }) if !strings.Contains(jmesPath, "kyvernojmespathvariable") && entry.Variable.JMESPath != "" { if _, err := jmespath.NewParser().Parse(entry.Variable.JMESPath); err != nil { return fmt.Errorf("failed to parse JMESPath %s: %v", entry.Variable.JMESPath, err) } } if entry.Variable.Value == nil && jmesPath == "" { return fmt.Errorf("a variable must define a value or a jmesPath expression") } if entry.Variable.Default != nil && jmesPath == "" { return fmt.Errorf("a variable must define a default value only when a jmesPath expression is defined") } return nil } func validateConfigMap(entry kyvernov1.ContextEntry) error { if entry.ConfigMap.Name == "" { return fmt.Errorf("a name is required for configMap context entry") } if entry.ConfigMap.Namespace == "" { return fmt.Errorf("a namespace is required for configMap context entry") } return nil } func validateAPICall(entry kyvernov1.ContextEntry) error { if entry.APICall == nil { return nil } if entry.APICall.URLPath != "" { if entry.APICall.Service != nil { return fmt.Errorf("a URLPath cannot be used for service API calls") } } // If JMESPath contains variables, the validation will fail because it's not // possible to infer which value will be inserted by the variable // Skip validation if a variable is detected jmesPath := variables.ReplaceAllVars(entry.APICall.JMESPath, func(s string) string { return "kyvernojmespathvariable" }) if !strings.Contains(jmesPath, "kyvernojmespathvariable") && entry.APICall.JMESPath != "" { if _, err := jmespath.NewParser().Parse(entry.APICall.JMESPath); err != nil { return fmt.Errorf("failed to parse JMESPath %s: %v", entry.APICall.JMESPath, err) } } return nil } func validateImageRegistry(entry kyvernov1.ContextEntry) error { if entry.ImageRegistry.Reference == "" { return fmt.Errorf("a ref is required for imageRegistry context entry") } // Replace all variables to prevent validation failing on variable keys. ref := variables.ReplaceAllVars(entry.ImageRegistry.Reference, func(s string) string { return "kyvernoimageref" }) // it's no use validating a reference that contains a variable if !strings.Contains(ref, "kyvernoimageref") { _, err := reference.Parse(ref) if err != nil { return fmt.Errorf("bad image: %s: %w", ref, err) } } // If JMESPath contains variables, the validation will fail because it's not possible to infer which value // will be inserted by the variable // Skip validation if a variable is detected jmesPath := variables.ReplaceAllVars(entry.ImageRegistry.JMESPath, func(s string) string { return "kyvernojmespathvariable" }) if !strings.Contains(jmesPath, "kyvernojmespathvariable") && entry.ImageRegistry.JMESPath != "" { if _, err := jmespath.NewParser().Parse(entry.ImageRegistry.JMESPath); err != nil { return fmt.Errorf("failed to parse JMESPath %s: %v", entry.ImageRegistry.JMESPath, err) } } return nil } // validateResourceDescription checks if all necessary fields are present and have values. Also checks a Selector. // field type is checked through openapi // Returns error if // - kinds is empty array in matched resource block, i.e. kinds: [] // - selector is invalid func validateMatchedResourceDescription(rd kyvernov1.ResourceDescription) (string, error) { if datautils.DeepEqual(rd, kyvernov1.ResourceDescription{}) { return "", fmt.Errorf("match resources not specified") } return "", nil } // jsonPatchOnPod checks if a rule applies JSON patches to Pod func jsonPatchOnPod(rule kyvernov1.Rule) bool { if !rule.HasMutate() { return false } if slices.Contains(rule.MatchResources.Kinds, "Pod") && rule.Mutation.PatchesJSON6902 != "" { return true } return false } func podControllerAutoGenExclusion(policy kyvernov1.PolicyInterface) bool { annotations := policy.GetAnnotations() val, ok := annotations[kyverno.PodControllersAnnotation] if !ok || val == "none" { return false } reorderVal := strings.Split(strings.ToLower(val), ",") sort.Slice(reorderVal, func(i, j int) bool { return reorderVal[i] < reorderVal[j] }) if ok && !datautils.DeepEqual(reorderVal, []string{"cronjob", "daemonset", "deployment", "job", "statefulset"}) { return true } return false } func validateKinds(kinds []string, rule kyvernov1.Rule, mock, background bool, client dclient.Interface) error { if err := validateWildcard(kinds, background, rule); err != nil { return err } if slices.Contains(kinds, "*") { return nil } if err := validKinds(kinds, mock, background, rule.HasValidate(), client); err != nil { return fmt.Errorf("the kind defined in the all match resource is invalid: %w", err) } return nil } // validateWildcard check for an Match/Exclude block contains "*" func validateWildcard(kinds []string, background bool, rule kyvernov1.Rule) error { if slices.Contains(kinds, "*") && background { return fmt.Errorf("wildcard policy not allowed in background mode. Set spec.background=false to disable background mode for this policy rule ") } if slices.Contains(kinds, "*") && len(kinds) > 1 { return fmt.Errorf("wildcard policy can not deal with more than one kind") } if slices.Contains(kinds, "*") { if rule.HasGenerate() || rule.HasVerifyImages() || rule.Validation.ForEachValidation != nil { return fmt.Errorf("wildcard policy does not support rule type") } if rule.HasValidate() { if rule.Validation.GetPattern() != nil || rule.Validation.GetAnyPattern() != nil { if !ruleOnlyDealsWithResourceMetaData(rule) { return fmt.Errorf("policy can only deal with the metadata field of the resource if" + " the rule does not match any kind") } } if rule.Validation.Deny != nil { kyvernoConditions, _ := apiutils.ApiextensionsJsonToKyvernoConditions(rule.Validation.Deny.GetAnyAllConditions()) switch typedConditions := kyvernoConditions.(type) { case []kyvernov1.Condition: // backwards compatibility for _, condition := range typedConditions { key := condition.GetKey() if !strings.Contains(key.(string), "request.object.metadata.") && (!wildCardAllowedVariables.MatchString(key.(string)) || strings.Contains(key.(string), "request.object.spec")) { return fmt.Errorf("policy can only deal with the metadata field of the resource if" + " the rule does not match any kind") } } } } } if rule.HasMutate() { if !ruleOnlyDealsWithResourceMetaData(rule) { return fmt.Errorf("policy can only deal with the metadata field of the resource if" + " the rule does not match any kind") } } } return nil } // validKinds verifies if an API resource that matches 'kind' is valid kind // and found in the cache, returns error if not found. It also returns an error if background scanning // is enabled for a subresource. func validKinds(kinds []string, mock, backgroundScanningEnabled, isValidationPolicy bool, client dclient.Interface) error { if !mock { for _, k := range kinds { group, version, kind, subresource := kubeutils.ParseKindSelector(k) gvrss, err := client.Discovery().FindResources(group, version, kind, subresource) if err != nil { return fmt.Errorf("unable to convert GVK to GVR for kinds %s, err: %s", k, err) } if len(gvrss) == 0 { return fmt.Errorf("unable to convert GVK to GVR for kinds %s", k) } if isValidationPolicy && backgroundScanningEnabled { for gvrs := range gvrss { if gvrs.SubResource != "" { return fmt.Errorf("background scan enabled with subresource %s", k) } } } } } return nil } func validateWildcardsWithNamespaces(enforce, audit, enforceW, auditW []string) error { pat, ns, notOk := wildcard.MatchPatterns(auditW, enforce...) if notOk { return fmt.Errorf("wildcard pattern '%s' matches with namespace '%s'", pat, ns) } pat, ns, notOk = wildcard.MatchPatterns(enforceW, audit...) if notOk { return fmt.Errorf("wildcard pattern '%s' matches with namespace '%s'", pat, ns) } pat1, pat2, notOk := wildcard.MatchPatterns(auditW, enforceW...) if notOk { return fmt.Errorf("wildcard pattern '%s' conflicts with the pattern '%s'", pat1, pat2) } pat1, pat2, notOk = wildcard.MatchPatterns(enforceW, auditW...) if notOk { return fmt.Errorf("wildcard pattern '%s' conflicts with the pattern '%s'", pat1, pat2) } return nil } func validateNamespaces(s *kyvernov1.Spec, path *field.Path) error { action := map[string]sets.Set[string]{ "enforce": sets.New[string](), "audit": sets.New[string](), "enforceW": sets.New[string](), "auditW": sets.New[string](), } for i, vfa := range s.ValidationFailureActionOverrides { patternList, nsList := wildcard.SeperateWildcards(vfa.Namespaces) if vfa.Action.Audit() { if action["enforce"].HasAny(nsList...) { return fmt.Errorf("conflicting namespaces found in path: %s: %s", path.Index(i).Child("namespaces").String(), strings.Join(sets.List(action["enforce"].Intersection(sets.New(nsList...))), ", ")) } action["auditW"].Insert(patternList...) } else if vfa.Action.Enforce() { if action["audit"].HasAny(nsList...) { return fmt.Errorf("conflicting namespaces found in path: %s: %s", path.Index(i).Child("namespaces").String(), strings.Join(sets.List(action["audit"].Intersection(sets.New(nsList...))), ", ")) } action["enforceW"].Insert(patternList...) } action[strings.ToLower(string(vfa.Action))].Insert(nsList...) err := validateWildcardsWithNamespaces( sets.List(action["enforce"]), sets.List(action["audit"]), sets.List(action["enforceW"]), sets.List(action["auditW"]), ) if err != nil { return fmt.Errorf("path: %s: %s", path.Index(i).Child("namespaces").String(), err.Error()) } } return nil } func checkForScaleSubresource(ruleTypeJson []byte, allKinds []string, warnings *[]string) { if strings.Contains(string(ruleTypeJson), "replicas") { for _, kind := range allKinds { if strings.Contains(strings.ToLower(kind), "scale") { return } } msg := "You are matching on replicas but not including the scale subresource in the policy." *warnings = append(*warnings, msg) } } func checkForStatusSubresource(ruleTypeJson []byte, allKinds []string, warnings *[]string) { if strings.Contains(string(ruleTypeJson), "status") { for _, kind := range allKinds { if strings.Contains(strings.ToLower(kind), "status") { return } } msg := "You are matching on status but not including the status subresource in the policy." *warnings = append(*warnings, msg) } } func checkForDeprecatedFieldsInVerifyImages(rule kyvernov1.Rule, warnings *[]string) { for _, imageVerify := range rule.VerifyImages { for _, attestation := range imageVerify.Attestations { if attestation.PredicateType != "" { msg := fmt.Sprintf("predicateType has been deprecated use 'type: %s' instead of 'prediacteType: %s'", attestation.PredicateType, attestation.PredicateType) *warnings = append(*warnings, msg) } } } }