Repository navigation
Deployment
| Mode | Privileges | Where |
|---|---|---|
watch, watch --logs
|
a kubeconfig with the RBAC below | anywhere |
watch --ebpf, --rules
|
root, or CAP_BPF + CAP_PERFMON
|
on each node |
inventory, vulns, watch --vulns
|
root (reads /proc/<pid>/root of containers), host PID namespace |
on each node |
The node-local modes only see the containers with a process on the host they
run on. Run one k3sec per node (DaemonSet), with --node or $NODE_NAME set
so it lists only its node's pods.
Without root, inventory / vulns stop before touching the cluster (real
output):
$ k3sec inventory
2026-10-29T14:42:54.266458Z ERROR inventory needs root to read containers' filesystems (/proc/<pid>/root): re-run with sudo, or as a privileged DaemonSet with hostPID
error: inventory needs root to read containers' filesystems (/proc/<pid>/root): re-run with sudo, or as a privileged DaemonSet with hostPID
Read-only, cluster-wide:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: k3sec
rules:
- apiGroups: [""]
resources: ["events"] # watch
verbs: ["list", "watch"]
- apiGroups: [""]
resources: ["pods"] # watch --logs/--ebpf/--vulns, inventory, vulns
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"] # watch --logs
verbs: ["get"]
- apiGroups: [""]
resources: ["nodes"] # vulns: k3s / containerd / kernel versions
verbs: ["get", "list"]Without nodes, vulns warns and skips the node stack; the
rest works.
sudo NODE_NAME=$(hostname) k3sec vulns --kubeconfig /etc/rancher/k3s/k3s.yaml/etc/rancher/k3s/k3s.yaml is the admin kubeconfig k3s writes on server nodes.
Under sudo, the cache lands in root's ~/.cache/k3sec; set
XDG_CACHE_HOME to choose.
The lab runs k3sec as root in the host PID namespace, from a container that chroots into the host. This is the setup every root output in this wiki comes from:
docker run --rm --privileged --pid=host --network host -v /:/host \
-e XDG_CACHE_HOME=/var/cache -e MLAB_API_KEY \
alpine:3.19 chroot /host /path/to/k3sec --kubeconfig /path/to/kubeconfig vulnsdeploy/kubernetes/k3sec.yaml
runs one pod per node from ghcr.io/mlab-sh/k3sec (k3sec, mlab-agent, the
rules). The agent enrols the node in an mlab workspace, holds its keys and the
only outbound connection, and starts k3sec serve as its tool
(Tool API). Everything k3sec sees lands in the workspace's
Cluster tab.
kubectl create namespace k3sec
kubectl -n k3sec create secret generic k3sec-enroll --from-literal=code=<cluster enrolment code>
kubectl apply -f k3sec.yamlA cluster enrolment code (workspace → Agents → Enrol an agent → A Kubernetes cluster)
enrols up to the number of nodes it was made for, for 24 hours. Each node
enrols once: its keys stay on the node (hostPath /var/lib/mlab-agent, mode
0700), never in a Secret, and a restarted pod reuses them. The Secret can be
deleted once every node shows up.
What the pod runs as:
| RBAC | read-only: events list/watch, pods get/list/watch, pods/log get, nodes get/list, the kube-system namespace (its UID is the cluster id) |
| Host |
hostPID (container processes, their /proc/<pid>/root and maps) and tracefs, read-only |
| Capabilities |
drop: ALL, then BPF, PERFMON, SYS_RESOURCE, SYS_PTRACE, DAC_READ_SEARCH; no privileged mode, readOnlyRootFilesystem
|
| Network | the agent to mlab's relay (WebSocket) and upload host; k3sec to the API server only |
The ConfigMap is the node's policy, and the ceiling. mlab chooses the
k3sec config from the workspace (sources, namespaces, severity, rule set) and
pushes it live, without a restart; it can never go beyond
[tools.k3sec.settings]:
[tools.k3sec.settings]
allow_k8s_events = true
allow_logs = false # pod logs stay on the cluster, whatever mlab asks
allow_ebpf = true
allow_rules = true
allow_vulns = true
namespaces = [] # empty: all; otherwise mlab can only narrow this list
rulesets = ["/usr/share/k3sec/rules"]
allow_remote_rules = falseallowed_ops is the closed list of what mlab may call, and k3sec is pinned to
the SHA-256 of the binary in the image (the entrypoint fills it in). Set
upload_hosts to the upload host mlab's policy editor shows: inventories and
SBOMs go there, nowhere else.
Logs: kubectl -n k3sec logs ds/k3sec (the agent's, and k3sec's stderr).
The same binary runs standalone: a privileged pod (hostPID,
privileged: true) with args: ["watch", "--vulns", "-o", "json"], the RBAC
above, and NDJSON on stdout for your log collector. --ebpf --rules /usr/share/k3sec/rules adds the kernel events and YARA detections.
vulns and watch --vulns call https://vuln.mlab.sh (or --mlab-url).
Air-gapped: point --mlab-url at an on-prem mlab; the inventory, --sbom and
everything else work without network beyond the API server.
Commands
Live stream
Vulnerabilities
Operations
Internals