Kubernetes Deployment
August 13, 2026 ยท View on GitHub
Syva's Kubernetes path is node-local. The DaemonSet runs syva-core and
syva-k8s on each node; the adapter talks to the local Unix socket and does not
introduce a remote control plane.
Quickstart
kubectl apply -f deploy/k8s/
That applies the SyvaZonePolicy CRD, the syva-system namespace + RBAC, and
the DaemonSet (core + adapter per node). Then declare zones and annotate
workloads:
kubectl apply -f - <<'EOF'
apiVersion: syva.dev/v1alpha1
kind: SyvaZonePolicy
metadata:
name: payments
namespace: default
spec: {}
EOF
kubectl annotate pod my-pod syva.false.systems/zone=payments
Watch denials with names, process, path/destination, and reasons:
kubectl exec -n syva-system ds/syva -c syva-core -- \
syvactl events --follow
The core's socket is also on each node at /run/syva/syva-core.sock, so
node-local syvactl works directly, and sudo -E make verify-deployment
proves the deployed DaemonSet blocks a real container. A file-adapter variant
(TOML policy instead of CRDs) lives at deploy/k8s/variants/daemonset-file.yaml.
One rule of containerized deployment: the core runs in its own mount
namespace, so RegisterHostPath can only resolve host paths that are
mounted into the core container at the same path. The DaemonSet mounts
the host's /tmp by default (it also hosts the startup self-test); add
volume mounts for the directories your zone policies protect.
Container Images
Released images are published to GHCR for linux/amd64 and linux/arm64 by
the release-images workflow (.github/workflows/release-images.yml), which
runs on version tags and on manual dispatch:
ghcr.io/false-systems/syva-core:<version>
ghcr.io/false-systems/syva-adapter-k8s:<version>
The syva-core image bundles the release eBPF object at
/usr/lib/syva/syva-ebpf (the first path object discovery checks) plus
syvactl; the adapter image contains syva-k8s.
Builds are two-step: the root Dockerfile compiles inside the published
toolchain image ghcr.io/false-systems/syva-builder (stable Rust, nightly +
rust-src, bpf-linker, protoc; defined in docker/builder.Dockerfile,
published by the builder-image workflow) and copies the artifacts into slim
runtime images. To build locally:
docker build --target syva-core -t syva-core:dev .
docker build --target syva-adapter-k8s -t syva-adapter-k8s:dev .
To rebuild against a locally built toolchain image instead of the published
one, pass --build-arg BUILDER_IMAGE=<your-builder-ref>.
Node prerequisites are unchanged by the packaging: a BPF-LSM-enabled kernel
(bpf in /sys/kernel/security/lsm), cgroup v2, BTF at
/sys/kernel/btf/vmlinux, and a containerd-style runtime layout. Images do not
relax any of the privileged requirements in the DaemonSet.
Zone Policy
syva-k8s watches SyvaZonePolicy CRDs and reconciles them into the local
syva.core.v1 API:
RegisterZoneRemoveZoneAllowCommDenyComm
The CRD remains the source of truth for zone policy. Selectors are not sent to
syva-core; they are Kubernetes adapter concerns.
Pod Membership
The first supported assignment model is an explicit pod annotation:
metadata:
annotations:
syva.false.systems/zone: zone-a
For pods scheduled to the adapter's local node, syva-k8s watches running
containers and calls:
AttachContainer(container_id, zone_name, cgroup_id, source="syva-k8s", generation=N)
When the pod is deleted, moves away from the node, stops running, or loses or changes its annotation, the adapter calls:
DetachContainer(container_id, source="syva-k8s", generation=N+1)
Pods without the annotation are ignored.
Reconciliation is generation-aware and outcome-checked:
-
A replacement core attaches a fresh disabled generation. The adapter waits for the CRD, node-membership, and cluster pod-IP initial watches to reach
InitDonewith no pending writes, then activates the exact staging ID. -
A core-only restart is detected from a new staging ID and triggers a full authoritative replay while the previous pinned generation keeps enforcing.
-
Generations are seeded from the clock at adapter start, so a restarted adapter keeps issuing generations above anything a previous instance sent and the core's stale-generation fencing keeps working across restarts.
-
An attach the core did not confirm (RPC error, stale, conflict, or cgroup resolution failure) remains pending and is retried on a timer; activation never depends on another pod event arriving.
-
A detach the core did not confirm stays in a pending queue and is re-sent with every subsequent event batch until the core acknowledges it.
-
The pod watch is scoped to the local node with a
spec.nodeNamefield selector. If the pod membership watcher itself fails, the adapter exits so the DaemonSet restarts it; it never keeps running with membership reconciliation silently disabled.
Cgroup Resolution
The adapter does not fake cgroup IDs. It attaches a container only when it can resolve:
- the runtime container ID from pod
status.containerStatuses[].containerID, - a host process for that container by scanning host
/proc, - the cgroup-v2 path from
/proc/<pid>/cgroup, and - the host cgroup inode from
/sys/fs/cgroup/<path>.
The DaemonSet mounts host /proc at /host/proc and host cgroup v2 at
/sys/fs/cgroup. Configure alternative paths with:
syva-k8s \
--host-proc /host/proc \
--host-cgroup /sys/fs/cgroup
If resolution fails, the adapter does not attach the container. It logs a structured warning and increments:
syva_k8s_reconcile_errors_total{reason="cgroup_resolution"}
The initial resolver is intentionally simple and targets runtimes whose host
cgroup-v2 process paths include the Kubernetes runtime container ID, including
the k3s/containerd layout proven by verify-k8s-membership. It does not use
mountinfo-only matches for PID selection because host mount tables can reference
container rootfs paths from unrelated processes. Container IDs shorter than 12
characters are rejected before any /proc scan, and the resolved cgroup path is
truncated at the path component naming the container scope, so a process the
workload moved into a nested sub-cgroup still resolves to the container scope
cgroup that enforcement keys on. Runtime-specific resolvers for CRI-O, Docker
variants, or unusual distributions should be added behind the same resolver
boundary.
Metrics
syva-k8s exposes Prometheus text metrics on --metrics-listen, default
0.0.0.0:9092:
syva_k8s_membership_attach_total{result}
syva_k8s_membership_detach_total{result}
syva_k8s_memberships_active
syva_k8s_reconcile_errors_total{reason}
result is one of applied, rejected, stale, or error.
End-to-End Verification
Implemented:
- annotation-based pod membership assignment,
- local-node pod filtering,
- running-container attach,
- deletion/annotation-change detach,
- generation-aware AttachContainer/DetachContainer calls,
- adapter membership metrics.
The privileged Kubernetes proof is:
sudo -E make verify-k8s-membership
The target expects an existing single-node Kubernetes cluster reachable by
kubectl on the same Linux host or VM that runs the command. k3s is the
preferred environment for the current proof because it is the simplest
single-node Lima setup; kind or another local cluster can work only if the pod
runtime's host cgroups are visible under the host /proc and /sys/fs/cgroup.
At the beginning of the run, the target prints the declared contract:
=== syva k8s membership integration contract ===
PASS: annotated pod in zone-a can read its own zone-a file.
BLOCK: annotated pod in zone-a cannot read protected zone-b file.
ASSIGNMENT: syva.false.systems/zone=zone-a
HOOK: file_open
EXPECTED DENIAL: EPERM / Operation not permitted
EXPECTED KERNEL EVIDENCE: file_open deny_delta=1
It then starts a local syva-core, starts syva-k8s against the current
cluster, registers syva-it-zone-a and syva-it-zone-b, creates a uniquely
named namespace and annotated pod, waits for the adapter to attach the pod's
container, and proves:
- deploying a Kubernetes pod annotated with a Syva zone,
- proving that pod can read its own zone file,
- proving it is blocked from another zone's file with
EPERM, - asserting
file_open deny_delta=1for that Kubernetes workload.
The output includes the pod namespace/name, node, runtime container ID, resolved
host cgroup-v2 path, resolved cgroup inode/id, AttachContainer result, allowed
read result, blocked read result, adapter metrics, and detach result. The target
is rerun-safe: it uses names derived from syva-k8s-it-<pid> and removes the
pod, namespace, local processes, and /tmp/syva-k8s-it-<pid> on exit.
The target fails red if the pod cannot be attached with a real cgroup id, if the
forbidden read succeeds, if the error is not EPERM / Operation not permitted, or if file_open deny_delta is not exactly 1.
Still out of scope:
- cgroup movement / zone escape protection,
- a remote control plane,
- REST expansion,
- UI,
- new BPF hook semantics.