diff --git a/Makefile b/Makefile index 87556fe4d19..57d79d2e1fb 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,7 @@ MANAGER_IMAGE_PATCH := "apiVersion: apps/v1\ \n - --port=8443\ \n - --logtostderr\ \n - --emit-admission-events\ +\n - --admission-events-involved-namespace\ \n - --exempt-namespace=${GATEKEEPER_NAMESPACE}\ \n - --operation=webhook\ \n - --operation=mutation-webhook\ @@ -80,6 +81,7 @@ MANAGER_IMAGE_PATCH := "apiVersion: apps/v1\ \n name: manager\ \n args:\ \n - --emit-audit-events\ +\n - --audit-events-involved-namespace\ \n - --operation=audit\ \n - --operation=status\ \n - --operation=mutation-status\ @@ -185,6 +187,8 @@ e2e-helm-deploy: e2e-helm-install --set postInstall.probeWebhook.enabled=true \ --set emitAdmissionEvents=true \ --set emitAuditEvents=true \ + --set admissionEventsInvolvedNamespace=true \ + --set auditEventsInvolvedNamespace=true \ --set disabledBuiltins={http.send} \ --set logMutations=true \ --set mutationAnnotations=true;\ @@ -196,6 +200,8 @@ e2e-helm-upgrade-init: e2e-helm-install --debug --wait \ --set emitAdmissionEvents=true \ --set emitAuditEvents=true \ + --set admissionEventsInvolvedNamespace=true \ + --set auditEventsInvolvedNamespace=true \ --set postInstall.labelNamespace.enabled=true \ --set postInstall.probeWebhook.enabled=true \ --set disabledBuiltins={http.send} \ @@ -217,6 +223,8 @@ e2e-helm-upgrade: --set postInstall.probeWebhook.enabled=true \ --set emitAdmissionEvents=true \ --set emitAuditEvents=true \ + --set admissionEventsInvolvedNamespace=true \ + --set auditEventsInvolvedNamespace=true \ --set disabledBuiltins={http.send} \ --set logMutations=true \ --set mutationAnnotations=true;\ diff --git a/cmd/build/helmify/kustomize-for-helm.yaml b/cmd/build/helmify/kustomize-for-helm.yaml index 0a28ff2a03b..4a74a876d6d 100644 --- a/cmd/build/helmify/kustomize-for-helm.yaml +++ b/cmd/build/helmify/kustomize-for-helm.yaml @@ -76,6 +76,7 @@ spec: - --logtostderr - --log-denies={{ .Values.logDenies }} - --emit-admission-events={{ .Values.emitAdmissionEvents }} + - --admission-events-involved-namespace={{ .Values.admissionEventsInvolvedNamespace }} - --log-level={{ (.Values.controllerManager.logLevel | empty | not) | ternary .Values.controllerManager.logLevel .Values.logLevel }} - --exempt-namespace={{ .Release.Namespace }} - --operation=webhook @@ -158,6 +159,7 @@ spec: - --audit-chunk-size={{ .Values.auditChunkSize }} - --audit-match-kind-only={{ .Values.auditMatchKindOnly }} - --emit-audit-events={{ .Values.emitAuditEvents }} + - --audit-events-involved-namespace={{ .Values.auditEventsInvolvedNamespace }} - --operation=audit - --operation=status - HELMSUBST_MUTATION_STATUS_ENABLED_ARG diff --git a/cmd/build/helmify/static/README.md b/cmd/build/helmify/static/README.md index c5fbd2356fe..db031d2c68b 100644 --- a/cmd/build/helmify/static/README.md +++ b/cmd/build/helmify/static/README.md @@ -147,8 +147,10 @@ _See [Exempting Namespaces](https://open-policy-agent.github.io/gatekeeper/websi | mutatingWebhookObjectSelector | The label selector to further refine which namespaced resources will be selected by the webhook. Please note that an exemption label means users can circumvent Gatekeeper's mutation webhook unless measures are taken to control how exemption labels can be set. | `{}` | | 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. | `{}` | -| emitAdmissionEvents | Emit K8s events in gatekeeper namespace for admission violations (alpha feature) | `false` | -| emitAuditEvents | Emit K8s events in gatekeeper namespace for audit violations (alpha feature) | `false` | +| emitAdmissionEvents | Emit K8s events in configurable namespace for admission violations (alpha feature) | `false` | +| emitAuditEvents | Emit K8s events in configurable namespace for audit violations (alpha feature) | `false` | +| 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` | | logLevel | Minimum log level | `INFO` | | image.pullPolicy | The image pull policy | `IfNotPresent` | diff --git a/cmd/build/helmify/static/values.yaml b/cmd/build/helmify/static/values.yaml index d9bdb618d44..2c94fc250c1 100644 --- a/cmd/build/helmify/static/values.yaml +++ b/cmd/build/helmify/static/values.yaml @@ -34,6 +34,8 @@ logDenies: false logMutations: false emitAdmissionEvents: false emitAuditEvents: false +admissionEventsInvolvedNamespace: false +auditEventsInvolvedNamespace: false resourceQuota: true image: repository: openpolicyagent/gatekeeper diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index a7a893f5933..b81a0171252 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -5,6 +5,13 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - '*' resources: diff --git a/manifest_staging/charts/gatekeeper/README.md b/manifest_staging/charts/gatekeeper/README.md index c5fbd2356fe..db031d2c68b 100644 --- a/manifest_staging/charts/gatekeeper/README.md +++ b/manifest_staging/charts/gatekeeper/README.md @@ -147,8 +147,10 @@ _See [Exempting Namespaces](https://open-policy-agent.github.io/gatekeeper/websi | mutatingWebhookObjectSelector | The label selector to further refine which namespaced resources will be selected by the webhook. Please note that an exemption label means users can circumvent Gatekeeper's mutation webhook unless measures are taken to control how exemption labels can be set. | `{}` | | 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. | `{}` | -| emitAdmissionEvents | Emit K8s events in gatekeeper namespace for admission violations (alpha feature) | `false` | -| emitAuditEvents | Emit K8s events in gatekeeper namespace for audit violations (alpha feature) | `false` | +| emitAdmissionEvents | Emit K8s events in configurable namespace for admission violations (alpha feature) | `false` | +| emitAuditEvents | Emit K8s events in configurable namespace for audit violations (alpha feature) | `false` | +| 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` | | logLevel | Minimum log level | `INFO` | | image.pullPolicy | The image pull policy | `IfNotPresent` | diff --git a/manifest_staging/charts/gatekeeper/templates/gatekeeper-audit-deployment.yaml b/manifest_staging/charts/gatekeeper/templates/gatekeeper-audit-deployment.yaml index f3809df231f..a2437efc37c 100644 --- a/manifest_staging/charts/gatekeeper/templates/gatekeeper-audit-deployment.yaml +++ b/manifest_staging/charts/gatekeeper/templates/gatekeeper-audit-deployment.yaml @@ -57,6 +57,7 @@ spec: - --audit-chunk-size={{ .Values.auditChunkSize }} - --audit-match-kind-only={{ .Values.auditMatchKindOnly }} - --emit-audit-events={{ .Values.emitAuditEvents }} + - --audit-events-involved-namespace={{ .Values.auditEventsInvolvedNamespace }} - --operation=audit - --operation=status {{ if not .Values.disableMutation}}- --operation=mutation-status{{- end }} 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 b1e5ea7f737..e645fe44d61 100644 --- a/manifest_staging/charts/gatekeeper/templates/gatekeeper-controller-manager-deployment.yaml +++ b/manifest_staging/charts/gatekeeper/templates/gatekeeper-controller-manager-deployment.yaml @@ -54,6 +54,7 @@ spec: - --logtostderr - --log-denies={{ .Values.logDenies }} - --emit-admission-events={{ .Values.emitAdmissionEvents }} + - --admission-events-involved-namespace={{ .Values.admissionEventsInvolvedNamespace }} - --log-level={{ (.Values.controllerManager.logLevel | empty | not) | ternary .Values.controllerManager.logLevel .Values.logLevel }} - --exempt-namespace={{ .Release.Namespace }} - --operation=webhook diff --git a/manifest_staging/charts/gatekeeper/templates/gatekeeper-manager-role-clusterrole.yaml b/manifest_staging/charts/gatekeeper/templates/gatekeeper-manager-role-clusterrole.yaml index 40376142aa3..a57b2b80c88 100644 --- a/manifest_staging/charts/gatekeeper/templates/gatekeeper-manager-role-clusterrole.yaml +++ b/manifest_staging/charts/gatekeeper/templates/gatekeeper-manager-role-clusterrole.yaml @@ -11,6 +11,13 @@ metadata: release: '{{ .Release.Name }}' name: gatekeeper-manager-role rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - '*' resources: diff --git a/manifest_staging/charts/gatekeeper/values.yaml b/manifest_staging/charts/gatekeeper/values.yaml index d9bdb618d44..2c94fc250c1 100644 --- a/manifest_staging/charts/gatekeeper/values.yaml +++ b/manifest_staging/charts/gatekeeper/values.yaml @@ -34,6 +34,8 @@ logDenies: false logMutations: false emitAdmissionEvents: false emitAuditEvents: false +admissionEventsInvolvedNamespace: false +auditEventsInvolvedNamespace: false resourceQuota: true image: repository: openpolicyagent/gatekeeper diff --git a/manifest_staging/deploy/gatekeeper.yaml b/manifest_staging/deploy/gatekeeper.yaml index 08093205b6e..98bc929522d 100644 --- a/manifest_staging/deploy/gatekeeper.yaml +++ b/manifest_staging/deploy/gatekeeper.yaml @@ -3222,6 +3222,13 @@ metadata: gatekeeper.sh/system: "yes" name: gatekeeper-manager-role rules: +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - '*' resources: diff --git a/pkg/audit/manager.go b/pkg/audit/manager.go index 0b3d8d9dbfa..c8f90dc571b 100644 --- a/pkg/audit/manager.go +++ b/pkg/audit/manager.go @@ -53,14 +53,15 @@ const ( ) var ( - auditInterval = flag.Uint("audit-interval", defaultAuditInterval, "interval to run audit in seconds. defaulted to 60 secs if unspecified, 0 to disable") - constraintViolationsLimit = flag.Uint("constraint-violations-limit", defaultConstraintViolationsLimit, "limit of number of violations per constraint. defaulted to 20 violations if unspecified") - auditChunkSize = flag.Uint64("audit-chunk-size", defaultListLimit, "(alpha) Kubernetes API chunking List results when retrieving cluster resources using discovery client. defaulted to 500 if unspecified") - auditFromCache = flag.Bool("audit-from-cache", false, "pull resources from audit cache when auditing") - emitAuditEvents = flag.Bool("emit-audit-events", false, "(alpha) emit Kubernetes events in gatekeeper namespace with detailed info for each violation from an audit") - auditMatchKindOnly = flag.Bool("audit-match-kind-only", false, "only use kinds specified in all constraints for auditing cluster resources. if kind is not specified in any of the constraints, it will audit all resources (same as setting this flag to false)") - apiCacheDir = flag.String("api-cache-dir", defaultAPICacheDir, "The directory where audit from api server cache are stored, defaults to /tmp/audit") - emptyAuditResults []updateListEntry + auditInterval = flag.Uint("audit-interval", defaultAuditInterval, "interval to run audit in seconds. defaulted to 60 secs if unspecified, 0 to disable") + constraintViolationsLimit = flag.Uint("constraint-violations-limit", defaultConstraintViolationsLimit, "limit of number of violations per constraint. defaulted to 20 violations if unspecified") + auditChunkSize = flag.Uint64("audit-chunk-size", defaultListLimit, "(alpha) Kubernetes API chunking List results when retrieving cluster resources using discovery client. defaulted to 500 if unspecified") + auditFromCache = flag.Bool("audit-from-cache", false, "pull resources from OPA cache when auditing") + emitAuditEvents = flag.Bool("emit-audit-events", false, "(alpha) emit Kubernetes events with detailed info for each violation from an audit") + auditEventsInvolvedNamespace = flag.Bool("audit-events-involved-namespace", false, "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 still follow the default behavior") + auditMatchKindOnly = flag.Bool("audit-match-kind-only", false, "only use kinds specified in all constraints for auditing cluster resources. if kind is not specified in any of the constraints, it will audit all resources (same as setting this flag to false)") + apiCacheDir = flag.String("api-cache-dir", defaultAPICacheDir, "The directory where audit from api server cache are stored, defaults to /tmp/audit") + emptyAuditResults []updateListEntry ) // Manager allows us to audit resources periodically. @@ -737,6 +738,8 @@ func (am *Manager) addAuditResponsesToUpdateLists( gvk := r.obj.GroupVersionKind() namespace := r.obj.GetNamespace() name := r.obj.GetName() + uid := r.obj.GetUID() + rv := r.obj.GetResourceVersion() ea := util.EnforcementAction(r.EnforcementAction) // append audit results only if it is below violations limit @@ -760,7 +763,7 @@ func (am *Manager) addAuditResponsesToUpdateLists( totalViolationsPerEnforcementAction[ea]++ logViolation(am.log, r.Constraint, ea, gvk, namespace, name, r.Msg, details, r.obj.GetLabels()) if *emitAuditEvents { - emitEvent(r.Constraint, timestamp, ea, gvk, namespace, name, r.Msg, am.gkNamespace, am.eventRecorder) + emitEvent(r.Constraint, timestamp, ea, gvk, namespace, name, rv, r.Msg, am.gkNamespace, uid, am.eventRecorder) } } return nil @@ -1042,7 +1045,7 @@ func logViolation(l logr.Logger, } func emitEvent(constraint *unstructured.Unstructured, - timestamp string, enforcementAction util.EnforcementAction, resourceGroupVersionKind schema.GroupVersionKind, rnamespace, rname, message, gkNamespace string, + timestamp string, enforcementAction util.EnforcementAction, resourceGroupVersionKind schema.GroupVersionKind, rnamespace, rname, rrv, message, gkNamespace string, ruid types.UID, eventRecorder record.EventRecorder, ) { annotations := map[string]string{ @@ -1061,19 +1064,34 @@ func emitEvent(constraint *unstructured.Unstructured, logging.ResourceNamespace: rnamespace, logging.ResourceName: rname, } + reason := "AuditViolation" - ref := getViolationRef(gkNamespace, resourceGroupVersionKind.Kind, rname, rnamespace, constraint.GetKind(), constraint.GetName(), constraint.GetNamespace()) + ref := getViolationRef(gkNamespace, resourceGroupVersionKind.Kind, rname, rnamespace, rrv, ruid, constraint.GetKind(), constraint.GetName(), constraint.GetNamespace(), *auditEventsInvolvedNamespace) - eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeWarning, reason, "Timestamp: %s, Resource Namespace: %s, Constraint: %s, Message: %s", timestamp, rnamespace, constraint.GetName(), message) + if *auditEventsInvolvedNamespace { + eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeWarning, reason, "Constraint: %s, Message: %s", constraint.GetName(), message) + } else { + eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeWarning, reason, "Resource Namespace: %s, Constraint: %s, Message: %s", rnamespace, constraint.GetName(), message) + } } -func getViolationRef(gkNamespace, rkind, rname, rnamespace, ckind, cname, cnamespace string) *corev1.ObjectReference { - return &corev1.ObjectReference{ +func getViolationRef(gkNamespace, rkind, rname, rnamespace, rrv string, ruid types.UID, ckind, cname, cnamespace string, emitInvolvedNamespace bool) *corev1.ObjectReference { + enamespace := gkNamespace + if emitInvolvedNamespace && len(rnamespace) > 0 { + enamespace = rnamespace + } + ref := &corev1.ObjectReference{ Kind: rkind, Name: rname, - UID: types.UID(rkind + "/" + rnamespace + "/" + rname + "/" + ckind + "/" + cnamespace + "/" + cname), - Namespace: gkNamespace, + Namespace: enamespace, + } + if emitInvolvedNamespace && len(ruid) > 0 && len(rrv) > 0 { + ref.UID = ruid + ref.ResourceVersion = rrv + } else if !emitInvolvedNamespace { + ref.UID = types.UID(rkind + "/" + rnamespace + "/" + rname + "/" + ckind + "/" + cnamespace + "/" + cname) } + return ref } // mergeErrors concatenates errs into a single error. None of the original errors diff --git a/pkg/audit/manager_test.go b/pkg/audit/manager_test.go index 38733e7d1d5..8f4a2e8e8e8 100644 --- a/pkg/audit/manager_test.go +++ b/pkg/audit/manager_test.go @@ -9,6 +9,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" ) func Test_newNSCache(t *testing.T) { @@ -187,9 +188,12 @@ func Test_getViolationRef(t *testing.T) { rkind string rname string rnamespace string + rrv string ckind string cname string cnamespace string + ruid types.UID + einvolved bool } tests := []struct { name string @@ -197,7 +201,7 @@ func Test_getViolationRef(t *testing.T) { want *corev1.ObjectReference }{ { - name: "Test case 1", + name: "Test case 1 - Gatekeeper Namespace", args: args{ gkNamespace: "default", rkind: "Pod", @@ -206,6 +210,7 @@ func Test_getViolationRef(t *testing.T) { ckind: "LimitRange", cname: "my-limit-range", cnamespace: "default", + einvolved: false, }, want: &corev1.ObjectReference{ Kind: "Pod", @@ -215,7 +220,7 @@ func Test_getViolationRef(t *testing.T) { }, }, { - name: "Test case 2", + name: "Test case 2 - GK Namespace", args: args{ gkNamespace: "kube-system", rkind: "Service", @@ -224,6 +229,7 @@ func Test_getViolationRef(t *testing.T) { ckind: "PodSecurityPolicy", cname: "my-pod-security-policy", cnamespace: "kube-system", + einvolved: false, }, want: &corev1.ObjectReference{ Kind: "Service", @@ -232,10 +238,75 @@ func Test_getViolationRef(t *testing.T) { Namespace: "kube-system", }, }, + { + name: "Test case 3 - Involved Namespace", + args: args{ + gkNamespace: "kube-system", + rkind: "Pod", + rname: "my-pod", + rrv: "123456", + ruid: "abcde-123456", + rnamespace: "default", + ckind: "LimitRange", + cname: "my-limit-range", + cnamespace: "default", + einvolved: true, + }, + want: &corev1.ObjectReference{ + Kind: "Pod", + Name: "my-pod", + Namespace: "default", + ResourceVersion: "123456", + UID: "abcde-123456", + }, + }, + { + name: "Test case 4 - Involved Namespace Cluster Scoped", + args: args{ + gkNamespace: "kube-system", + rkind: "Service", + rname: "my-service", + rrv: "123456", + ruid: "abcde-123456", + ckind: "PodSecurityPolicy", + cname: "my-pod-security-policy", + cnamespace: "kube-system", + einvolved: true, + }, + want: &corev1.ObjectReference{ + Kind: "Service", + Name: "my-service", + Namespace: "kube-system", + ResourceVersion: "123456", + UID: "abcde-123456", + }, + }, + { + name: "Test case 5 - Involved Namespace RV/UID", + args: args{ + gkNamespace: "kube-system", + rkind: "Service", + rname: "my-service", + rrv: "", + ruid: "", + rnamespace: "default", + ckind: "PodSecurityPolicy", + cname: "my-pod-security-policy", + cnamespace: "kube-system", + einvolved: true, + }, + want: &corev1.ObjectReference{ + Kind: "Service", + Name: "my-service", + Namespace: "default", + ResourceVersion: "", + UID: "", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := getViolationRef(tt.args.gkNamespace, tt.args.rkind, tt.args.rname, tt.args.rnamespace, tt.args.ckind, tt.args.cname, tt.args.cnamespace); !reflect.DeepEqual(got, tt.want) { + if got := getViolationRef(tt.args.gkNamespace, tt.args.rkind, tt.args.rname, tt.args.rnamespace, tt.args.rrv, tt.args.ruid, tt.args.ckind, tt.args.cname, tt.args.cnamespace, tt.args.einvolved); !reflect.DeepEqual(got, tt.want) { t.Errorf("getViolationRef() = %v, want %v", got, tt.want) } }) diff --git a/pkg/controller/config/config_controller.go b/pkg/controller/config/config_controller.go index 81baf0c6876..b896a93dcb0 100644 --- a/pkg/controller/config/config_controller.go +++ b/pkg/controller/config/config_controller.go @@ -193,6 +193,7 @@ type ReconcileConfig struct { // +kubebuilder:rbac:groups=policy,resources=podsecuritypolicies,resourceNames=gatekeeper-admin,verbs=use // +kubebuilder:rbac:groups=config.gatekeeper.sh,resources=configs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=config.gatekeeper.sh,resources=configs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch; // Reconcile reads that state of the cluster for a Config object and makes changes based on the state read // and what is in the Config.Spec diff --git a/pkg/webhook/common.go b/pkg/webhook/common.go index 951829e148d..01180cdc510 100644 --- a/pkg/webhook/common.go +++ b/pkg/webhook/common.go @@ -51,7 +51,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 in gatekeeper namespace for each admission violation") + emitAdmissionEvents = flag.Bool("emit-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") tlsMinVersion = flag.String("tls-min-version", "1.3", "minimum version of TLS supported") serviceaccount = fmt.Sprintf("system:serviceaccount:%s:%s", util.GetNamespace(), serviceAccountName) clientCAName = flag.String("client-ca-name", "", "name of the certificate authority bundle to authenticate the Kubernetes API server requests against") diff --git a/pkg/webhook/policy.go b/pkg/webhook/policy.go index f120c46869f..0159f716348 100644 --- a/pkg/webhook/policy.go +++ b/pkg/webhook/policy.go @@ -230,14 +230,17 @@ 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 resourceName string + obj := &unstructured.Unstructured{} + if len(res) > 0 && (*logDenies || *emitAdmissionEvents) { resourceName = req.AdmissionRequest.Name - if len(resourceName) == 0 && req.AdmissionRequest.Object.Raw != nil { - // On a CREATE operation, the client may omit name and - // rely on the server to generate the name. - obj := &unstructured.Unstructured{} + if req.AdmissionRequest.Object.Raw != nil { if _, _, err := deserializer.Decode(req.AdmissionRequest.Object.Raw, nil, obj); err == nil { - resourceName = obj.GetName() + // On a CREATE operation, the client may omit name and + // rely on the server to generate the name. + if len(resourceName) == 0 { + resourceName = obj.GetName() + } } } } @@ -290,24 +293,14 @@ func (h *validationHandler) getValidationMessages(res []*rtypes.Result, req *adm eventMsg = "Admission webhook \"validation.gatekeeper.sh\" denied request" reason = "FailedAdmission" } - ref := getViolationRef( - h.gkNamespace, - req.AdmissionRequest.Kind.Kind, - resourceName, - req.AdmissionRequest.Namespace, - r.Constraint.GetKind(), - r.Constraint.GetName(), - r.Constraint.GetNamespace()) - h.eventRecorder.AnnotatedEventf( - ref, - annotations, - corev1.EventTypeWarning, - reason, - "%s, Resource Namespace: %s, Constraint: %s, Message: %s", - eventMsg, - req.AdmissionRequest.Namespace, - r.Constraint.GetName(), - r.Msg) + + 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) + + if *admissionEventsInvolvedNamespace { + h.eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeWarning, reason, "%s, Constraint: %s, Message: %s", eventMsg, r.Constraint.GetName(), r.Msg) + } else { + h.eventRecorder.AnnotatedEventf(ref, annotations, corev1.EventTypeWarning, reason, "%s, Resource Namespace: %s, Constraint: %s, Message: %s", eventMsg, req.AdmissionRequest.Namespace, r.Constraint.GetName(), r.Msg) + } } if r.EnforcementAction == string(util.Deny) { @@ -624,13 +617,23 @@ func createReviewForResultant(obj *unstructured.Unstructured, ns *corev1.Namespa } } -func getViolationRef(gkNamespace, rkind, rname, rnamespace, ckind, cname, cnamespace string) *corev1.ObjectReference { - return &corev1.ObjectReference{ +func getViolationRef(gkNamespace, rkind, rname, rnamespace, rrv string, ruid types.UID, ckind, cname, cnamespace string, emitInvolvedNamespace bool) *corev1.ObjectReference { + enamespace := gkNamespace + if emitInvolvedNamespace && len(rnamespace) > 0 { + enamespace = rnamespace + } + ref := &corev1.ObjectReference{ Kind: rkind, Name: rname, - UID: types.UID(rkind + "/" + rnamespace + "/" + rname + "/" + ckind + "/" + cnamespace + "/" + cname), - Namespace: gkNamespace, + Namespace: enamespace, + } + if emitInvolvedNamespace && len(ruid) > 0 && len(rrv) > 0 { + ref.UID = ruid + ref.ResourceVersion = rrv + } else if !emitInvolvedNamespace { + ref.UID = types.UID(rkind + "/" + rnamespace + "/" + rname + "/" + ckind + "/" + cnamespace + "/" + cname) } + return ref } func AppendValidationWebhookIfEnabled(webhooks []rotator.WebhookInfo) []rotator.WebhookInfo { diff --git a/test/bats/test.bats b/test/bats/test.bats index a6e40886ca2..307540d715a 100644 --- a/test/bats/test.bats +++ b/test/bats/test.bats @@ -228,14 +228,14 @@ __required_labels_audit_test() { @test "emit events test" { # list events for easy debugging - kubectl get events -n ${GATEKEEPER_NAMESPACE} - events=$(kubectl get events -n ${GATEKEEPER_NAMESPACE} --field-selector reason=FailedAdmission -o json | jq -r '.items[] | select(.metadata.annotations.constraint_kind=="K8sRequiredLabels" )' | jq -s '. | length') + kubectl get events -n gatekeeper-test-playground + 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 ]] - events=$(kubectl get events -n ${GATEKEEPER_NAMESPACE} --field-selector reason=DryrunViolation -o json | jq -r '.items[] | select(.metadata.annotations.constraint_kind=="K8sRequiredLabels" )' | jq -s '. | length') + events=$(kubectl get events -n gatekeeper-test-playground --field-selector reason=DryrunViolation -o json | jq -r '.items[] | select(.metadata.annotations.constraint_kind=="K8sRequiredLabels" )' | jq -s '. | length') [[ "$events" -ge 1 ]] - events=$(kubectl get events -n ${GATEKEEPER_NAMESPACE} --field-selector reason=AuditViolation -o json | jq -r '.items[] | select(.metadata.annotations.constraint_kind=="K8sRequiredLabels" )' | jq -s '. | length') + events=$(kubectl get events -n gatekeeper-test-playground --field-selector reason=AuditViolation -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 9fe4fad10b9..601f4d5f44a 100644 --- a/website/docs/customize-startup.md +++ b/website/docs/customize-startup.md @@ -23,11 +23,15 @@ 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 in the Gatekeeper namespace. This flag is in alpha stage and it is set to `false` by default. +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-audit-events` flag enables the emission of all audit violation as Kubernetes events in the Gatekeeper namespace. 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. -There are three types of events that are emitted by Gatekeeper when the above flags are enabled: +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: | Event | Description | | ------------------ | ----------------------------------------------------------------------- |