diff --git a/Makefile b/Makefile index 7a147dee4fd..65d33c3d0e9 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,8 @@ MANAGER_IMAGE_PATCH := "apiVersion: apps/v1\ \n args:\ \n - --port=8443\ \n - --logtostderr\ -\n - --emit-admission-events\ +\n - --emit-allow-admission-events\ +\n - --emit-deny-admission-events\ \n - --admission-events-involved-namespace\ \n - --exempt-namespace=${GATEKEEPER_NAMESPACE}\ \n - --operation=webhook\ @@ -205,8 +206,9 @@ ifeq ($(ENABLE_PUBSUB),true) --set postInstall.labelNamespace.image.tag=${HELM_RELEASE} \ --set postInstall.labelNamespace.enabled=true \ --set postInstall.probeWebhook.enabled=true \ - --set emitAdmissionEvents=true \ + --set emitAllowAdmissionEvents=true \ --set emitAuditEvents=true \ + --set emitDenyAdmissionEvents=true \ --set admissionEventsInvolvedNamespace=true \ --set auditEventsInvolvedNamespace=true \ --set disabledBuiltins={http.send} \ @@ -230,8 +232,9 @@ else --set postInstall.labelNamespace.image.tag=${HELM_RELEASE} \ --set postInstall.labelNamespace.enabled=true \ --set postInstall.probeWebhook.enabled=true \ - --set emitAdmissionEvents=true \ + --set emitAllowAdmissionEvents=true \ --set emitAuditEvents=true \ + --set emitDenyAdmissionEvents=true \ --set admissionEventsInvolvedNamespace=true \ --set auditEventsInvolvedNamespace=true \ --set disabledBuiltins={http.send} \ @@ -242,13 +245,16 @@ else --set mutationAnnotations=true endif +# TODO(#3440): After next release (3.16.4), we can remove --set emitAdmissionEvents=true e2e-helm-upgrade-init: e2e-helm-install ./.staging/helm/linux-amd64/helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts;\ ./.staging/helm/linux-amd64/helm install gatekeeper gatekeeper/gatekeeper --version ${BASE_RELEASE} \ --namespace ${GATEKEEPER_NAMESPACE} --create-namespace \ --debug --wait \ --set emitAdmissionEvents=true \ + --set emitAllowAdmissionEvents=true \ --set emitAuditEvents=true \ + --set emitDenyAdmissionEvents=true \ --set admissionEventsInvolvedNamespace=true \ --set auditEventsInvolvedNamespace=true \ --set postInstall.labelNamespace.enabled=true \ @@ -271,8 +277,9 @@ e2e-helm-upgrade: --set postInstall.labelNamespace.image.tag=${HELM_RELEASE} \ --set postInstall.labelNamespace.enabled=true \ --set postInstall.probeWebhook.enabled=true \ - --set emitAdmissionEvents=true \ + --set emitAllowAdmissionEvents=true \ --set emitAuditEvents=true \ + --set emitDenyAdmissionEvents=true \ --set admissionEventsInvolvedNamespace=true \ --set auditEventsInvolvedNamespace=true \ --set disabledBuiltins={http.send} \ diff --git a/cmd/build/helmify/kustomize-for-helm.yaml b/cmd/build/helmify/kustomize-for-helm.yaml index b669f94aabc..3cc6070b9e7 100644 --- a/cmd/build/helmify/kustomize-for-helm.yaml +++ b/cmd/build/helmify/kustomize-for-helm.yaml @@ -78,7 +78,8 @@ spec: - --prometheus-port=HELMSUBST_DEPLOYMENT_CONTROLLER_MANAGER_METRICS_PORT - --logtostderr - --log-denies={{ .Values.logDenies }} - - --emit-admission-events={{ .Values.emitAdmissionEvents }} + - --emit-allow-admission-events={{ .Values.emitAllowAdmissionEvents }} + - --emit-deny-admission-events={{ .Values.emitDenyAdmissionEvents }} - --admission-events-involved-namespace={{ .Values.admissionEventsInvolvedNamespace }} - --log-level={{ (.Values.controllerManager.logLevel | empty | not) | ternary .Values.controllerManager.logLevel .Values.logLevel }} - --exempt-namespace={{ .Release.Namespace }} diff --git a/cmd/build/helmify/static/README.md b/cmd/build/helmify/static/README.md index 0a69ba9e758..2e5c6890243 100644 --- a/cmd/build/helmify/static/README.md +++ b/cmd/build/helmify/static/README.md @@ -163,10 +163,11 @@ information._ | mutatingWebhookTimeoutSeconds | The timeout for the mutating webhook in seconds | `3` | | mutatingWebhookCustomRules | Custom rules for selecting which API resources trigger the webhook. NOTE: If you change this, ensure all your constraints are still being enforced. | `{}` | | mutatingWebhookURL | Custom URL for Kubernetes API server to use to reach the mutating webhook pod. If not set, the default of connecting via the kubernetes service endpoint is used. | `null` | -| emitAdmissionEvents | Emit K8s events in configurable namespace for admission violations (alpha feature) | `false` | +| emitAllowAdmissionEvents | Emit K8s events in configurable namespace for allowed admission requests (alpha feature) | `false` | | emitAuditEvents | Emit K8s events in configurable namespace for audit violations (alpha feature) | `false` | -| enableK8sNativeValidation | Enable the K8s Native Validating driver to create CEL-based rules (alpha feature) | `false` | -| vapEnforcement | Generate K8s Validating Admission Policy resource. Allowed values are NONE: do not generate, GATEKEEPER_DEFAULT: do not generate unless label gatekeeper.sh/use-vap: yes is added to policy explicitly, VAP_DEFAULT: generate unless label gatekeeper.sh/use-vap: no is added to policy explicitly. (alpha feature) | `GATEKEEPER_DEFAULT` | +| emitDenyAdmissionEvents | Emit K8s events in configurable namespace for admission violations (alpha feature) | `false` | +| enableK8sNativeValidation | Enable the K8s Native Validating driver to create CEL-based rules (alpha feature) | `false` | +| vapEnforcement | Generate K8s Validating Admission Policy resource. Allowed values are NONE: do not generate, GATEKEEPER_DEFAULT: do not generate unless label gatekeeper.sh/use-vap: yes is added to policy explicitly, VAP_DEFAULT: generate unless label gatekeeper.sh/use-vap: no is added to policy explicitly. (alpha feature) | `GATEKEEPER_DEFAULT` | | auditEventsInvolvedNamespace | Emit audit events for each violation in the involved objects namespace, the default (false) generates events in the namespace Gatekeeper is installed in. Audit events from cluster-scoped resources will continue to generate events in the namespace that Gatekeeper is installed in | `false` | | admissionEventsInvolvedNamespace | Emit admission events for each violation in the involved objects namespace, the default (false) generates events in the namespace Gatekeeper is installed in. Admission events from cluster-scoped resources will continue to generate events in the namespace that Gatekeeper is installed in | `false` | | logDenies | Log detailed info on each deny | `false` | diff --git a/cmd/build/helmify/static/values.yaml b/cmd/build/helmify/static/values.yaml index ac84eabf1b4..168bd57b1b3 100644 --- a/cmd/build/helmify/static/values.yaml +++ b/cmd/build/helmify/static/values.yaml @@ -38,8 +38,9 @@ auditChunkSize: 500 logLevel: INFO logDenies: false logMutations: false -emitAdmissionEvents: false +emitAllowAdmissionEvents: false emitAuditEvents: false +emitDenyAdmissionEvents: false admissionEventsInvolvedNamespace: false auditEventsInvolvedNamespace: false resourceQuota: true diff --git a/manifest_staging/charts/gatekeeper/README.md b/manifest_staging/charts/gatekeeper/README.md index 0a69ba9e758..2e5c6890243 100644 --- a/manifest_staging/charts/gatekeeper/README.md +++ b/manifest_staging/charts/gatekeeper/README.md @@ -163,10 +163,11 @@ information._ | mutatingWebhookTimeoutSeconds | The timeout for the mutating webhook in seconds | `3` | | mutatingWebhookCustomRules | Custom rules for selecting which API resources trigger the webhook. NOTE: If you change this, ensure all your constraints are still being enforced. | `{}` | | mutatingWebhookURL | Custom URL for Kubernetes API server to use to reach the mutating webhook pod. If not set, the default of connecting via the kubernetes service endpoint is used. | `null` | -| emitAdmissionEvents | Emit K8s events in configurable namespace for admission violations (alpha feature) | `false` | +| emitAllowAdmissionEvents | Emit K8s events in configurable namespace for allowed admission requests (alpha feature) | `false` | | emitAuditEvents | Emit K8s events in configurable namespace for audit violations (alpha feature) | `false` | -| enableK8sNativeValidation | Enable the K8s Native Validating driver to create CEL-based rules (alpha feature) | `false` | -| vapEnforcement | Generate K8s Validating Admission Policy resource. Allowed values are NONE: do not generate, GATEKEEPER_DEFAULT: do not generate unless label gatekeeper.sh/use-vap: yes is added to policy explicitly, VAP_DEFAULT: generate unless label gatekeeper.sh/use-vap: no is added to policy explicitly. (alpha feature) | `GATEKEEPER_DEFAULT` | +| emitDenyAdmissionEvents | Emit K8s events in configurable namespace for admission violations (alpha feature) | `false` | +| enableK8sNativeValidation | Enable the K8s Native Validating driver to create CEL-based rules (alpha feature) | `false` | +| vapEnforcement | Generate K8s Validating Admission Policy resource. Allowed values are NONE: do not generate, GATEKEEPER_DEFAULT: do not generate unless label gatekeeper.sh/use-vap: yes is added to policy explicitly, VAP_DEFAULT: generate unless label gatekeeper.sh/use-vap: no is added to policy explicitly. (alpha feature) | `GATEKEEPER_DEFAULT` | | auditEventsInvolvedNamespace | Emit audit events for each violation in the involved objects namespace, the default (false) generates events in the namespace Gatekeeper is installed in. Audit events from cluster-scoped resources will continue to generate events in the namespace that Gatekeeper is installed in | `false` | | admissionEventsInvolvedNamespace | Emit admission events for each violation in the involved objects namespace, the default (false) generates events in the namespace Gatekeeper is installed in. Admission events from cluster-scoped resources will continue to generate events in the namespace that Gatekeeper is installed in | `false` | | logDenies | Log detailed info on each deny | `false` | diff --git a/manifest_staging/charts/gatekeeper/templates/gatekeeper-controller-manager-deployment.yaml b/manifest_staging/charts/gatekeeper/templates/gatekeeper-controller-manager-deployment.yaml index df9807a6d96..772a693ca10 100644 --- a/manifest_staging/charts/gatekeeper/templates/gatekeeper-controller-manager-deployment.yaml +++ b/manifest_staging/charts/gatekeeper/templates/gatekeeper-controller-manager-deployment.yaml @@ -57,7 +57,8 @@ spec: - --prometheus-port={{ .Values.controllerManager.metricsPort }} - --logtostderr - --log-denies={{ .Values.logDenies }} - - --emit-admission-events={{ .Values.emitAdmissionEvents }} + - --emit-allow-admission-events={{ .Values.emitAllowAdmissionEvents }} + - --emit-deny-admission-events={{ .Values.emitDenyAdmissionEvents }} - --admission-events-involved-namespace={{ .Values.admissionEventsInvolvedNamespace }} - --log-level={{ (.Values.controllerManager.logLevel | empty | not) | ternary .Values.controllerManager.logLevel .Values.logLevel }} - --exempt-namespace={{ .Release.Namespace }} diff --git a/manifest_staging/charts/gatekeeper/values.yaml b/manifest_staging/charts/gatekeeper/values.yaml index ac84eabf1b4..168bd57b1b3 100644 --- a/manifest_staging/charts/gatekeeper/values.yaml +++ b/manifest_staging/charts/gatekeeper/values.yaml @@ -38,8 +38,9 @@ auditChunkSize: 500 logLevel: INFO logDenies: false logMutations: false -emitAdmissionEvents: false +emitAllowAdmissionEvents: false emitAuditEvents: false +emitDenyAdmissionEvents: false admissionEventsInvolvedNamespace: false auditEventsInvolvedNamespace: false resourceQuota: true diff --git a/pkg/webhook/common.go b/pkg/webhook/common.go index 193c5ccc13d..3d38cc19728 100644 --- a/pkg/webhook/common.go +++ b/pkg/webhook/common.go @@ -50,7 +50,8 @@ var ( deserializer = codecs.UniversalDeserializer() disableEnforcementActionValidation = flag.Bool("disable-enforcementaction-validation", false, "disable validation of the enforcementAction field of a constraint") logDenies = flag.Bool("log-denies", false, "log detailed info on each deny") - emitAdmissionEvents = flag.Bool("emit-admission-events", false, "(alpha) emit Kubernetes events for each admission violation") + emitAllowAdmissionEvents = flag.Bool("emit-allow-admission-events", false, "(alpha) emit Kubernetes events for each allowed admission request") + emitDenyAdmissionEvents = flag.Bool("emit-deny-admission-events", false, "(alpha) emit Kubernetes events for each admission violation") admissionEventsInvolvedNamespace = flag.Bool("admission-events-involved-namespace", false, "emit admission events for each violation in the involved objects namespace, the default (false) generates events in the namespace Gatekeeper is installed in. Admission events from cluster-scoped resources will still follow the default behavior") logStatsAdmission = flag.Bool("log-stats-admission", false, "(alpha) log stats for admission webhook") serviceaccount = fmt.Sprintf("system:serviceaccount:%s:%s", util.GetNamespace(), serviceAccountName) diff --git a/pkg/webhook/policy.go b/pkg/webhook/policy.go index e6c36d46ac1..fd6d6da1a25 100644 --- a/pkg/webhook/policy.go +++ b/pkg/webhook/policy.go @@ -242,10 +242,12 @@ func (h *validationHandler) Handle(ctx context.Context, req admission.Request) a func (h *validationHandler) getValidationMessages(res []*rtypes.Result, req *admission.Request) ([]string, []string) { var denyMsgs, warnMsgs []string + var eventMsg, reason string var resourceName string obj := &unstructured.Unstructured{} - if len(res) > 0 && (*logDenies || *emitAdmissionEvents) { + if len(res) > 0 && (*logDenies || *emitDenyAdmissionEvents) || + len(res) == 0 && *emitAllowAdmissionEvents { resourceName = req.AdmissionRequest.Name if req.AdmissionRequest.Object.Raw != nil { if _, _, err := deserializer.Decode(req.AdmissionRequest.Object.Raw, nil, obj); err == nil { @@ -257,6 +259,28 @@ func (h *validationHandler) getValidationMessages(res []*rtypes.Result, req *adm } } } + if len(res) == 0 && *emitAllowAdmissionEvents { + annotations := map[string]string{ + logging.Process: "admission", + logging.EventType: "passed", + logging.ResourceGroup: req.AdmissionRequest.Kind.Group, + logging.ResourceAPIVersion: req.AdmissionRequest.Kind.Version, + logging.ResourceKind: req.AdmissionRequest.Kind.Kind, + logging.ResourceNamespace: req.AdmissionRequest.Namespace, + logging.ResourceName: resourceName, + logging.RequestUsername: req.AdmissionRequest.UserInfo.Username, + } + eventMsg = "Admission webhook \"validation.gatekeeper.sh\" allowed request" + reason = "AllowedAdmission" + + ref := getAdmissionRef(nil, h.gkNamespace, req.AdmissionRequest.Kind.Kind, resourceName, obj.GetNamespace(), obj.GetResourceVersion(), obj.GetUID(), *admissionEventsInvolvedNamespace) + + if *admissionEventsInvolvedNamespace { + h.eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeNormal, reason, "%s", eventMsg) + } else { + h.eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeNormal, reason, "%s, Resource Namespace: %s", eventMsg, req.AdmissionRequest.Namespace) + } + } for _, r := range res { if err := util.ValidateEnforcementAction(util.EnforcementAction(r.EnforcementAction)); err != nil { continue @@ -280,7 +304,7 @@ func (h *validationHandler) getValidationMessages(res []*rtypes.Result, req *adm ).Info( fmt.Sprintf("denied admission: %s", r.Msg)) } - if *emitAdmissionEvents { + if *emitDenyAdmissionEvents { annotations := map[string]string{ logging.Process: "admission", logging.EventType: "violation", @@ -296,7 +320,6 @@ func (h *validationHandler) getValidationMessages(res []*rtypes.Result, req *adm logging.ResourceName: resourceName, logging.RequestUsername: req.AdmissionRequest.UserInfo.Username, } - var eventMsg, reason string switch r.EnforcementAction { case string(util.Dryrun): eventMsg = "Dryrun violation" @@ -309,7 +332,7 @@ func (h *validationHandler) getValidationMessages(res []*rtypes.Result, req *adm reason = "FailedAdmission" } - ref := getViolationRef(h.gkNamespace, req.AdmissionRequest.Kind.Kind, resourceName, obj.GetNamespace(), obj.GetResourceVersion(), obj.GetUID(), r.Constraint.GetKind(), r.Constraint.GetName(), r.Constraint.GetNamespace(), *admissionEventsInvolvedNamespace) + ref := getAdmissionRef(r.Constraint, h.gkNamespace, req.AdmissionRequest.Kind.Kind, resourceName, obj.GetNamespace(), obj.GetResourceVersion(), obj.GetUID(), *admissionEventsInvolvedNamespace) if *admissionEventsInvolvedNamespace { h.eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeWarning, reason, "%s, Constraint: %s, Message: %s", eventMsg, r.Constraint.GetName(), r.Msg) @@ -653,7 +676,7 @@ func createReviewForResultant(obj *unstructured.Unstructured, ns *corev1.Namespa } } -func getViolationRef(gkNamespace, rkind, rname, rnamespace, rrv string, ruid types.UID, ckind, cname, cnamespace string, emitInvolvedNamespace bool) *corev1.ObjectReference { +func getAdmissionRef(constraint *unstructured.Unstructured, gkNamespace, rkind, rname, rnamespace, rrv string, ruid types.UID, emitInvolvedNamespace bool) *corev1.ObjectReference { enamespace := gkNamespace if emitInvolvedNamespace && len(rnamespace) > 0 { enamespace = rnamespace @@ -667,7 +690,11 @@ func getViolationRef(gkNamespace, rkind, rname, rnamespace, rrv string, ruid typ ref.UID = ruid ref.ResourceVersion = rrv } else if !emitInvolvedNamespace { - ref.UID = types.UID(rkind + "/" + rnamespace + "/" + rname + "/" + ckind + "/" + cnamespace + "/" + cname) + if constraint != nil { + ref.UID = types.UID(rkind + "/" + rnamespace + "/" + rname + "/" + constraint.GetKind() + "/" + constraint.GetName() + "/" + constraint.GetNamespace()) + } else { + ref.UID = types.UID(rkind + "/" + rnamespace + "/" + rname) + } } return ref } diff --git a/test/bats/test.bats b/test/bats/test.bats index 5e67e773e23..690efad1a11 100644 --- a/test/bats/test.bats +++ b/test/bats/test.bats @@ -261,7 +261,14 @@ __required_labels_audit_test() { @test "emit events test" { # list events for easy debugging - kubectl get events -n gatekeeper-test-playground + kubectl get events -n gatekeeper-test-playground --field-selector reason=AllowedAdmission -o wide + kubectl get events -n gatekeeper-test-playground --field-selector reason=FailedAdmission -o wide + kubectl get events -n gatekeeper-test-playground --field-selector reason=DryrunViolation -o wide + kubectl get events -n gatekeeper-test-playground --field-selector reason=AuditViolation -o wide + + events=$(kubectl get events -n gatekeeper-test-playground --field-selector reason=AllowedAdmission -o json | jq -r '.items[] | select(.metadata.annotations.process=="admission" )' | jq -s '. | length') + [[ "$events" -ge 1 ]] + events=$(kubectl get events -n gatekeeper-test-playground --field-selector reason=FailedAdmission -o json | jq -r '.items[] | select(.metadata.annotations.constraint_kind=="K8sRequiredLabels" )' | jq -s '. | length') [[ "$events" -ge 1 ]] diff --git a/website/docs/customize-startup.md b/website/docs/customize-startup.md index abf2b93b459..e5f7e979e3e 100644 --- a/website/docs/customize-startup.md +++ b/website/docs/customize-startup.md @@ -23,22 +23,25 @@ The `--disable-opa-builtin` flag disables specific [OPA built-ins functions](htt ## [Alpha] Emit admission and audit events -The `--emit-admission-events` flag enables the emission of all admission violations as Kubernetes events. This flag is in alpha stage and it is set to `false` by default. +The `--emit-allow-admission-events` flag enables the emission of all allowed admission requests as Kubernetes events. This flag is in alpha stage and it is set to `false` by default. The `--emit-audit-events` flag enables the emission of all audit violation as Kubernetes events. This flag is in alpha stage and it is set to `false` by default. +The `--emit-deny-admission-events` flag enables the emission of all admission violations as Kubernetes events. This flag is in alpha stage and it is set to `false` by default. + The `--admission-events-involved-namespace` flag controls which namespace admission events will be created in. When set to `true`, admission events will be created in the namespace of the object violating the constraint. If the object has no namespace (ie. cluster scoped resources), they will be created in the namespace Gatekeeper is installed in. Setting to `false` will cause all admission events to be created in the Gatekeeper namespace. The `--audit-events-involved-namespace` flag controls which namespace audit events will be created in. When set to `true`, audit events will be created in the namespace of the object violating the constraint. If the object has no namespace (ie. cluster scoped resources), they will be created in the namespace Gatekeeper is installed in. Setting to `false` will cause all audit events to be created in the Gatekeeper namespace. -There are four types of events that are emitted by Gatekeeper when the emit event flags are enabled: +There are five types of events that are emitted by Gatekeeper when the emit event flags are enabled: -| Event | Description | -| ------------------ | ----------------------------------------------------------------------- | -| `FailedAdmission` | The Gatekeeper webhook denied the admission request (default behavior). | -| `WarningAdmission` | When `enforcementAction: warn` is specified in the constraint. | -| `DryrunViolation` | When `enforcementAction: dryrun` is specified in the constraint. | -| `AuditViolation` | A violation is detected during an audit. | +| Flag | Event | Description | +| ------------------------- | ------------------ | ------------------------------------------------------------------------------- | +| emit-allow-admission-events | `AllowedAdmission` | The Gatekeeper webhook allowed the admission of the request (default behavior). | +| emit-deny-admission-events | `FailedAdmission` | The Gatekeeper webhook denied the admission request (default behavior). | +| emit-deny-admission-events | `WarningAdmission` | When `enforcementAction: warn` is specified in the constraint. | +| emit-deny-admission-events | `DryrunViolation` | When `enforcementAction: dryrun` is specified in the constraint. | +| emit-audit-events | `AuditViolation` | A violation is detected during an audit. | > ❗ Warning: if the same constraint and violating resource tuple was emitted for [more than 10 times in a 10-minute rolling interval](https://github.com/kubernetes/kubernetes/blob/v1.23.3/staging/src/k8s.io/client-go/tools/record/events_cache.go#L429-L438), the Kubernetes event recorder will aggregate the events, e.g. > ```