Defense in depth
Each boundary below has to be defeated before an attacker reaches the next one. The untrusted workload is at the center; the cluster is the outermost ring. A successful escape from the workload has to defeat every layer above before it can reach another tenant or the host.VM isolation
Every sandbox is a Kata Containers pod, which means a real VM with its own kernel running on KVM. The workload sees a virtio block device for its rootfs, a virtio NIC, and not much else. The virtual hardware surface is tiny compared to a shared host kernel.kata-firecracker-devmapperuses Firecracker — ~50k LoC Rust microVMM, ~5MB resident, ~125ms boot. The Firecracker process runs inside the jailer: a chroot jail with an empty filesystem, dropped capabilities, a restrictive seccomp filter, and an unprivileged UID. An integration test (tests/integration/test_firecracker.py::test_jailer_active) verifies the jailer is active on every install.kata-qemu-longhornuses QEMU — bigger surface than Firecracker but still hardware-isolated, and Kata applies its own sandboxing primitives.k7duses Katakate’s own KVM VMM (runtimeClassName: k7). One daemon hosts every VM; privilege separation splits that daemon into a root node broker (no listening socket) and a uid-k7dVM broker that parses guest and shim input. Live CoW fork still requires sibling forks of one tree to share the VM broker’s address space — that is what makes the ~5 ms fork possible, and it means sibling forks of one tree are isolated more weakly than VMs on separate VMMs. See the k7d security model.k7d-fckeeps the k7d daemon for fork / pause but boots each guest on upstream Firecracker under the upstream jailer (runtimeClassName: k7-fc): a separate VMM process per VM with its own uid, chroot, pid namespace, cgroup, and Firecracker’s seccomp. A guest→VMM escape lands in that jail, not in the daemon. Pick it for multi-tenant--docker. Trade-offs: no hostPath / virtiofs, no time warp, a 32 MiB / 50m PodOverhead.
securityContext reaches the runtime inside the VM: Kata’s guest seccomp is enforced (disable_guest_seccomp = false), so the container’s RuntimeDefault profile is applied by the in-guest runtime rather than dropped. The k7d shim likewise passes linux.seccomp, maskedPaths / readonlyPaths, device cgroups, capabilities, and noNewPrivileges through to guest runc, stripping only what the guest kernel cannot honour (AppArmor profile, host cgroup path, host namespace paths).
Container hardening
Inside the VM, the user container is further locked down:Some package managers (
apk add, apt-get install) require root inside the container. Either run them in before_script with the defaults, or prebuild your image with the dependencies baked in.Network isolation
- Sandboxes never reach the platform. A cluster-wide deny-only
CiliumClusterwideNetworkPolicy(k7-sandbox-platform-deny) blocks every sandbox — even--egress-openones — from the nodes, the Kubernetes API server, cloud metadata / link-local, andkube-system/longhorn-systempods except CoreDNS. Cilium-only; Flannel clusters do not get this. - Ingress is denied by default and opt-in per sandbox:
--ingress-portopens TCP ports,--ingress-from(sandbox:/namespace:/cidr:) scopes who may connect,--expose-portpublishes through aNodePortwith the real client IP preserved. Opening a port without a source means “other sandboxes in this namespace”, never the world.kubectl exec/k7 shellare unaffected (they use the K8s API). - Egress is configurable per sandbox via
egress_whitelist: open by default, blocked with[], restricted by CIDRs, or restricted by FQDNs (with Cilium). DNS is blocked by default when egress is locked down. Forks inherit the source’s policy. - Details and the
cidr:-on-Cilium caveat: Networking.
API transport (TLS)
k7 install serves k7-api on NodePort 31007 over HTTPS by default. The API container stays HTTP on :8000 (kubelet probes unchanged); a Caddy sidecar in the same pod terminates TLS. API keys no longer travel in cleartext.
The CLI and SDK refuse
verify=False on an https:// URL — point them at the CA instead. Optionally restrict the NodePort to your client CIDRs with --api-allow-cidr (Networking). None of this is rate limiting.
API authentication
- API keys are generated with
secrets.token_urlsafe(32)and only shown once at creation. - Stored in
/etc/k7/api_keys.json(mode 0600) as SHA-256 hashes — the plaintext key is never written to disk. - Comparison uses
secrets.compare_digest(constant-time, timing-attack resistant). - Optional expiry; expired keys are purged opportunistically on every authenticated request.
- Optional namespace scope:
k7 generate-api-key NAME -n <ns>(repeatable). Enforced on every namespace-bearing endpoint — a scoped key cannot touch another namespace or run all-namespaces operations (403 Forbidden). Cluster-scoped reads such asGET /api/v1/nodes/storageare treated as all-namespaces operations and are denied to scoped keys too (CWE-862 fix, reported by @skeletonsec). Keys without a scope stay unrestricted (backward compatible). last_usedtimestamp recorded for audit.- Authenticate via
X-API-Key: <key>orAuthorization: Bearer <key>. - Registry SSRF guard (0.2.1+): control-plane OCI inspection rejects registry hosts that are not on the allowlist (default
registry-1.docker.io,ghcr.io,quay.io,public.ecr.aws; extend withK7_REGISTRY_ALLOWLIST) or that resolve to loopback / private / link-local / reserved addresses. Redirects are disabled; the oldlocalhost→ HTTP downgrade is gone.
k7-api control plane. 0.2.0 and earlier are unsupported.
The API runs as a K3s Deployment with its own ServiceAccount and a scoped ClusterRole — no admin kubeconfig is ever mounted.
Operational guidance
- Always lock down egress for untrusted workloads. Prefer FQDN allowlists (Cilium) — much less error-prone than tracking CIDR ranges that change.
- Don’t whitelist public DNS resolvers (
1.1.1.1,8.8.8.8) — that re-enables DNS exfiltration. - Rotate API keys with
k7 revoke-api-key+k7 generate-api-key. Treat them as credentials. Scope production keys with-nso a leaked key cannot list every namespace. - Keep TLS on and distribute the CA (default install) — or use
--api-hostnamefor a publicly trusted certificate. Add--api-allow-cidrwhen you know your client CIDRs. Do not fall back to--api-insecure-httpon a public node. - Be deliberate with ingress. Open only the ports you need, scope them with
sandbox:/namespace:, and treat--expose-port+cidr:0.0.0.0/0as publishing to the internet. - Pin image tags —
:latestis convenient but non-reproducible. K7 itself pins every image and Kubernetes manifest version it ships; do the same in your sandbox configs.
Known limitations
- Pre-1.0 software under active security review. Avoid for hyper-sensitive workloads until 1.0.
- No API rate limiting yet — put a proxy with rate limits in front for public exposure. TLS and
--api-allow-cidrare not a “secure API” claim. - Platform isolation and
--api-allow-cidrare Cilium-only —--cni flannelclusters keep per-sandbox policies but not the cluster-wide deny. - AppArmor profiles for the sandbox container are on the roadmap (the k7d Ubuntu guest ships its own).
- TEE (Trusted Execution Environment) support is on the roadmap.
- Tetragon (runtime enforcement) was evaluated and rejected for now; Hubble observability is available opt-in.
