diff --git a/cmd/kyverno/main.go b/cmd/kyverno/main.go index f7007b4623..aec5dfae4c 100644 --- a/cmd/kyverno/main.go +++ b/cmd/kyverno/main.go @@ -12,6 +12,7 @@ import ( "github.com/go-logr/logr" "github.com/kyverno/kyverno/cmd/internal" + "github.com/kyverno/kyverno/pkg/auth/checker" "github.com/kyverno/kyverno/pkg/client/clientset/versioned" kyvernoinformer "github.com/kyverno/kyverno/pkg/client/informers/externalversions" "github.com/kyverno/kyverno/pkg/clients/dclient" @@ -181,6 +182,7 @@ func createrLeaderControllers( leaderControllers = append(leaderControllers, internal.NewController(exceptionWebhookControllerName, exceptionWebhookController, 1)) if generateVAPs { + checker := checker.NewSelfChecker(kubeClient.AuthorizationV1().SelfSubjectAccessReviews()) vapController := vapcontroller.NewController( kubeClient, kyvernoClient, @@ -190,6 +192,7 @@ func createrLeaderControllers( kubeInformer.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies(), kubeInformer.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings(), eventGenerator, + checker, ) leaderControllers = append(leaderControllers, internal.NewController(vapcontroller.ControllerName, vapController, vapcontroller.Workers)) } @@ -200,17 +203,16 @@ func main() { var ( // TODO: this has been added to backward support command line arguments // will be removed in future and the configuration will be set only via configmaps - serverIP string - webhookTimeout int - maxQueuedEvents int - omitEvents string - autoUpdateWebhooks bool - webhookRegistrationTimeout time.Duration - admissionReports bool - dumpPayload bool - servicePort int - backgroundServiceAccountName string - generateValidatingAdmissionPolicy bool + serverIP string + webhookTimeout int + maxQueuedEvents int + omitEvents string + autoUpdateWebhooks bool + webhookRegistrationTimeout time.Duration + admissionReports bool + dumpPayload bool + servicePort int + backgroundServiceAccountName string ) flagset := flag.NewFlagSet("kyverno", flag.ExitOnError) flagset.BoolVar(&dumpPayload, "dumpPayload", false, "Set this flag to activate/deactivate debug mode.") @@ -222,8 +224,8 @@ func main() { flagset.DurationVar(&webhookRegistrationTimeout, "webhookRegistrationTimeout", 120*time.Second, "Timeout for webhook registration, e.g., 30s, 1m, 5m.") flagset.Func(toggle.ProtectManagedResourcesFlagName, toggle.ProtectManagedResourcesDescription, toggle.ProtectManagedResources.Parse) flagset.Func(toggle.ForceFailurePolicyIgnoreFlagName, toggle.ForceFailurePolicyIgnoreDescription, toggle.ForceFailurePolicyIgnore.Parse) + flagset.Func(toggle.GenerateValidatingAdmissionPolicyFlagName, toggle.GenerateValidatingAdmissionPolicyDescription, toggle.GenerateValidatingAdmissionPolicy.Parse) flagset.BoolVar(&admissionReports, "admissionReports", true, "Enable or disable admission reports.") - flagset.BoolVar(&generateValidatingAdmissionPolicy, "generateValidatingAdmissionPolicy", false, "Set this flag 'true' to generate validating admission policies.") flagset.IntVar(&servicePort, "servicePort", 443, "Port used by the Kyverno Service resource and for webhook configurations.") flagset.StringVar(&backgroundServiceAccountName, "backgroundServiceAccountName", "", "Background service account name.") flagset.StringVar(&caSecretName, "caSecretName", "", "Name of the secret containing CA.") @@ -261,6 +263,7 @@ func main() { os.Exit(1) } // check if validating admission policies are registered in the API server + generateValidatingAdmissionPolicy := toggle.FromContext(context.TODO()).GenerateValidatingAdmissionPolicy() if generateValidatingAdmissionPolicy { groupVersion := schema.GroupVersion{Group: "admissionregistration.k8s.io", Version: "v1alpha1"} if _, err := setup.KyvernoDynamicClient.GetKubeClient().Discovery().ServerResourcesForGroupVersion(groupVersion.String()); err != nil { diff --git a/pkg/controllers/validatingadmissionpolicy-generate/controller.go b/pkg/controllers/validatingadmissionpolicy-generate/controller.go index 3a80b38d94..44e7c82bdf 100644 --- a/pkg/controllers/validatingadmissionpolicy-generate/controller.go +++ b/pkg/controllers/validatingadmissionpolicy-generate/controller.go @@ -6,6 +6,7 @@ import ( "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + "github.com/kyverno/kyverno/pkg/auth/checker" "github.com/kyverno/kyverno/pkg/client/clientset/versioned" kyvernov1informers "github.com/kyverno/kyverno/pkg/client/informers/externalversions/kyverno/v1" kyvernov1listers "github.com/kyverno/kyverno/pkg/client/listers/kyverno/v1" @@ -16,6 +17,7 @@ import ( controllerutils "github.com/kyverno/kyverno/pkg/utils/controller" datautils "github.com/kyverno/kyverno/pkg/utils/data" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" + "github.com/kyverno/kyverno/pkg/utils/validatingadmissionpolicy" admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -48,6 +50,7 @@ type controller struct { queue workqueue.RateLimitingInterface eventGen event.Interface + checker checker.AuthChecker } func NewController( @@ -59,6 +62,7 @@ func NewController( vapInformer admissionregistrationv1alpha1informers.ValidatingAdmissionPolicyInformer, vapbindingInformer admissionregistrationv1alpha1informers.ValidatingAdmissionPolicyBindingInformer, eventGen event.Interface, + checker checker.AuthChecker, ) controllers.Controller { queue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), ControllerName) c := &controller{ @@ -70,6 +74,7 @@ func NewController( vapbindingLister: vapbindingInformer.Lister(), queue: queue, eventGen: eventGen, + checker: checker, } // Set up an event handler for when Kyverno policies change @@ -307,6 +312,20 @@ func (c *controller) reconcile(ctx context.Context, logger logr.Logger, key, nam return nil } + // check if the controller has the required permissions to generate validating admission policies. + if !validatingadmissionpolicy.HasValidatingAdmissionPolicyPermission(c.checker) { + logger.Info("doesn't have required permissions for generating ValidatingAdmissionPolicies") + c.updateClusterPolicyStatus(ctx, *policy, false, "doesn't have required permissions for generating ValidatingAdmissionPolicies") + return nil + } + + // check if the controller has the required permissions to generate validating admission policy bindings. + if !validatingadmissionpolicy.HasValidatingAdmissionPolicyBindingPermission(c.checker) { + logger.Info("doesn't have required permissions for generating ValidatingAdmissionPolicyBindings") + c.updateClusterPolicyStatus(ctx, *policy, false, "doesn't have required permissions for generating ValidatingAdmissionPolicyBindings") + return nil + } + if ok, msg := canGenerateVAP(spec); !ok { c.updateClusterPolicyStatus(ctx, *policy, false, msg) return nil diff --git a/pkg/toggle/context.go b/pkg/toggle/context.go index 3ce6f4d953..44bf0aa091 100644 --- a/pkg/toggle/context.go +++ b/pkg/toggle/context.go @@ -10,6 +10,7 @@ type Toggles interface { ProtectManagedResources() bool ForceFailurePolicyIgnore() bool EnableDeferredLoading() bool + GenerateValidatingAdmissionPolicy() bool } type defaultToggles struct{} @@ -26,6 +27,10 @@ func (defaultToggles) EnableDeferredLoading() bool { return EnableDeferredLoading.enabled() } +func (defaultToggles) GenerateValidatingAdmissionPolicy() bool { + return GenerateValidatingAdmissionPolicy.enabled() +} + type contextKey struct{} func NewContext(ctx context.Context, toggles Toggles) context.Context { diff --git a/pkg/toggle/toggle.go b/pkg/toggle/toggle.go index 491084eb74..a0a7c5d12e 100644 --- a/pkg/toggle/toggle.go +++ b/pkg/toggle/toggle.go @@ -21,12 +21,18 @@ const ( EnableDeferredLoadingDescription = "enable deferred loading of context variables" enableDeferredLoadingEnvVar = "FLAG_ENABLE_DEFERRED_LOADING" defaultEnableDeferredLoading = true + // generate validating admission policies + GenerateValidatingAdmissionPolicyFlagName = "generateValidatingAdmissionPolicy" + GenerateValidatingAdmissionPolicyDescription = "Set the flag to 'true', to generate validating admission policies." + generateValidatingAdmissionPolicyEnvVar = "FLAG_GENERATE_VALIDATING_ADMISSION_POLICY" + defaultGenerateValidatingAdmissionPolicy = false ) var ( - ProtectManagedResources = newToggle(defaultProtectManagedResources, protectManagedResourcesEnvVar) - ForceFailurePolicyIgnore = newToggle(defaultForceFailurePolicyIgnore, forceFailurePolicyIgnoreEnvVar) - EnableDeferredLoading = newToggle(defaultEnableDeferredLoading, enableDeferredLoadingEnvVar) + ProtectManagedResources = newToggle(defaultProtectManagedResources, protectManagedResourcesEnvVar) + ForceFailurePolicyIgnore = newToggle(defaultForceFailurePolicyIgnore, forceFailurePolicyIgnoreEnvVar) + EnableDeferredLoading = newToggle(defaultEnableDeferredLoading, enableDeferredLoadingEnvVar) + GenerateValidatingAdmissionPolicy = newToggle(defaultGenerateValidatingAdmissionPolicy, generateValidatingAdmissionPolicyEnvVar) ) type ToggleFlag interface { diff --git a/pkg/utils/validatingadmissionpolicy/permissions.go b/pkg/utils/validatingadmissionpolicy/permissions.go new file mode 100644 index 0000000000..968c337207 --- /dev/null +++ b/pkg/utils/validatingadmissionpolicy/permissions.go @@ -0,0 +1,30 @@ +package validatingadmissionpolicy + +import ( + "context" + + "github.com/kyverno/kyverno/pkg/auth/checker" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func hasPermissions(resource schema.GroupVersionResource, s checker.AuthChecker) bool { + can, err := checker.Check(context.TODO(), s, resource.Group, resource.Version, resource.Resource, "", "", "create", "update", "list", "delete") + if err != nil { + return false + } + return can +} + +// HasValidatingAdmissionPolicyPermission check if the admission controller has the required permissions to generate +// Kubernetes ValidatingAdmissionPolicy +func HasValidatingAdmissionPolicyPermission(s checker.AuthChecker) bool { + gvr := schema.GroupVersionResource{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Resource: "validatingadmissionpolicies"} + return hasPermissions(gvr, s) +} + +// HasValidatingAdmissionPolicyBindingPermission check if the admission controller has the required permissions to generate +// Kubernetes ValidatingAdmissionPolicyBinding +func HasValidatingAdmissionPolicyBindingPermission(s checker.AuthChecker) bool { + gvr := schema.GroupVersionResource{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Resource: "validatingadmissionpolicybindings"} + return hasPermissions(gvr, s) +} diff --git a/pkg/validation/policy/actions.go b/pkg/validation/policy/actions.go index 44be497304..a6e458b57e 100644 --- a/pkg/validation/policy/actions.go +++ b/pkg/validation/policy/actions.go @@ -6,11 +6,14 @@ import ( "slices" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + authChecker "github.com/kyverno/kyverno/pkg/auth/checker" "github.com/kyverno/kyverno/pkg/clients/dclient" "github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/policy/generate" "github.com/kyverno/kyverno/pkg/policy/mutate" "github.com/kyverno/kyverno/pkg/policy/validate" + "github.com/kyverno/kyverno/pkg/toggle" + "github.com/kyverno/kyverno/pkg/utils/validatingadmissionpolicy" ) // Validation provides methods to validate a rule @@ -22,9 +25,9 @@ type Validation interface { // - Mutate // - Validation // - Generate -func validateActions(idx int, rule *kyvernov1.Rule, client dclient.Interface, mock bool, username string) error { +func validateActions(idx int, rule *kyvernov1.Rule, client dclient.Interface, mock bool, username string) (string, error) { if rule == nil { - return nil + return "", nil } var checker Validation @@ -32,7 +35,7 @@ func validateActions(idx int, rule *kyvernov1.Rule, client dclient.Interface, mo if rule.HasMutate() { checker = mutate.NewMutateFactory(rule.Mutation, client, username) if path, err := checker.Validate(context.TODO()); err != nil { - return fmt.Errorf("path: spec.rules[%d].mutate.%s.: %v", idx, path, err) + return "", fmt.Errorf("path: spec.rules[%d].mutate.%s.: %v", idx, path, err) } } @@ -40,7 +43,21 @@ func validateActions(idx int, rule *kyvernov1.Rule, client dclient.Interface, mo if rule.HasValidate() { checker = validate.NewValidateFactory(&rule.Validation) if path, err := checker.Validate(context.TODO()); err != nil { - return fmt.Errorf("path: spec.rules[%d].validate.%s.: %v", idx, path, err) + return "", fmt.Errorf("path: spec.rules[%d].validate.%s.: %v", idx, path, err) + } + + // In case generateValidatingAdmissionPolicy flag is set to true, check the required permissions. + if toggle.FromContext(context.TODO()).GenerateValidatingAdmissionPolicy() { + authCheck := authChecker.NewSelfChecker(client.GetKubeClient().AuthorizationV1().SelfSubjectAccessReviews()) + // check if the controller has the required permissions to generate validating admission policies. + if !validatingadmissionpolicy.HasValidatingAdmissionPolicyPermission(authCheck) { + return "doesn't have required permissions for generating ValidatingAdmissionPolicies", nil + } + + // check if the controller has the required permissions to generate validating admission policy bindings. + if !validatingadmissionpolicy.HasValidatingAdmissionPolicyBindingPermission(authCheck) { + return "doesn't have required permissions for generating ValidatingAdmissionPolicyBindings", nil + } } } @@ -52,19 +69,19 @@ func validateActions(idx int, rule *kyvernov1.Rule, client dclient.Interface, mo if mock { checker = generate.NewFakeGenerate(rule.Generation) if path, err := checker.Validate(context.TODO()); err != nil { - return fmt.Errorf("path: spec.rules[%d].generate.%s.: %v", idx, path, err) + return "", fmt.Errorf("path: spec.rules[%d].generate.%s.: %v", idx, path, err) } } else { checker = generate.NewGenerateFactory(client, rule.Generation, username, logging.GlobalLogger()) if path, err := checker.Validate(context.TODO()); err != nil { - return fmt.Errorf("path: spec.rules[%d].generate.%s.: %v", idx, path, err) + return "", fmt.Errorf("path: spec.rules[%d].generate.%s.: %v", idx, path, err) } } if slices.Contains(rule.MatchResources.Kinds, rule.Generation.Kind) { - return fmt.Errorf("generation kind and match resource kind should not be the same") + return "", fmt.Errorf("generation kind and match resource kind should not be the same") } } - return nil + return "", nil } diff --git a/pkg/validation/policy/validate.go b/pkg/validation/policy/validate.go index 8d7a3e20ef..fd2d8374a5 100644 --- a/pkg/validation/policy/validate.go +++ b/pkg/validation/policy/validate.go @@ -299,8 +299,13 @@ func Validate(policy, oldPolicy kyvernov1.PolicyInterface, client dclient.Interf } } - if err := validateActions(i, &rules[i], client, mock, username); err != nil { + msg, err := validateActions(i, &rules[i], client, mock, username) + if err != nil { return warnings, err + } else { + if len(msg) != 0 { + warnings = append(warnings, msg) + } } if rule.HasVerifyImages() {