Repository: projectcapsule/capsule

Issue:
Store policy violations events for audit
# Describe the feature
For audit purpose, it would nice to keep history of Tenants policy violations.
Just tracking events as for #289 could be not enough since events are lost after a while. 




Hints from issue comments:
@prometherion: We have to understand where we should store these events, user story is not enough detailed for a planning.@prometherion: @maxgio92 I'm not a guru of the Falco ecosystem, do you think that a sidekick could be worth it?

Keep in mind that the context is the audit purpose, not sure if it's the correct project.@maxgio92: @prometherion generally speaking I think that Falco helps understand whether an event is malicious.
Then, if the events are all related to violations, maybe it could further filter them by obtaining malicious violations.
I think it depends on whether we consider violations all malicious or not.

If we don't consider all violations to be malicious, based on the type of the event (data structure), it could be convenient and needed to write a Falco plugin, by leveraging its plugin framework.
A plugin introduces a new event data source, teaching Falco how to support events of that type.@maxgio92: If the type of the event is of the native Kind Audit event [`Event`](https://kubernetes.io/docs/reference/config-api/apiserver-audit.v1/#audit-k8s-io-v1-Event) I think we don't need a new plugin, as there's already one [in place](https://falco.org/docs/event-sources/kubernetes-audit/).

If so, based on that data source, Falco inspects those events and is able to filter based on user-defined rules (defaults [here](https://falco.org/docs/event-sources/kubernetes-audit/#kubernetes-audit-rules)).@prometherion: @bsctl any thoughts on this? Do you think we could close this?@maxgio92: Sorry @prometherion for the very late response.

I think Falco would be beneficial analysing the events for malicious ones.

If Capsule already filters them, I'm not quite sure.

Could we expose all Tenant related events offloading the duty to analyse, based on rules, to Falco?

Would that mean to send all Kubernetes Events? @bsctl: The intention of this feature request is to implement the standard Policy Report Custom Resource Definition (CRD) that can be used as a common way to provide policy results to Kubernetes cluster administrators and users, using native tools, ref.https://github.com/kubernetes-sigs/wg-policy-prototypes/tree/master/policy-report 

It has been implemented also in Kyverno  https://kyverno.io/docs/policy-reports/

Suggest to keep it open as Auditing is an important part of policy based systems.@github-actions: This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.

Solution patch:
diff --git a/controllers/tenant_controller.go b/controllers/tenant_controller.go
index 1ad3f9e..96e40bf 100644
--- a/controllers/tenant_controller.go
+++ b/controllers/tenant_controller.go
@@ -261,7 +261,7 @@ func (r *TenantReconciler) syncAdditionalRoleBindings(tenant *capsulev1alpha1.Te
 				return controllerutil.SetControllerReference(tenant, rb, r.Scheme)
 			})
 
-			r.emitEvent(tenant, rb.GetNamespace(), res, fmt.Sprintf("ensuring additional RoleBinding %s", rb.GetName()), err)
+			r.emitEvent(tenant, rb.GetNamespace(), res, fmt.Sprintf("Ensuring additional RoleBinding %s", rb.GetName()), err)
 
 			if err != nil {
 				r.Log.Error(err, "Cannot sync Additional RoleBinding")
@@ -394,7 +394,7 @@ func (r *TenantReconciler) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) er
 				return controllerutil.SetControllerReference(tenant, target, r.Scheme)
 			})
 
-			r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("ensuring ResourceQuota %s", target.GetName()), err)
+			r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring ResourceQuota %s", target.GetName()), err)
 
 			r.Log.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
 			if err != nil {
@@ -444,7 +444,7 @@ func (r *TenantReconciler) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error
 				return controllerutil.SetControllerReference(tenant, t, r.Scheme)
 			})
 
-			r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("ensuring LimitRange %s", t.GetName()), err)
+			r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("Ensuring LimitRange %s", t.GetName()), err)
 
 			r.Log.Info("LimitRange sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
 			if err != nil {
@@ -466,9 +466,10 @@ func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev
 		}
 
 		res, conflictErr = controllerutil.CreateOrUpdate(context.TODO(), r.Client, ns, func() error {
-			a := tnt.Spec.NamespacesMetadata.AdditionalAnnotations
-			if a == nil {
-				a = make(map[string]string)
+			a := make(map[string]string)
+
+			for k, v := range tnt.Spec.NamespacesMetadata.AdditionalAnnotations {
+				a[k] = v
 			}
 
 			if tnt.Spec.NodeSelector != nil {
@@ -508,10 +509,12 @@ func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev
 
 			ns.SetAnnotations(a)
 
-			l := tnt.Spec.NamespacesMetadata.AdditionalLabels
-			if l == nil {
-				l = make(map[string]string)
+			l := make(map[string]string)
+
+			for k, v := range tnt.Spec.NamespacesMetadata.AdditionalLabels {
+				l[k] = v
 			}
+
 			l["name"] = namespace
 			capsuleLabel, _ := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
 			l[capsuleLabel] = tnt.GetName()
@@ -523,7 +526,7 @@ func (r *TenantReconciler) syncNamespaceMetadata(namespace string, tnt *capsulev
 		return
 	})
 
-	r.emitEvent(tnt, namespace, res, "ensuring Namespace metadata", err)
+	r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err)
 
 	return
 }
@@ -585,7 +588,7 @@ func (r *TenantReconciler) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) e
 				return controllerutil.SetControllerReference(tenant, t, r.Scheme)
 			})
 
-			r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("ensuring NetworkPolicy %s", t.GetName()), err)
+			r.emitEvent(tenant, t.GetNamespace(), res, fmt.Sprintf("Ensuring NetworkPolicy %s", t.GetName()), err)
 
 			r.Log.Info("Network Policy sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
 			if err != nil {
@@ -646,7 +649,7 @@ func (r *TenantReconciler) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) erro
 			return controllerutil.SetControllerReference(tenant, target, r.Scheme)
 		})
 
-		r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("ensuring Capsule RoleBinding %s", target.GetName()), err)
+		r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring Capsule RoleBinding %s", target.GetName()), err)
 
 		r.Log.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
 		if err != nil {
diff --git a/main.go b/main.go
index f963859..c5bfd84 100644
--- a/main.go
+++ b/main.go
@@ -139,8 +139,6 @@ func main() {
 
 	cfg := configuration.NewCapsuleConfiguration(manager.GetClient(), configurationName)
 
-	webhookRecorder := manager.GetEventRecorderFor("tenant-webhook")
-
 	// webhooks: the order matters, don't change it and just append
 	webhooksList := append(
 		make([]webhook.Webhook, 0),
@@ -149,10 +147,10 @@ func main() {
 		registry.Webhook(registry.Handler()),
 		podpriority.Webhook(podpriority.Handler()),
 		services.Webhook(services.Handler()),
-		ownerreference.Webhook(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg, webhookRecorder))),
-		namespacequota.Webhook(utils.InCapsuleGroups(cfg, namespacequota.Handler(webhookRecorder))),
+		ownerreference.Webhook(utils.InCapsuleGroups(cfg, ownerreference.Handler(cfg))),
+		namespacequota.Webhook(utils.InCapsuleGroups(cfg, namespacequota.Handler())),
 		networkpolicies.Webhook(utils.InCapsuleGroups(cfg, networkpolicies.Handler())),
-		tenantprefix.Webhook(utils.InCapsuleGroups(cfg, tenantprefix.Handler(cfg, webhookRecorder))),
+		tenantprefix.Webhook(utils.InCapsuleGroups(cfg, tenantprefix.Handler(cfg))),
 		tenant.Webhook(tenant.Handler(cfg)),
 		imagepullpolicy.Webhook(imagepullpolicy.Handler()),
 	)
diff --git a/pkg/webhook/handler.go b/pkg/webhook/handler.go
index 368445e..d0666eb 100644
--- a/pkg/webhook/handler.go
+++ b/pkg/webhook/handler.go
@@ -6,6 +6,7 @@ package webhook
 import (
 	"context"
 
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 )
@@ -13,7 +14,7 @@ import (
 type Func func(ctx context.Context, req admission.Request) admission.Response
 
 type Handler interface {
-	OnCreate(client client.Client, decoder *admission.Decoder) Func
-	OnDelete(client client.Client, decoder *admission.Decoder) Func
-	OnUpdate(client client.Client, decoder *admission.Decoder) Func
+	OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) Func
+	OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) Func
+	OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) Func
 }
diff --git a/pkg/webhook/imagepullpolicy/errors.go b/pkg/webhook/imagepullpolicy/errors.go
index 1792ddb..fe8b803 100644
--- a/pkg/webhook/imagepullpolicy/errors.go
+++ b/pkg/webhook/imagepullpolicy/errors.go
@@ -23,5 +23,5 @@ func NewImagePullPolicyForbidden(usedPullPolicy, containerName string, allowedPu
 }
 
 func (f podPriorityClassForbidden) Error() (err string) {
-	return fmt.Sprintf("the ImagePullPolicy %s for container %s is not allowed, use one of the followings: %s", f.usedPullPolicy, f.containerName, strings.Join(f.allowedPullPolicies, ", "))
+	return fmt.Sprintf("the ImagePullPolicy %s for container %s is forbidden, use one of the followings: %s", f.usedPullPolicy, f.containerName, strings.Join(f.allowedPullPolicies, ", "))
 }
diff --git a/pkg/webhook/imagepullpolicy/validating.go b/pkg/webhook/imagepullpolicy/validating.go
index 5bf7db7..a1d83e1 100644
--- a/pkg/webhook/imagepullpolicy/validating.go
+++ b/pkg/webhook/imagepullpolicy/validating.go
@@ -9,6 +9,7 @@ import (
 
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -45,7 +46,7 @@ func Handler() capsulewebhook.Handler {
 	return &handler{}
 }
 
-func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		var pod = &corev1.Pod{}
 
@@ -77,6 +78,8 @@ func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew
 			usedPullPolicy := string(container.ImagePullPolicy)
 
 			if !policy.IsPolicySupported(usedPullPolicy) {
+				recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenPullPolicy", "Pod %s/%s pull policy %s is forbidden for the current Tenant", req.Namespace, req.Name, usedPullPolicy)
+
 				return admission.Denied(NewImagePullPolicyForbidden(usedPullPolicy, container.Name, policy.AllowedPullPolicies()).Error())
 			}
 		}
@@ -85,13 +88,13 @@ func (r *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew
 	}
 }
 
-func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
diff --git a/pkg/webhook/ingress/validating.go b/pkg/webhook/ingress/validating.go
index b1705c8..63998b2 100644
--- a/pkg/webhook/ingress/validating.go
+++ b/pkg/webhook/ingress/validating.go
@@ -8,12 +8,15 @@ import (
 	"fmt"
 	"net/http"
 	"regexp"
+	"strings"
 
 	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
 	extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
 	networkingv1 "k8s.io/api/networking/v1"
 	networkingv1beta1 "k8s.io/api/networking/v1beta1"
 	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -53,29 +56,29 @@ func Handler(configuration configuration.Configuration) capsulewebhook.Handler {
 	return &handler{configuration: configuration}
 }
 
-func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		ingress, err := r.ingressFromRequest(req, decoder)
 		if err != nil {
 			return admission.Errored(http.StatusBadRequest, err)
 		}
 
-		return r.validateIngress(ctx, client, ingress)
+		return r.validateIngress(ctx, client, ingress, recorder)
 	}
 }
 
-func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		ingress, err := r.ingressFromRequest(req, decoder)
 		if err != nil {
 			return admission.Errored(http.StatusBadRequest, err)
 		}
 
-		return r.validateIngress(ctx, client, ingress)
+		return r.validateIngress(ctx, client, ingress, recorder)
 	}
 }
 
-func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
@@ -173,7 +176,7 @@ func (r *handler) validateHostnames(tenant v1alpha1.Tenant, hostnames []string)
 	return nil
 }
 
-func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress Ingress) admission.Response {
+func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress Ingress, recorder record.EventRecorder) admission.Response {
 	tenantList := &v1alpha1.TenantList{}
 	if err := c.List(ctx, tenantList, client.MatchingFieldsSelector{
 		Selector: fields.OneTermEqualSelector(".status.namespaces", ingress.Namespace()),
@@ -187,14 +190,24 @@ func (r *handler) validateIngress(ctx context.Context, c client.Client, ingress
 	tenant := tenantList.Items[0]
 
 	if err := r.validateClass(tenant, ingress.IngressClass()); err != nil {
+		if ic := ingress.IngressClass(); ic != nil {
+			recorder.Eventf(&tenant, corev1.EventTypeWarning, "ForbidenIngressClass", "Ingress %s/%s class %s is forbidden for the current Tenant", ingress.Namespace(), ingress.Name(), *ic)
+		} else {
+			recorder.Eventf(&tenant, corev1.EventTypeWarning, "MissingIngressClass", "Ingress %s/%s is missing IngressClass", ingress.Namespace(), ingress.Name())
+		}
+
 		return admission.Errored(http.StatusBadRequest, err)
 	}
 
 	if err := r.validateHostnames(tenant, ingress.Hostnames()); err != nil {
+		recorder.Eventf(&tenant, corev1.EventTypeWarning, "ForbiddenHostname", "Ingress %s/%s hostnames %s is forbidden for the current Tenant", ingress.Namespace(), ingress.Name(), strings.Join(ingress.Hostnames(), ","))
+
 		return admission.Errored(http.StatusBadRequest, err)
 	}
 
 	if err := r.validateCollision(ctx, c, ingress); err != nil {
+		recorder.Eventf(&tenant, corev1.EventTypeWarning, "HostnameCollision", "Ingress %s/%s hostnames collision for %s ", ingress.Namespace(), ingress.Name(), strings.Join(ingress.Hostnames(), ","))
+
 		return admission.Errored(http.StatusBadRequest, err)
 	}
 
diff --git a/pkg/webhook/namespacequota/validating.go b/pkg/webhook/namespacequota/validating.go
index b90b20c..6561882 100644
--- a/pkg/webhook/namespacequota/validating.go
+++ b/pkg/webhook/namespacequota/validating.go
@@ -42,16 +42,13 @@ func (w *webhook) GetPath() string {
 }
 
 type handler struct {
-	recorder record.EventRecorder
 }
 
-func Handler(recorder record.EventRecorder) capsulewebhook.Handler {
-	return &handler{
-		recorder: recorder,
-	}
+func Handler() capsulewebhook.Handler {
+	return &handler{}
 }
 
-func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		ns := &corev1.Namespace{}
 		if err := decoder.Decode(req, ns); err != nil {
@@ -65,7 +62,7 @@ func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) cap
 				return admission.Errored(http.StatusBadRequest, err)
 			}
 			if tnt.IsFull() {
-				r.recorder.Eventf(tnt, corev1.EventTypeWarning, "Error", "the Namespace quota has been exceeded, Namespace %s cannot been attached", ns.GetName())
+				recorder.Eventf(tnt, corev1.EventTypeWarning, "NamespaceQuotaExceded", "Namespace %s cannot be attached, quota exceeded for the current Tenant", ns.GetName())
 
 				return admission.Denied(NewNamespaceQuotaExceededError().Error())
 			}
@@ -75,13 +72,13 @@ func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) cap
 	}
 }
 
-func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
diff --git a/pkg/webhook/networkpolicies/validating.go b/pkg/webhook/networkpolicies/validating.go
index a59f915..732b86d 100644
--- a/pkg/webhook/networkpolicies/validating.go
+++ b/pkg/webhook/networkpolicies/validating.go
@@ -7,8 +7,10 @@ import (
 	"context"
 	"net/http"
 
+	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -45,30 +47,44 @@ func Handler() capsulewebhook.Handler {
 	return &handler{}
 }
 
-func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (r *handler) generic(ctx context.Context, req admission.Request, client client.Client, _ *admission.Decoder) (bool, error) {
+func (r *handler) generic(ctx context.Context, req admission.Request, client client.Client, _ *admission.Decoder) (*v1alpha1.Tenant, error) {
 	var err error
 	np := &networkingv1.NetworkPolicy{}
 	err = client.Get(ctx, types.NamespacedName{Namespace: req.AdmissionRequest.Namespace, Name: req.AdmissionRequest.Name}, np)
 	if err != nil {
-		return false, err
+		return nil, err
 	}
 
-	return r.isCapsuleNetworkPolicy(np), nil
+	tnt := &v1alpha1.Tenant{}
+
+	l, _ := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{})
+	if v, ok := np.GetLabels()[l]; ok {
+		if err = client.Get(ctx, types.NamespacedName{Name: v}, tnt); err != nil {
+			return nil, err
+		}
+
+		return tnt, nil
+	}
+
+	return nil, nil
 }
 
-func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+// nolint:dupl
+func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
-		ok, err := r.generic(ctx, req, client, decoder)
+		tnt, err := r.generic(ctx, req, client, decoder)
 		if err != nil {
 			return admission.Errored(http.StatusInternalServerError, err)
 		}
-		if ok {
+		if tnt != nil {
+			recorder.Eventf(tnt, corev1.EventTypeWarning, "NetworkPolicyDeletion", "NetworkPolicy %s/%s cannot be deleted", req.Namespace, req.Name)
+
 			return admission.Denied("Capsule Network Policies cannot be deleted: please, reach out to the system administrators")
 		}
 
@@ -76,19 +92,16 @@ func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) cap
 	}
 }
 
-func (r *handler) isCapsuleNetworkPolicy(np *networkingv1.NetworkPolicy) (ok bool) {
-	l, _ := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{})
-	_, ok = np.GetLabels()[l]
-	return
-}
-
-func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+// nolint:dupl
+func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
-		ok, err := r.generic(ctx, req, client, decoder)
+		tnt, err := r.generic(ctx, req, client, decoder)
 		if err != nil {
 			return admission.Errored(http.StatusInternalServerError, err)
 		}
-		if ok {
+		if tnt != nil {
+			recorder.Eventf(tnt, corev1.EventTypeWarning, "NetworkPolicyUpdate", "NetworkPolicy %s/%s cannot be updated", req.Namespace, req.Name)
+
 			return admission.Denied("Capsule Network Policies cannot be updated: please, reach out to the system administrators")
 		}
 
diff --git a/pkg/webhook/ownerreference/patching.go b/pkg/webhook/ownerreference/patching.go
index e083f53..d1e0177 100644
--- a/pkg/webhook/ownerreference/patching.go
+++ b/pkg/webhook/ownerreference/patching.go
@@ -48,18 +48,16 @@ func (w *webhook) GetPath() string {
 }
 
 type handler struct {
-	cfg      configuration.Configuration
-	recorder record.EventRecorder
+	cfg configuration.Configuration
 }
 
-func Handler(cfg configuration.Configuration, recorder record.EventRecorder) capsulewebhook.Handler {
+func Handler(cfg configuration.Configuration) capsulewebhook.Handler {
 	return &handler{
-		cfg:      cfg,
-		recorder: recorder,
+		cfg: cfg,
 	}
 }
 
-func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		ns := &corev1.Namespace{}
 		if err := decoder.Decode(req, ns); err != nil {
@@ -78,10 +76,12 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul
 			}
 			// Tenant owner must adhere to user that asked for NS creation
 			if !h.isTenantOwner(tnt.Spec.Owner, req.UserInfo) {
+				recorder.Eventf(tnt, corev1.EventTypeWarning, "NonOwnedTenant", "Namespace %s cannot be assigned to the current Tenant", ns.GetName())
+
 				return admission.Denied("Cannot assign the desired namespace to a non-owned Tenant")
 			}
 			// Patching the response
-			return h.patchResponseForOwnerRef(tnt, ns)
+			return h.patchResponseForOwnerRef(tnt, ns, recorder)
 		}
 
 		// If we forceTenantPrefix -> find Tenant from NS name
@@ -117,13 +117,13 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul
 		}
 
 		if len(tenants) == 1 {
-			return h.patchResponseForOwnerRef(&tenants[0], ns)
+			return h.patchResponseForOwnerRef(&tenants[0], ns, recorder)
 		}
 
 		if h.cfg.ForceTenantPrefix() {
 			for _, tnt := range tenants {
 				if strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
-					return h.patchResponseForOwnerRef(tnt.DeepCopy(), ns)
+					return h.patchResponseForOwnerRef(tnt.DeepCopy(), ns, recorder)
 				}
 			}
 			admission.Denied("The Namespace prefix used doesn't match any available Tenant")
@@ -132,31 +132,31 @@ func (h *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul
 		return admission.Denied("Unable to assign namespace to tenant. Please use " + ln + " label when creating a namespace")
 	}
 }
-func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Denied("Capsule user cannot update a Namespace")
 	}
 }
 
-func (h *handler) patchResponseForOwnerRef(tenant *capsulev1alpha1.Tenant, ns *corev1.Namespace) admission.Response {
+func (h *handler) patchResponseForOwnerRef(tenant *capsulev1alpha1.Tenant, ns *corev1.Namespace, recorder record.EventRecorder) admission.Response {
 	scheme := runtime.NewScheme()
 	_ = capsulev1alpha1.AddToScheme(scheme)
 	_ = corev1.AddToScheme(scheme)
 
 	o, _ := json.Marshal(ns.DeepCopy())
 	if err := controllerutil.SetControllerReference(tenant, ns, scheme); err != nil {
-		h.recorder.Eventf(tenant, corev1.EventTypeWarning, "Error", "namespace %s cannot be assigned to the desired Tenant", ns.GetName())
+		recorder.Eventf(tenant, corev1.EventTypeWarning, "Error", "Namespace %s cannot be assigned to the desired Tenant", ns.GetName())
 
 		return admission.Errored(http.StatusInternalServerError, err)
 	}
 
-	h.recorder.Eventf(tenant, corev1.EventTypeNormal, "NamespaceCreationWebhook", "namespace %s has been assigned to the desired Tenant", ns.GetName())
+	recorder.Eventf(tenant, corev1.EventTypeNormal, "NamespaceCreationWebhook", "Namespace %s has been assigned to the desired Tenant", ns.GetName())
 
 	c, _ := json.Marshal(ns)
 	return admission.PatchResponseFromRaw(o, c)
diff --git a/pkg/webhook/podpriority/validating.go b/pkg/webhook/podpriority/validating.go
index 9155145..006c500 100644
--- a/pkg/webhook/podpriority/validating.go
+++ b/pkg/webhook/podpriority/validating.go
@@ -7,8 +7,9 @@ import (
 	"context"
 	"net/http"
 
-	v1 "k8s.io/api/core/v1"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -46,9 +47,9 @@ func Handler() capsulewebhook.Handler {
 	return &handler{}
 }
 
-func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
-		var pod = &v1.Pod{}
+		var pod = &corev1.Pod{}
 
 		if err := decoder.Decode(req, pod); err != nil {
 			return admission.Errored(http.StatusBadRequest, err)
@@ -78,6 +79,8 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew
 			// We don't have to force Pod to specify a Priority Class
 			return admission.Allowed("")
 		case !allowed.ExactMatch(priorityClassName) && !allowed.RegexMatch(priorityClassName):
+			recorder.Eventf(&tntList.Items[0], corev1.EventTypeWarning, "ForbiddenPriorityClass", "Pod %s/%s is using Priority Class %s is forbidden for the current Tenant", pod.Namespace, pod.Name, priorityClassName)
+
 			return admission.Errored(http.StatusBadRequest, NewPodPriorityClassForbidden(priorityClassName, *allowed))
 		default:
 			return admission.Allowed("")
@@ -85,13 +88,13 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew
 	}
 }
 
-func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
diff --git a/pkg/webhook/pvc/validating.go b/pkg/webhook/pvc/validating.go
index 3483eb5..b4225a4 100644
--- a/pkg/webhook/pvc/validating.go
+++ b/pkg/webhook/pvc/validating.go
@@ -7,8 +7,9 @@ import (
 	"context"
 	"net/http"
 
-	v1 "k8s.io/api/core/v1"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -45,10 +46,10 @@ func Handler() capsulewebhook.Handler {
 	return &handler{}
 }
 
-func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		var valid, matched bool
-		pvc := &v1.PersistentVolumeClaim{}
+		pvc := &corev1.PersistentVolumeClaim{}
 
 		if err := decoder.Decode(req, pvc); err != nil {
 			return admission.Errored(http.StatusBadRequest, err)
@@ -72,6 +73,8 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew
 		}
 
 		if pvc.Spec.StorageClassName == nil {
+			recorder.Eventf(&tnt, corev1.EventTypeWarning, "MissingStorageClass", "PersistentVolumeClaim %s/%s is missing StorageClass", req.Namespace, req.Name)
+
 			return admission.Errored(http.StatusBadRequest, NewStorageClassNotValid(*tntList.Items[0].Spec.StorageClasses))
 		}
 
@@ -79,19 +82,21 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew
 		valid = tnt.Spec.StorageClasses.ExactMatch(sc)
 		matched = tnt.Spec.StorageClasses.RegexMatch(sc)
 		if !valid && !matched {
+			recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenStorageClass", "PersistentVolumeClaim %s/%s StorageClass %s is forbidden for the current Tenant", req.Namespace, req.Name, sc)
+
 			return admission.Errored(http.StatusBadRequest, NewStorageClassForbidden(*pvc.Spec.StorageClassName, *tnt.Spec.StorageClasses))
 		}
 		return admission.Allowed("")
 	}
 }
 
-func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
diff --git a/pkg/webhook/registry/validating.go b/pkg/webhook/registry/validating.go
index 509be30..cfecebd 100644
--- a/pkg/webhook/registry/validating.go
+++ b/pkg/webhook/registry/validating.go
@@ -7,8 +7,9 @@ import (
 	"context"
 	"net/http"
 
-	v1 "k8s.io/api/core/v1"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -46,9 +47,9 @@ func Handler() capsulewebhook.Handler {
 	return &handler{}
 }
 
-func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
-		pod := &v1.Pod{}
+		pod := &corev1.Pod{}
 		if err := decoder.Decode(req, pod); err != nil {
 			return admission.Errored(http.StatusBadRequest, err)
 		}
@@ -72,6 +73,8 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew
 				valid = tnt.Spec.ContainerRegistries.ExactMatch(registry.Registry())
 				matched = tnt.Spec.ContainerRegistries.RegexMatch(registry.Registry())
 				if !valid && !matched {
+					recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenContainerRegistry", "Pod %s/%s is using a forbidden registry %s is forbidden for the current Tenant", req.Namespace, req.Name, registry.Registry())
+
 					return admission.Errored(http.StatusBadRequest, NewContainerRegistryForbidden(container.Image, *tnt.Spec.ContainerRegistries))
 				}
 			}
@@ -81,13 +84,13 @@ func (h *handler) OnCreate(c client.Client, decoder *admission.Decoder) capsulew
 	}
 }
 
-func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
diff --git a/pkg/webhook/router.go b/pkg/webhook/router.go
index e84a15a..5a74a09 100644
--- a/pkg/webhook/router.go
+++ b/pkg/webhook/router.go
@@ -8,6 +8,7 @@ import (
 	"io/ioutil"
 
 	admissionv1 "k8s.io/api/admission/v1"
+	"k8s.io/client-go/tools/record"
 	controllerruntime "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -21,11 +22,14 @@ func Register(manager controllerruntime.Manager, webhookList ...Webhook) error {
 		return nil
 	}
 
+	recorder := manager.GetEventRecorderFor("tenant-webhook")
+
 	server := manager.GetWebhookServer()
 	for _, wh := range webhookList {
 		server.Register(wh.GetPath(), &webhook.Admission{
 			Handler: &handlerRouter{
-				handler: wh.GetHandler(),
+				recorder: recorder,
+				handler:  wh.GetHandler(),
 			},
 		})
 	}
@@ -33,19 +37,20 @@ func Register(manager controllerruntime.Manager, webhookList ...Webhook) error {
 }
 
 type handlerRouter struct {
-	handler Handler
-	client  client.Client
-	decoder *admission.Decoder
+	handler  Handler
+	client   client.Client
+	decoder  *admission.Decoder
+	recorder record.EventRecorder
 }
 
 func (r *handlerRouter) Handle(ctx context.Context, req admission.Request) admission.Response {
 	switch req.Operation {
 	case admissionv1.Create:
-		return r.handler.OnCreate(r.client, r.decoder)(ctx, req)
+		return r.handler.OnCreate(r.client, r.decoder, r.recorder)(ctx, req)
 	case admissionv1.Update:
-		return r.handler.OnUpdate(r.client, r.decoder)(ctx, req)
+		return r.handler.OnUpdate(r.client, r.decoder, r.recorder)(ctx, req)
 	case admissionv1.Delete:
-		return r.handler.OnDelete(r.client, r.decoder)(ctx, req)
+		return r.handler.OnDelete(r.client, r.decoder, r.recorder)(ctx, req)
 	default:
 		return admission.Allowed("")
 	}
diff --git a/pkg/webhook/services/errors.go b/pkg/webhook/services/errors.go
index ac74240..dfb1292 100644
--- a/pkg/webhook/services/errors.go
+++ b/pkg/webhook/services/errors.go
@@ -35,5 +35,5 @@ func NewNodePortDisabledError() error {
 }
 
 func (nodePortDisabled) Error() string {
-	return "NodePort service types are disabled for a tenant: please, reach out to the system administrators"
+	return "NodePort service types are forbidden for the tenant: please, reach out to the system administrators"
 }
diff --git a/pkg/webhook/services/validating.go b/pkg/webhook/services/validating.go
index 3a34b93..10c955e 100644
--- a/pkg/webhook/services/validating.go
+++ b/pkg/webhook/services/validating.go
@@ -7,9 +7,11 @@ import (
 	"context"
 	"net"
 	"net/http"
+	"strings"
 
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -49,7 +51,7 @@ func Handler() capsulewebhook.Handler {
 	return &handler{}
 }
 
-func (r *handler) handleService(ctx context.Context, clt client.Client, decoder *admission.Decoder, req admission.Request) admission.Response {
+func (r *handler) handleService(ctx context.Context, clt client.Client, decoder *admission.Decoder, req admission.Request, recorder record.EventRecorder) admission.Response {
 	svc := &corev1.Service{}
 	if err := decoder.Decode(req, svc); err != nil {
 		return admission.Errored(http.StatusBadRequest, err)
@@ -67,6 +69,8 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder
 	tnt := tntList.Items[0]
 
 	if svc.Spec.Type == corev1.ServiceTypeNodePort && tnt.GetAnnotations()[enableNodePortsAnnotation] == "false" {
+		recorder.Eventf(&tnt, corev1.EventTypeWarning, "ForbiddenNodePort", "Service %s/%s cannot be type of NodePort for the current Tenant", req.Namespace, req.Name)
+
 		return admission.Errored(http.StatusBadRequest, NewNodePortDisabledError())
 	}
 
@@ -84,22 +88,24 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder
 		}
 	}
 
+	recorder.Eventf(&tnt, corev1.EventTypeWarning, "ExternalServiceIP", "Service %s/%s external IPs %s are not in the expected range for the current Tenant", req.Namespace, req.Name, strings.Join(svc.Spec.ExternalIPs, ","))
+
 	return admission.Errored(http.StatusBadRequest, NewExternalServiceIPForbidden(tnt.Spec.ExternalServiceIPs.Allowed))
 }
 
-func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
-		return r.handleService(ctx, client, decoder, req)
+		return r.handleService(ctx, client, decoder, req, recorder)
 	}
 }
 
-func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
-		return r.handleService(ctx, client, decoder, req)
+		return r.handleService(ctx, client, decoder, req, recorder)
 	}
 }
 
-func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
diff --git a/pkg/webhook/tenant/validating.go b/pkg/webhook/tenant/validating.go
index 12cdab3..d5815ce 100644
--- a/pkg/webhook/tenant/validating.go
+++ b/pkg/webhook/tenant/validating.go
@@ -9,6 +9,7 @@ import (
 	"regexp"
 
 	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -153,7 +154,7 @@ func (h *handler) validateTenantByRegex(tenant *v1alpha1.Tenant) error {
 	return nil
 }
 
-func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		if err := h.validateTenant(ctx, req, client, decoder); err != nil {
 			return admission.Denied(err.Error())
@@ -162,13 +163,13 @@ func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder) cap
 	}
 }
 
-func (h *handler) OnDelete(client.Client, *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(context.Context, admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, _ record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		if err := h.validateTenant(ctx, req, client, decoder); err != nil {
 			return admission.Denied(err.Error())
diff --git a/pkg/webhook/tenantprefix/validating.go b/pkg/webhook/tenantprefix/validating.go
index 3dd8191..2a5be32 100644
--- a/pkg/webhook/tenantprefix/validating.go
+++ b/pkg/webhook/tenantprefix/validating.go
@@ -46,17 +46,15 @@ func (w *webhook) GetPath() string {
 
 type handler struct {
 	configuration configuration.Configuration
-	recorder      record.EventRecorder
 }
 
-func Handler(configuration configuration.Configuration, recorder record.EventRecorder) capsulewebhook.Handler {
+func Handler(configuration configuration.Configuration) capsulewebhook.Handler {
 	return &handler{
 		configuration: configuration,
-		recorder:      recorder,
 	}
 }
 
-func (r *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnCreate(clt client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		ns := &corev1.Namespace{}
 		if err := decoder.Decode(req, ns); err != nil {
@@ -79,7 +77,7 @@ func (r *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul
 				return admission.Errored(http.StatusBadRequest, err)
 			}
 			if e := fmt.Sprintf("%s-%s", tnt.GetName(), ns.GetName()); !strings.HasPrefix(ns.GetName(), fmt.Sprintf("%s-", tnt.GetName())) {
-				r.recorder.Eventf(tnt, corev1.EventTypeWarning, "TenantPrefix", "Namespace %s does not match the expected prefix", ns.GetName())
+				recorder.Eventf(tnt, corev1.EventTypeWarning, "InvalidTenantPrefix", "Namespace %s does not match the expected prefix for the current Tenant", ns.GetName())
 
 				return admission.Denied("The namespace doesn't match the tenant prefix, expected " + e)
 			}
@@ -88,13 +86,13 @@ func (r *handler) OnCreate(clt client.Client, decoder *admission.Decoder) capsul
 	}
 }
 
-func (r *handler) OnDelete(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnDelete(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
 }
 
-func (r *handler) OnUpdate(client client.Client, decoder *admission.Decoder) capsulewebhook.Func {
+func (r *handler) OnUpdate(client.Client, *admission.Decoder, record.EventRecorder) capsulewebhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		return admission.Allowed("")
 	}
diff --git a/pkg/webhook/utils/in_capsule_groups.go b/pkg/webhook/utils/in_capsule_groups.go
index 9734447..3554bab 100644
--- a/pkg/webhook/utils/in_capsule_groups.go
+++ b/pkg/webhook/utils/in_capsule_groups.go
@@ -6,6 +6,7 @@ package utils
 import (
 	"context"
 
+	"k8s.io/client-go/tools/record"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
@@ -43,30 +44,30 @@ func (h handler) isCapsuleUser(req admission.Request) bool {
 	return false
 }
 
-func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder) webhook.Func {
+func (h *handler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		if !h.isCapsuleUser(req) {
 			return admission.Allowed("")
 		}
 
-		return h.handler.OnCreate(client, decoder)(ctx, req)
+		return h.handler.OnCreate(client, decoder, recorder)(ctx, req)
 	}
 }
 
-func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder) webhook.Func {
+func (h *handler) OnDelete(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		if !h.isCapsuleUser(req) {
 			return admission.Allowed("")
 		}
-		return h.handler.OnDelete(client, decoder)(ctx, req)
+		return h.handler.OnDelete(client, decoder, recorder)(ctx, req)
 	}
 }
 
-func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder) webhook.Func {
+func (h *handler) OnUpdate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) webhook.Func {
 	return func(ctx context.Context, req admission.Request) admission.Response {
 		if !h.isCapsuleUser(req) {
 			return admission.Allowed("")
 		}
-		return h.handler.OnUpdate(client, decoder)(ctx, req)
+		return h.handler.OnUpdate(client, decoder, recorder)(ctx, req)
 	}
 }



Test patch:
diff --git a/e2e/ingress_hostnames_test.go b/e2e/ingress_hostnames_test.go
index 6d903b3..0cccf6d 100644
--- a/e2e/ingress_hostnames_test.go
+++ b/e2e/ingress_hostnames_test.go
@@ -108,20 +108,22 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
 			return k8sClient.Create(context.TODO(), tnt)
 		}).Should(Succeed())
 	})
+
 	JustAfterEach(func() {
 		Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed())
 	})
 
+
 	It("should block a non allowed Hostname", func() {
-		maj, min, _ := GetKubernetesSemVer()
+		maj, min, v := GetKubernetesSemVer()
 
-		ns := NewNamespace("disallowed-hostname")
-		cs := ownerClient(tnt)
+		if maj == 1 && min > 18 {
+			ns := NewNamespace("disallowed-hostname-networking")
+			cs := ownerClient(tnt)
 
-		NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
-		TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+			NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
+			TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
 
-		if maj == 1 && min > 18 {
 			By("testing networking.k8s.io", func() {
 				Eventually(func() (err error) {
 					obj := networkingIngress("denied-networking", "kubernetes.io")
@@ -129,28 +131,47 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
 					return
 				}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
 			})
+
+			return
 		}
 
+		Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io")
+	})
+
+	It("should block a non allowed Hostname", func() {
+		maj, min, v := GetKubernetesSemVer()
+
 		if maj == 1 && min < 22 {
 			By("testing extensions", func() {
+				ns := NewNamespace("disallowed-hostname-extensions")
+				cs := ownerClient(tnt)
+
+				NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
+				TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+
 				Eventually(func() (err error) {
 					obj := extensionsIngress("denied-extensions", "kubernetes.io")
 					_, err = cs.ExtensionsV1beta1().Ingresses(ns.GetName()).Create(context.TODO(), obj, metav1.CreateOptions{})
 					return
 				}, defaultTimeoutInterval, defaultPollInterval).ShouldNot(Succeed())
 			})
+
+			return
 		}
+
+		Skip("Running test on Kubernetes " + v + ", extensions is deprecated")
 	})
 
 	It("should allow Hostnames in list", func() {
-		maj, min, _ := GetKubernetesSemVer()
-		ns := NewNamespace("allowed-hostname-list")
-		cs := ownerClient(tnt)
-
-		NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
-		TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+		maj, min, v := GetKubernetesSemVer()
 
 		if maj == 1 && min > 18 {
+			ns := NewNamespace("allowed-hostname-list-networking")
+			cs := ownerClient(tnt)
+
+			NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
+			TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+
 			By("testing networking.k8s.io", func() {
 				for i, h := range tnt.Spec.IngressHostnames.Exact {
 					Eventually(func() (err error) {
@@ -160,9 +181,23 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
 					}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
 				}
 			})
+
+			return
 		}
 
+		Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io")
+	})
+
+	It("should allow Hostnames in list", func() {
+		maj, min, v := GetKubernetesSemVer()
+
 		if maj == 1 && min < 22 {
+			ns := NewNamespace("allowed-hostname-list-extensions")
+			cs := ownerClient(tnt)
+
+			NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
+			TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+
 			By("testing extensions", func() {
 				for i, h := range tnt.Spec.IngressHostnames.Exact {
 					Eventually(func() (err error) {
@@ -172,18 +207,23 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
 					}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
 				}
 			})
+
+			return
 		}
+
+		Skip("Running test on Kubernetes " + v + ", extensions is deprecated")
 	})
 
 	It("should allow Hostnames in regex", func() {
-		maj, min, _ := GetKubernetesSemVer()
-		ns := NewNamespace("allowed-hostname-regex")
-		cs := ownerClient(tnt)
-
-		NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
-		TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+		maj, min, v := GetKubernetesSemVer()
 
 		if maj == 1 && min > 18 {
+			ns := NewNamespace("allowed-hostname-regex-networking")
+			cs := ownerClient(tnt)
+
+			NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
+			TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+
 			By("testing networking.k8s.io", func() {
 				for _, h := range []string{"foo", "bar", "bizz"} {
 					Eventually(func() (err error) {
@@ -193,10 +233,24 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
 					}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
 				}
 			})
+
+			return
 		}
 
+		Skip("Running test on Kubernetes " + v + ", doesn't provide networking.k8s.io")
+	})
+
+	It("should allow Hostnames in regex", func() {
+		maj, min, v := GetKubernetesSemVer()
+
 		if maj == 1 && min < 22 {
 			By("testing extensions", func() {
+				ns := NewNamespace("allowed-hostname-regex-extensions")
+				cs := ownerClient(tnt)
+
+				NamespaceCreation(ns, tnt, defaultTimeoutInterval).Should(Succeed())
+				TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName()))
+
 				for _, h := range []string{"foo", "bar", "bizz"} {
 					Eventually(func() (err error) {
 						obj := extensionsIngress(fmt.Sprintf("allowed-extensions-%s", h), fmt.Sprintf("%s.clastix.io", h))
@@ -206,5 +260,7 @@ var _ = Describe("when Tenant handles Ingress hostnames", func() {
 				}
 			})
 		}
+
+		Skip("Running test on Kubernetes " + v + ", extensions is deprecated")
 	})
 })
diff --git a/e2e/resource_quota_exceeded_test.go b/e2e/resource_quota_exceeded_test.go
index 09f137f..3718c49 100644
--- a/e2e/resource_quota_exceeded_test.go
+++ b/e2e/resource_quota_exceeded_test.go
@@ -11,7 +11,7 @@ import (
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
-	v1 "k8s.io/api/apps/v1"
+	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -124,11 +124,11 @@ var _ = Describe("exceeding a Tenant resource quota", func() {
 		cs := ownerClient(tnt)
 		for _, namespace := range nsl {
 			Eventually(func() (err error) {
-				d := &v1.Deployment{
+				d := &appsv1.Deployment{
 					ObjectMeta: metav1.ObjectMeta{
 						Name: "my-pause",
 					},
-					Spec: v1.DeploymentSpec{
+					Spec: appsv1.DeploymentSpec{
 						Replicas: pointer.Int32Ptr(5),
 						Selector: &metav1.LabelSelector{
 							MatchLabels: map[string]string{