Kubernetes workloads introduce security risks when deployed with excessive privileges, unsafe configurations, or settings that do not meet organizational requirements. Admission controls evaluate resource requests before Kubernetes creates or updates resources. These policy decisions provide useful security telemetry by identifying non-compliant workloads, rejected deployment attempts, and configuration changes.
Kyverno is a Kubernetes-native policy engine that validates resource requests against declarative policies. In Audit mode, Kyverno admits non-compliant resources and records the policy violations in policy reports. When enforcement is enabled, Kyverno rejects non-compliant requests. Kyverno writes Kubernetes events in both Audit and Deny modes, and this integration alerts only on requests that a Deny policy rejects. By default, policy reports and events stay within the Kubernetes cluster, making them invisible to security teams.
Wazuh is an open source security platform that collects and analyzes security telemetry from monitored endpoints. This blog post uses a custom Python collector to retrieve Kyverno policy reports, Kubernetes events, policy settings, and exception declarations. Wazuh analyzes the collector’s output to alert on policy violations, enforcement downgrades, policy removals, and exception declarations. Each policy finding includes policy, resource, namespace, and current enforcement context. Wazuh file integrity monitoring also detects changes to local policy manifests.
Infrastructure
We use the following infrastructure to demonstrate monitoring Kyverno policy violations:
- A pre-built, ready-to-use Wazuh OVA 4.14.8, which includes the Wazuh central components (Wazuh server, Wazuh indexer, and Wazuh dashboard). Follow this guide to download and set up the Wazuh virtual machine.
- An Ubuntu 24.04 endpoint with the following installed:
- The Wazuh agent 4.14.8 installed and enrolled in the Wazuh server.
- A self-managed Kubernetes cluster. We use K3s.
Kyverno policy violation reporting
A Kyverno ValidatingPolicy evaluates Kubernetes admission requests against the conditions defined in its validations field. The matchConstraints field specifies which resources the policy evaluates.
When a validation fails, the validationActions field determines what Kyverno does. An Audit action allows the resource to be created and records the violation in a PolicyReport. Kyverno uses ClusterPolicyReport for cluster-scoped resources.
A Deny action rejects the admission request before Kubernetes creates the resource. Because the resource does not exist, the denied request does not appear as a failed PolicyReport result. Kyverno instead creates a Kubernetes Event with the reason PolicyViolation.
Note
Kyverno 1.19.1 can generate admission events containing fail (blocked) for Audit policies that still allow the resource. The integration therefore reads the validationActions field from the live policy instead of determining enforcement from the Event message.
Kyverno also evaluates existing resources when evaluation.background.enabled is set to true. This lets policy reports show resources that currently violate a policy even if they existed before the policy changed. The collector therefore reads both policy reports and PolicyViolation events to capture existing violations and rejected admission attempts.
Architecture
The collector reads Kyverno findings from PolicyReport and ClusterPolicyReport objects and PolicyViolation events, along with the live policy settings and PolicyException objects. It reads each source through the Kubernetes API and checks each policy’s validationActions field to classify a finding as Audit or Deny.
The collector runs every 60 seconds and writes one JSON record per finding to /var/log/kyverno/kyverno-wazuh.log. It writes a record only when a failing result is new, recurrent, or is still failing 24 hours later. It stores processed findings in /var/lib/kyverno-wazuh/seen.json and the previous policy settings in /var/lib/kyverno-wazuh/policies.json, so it doesn’t report the same result on every run.
The Wazuh agent reads the JSON log file and forwards the records to the Wazuh server. The JSON decoder and custom rules generate alerts, while file integrity monitoring separately tracks changes in the /opt/kyverno-policies directory.

Configuration
Ubuntu endpoint
Run the following tests on the Ubuntu endpoint. Each test builds on the resources that the previous tests create. The collector runs every 60 seconds, so allow up to a minute before you look for each alert on the Wazuh dashboard.
Install Kyverno
- Install Kyverno
v1.19.1:
# export KUBECONFIG=/etc/rancher/k3s/k3s.yaml # kubectl create -f https://github.com/kyverno/kyverno/releases/download/v1.19.1/install.yaml
- Confirm that the Kyverno controllers are running:
# kubectl -n kyverno get pods
NAME READY STATUS RESTARTS AGE kyverno-admission-controller-687f76f7f4-cbc5b 1/1 Running 0 2m21s kyverno-background-controller-66bb47c44c-6f2j8 1/1 Running 0 2m21s kyverno-cleanup-controller-6d5587df98-gcmtn 1/1 Running 0 2m21s kyverno-reports-controller-7db7dcdd79-pdl7n 1/1 Running 0 2m21s
Create Kyverno policies
- Create the following directories in the
/opt/directory:
kyverno-policiesfor the Kyverno policy manifests.kyverno-wazuhfor the integration manifests.
# mkdir -p /opt/kyverno-policies /opt/kyverno-wazuh
- Set
rootas the owner and assign0755permissions. These permissions allow all users to read and execute, while onlyrootcan write to the directories:
# chown root:root /opt/kyverno-policies /opt/kyverno-wazuh # chmod 0755 /opt/kyverno-policies /opt/kyverno-wazuh
- Create a file
/opt/kyverno-wazuh/kyverno-ephemeralcontainers-rbac.yamlwith the following role-based access control (RBAC) configuration to grant Kyverno read access topods/ephemeralcontainersfor policy reporting:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kyverno:ephemeralcontainers-reader
labels:
rbac.kyverno.io/aggregate-to-reports-controller: "true"
rbac.kyverno.io/aggregate-to-background-controller: "true"
rbac.kyverno.io/aggregate-to-admission-controller: "true"
rules:
- apiGroups: [""]
resources: ["pods/ephemeralcontainers"]
verbs: ["get", "list", "watch"]
- Apply the RBAC manifest:
# kubectl apply -f /opt/kyverno-wazuh/kyverno-ephemeralcontainers-rbac.yaml
- Create the file
/opt/kyverno-policies/disallow-privileged-containers.yamlwith the following policy. This policy checks regular, init, and ephemeral containers for privileged mode. It runs in Audit mode, allowing the request while reporting containers configured withsecurityContext.privileged: true.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: disallow-privileged-containers
annotations:
policies.kyverno.io/title: Disallow Privileged Containers
policies.kyverno.io/category: Pod Security Standards (Baseline)
policies.kyverno.io/severity: high
spec:
validationActions:
- Audit
evaluation:
background:
enabled: true
matchConstraints:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values:
- kube-system
- kube-public
- kube-node-lease
- kyverno
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods", "pods/ephemeralcontainers"]
validations:
- message: >-
Privileged mode is disallowed. The fields
spec.containers[*].securityContext.privileged must be unset or set to false.
expression: >-
object.spec.containers.all(c,
!has(c.securityContext) || !has(c.securityContext.privileged) ||
c.securityContext.privileged == false) &&
(!has(object.spec.initContainers) || object.spec.initContainers.all(c,
!has(c.securityContext) || !has(c.securityContext.privileged) ||
c.securityContext.privileged == false)) &&
(!has(object.spec.ephemeralContainers) || object.spec.ephemeralContainers.all(c,
!has(c.securityContext) || !has(c.securityContext.privileged) ||
c.securityContext.privileged == false))
- Create the file
/opt/kyverno-policies/restrict-image-registries.yamlwith the following policy. This policy requires regular, init, and ephemeral containers to use images fromregistry.k8s.io. It runs in Deny mode and rejects matching admission requests containing images from other registries.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: restrict-image-registries
annotations:
policies.kyverno.io/title: Restrict Image Registries
policies.kyverno.io/category: Supply Chain Security
policies.kyverno.io/severity: high
spec:
validationActions:
- Deny
evaluation:
background:
enabled: true
matchConstraints:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values:
- kube-system
- kube-public
- kube-node-lease
- kyverno
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods", "pods/ephemeralcontainers"]
validations:
- message: >-
Container images must be pulled from the approved registry registry.k8s.io.
expression: >-
object.spec.containers.all(c, c.image.startsWith('registry.k8s.io/')) &&
(!has(object.spec.initContainers) ||
object.spec.initContainers.all(c, c.image.startsWith('registry.k8s.io/'))) &&
(!has(object.spec.ephemeralContainers) ||
object.spec.ephemeralContainers.all(c, c.image.startsWith('registry.k8s.io/')))
- Create the file
/opt/kyverno-policies/require-namespace-owner-label.yamlwith the following policy. This policy checks that namespaces have an owner label identifying the responsible team. It runs in Audit mode, allowing namespace creation while reporting missing labels. The policy excludeskube-system,kube-public,kube-node-lease,kyverno, and thedefaultnamespace.
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-namespace-owner-label
annotations:
policies.kyverno.io/title: Require Namespace Owner Label
policies.kyverno.io/category: Namespace Governance
policies.kyverno.io/severity: medium
spec:
validationActions:
- Audit
evaluation:
background:
enabled: true
matchConditions:
- name: exclude-system-namespaces
expression: >-
!(object.metadata.name in
['kube-system', 'kube-public', 'kube-node-lease', 'kyverno', 'default'])
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["namespaces"]
validations:
- message: >-
Namespaces must carry an owner label identifying the responsible team.
expression: "'owner' in object.metadata.?labels.orValue({})"
- Apply the policies:
# kubectl apply -f /opt/kyverno-policies/
validatingpolicy.policies.kyverno.io/disallow-privileged-containers created validatingpolicy.policies.kyverno.io/require-namespace-owner-label created validatingpolicy.policies.kyverno.io/restrict-image-registries created
- Confirm that the Kyverno policies are ready:
# kubectl get validatingpolicy
NAME AGE READY disallow-privileged-containers 2s true require-namespace-owner-label 2s true restrict-image-registries 2s true
Note
If a policy shows READY=false, wait a few seconds and run the command again.
Configure the collector
- Create a manifest
/opt/kyverno-wazuh/rbac.yamlto provision a dedicated service account with read-only access to the Kubernetes resources the collector needs:
apiVersion: v1
kind: ServiceAccount
metadata:
name: kyverno-wazuh-collector
namespace: kyverno
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kyverno-wazuh-collector
rules:
- apiGroups: ["wgpolicyk8s.io"]
resources: ["policyreports", "clusterpolicyreports"]
verbs: ["get", "list"]
- apiGroups: ["policies.kyverno.io"]
resources:
- validatingpolicies
- namespacedvalidatingpolicies
- policyexceptions
verbs: ["get", "list"]
- apiGroups: ["kyverno.io"]
resources:
- clusterpolicies
- policies
- policyexceptions
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kyverno-wazuh-collector
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kyverno-wazuh-collector
subjects:
- kind: ServiceAccount
name: kyverno-wazuh-collector
namespace: kyverno
---
apiVersion: v1
kind: Secret
metadata:
name: kyverno-wazuh-collector-token
namespace: kyverno
annotations:
kubernetes.io/service-account.name: kyverno-wazuh-collector
type: kubernetes.io/service-account-token
- Apply the
/opt/kyverno-wazuh/rbac.yamlmanifest:
# kubectl apply -f /opt/kyverno-wazuh/rbac.yaml
- Create a dedicated system account to run the collector without root privileges, then create its working directories:
# useradd --system --no-create-home --shell /usr/sbin/nologin kyverno-wazuh
- Create the following directories:
/etc/kyverno-wazuhfor thekubeconfigfile containing the Kubernetes service account token./var/lib/kyverno-wazuhforseen.jsonandpolicies.json, which track collected findings and previous policy settings./var/log/kyvernofor the collector’s output file,kyverno-wazuh.log.
# mkdir -p /etc/kyverno-wazuh /var/lib/kyverno-wazuh /var/log/kyverno
- Set
kyverno-wazuhas the owner and assign the directory permissions:
0700gives the owner read, write, and execute permissions to the credentials directory.0750gives the owner read, write, and execute permissions, and the group read and execute permissions for the state directory.0755gives the owner read, write, and execute permissions, and all other users read and execute permissions for the log directory.
# chown kyverno-wazuh:kyverno-wazuh /etc/kyverno-wazuh /var/lib/kyverno-wazuh /var/log/kyverno # chmod 0700 /etc/kyverno-wazuh # chmod 0750 /var/lib/kyverno-wazuh # chmod 0755 /var/log/kyverno
- Run the following commands to build a kubeconfig from the service account token:
# TOKEN=$(kubectl -n kyverno get secret kyverno-wazuh-collector-token -o jsonpath='{.data.token}' | base64 -d)
# CA=$(kubectl -n kyverno get secret kyverno-wazuh-collector-token -o jsonpath='{.data.ca\.crt}')
# cat > /etc/kyverno-wazuh/kubeconfig <<EOF
apiVersion: v1
kind: Config
clusters:
- name: local
cluster:
server: https://127.0.0.1:6443
certificate-authority-data: ${CA}
users:
- name: kyverno-wazuh-collector
user:
token: ${TOKEN}
contexts:
- name: default
context:
cluster: local
user: kyverno-wazuh-collector
current-context: default
EOF
- Set
kyverno-wazuhas the owner and group of the kubeconfig file:
# chown kyverno-wazuh:kyverno-wazuh /etc/kyverno-wazuh/kubeconfig
- Set
0600permissions so only the owner can read and modify the file containing the Kubernetes credentials:
# chmod 0600 /etc/kyverno-wazuh/kubeconfig
- Create the collector file
/usr/local/bin/kyverno-wazuh-collector.pywith the following content. The script collects Kyverno policy reports, Kubernetes events, policy changes, and exception declarations through the Kubernetes API. It writes each finding as a JSON record in/var/log/kyverno/kyverno-wazuh.logfor the Wazuh agent to collect.
#!/usr/bin/env python3
import hashlib
import json
from datetime import datetime
import os
import re
import subprocess
import sys
import time
KUBECONFIG = os.environ.get("KUBECONFIG", "/etc/kyverno-wazuh/kubeconfig")
KUBECTL = os.environ.get("KUBECTL", "/usr/local/bin/kubectl")
OUT_LOG = os.environ.get("KYVERNO_OUT_LOG", "/var/log/kyverno/kyverno-wazuh.log")
STATE_FILE = os.environ.get("KYVERNO_STATE_FILE", "/var/lib/kyverno-wazuh/seen.json")
POLICY_STATE_FILE = os.environ.get("KYVERNO_POLICY_STATE", "/var/lib/kyverno-wazuh/policies.json")
REMINDER_AFTER = 86400
CEL_POLICY_KINDS = ("validatingpolicies", "namespacedvalidatingpolicies")
CEL_KINDS = ("ValidatingPolicy", "NamespacedValidatingPolicy")
LEGACY_KINDS = ("ClusterPolicy", "Policy")
REPORT_SOURCE_KINDS = {
"KyvernoValidatingPolicy": CEL_KINDS,
"KyvernoPolicy": LEGACY_KINDS,
"kyverno": LEGACY_KINDS,
}
FAILING_RESULTS = ("fail", "error")
LEGACY_POLICY_KINDS = ("clusterpolicies", "policies")
EXCEPTION_KINDS = ("policyexceptions.policies.kyverno.io", "policyexceptions.kyverno.io")
POLICY_EVENT_KINDS = ("ClusterPolicy", "Policy", "ValidatingPolicy", "NamespacedValidatingPolicy")
ADMISSION_COMPONENT = "kyverno-admission"
ACTION_LABEL = {"enforce": "Deny", "audit": "Audit"}
def action_label(mode):
return ACTION_LABEL.get(mode, mode or "unknown")
ABSENT_TYPE_MARKERS = ("doesn't have a resource type", "could not find the requested resource")
EVENT_RE = re.compile(
r"^(?P<kind>\S+)\s+(?:(?P<namespace>[^/\s]+)/)?(?P<name>\S+?):\s+"
r"(?:\[(?P<rule>[^\]]+)\]\s+)?(?P<result>\w+)(?:\s+\(blocked\))?;\s*(?P<message>.*)$"
)
def kubectl_json(*args, optional=False):
env = dict(os.environ, KUBECONFIG=KUBECONFIG)
proc = subprocess.run(
[KUBECTL, *args, "-o", "json"],
capture_output=True, text=True, env=env, timeout=60,
)
if proc.returncode != 0:
stderr = proc.stderr.strip()
if optional and any(marker in stderr for marker in ABSENT_TYPE_MARKERS):
return {"items": []}
print("kubectl %s failed: %s" % (" ".join(args), stderr), file=sys.stderr)
sys.exit(1)
return json.loads(proc.stdout)
def policy_id(kind, namespace, name):
return "%s|%s|%s" % (kind or "", namespace or "", name or "")
def split_policy_ref(ref):
if ref and "/" in ref:
ns, _, name = ref.partition("/")
return ns, name
return "", ref or ""
def policy_inventory():
inventory = {}
for kind in CEL_POLICY_KINDS:
for item in kubectl_json("get", kind, "-A", optional=True).get("items", []):
meta, spec = item.get("metadata") or {}, item.get("spec") or {}
actions = spec.get("validationActions") or []
admission = ((spec.get("evaluation") or {}).get("admission") or {})
inventory[policy_id(item.get("kind"), meta.get("namespace"), meta.get("name"))] = {
"kind": item.get("kind"),
"namespace": meta.get("namespace") or "",
"name": meta.get("name"),
"uid": meta.get("uid"),
"mode": "enforce" if "Deny" in actions else "audit",
"admission": admission.get("enabled", True),
}
for kind in LEGACY_POLICY_KINDS:
for item in kubectl_json("get", kind, "-A", optional=True).get("items", []):
meta, spec = item.get("metadata") or {}, item.get("spec") or {}
actions = [(r.get("validate") or {}).get("failureAction")
for r in spec.get("rules") or []]
actions = [a.lower() for a in actions if a]
fallback = spec.get("validationFailureAction")
if not actions and fallback:
actions = [fallback.lower()]
if not actions:
continue
inventory[policy_id(item.get("kind"), meta.get("namespace"), meta.get("name"))] = {
"kind": item.get("kind"),
"namespace": meta.get("namespace") or "",
"name": meta.get("name"),
"uid": meta.get("uid"),
"mode": "enforce" if "enforce" in actions else "audit",
"admission": True,
}
return inventory
def lookup_policy(inventory, name, namespace="", kinds=()):
hits = [inventory[pid] for pid in
(policy_id(k, namespace, name) for k in kinds) if pid in inventory]
return hits[0] if len(hits) == 1 else None
def load_json(path, default):
try:
with open(path) as fh:
return json.load(fh)
except (OSError, ValueError):
return default
def save_json(path, data):
os.makedirs(os.path.dirname(path), exist_ok=True)
tmp = path + ".tmp"
with open(tmp, "w") as fh:
json.dump(data, fh)
os.replace(tmp, path)
def emit(records):
if not records:
return
os.makedirs(os.path.dirname(OUT_LOG), exist_ok=True)
with open(OUT_LOG, "a") as fh:
for rec in records:
fh.write(json.dumps({"kyverno": rec}, sort_keys=True) + "\n")
def is_expired(expires, now):
if not expires:
return False
try:
return datetime.fromisoformat(expires).timestamp() < now
except ValueError:
print("unparsable expiresAt: %s" % expires, file=sys.stderr)
return False
def digest(*parts):
return hashlib.sha256("|".join(parts).encode()).hexdigest()[:32]
def lifecycle_for(state, key, result, now):
previous = state.get(key)
if previous is None:
state[key] = {"result": result, "t": now}
return "new" if result in FAILING_RESULTS else None
was = previous.get("result")
previous["result"] = result
if result not in FAILING_RESULTS:
return None
if was != result:
previous["t"] = now
return "recurrence"
if now - previous.get("t", 0) >= REMINDER_AFTER:
previous["t"] = now
return "reminder"
return None
def prune_state(state, touched, now):
return {k: v for k, v in state.items()
if k in touched or now - v.get("t", 0) < REMINDER_AFTER * 2}
def result_timestamp(res):
ts = res.get("timestamp") or {}
seconds = ts.get("seconds")
if not seconds:
return ""
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(int(seconds)))
def collect_reports(state, touched, now, inventory):
records = []
for kind in ("policyreport", "clusterpolicyreport"):
for item in kubectl_json("get", kind, "-A", optional=True).get("items", []):
scope = item.get("scope") or {}
meta = item.get("metadata") or {}
for res in item.get("results") or []:
policy_ns, policy_name = split_policy_ref(res.get("policy"))
kinds = REPORT_SOURCE_KINDS.get(res.get("source"))
if kinds is None:
print("unsupported report source: %s" % res.get("source"), file=sys.stderr)
continue
entry = lookup_policy(inventory, policy_name, policy_ns, kinds)
if entry is None:
continue
mode = entry["mode"]
key = digest(kind, scope.get("uid", meta.get("name", "")),
entry["kind"], entry["namespace"], entry["name"],
entry.get("uid") or "", res.get("rule") or "", mode)
touched.add(key)
stage = lifecycle_for(state, key, res.get("result"), now)
if stage is None:
continue
records.append({
"source": kind,
"event_type": "existing_resource_violation" if mode == "enforce" else "violation",
"lifecycle": stage,
"enforcement": action_label(mode),
"policy": policy_name,
"policy_kind": entry["kind"],
"policy_namespace": policy_ns or entry["namespace"],
"rule": res.get("rule"),
"result": res.get("result"),
"severity": res.get("severity"),
"category": res.get("category"),
"policy_source": res.get("source"),
"resource_kind": scope.get("kind"),
"resource_name": scope.get("name"),
"resource_namespace": scope.get("namespace") or "cluster-scoped",
"resource_uid": scope.get("uid"),
"report_name": meta.get("name"),
"source_timestamp": result_timestamp(res),
"collected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)),
"message": res.get("message"),
})
return records
def collect_events(state, touched, now, inventory):
records = []
for item in kubectl_json("get", "events", "-A",
"--field-selector", "reason=PolicyViolation").get("items", []):
involved = item.get("involvedObject") or {}
if involved.get("kind") not in POLICY_EVENT_KINDS:
continue
meta = item.get("metadata") or {}
parsed = EVENT_RE.match(item.get("message", ""))
if not parsed:
print("unparsed PolicyViolation event: %s" % meta.get("uid"), file=sys.stderr)
continue
entry = lookup_policy(inventory, involved.get("name"),
involved.get("namespace") or "", (involved.get("kind"),))
if entry and involved.get("uid") and entry.get("uid") != involved["uid"]:
entry = None
mode = entry["mode"] if entry else "unknown"
component = item.get("reportingComponent") or (item.get("source") or {}).get("component")
if component != ADMISSION_COMPONENT:
continue
key = digest("event", meta.get("uid", ""), str(item.get("count", 1)))
touched.add(key)
if key in state:
continue
state[key] = {"result": "fail", "t": now}
if entry is None:
print("admission event for unresolved policy %s/%s, not classified: %s"
% (involved.get("namespace") or "-", involved.get("name"), meta.get("uid")),
file=sys.stderr)
continue
if mode != "enforce":
continue
records.append({
"source": "event",
"event_type": "rejected",
"lifecycle": "new",
"enforcement": action_label(mode),
"policy": involved.get("name"),
"policy_kind": involved.get("kind"),
"policy_namespace": involved.get("namespace") or "",
"rule": parsed.group("rule"),
"result": parsed.group("result"),
"resource_kind": parsed.group("kind"),
"resource_name": parsed.group("name"),
"resource_namespace": parsed.group("namespace") or "cluster-scoped",
"event_uid": meta.get("uid"),
"event_count": item.get("count", 1),
"reporting_component": component,
"source_timestamp": item.get("lastTimestamp") or item.get("eventTime") or "",
"collected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)),
"message": parsed.group("message"),
})
return records
def collect_policy_changes(previous, current, now):
if previous is None:
return []
stamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now))
records = []
for pid, entry in sorted(current.items()):
was = previous.get(pid)
if not was:
continue
base = {
"source": "policy_state",
"lifecycle": "new",
"policy": entry["name"],
"policy_kind": entry["kind"],
"policy_namespace": entry["namespace"],
"collected_at": stamp,
}
if was.get("mode") == "enforce" and entry["mode"] != "enforce":
records.append(dict(base,
event_type="policy_downgraded",
enforcement=action_label(entry["mode"]),
previous_enforcement=action_label(was.get("mode")),
message="Policy %s no longer denies admission requests."
% entry["name"]))
if was.get("uid") and entry.get("uid") and was["uid"] != entry["uid"]:
records.append(dict(base,
event_type="policy_replaced",
enforcement=action_label(entry["mode"]),
previous_enforcement=action_label(was.get("mode")),
message="Policy %s was replaced by a new object with the same "
"name." % entry["name"]))
if was.get("admission", True) and not entry["admission"]:
records.append(dict(base,
event_type="policy_admission_disabled",
enforcement=action_label(entry["mode"]),
previous_enforcement=action_label(was.get("mode")),
message="Policy %s no longer evaluates admission requests."
% entry["name"]))
for pid, was in sorted(previous.items()):
if pid not in current:
records.append({
"source": "policy_state",
"event_type": "policy_removed",
"lifecycle": "new",
"enforcement": "none",
"previous_enforcement": action_label(was.get("mode")),
"policy": was.get("name"),
"policy_kind": was.get("kind"),
"policy_namespace": was.get("namespace", ""),
"collected_at": stamp,
"message": "Policy %s is no longer present in the cluster." % was.get("name"),
})
return records
def collect_exceptions(state, touched, now):
records = []
stamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now))
for kind in EXCEPTION_KINDS:
for item in kubectl_json("get", kind, "-A", optional=True).get("items", []):
meta, spec = item.get("metadata") or {}, item.get("spec") or {}
refs = [r.get("name") for r in spec.get("policyRefs") or [] if r.get("name")]
for legacy in spec.get("exceptions") or []:
if legacy.get("policyName"):
refs.append(legacy["policyName"])
expires = spec.get("expiresAt") or ""
expired = is_expired(expires, now)
key = digest("exception", meta.get("uid", ""),
str(meta.get("generation", "")), "expired" if expired else "active")
touched.add(key)
if key in state:
continue
state[key] = {"result": "fail", "t": now}
policies = ",".join(sorted(set(refs))) or "unspecified"
records.append({
"source": "policyexception",
"event_type": "exception_expired" if expired else "exception_declared",
"lifecycle": "new",
"exception_name": meta.get("name"),
"exception_kind": item.get("kind"),
"exception_namespace": meta.get("namespace") or "cluster-scoped",
"policy": policies,
"expires_at": expires or "never",
"collected_at": stamp,
"message": "PolicyException %s declares an exemption from %s."
% (meta.get("name"), policies),
})
return records
def main():
now = time.time()
state = load_json(STATE_FILE, {})
touched = set()
inventory = policy_inventory()
records = (collect_policy_changes(load_json(POLICY_STATE_FILE, None), inventory, now)
+ collect_reports(state, touched, now, inventory)
+ collect_events(state, touched, now, inventory)
+ collect_exceptions(state, touched, now))
emit(records)
save_json(STATE_FILE, prune_state(state, touched, now))
save_json(POLICY_STATE_FILE, inventory)
print("emitted %d record(s)" % len(records))
if __name__ == "__main__":
main()
- Set the ownership and permissions on the collector script:
# chown root:root /usr/local/bin/kyverno-wazuh-collector.py # chmod 0755 /usr/local/bin/kyverno-wazuh-collector.py
- Create the file
/etc/systemd/system/kyverno-wazuh-collector.servicewith the following content. This service configures systemd to run the collector using the dedicated system account and Kubernetes credentials:
[Unit] Description=Kyverno to Wazuh policy telemetry collector After=k3s.service [Service] Type=oneshot User=kyverno-wazuh Group=kyverno-wazuh Environment=KUBECONFIG=/etc/kyverno-wazuh/kubeconfig Environment=HOME=/var/lib/kyverno-wazuh ExecStart=/usr/local/bin/kyverno-wazuh-collector.py
- Create the file
/etc/systemd/system/kyverno-wazuh-collector.timerwith the following content. This schedules the collector to run every 60 seconds:
Unit] Description=Run the Kyverno to Wazuh collector every 60 seconds [Timer] OnBootSec=60 OnUnitActiveSec=60 AccuracySec=5s Unit=kyverno-wazuh-collector.service [Install] WantedBy=timers.target
- Create the collector log file. This file stores Kyverno findings for the Wazuh agent to collect.
# install -o kyverno-wazuh -g kyverno-wazuh -m 0644 /dev/null /var/log/kyverno/kyverno-wazuh.log
- Reload systemd and start the
kyverno-wazuh-collectorservice:
# systemctl daemon-reload # systemctl start kyverno-wazuh-collector.service # systemctl enable --now kyverno-wazuh-collector.timer
- Confirm that the collector runs and writes records:
# systemctl status kyverno-wazuh-collector.service --no-pager # journalctl -u kyverno-wazuh-collector.service -n 20 --no-pager
Configure the Wazuh agent
- Append the following block to the
/var/ossec/etc/ossec.conffile to monitor the collector logs:
<ossec_config>
<localfile>
<log_format>json</log_format>
<location>/var/log/kyverno/kyverno-wazuh.log</location>
</localfile>
</ossec_config>
- Add the following setting inside the existing
<syscheck>block in/var/ossec/etc/ossec.confto monitor the/opt/kyverno-policiespolicy directory in realtime:
<directories check_all="yes" realtime="yes" report_changes="yes">/opt/kyverno-policies</directories>
- Restart the Wazuh agent to apply the configuration changes:
# systemctl restart wazuh-agent
| # systemctl restart wazuh-agent |
Wazuh server
Perform the following steps on the Wazuh dashboard:
- Navigate to Server management > Rules, then click + Add new rules file.
- Copy and paste the rules below and name the file
kyverno_rules.xml. Click Save, then Reload to apply the changes:
<group name="kyverno,kubernetes,">
<rule id="101900" level="0">
<decoded_as>json</decoded_as>
<field name="kyverno.source">\.+</field>
<description>Kyverno policy telemetry event.</description>
</rule>
<rule id="101901" level="6">
<if_sid>101900</if_sid>
<field name="kyverno.source">^policyreport$</field>
<field name="kyverno.event_type">^violation$</field>
<field name="kyverno.result">^fail$</field>
<field name="kyverno.lifecycle" type="pcre2">^(new|recurrence)$</field>
<description>Kyverno Audit policy violation: policy $(kyverno.policy) failed on $(kyverno.resource_kind) $(kyverno.resource_name) in namespace $(kyverno.resource_namespace). Finding is $(kyverno.lifecycle).</description>
<group>kyverno_audit_violation,</group>
</rule>
<rule id="101902" level="10">
<if_sid>101900</if_sid>
<field name="kyverno.event_type">^rejected$</field>
<field name="kyverno.result">^fail$</field>
<description>Kyverno admission request refused by Deny policy $(kyverno.policy): $(kyverno.resource_kind) $(kyverno.resource_name) in $(kyverno.resource_namespace).</description>
<group>kyverno_admission_refused,</group>
</rule>
<rule id="101903" level="10" frequency="5" timeframe="300">
<if_matched_sid>101901</if_matched_sid>
<same_field>kyverno.resource_namespace</same_field>
<description>Kyverno repeat offender: 5 or more new or recurring policy violations received for namespace $(kyverno.resource_namespace) within 5 minutes.</description>
<group>kyverno_repeat_offender,</group>
</rule>
<rule id="101904" level="10">
<if_sid>550,553,554</if_sid>
<field name="file">^/opt/kyverno-policies</field>
<description>Kyverno policy file $(file) was added, modified, or deleted.</description>
<group>syscheck,kyverno_policy_change,</group>
</rule>
<rule id="101905" level="12">
<if_sid>101900</if_sid>
<field name="kyverno.event_type">^existing_resource_violation$</field>
<field name="kyverno.result">^fail$</field>
<field name="kyverno.lifecycle" type="pcre2">^(new|recurrence)$</field>
<description>Kyverno Deny policy $(kyverno.policy) has a failed report for existing $(kyverno.resource_kind) $(kyverno.resource_name) in $(kyverno.resource_namespace).</description>
<group>kyverno_existing_violation,</group>
</rule>
<rule id="101906" level="10">
<if_sid>101902</if_sid>
<field name="kyverno.policy">^restrict-image-registries$</field>
<field name="kyverno.result">^fail$</field>
<description>Registry policy validation failed for $(kyverno.resource_kind) $(kyverno.resource_name) in $(kyverno.resource_namespace).</description>
<mitre>
<id>T1610</id>
</mitre>
<group>kyverno_untrusted_image,</group>
</rule>
<rule id="101907" level="12">
<if_sid>101900</if_sid>
<field name="kyverno.event_type">^policy_downgraded$</field>
<description>Kyverno policy $(kyverno.policy) was downgraded from $(kyverno.previous_enforcement) to $(kyverno.enforcement). It no longer refuses admission requests.</description>
<mitre>
<id>T1562.001</id>
</mitre>
<group>kyverno_enforcement_weakened,</group>
</rule>
<rule id="101908" level="12">
<if_sid>101900</if_sid>
<field name="kyverno.event_type">^policy_removed$</field>
<description>Kyverno policy $(kyverno.policy) was removed from the cluster.</description>
<mitre>
<id>T1562.001</id>
</mitre>
<group>kyverno_enforcement_weakened,</group>
</rule>
<rule id="101909" level="10">
<if_sid>101900</if_sid>
<field name="kyverno.event_type">^exception_declared$</field>
<description>Kyverno PolicyException $(kyverno.exception_name) in namespace $(kyverno.exception_namespace) declares an exemption from policy $(kyverno.policy), expiring $(kyverno.expires_at).</description>
<group>kyverno_policy_exception,</group>
</rule>
<rule id="101910" level="6">
<if_sid>101900</if_sid>
<field name="kyverno.source">^clusterpolicyreport$</field>
<field name="kyverno.event_type">^violation$</field>
<field name="kyverno.result">^fail$</field>
<field name="kyverno.lifecycle" type="pcre2">^(new|recurrence)$</field>
<description>Kyverno Audit policy violation on a cluster-scoped resource: policy $(kyverno.policy) failed on $(kyverno.resource_kind) $(kyverno.resource_name).</description>
<group>kyverno_cluster_scoped,</group>
</rule>
<rule id="101911" level="12">
<if_sid>101900</if_sid>
<field name="kyverno.event_type">^policy_admission_disabled$</field>
<description>Kyverno policy $(kyverno.policy) stopped evaluating admission requests while its action stayed $(kyverno.enforcement).</description>
<mitre>
<id>T1562.001</id>
</mitre>
<group>kyverno_enforcement_weakened,</group>
</rule>
<rule id="101912" level="3">
<if_sid>101900</if_sid>
<field name="kyverno.event_type">^exception_expired$</field>
<description>Kyverno PolicyException $(kyverno.exception_name) in namespace $(kyverno.exception_namespace) expired at $(kyverno.expires_at).</description>
<group>kyverno_policy_exception,</group>
</rule>
<rule id="101913" level="3">
<if_sid>101900</if_sid>
<field name="kyverno.lifecycle">^reminder$</field>
<description>Kyverno finding still unresolved after 24 hours: policy $(kyverno.policy) on $(kyverno.resource_kind) $(kyverno.resource_name).</description>
<group>kyverno_reminder,</group>
</rule>
<rule id="101914" level="10">
<if_sid>101900</if_sid>
<field name="kyverno.result">^error$</field>
<field name="kyverno.lifecycle" type="pcre2">^(new|recurrence)$</field>
<description>Kyverno could not complete policy evaluation for $(kyverno.resource_kind) $(kyverno.resource_name) in $(kyverno.resource_namespace) under policy $(kyverno.policy).</description>
<group>kyverno_evaluation_error,</group>
</rule>
<rule id="101915" level="10">
<if_sid>101900</if_sid>
<field name="kyverno.event_type">^policy_replaced$</field>
<description>Kyverno policy $(kyverno.policy) was deleted and recreated with the same name.</description>
<group>kyverno_enforcement_weakened,</group>
</rule>
</group>
Where:
- Rule
101900groups all Kyverno records and serves as the base rule. - Rule
101901is triggered when a new or recurring Audit policy violation is reported for a namespaced resource. - Rule
101902is triggered when Kyverno rejects an admission request under a Deny policy. - Rule
101903is triggered when rule101901fires five times for the same namespace within five minutes. - Rule
101904is triggered when a file under/opt/kyverno-policiesis added, modified, or deleted. - Rule
101905is triggered when a violation is reported for an existing resource under a policy configured with Deny. - Rule
101906is triggered when therestrict-image-registriespolicy rejects an admission request. - Rule
101907is triggered when a policy is downgraded from Deny to Audit. - Rule
101908is triggered when a policy is removed from the cluster. - Rule
101909is triggered when a PolicyException exempts resources from a policy. - Rule
101910is triggered when an Audit policy violation is reported for a cluster-scoped resource. - Rule
101911is triggered when admission evaluation is disabled for a policy. - Rule
101912is triggered when a PolicyException has expired. - Rule
101913is triggered when a failing finding is still unresolved 24 hours after the collector first reported it. - Rule
101914is triggered when Kyverno reports a policy evaluation error. - Rule
101915is triggered when a policy is deleted and recreated with the same name.
Testing
Perform the following tests on the Ubuntu endpoint to validate the integration.
Cluster-scoped policy violation
The require-namespace-owner-label policy runs in Audit mode and requires an owner label to identify the team responsible for each namespace. Namespaces without this label are allowed, but Kyverno records the violations in ClusterPolicyReport objects.
Create namespaces billing, dev-apps, and payments without an owner label:
# kubectl create ns billing # kubectl create ns dev-apps # kubectl create ns payments
This generates three alerts for rule 101910, one for each namespace.
Audit policy violation
The disallow-privileged-containers policy runs in Audit mode and checks whether containers have the privileged mode enabled. Kyverno allows pods that fail this check and records the violations in policy reports.
- Create a privileged pod on the Kubernetes endpoint:
# kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: privileged-app
namespace: dev-apps
spec:
containers:
- name: app
image: registry.k8s.io/pause:3.9
securityContext:
privileged: true
EOF
pod/privileged-app created
This generates an alert for rule 101901.
Repeated namespace violations
Multiple policy findings in one namespace can indicate a shared workload configuration that needs review. Wazuh correlates new or recurring Audit report findings for the same namespace and generates an alert when it receives five or more alerts within five minutes.
- Create five privileged pods in the payments namespace:
# for i in 1 2 3 4 5; do
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: legacy-batch-$i
namespace: payments
spec:
containers:
- name: app
image: registry.k8s.io/pause:3.9
securityContext:
privileged: true
EOF
done
This generates four alerts for rule 101901 and an alert for rule 101903.
- Confirm that the pods are running:
# kubectl get pods -n payments
NAME READY STATUS RESTARTS AGE legacy-batch-1 1/1 Running 0 8s legacy-batch-2 1/1 Running 0 7s legacy-batch-3 1/1 Running 0 5s legacy-batch-4 1/1 Running 0 3s legacy-batch-5 1/1 Running 0 2s
Denied admission request
The restrict-image-registries policy runs in Deny mode and permits container images only from registry.k8s.io. Kyverno rejects pod creation requests that reference other registries and records the failed evaluations in Kubernetes events.
- Create a manifest for a pod that uses an unapproved container registry:
# cat > /tmp/untrusted-registry-pod.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: untrusted-registry-app
namespace: payments
spec:
containers:
- name: app
image: docker.io/library/nginx:1.27
EOF
- Apply the manifest:
# kubectl apply -f /tmp/untrusted-registry-pod.yaml
Kyverno rejects the admission request:
Error from server: error when creating "/tmp/untrusted-registry-pod.yaml": admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy restrict-image-registries failed: Container images must be pulled from the approved registry registry.k8s.io.
This generates an alert for rule 101906.
Policy switched to Deny
Changing the disallow-privileged-containers policy from Audit to Deny affects subsequent admission requests without evicting existing pods. Existing privileged pods remain visible through failed policy reports, while new privileged pod creation requests are rejected.
- Change the
disallow-privileged-containerspolicy to Deny:
# kubectl patch validatingpolicy disallow-privileged-containers --type=json \
-p='[{"op":"replace","path":"/spec/validationActions","value":["Deny"]}]'
This generates six alerts for rule 101905, one for each privileged pod that is still running.
- Create a privileged pod:
# kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: privileged-retry
namespace: dev-apps
spec:
containers:
- name: app
image: registry.k8s.io/pause:3.9
securityContext:
privileged: true
EOF
Error from server: error when creating "STDIN": admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy disallow-privileged-containers failed: Privileged mode is disallowed. The fields spec.containers[*].securityContext.privileged must be unset or set to false.
This generates an alert for rule 101902.
PolicyException declaration
A PolicyException declares an exemption for selected resources from a policy. Monitoring these declarations helps security teams review changes that could weaken enforcement.
Create an exception declaration targeting the batch pods, against the privileged-container policy:
# kubectl apply -f - <<EOF
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata:
name: legacy-batch-exemption
namespace: payments
spec:
policyRefs:
- name: disallow-privileged-containers
kind: ValidatingPolicy
matchConditions:
- name: exempt-legacy-batch
expression: "object.metadata.name.startsWith('legacy-batch-')"
EOF
Warning: PolicyException resources would not be processed until it is enabled. policyexception.policies.kyverno.io/legacy-batch-exemption created
This generates an alert for rule 101909.
Note
The Kyverno 1.19.1 installation manifest defaults enablePolicyException to false, so the exception does not bypass admission enforcement. The report results for the five legacy-batch pods change from fail to skip, and the collector stops reporting them. Rule 101909 alerts on the exception declaration.
Weakened and removed policy
A local policy manifest change does not affect the policy running in Kubernetes until you apply the manifest. Wazuh file integrity monitoring (FIM) detects edits to the local manifests. The collector detects changes to the live policies by comparing each policy’s settings with its previous run.
- Downgrade the
restrict-image-registriespolicy from Deny to Audit in the manifest:
# sed -i 's/- Deny/- Audit/' /opt/kyverno-policies/restrict-image-registries.yaml
This generates an alert for rule 101904.
- Apply the edited manifest:
# kubectl apply -f /opt/kyverno-policies/restrict-image-registries.yaml
# systemctl start kyverno-wazuh-collector.service
# kubectl get validatingpolicy restrict-image-registries -o jsonpath='{.spec.validationActions}'
This generates an alert for rule 101907.
- Remove the policy from the cluster:
# kubectl delete validatingpolicy restrict-image-registries
This generates an alert for rule 101908.
Disabled admission evaluation
A policy keeps its validation action when admission evaluation is disabled, so the action alone doesn’t show the change. The collector reads the admission setting and alerts when it is turned off.
- Disable admission evaluation for the
require-namespace-owner-labelpolicy:
# kubectl patch validatingpolicy require-namespace-owner-label --type=json \
-p='[{"op":"add","path":"/spec/evaluation/admission","value":{"enabled":false}}]'
# systemctl start kyverno-wazuh-collector.service
The policy remains configured with Audit, but it no longer evaluates admission requests.
This generates an alert for rule 101911.
- Restore the admission evaluation before continuing:
# kubectl patch validatingpolicy require-namespace-owner-label --type=json \
-p='[{"op":"replace","path":"/spec/evaluation/admission/enabled","value":true}]'
# systemctl start kyverno-wazuh-collector.service
Expired policy exception
A PolicyException can include an expiry time to limit the duration of a declared exemption. The collector reports the declaration when it first discovers it and generates a separate alert after its expiry time passes.
- Create a PolicyException object that expires in two minutes:
# EXPIRY=$(date -u -d '+2 minutes' +%Y-%m-%dT%H:%M:%SZ)
# kubectl apply -f - <<EOF
apiVersion: policies.kyverno.io/v1
kind: PolicyException
metadata:
name: temporary-batch-exemption
namespace: payments
spec:
expiresAt: "${EXPIRY}"
policyRefs:
- name: disallow-privileged-containers
kind: ValidatingPolicy
matchConditions:
- name: exempt-temporary-batch
expression: "object.metadata.name.startsWith('temp-batch-')"
EOF
- Wait 150 seconds for the exception to expire, then run the collector. The collector detects the expiry only while the PolicyException object exists, so it must run before you delete the object:
# sleep 150 # systemctl start kyverno-wazuh-collector.service
This generates an alert for rule 101912.
- Remove the temporary PolicyException object:
# kubectl delete policyexception.policies.kyverno.io temporary-batch-exemption -n payments
Policy evaluation error
A policy expression can produce an error result when it accesses a field that is absent from a resource. This indicates that Kyverno could not complete the evaluation. Wazuh reports evaluation errors separately from policy violations.
- Create a policy that reads the label without checking whether it exists:
# kubectl apply -f - <<EOF
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-team-label
spec:
validationActions:
- Audit
evaluation:
background:
enabled: true
matchConstraints:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: In
values:
- dev-apps
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- message: >-
Pods must carry a team label identifying the owning team.
expression: "object.metadata.labels['team'] == 'platform'"
EOF
- Wait about 30 seconds for the background scan to finish, then inspect the reports for the pods in the
dev-appsnamespace:
# kubectl get policyreport -n dev-apps -o jsonpath='{range .items[*].results[*]}{.policy}{" "}{.result}{" "}{.message}{"\n"}{end}'
disallow-privileged-containers fail Privileged mode is disallowed. The fields spec.containers[*].securityContext.privileged must be unset or set to false. require-team-label error error: no such key: labels
This generates an alert for rule 101914.
Policy replaced by a new object
Deleting a policy and recreating it under the same name leaves the name in place while the object behind it changes. The collector compares object UIDs between runs, so the replacement is visible even though the policy list looks unchanged.
- Stop the timer so both commands fall between the same two collector runs:
# systemctl stop kyverno-wazuh-collector.timer
- Delete the require-namespace-owner-label policy and recreate it:
# kubectl delete validatingpolicy require-namespace-owner-label # kubectl apply -f /opt/kyverno-policies/require-namespace-owner-label.yaml
- Start the timer and collector service:
# systemctl start kyverno-wazuh-collector.service # systemctl start kyverno-wazuh-collector.timer
This generates an alert for rule 101915. It also generates two alerts for rule 101910, for the dev-apps and payments namespaces, because the collector tracks each finding per policy object and reports findings under the recreated policy as new. If the collector runs between the delete and the apply, it reports rule 101908 instead, because the policy is absent at that moment.
Visualizing the alerts
Perform the following steps on the Wazuh dashboard to view the generated alerts:
- Navigate to Threat intelligence > Threat Hunting > Events.
- Search for
rule.groups:kyverno.

Conclusion
Kyverno provides policy enforcement and compliance visibility for Kubernetes workloads. PolicyReports identify existing resources that violate configured policies, including violations detected through background evaluation. Events record admission requests that Kyverno rejects before Kubernetes creates the requested resource.
This integration uses a custom Python collector to retrieve Kyverno policy findings and configuration changes. Wazuh correlates Kyverno findings with policy configuration changes to provide centralized visibility into policy enforcement. The integration also identifies changes that can weaken enforcement, including policy downgrades, policy removals, and declared exceptions.
Wazuh file integrity monitoring complements this visibility by detecting additions, modifications, and deletions of local Kyverno policy manifests. Together, these capabilities provide a centralized view of policy violations, rejected deployments, and changes to the policies governing Kubernetes workloads.
Wazuh is a free and open source security platform. Check out our documentation to learn more, and join our community for help and discussion.