Files
k7/SECURITY.md
G 13fafe0997 Release 0.4.0
Per-key node pins, k7d-fc pause/resume/exec, and HA-soak fixes. Playbook
pins k7d 0.7.0. GitHub .deb, Launchpad PPA, and PyPI k7-sdk are 0.4.0.
2026-09-19 23:22:14 +02:00

11 KiB

Security Policy

Supported Versions

This project is pre-1.0 and under active development; breaking changes may occur until 1.0.0. Security fixes land on the latest release line only.

Version Supported
0.2.1 and later Yes
0.2.0 and earlier No — upgrade to 0.2.1

Reporting a Vulnerability

If you believe you have found a security vulnerability, please email:

  • security@katakate.org (preferred)
  • Or open a private security advisory via GitHub (Security → Advisories → Report a vulnerability)

Please include:

  • A detailed description of the issue and potential impact
  • Steps to reproduce or proof-of-concept
  • Affected versions / commit SHAs and environment details

We aim to acknowledge reports within 72 hours and provide a remediation plan or mitigation timeline when applicable.

Do not open a public issue for security-sensitive reports.

Scope and current model

  • Nodes run K3s. A cluster (or a single node) can install multiple sandbox backends; each sandbox picks one:
    • kfd (kata-firecracker-devmapper) — Firecracker via Kata. The Firecracker process runs inside the jailer (chroot + dropped capabilities + seccomp). An integration test asserts the jailer is active after install.
    • kql (kata-qemu-longhorn) — QEMU via Kata with a Longhorn PVC root (durable disk, named snapshots / restore / disk-only fork).
    • k7dKatakate/k7d (runtimeClassName: k7); CoW sibling-fork isolation differs — see k7d's SECURITY.md.
    • k7d-fc (runtimeClassName: k7-fc) — same daemon and shim; stock Firecracker under the stock jailer. What that buys (and does not) is k7d's SECURITY.md Firecracker profile table. hostPath volumes are refused before the pod is created.
  • Sandbox containers run as non-root with restricted capabilities on top of the VM boundary.
  • The control plane API uses API keys with hashed storage and expiry (file-backed by default at /etc/k7/api_keys.json — rotate and protect that file). Keys may optionally be scoped to one or more namespaces (k7 generate-api-key -n <ns>); they may also be scoped to one or more Kubernetes nodes (k7 generate-api-key --node <node>). Absent/empty scope keeps the historical unrestricted behaviour (backward compatible). Scoped keys are enforced on every namespace-bearing and cluster-scoped control-plane route — they cannot list across all namespaces, touch namespaces outside their list, or read cluster-wide topology (e.g. GET /api/v1/nodes/storage). A node-scoped key may only create/restore/fork sandboxes onto its listed nodes (a single-node scope auto-pins; several nodes require an explicit node_name). Namespace scope is the control-plane tenancy boundary; node scope is the placement boundary (API-key --node is a Kubernetes node name from k7 nodes list / kubectl get nodes — the Linux hostname K3s registered — joined via the kubelet-stamped kubernetes.io/hostname label; inventory.ini does not write that label). Together they keep a tenant off another tenant's k7d daemon only if that node is also dedicated (k7 nodes dedicate NODE --tenant ID writes k7.katakate.org/tenant as a label and a NoSchedule taint). Without the taint, an unscoped key can still land on the same node. Unscoped keys still have full cross-namespace control-plane access and unrestricted placement.
  • The control-plane API is on a public NodePort (31007) on a public node, over HTTPS by default. A Caddy sidecar terminates TLS; the API container and its probes stay HTTP on :8000. The default cert is a playbook-minted cluster CA at /etc/k7/tls/ca.crt (Let's Encrypt cannot issue for a bare IP). Clients trust it with k7 config set api.ca, --api-ca, or K7_API_CA. --api-hostname (Let's Encrypt via Caddy) and --api-tls-cert/--api-tls-key are the other tracks; --api-insecure-http restores a plain HTTP NodePort and must be treated as such (verify=False on an https:// URL is an error). k7 install --api-allow-cidr <cidr> (repeatable, Cilium only) restricts who can reach it: a CiliumNetworkPolicy on the k7-api pod allows the operator's CIDRs plus the reserved host / remote-node / health / kube-apiserver peers (the kubelet probe path), and the Service switches to externalTrafficPolicy: Local so the pod sees the real client IP. This is defence in depth for operators who know their client CIDRs, not a secure API: there is still no rate limiting. The flag is off by default and affects nothing when unset. It is a pod-level policy — SSH and the k3s API port are never touched, so a wrong CIDR costs API access from your workstation and nothing else (kubectl -n kube-system delete ciliumnetworkpolicy k7-api-ingress undoes it). A fromCIDR peer is not evaluated for Cilium-managed sources, so the allowlist constrains external clients only; in-cluster access is governed by the pod policies.
  • Control-plane OCI registry inspection (used to resolve image entrypoint/cmd) rejects registry hosts that are not on an allowlist (default: registry-1.docker.io, ghcr.io, quay.io, public.ecr.aws; extend via K7_REGISTRY_ALLOWLIST) and rejects any host that resolves to loopback/private/link-local/metadata/reserved addresses. Redirect following is disabled. The previous localhosthttp downgrade path has been removed.
  • Ingress to sandboxes is denied by default (NetworkPolicy) and opt-in per sandbox: --ingress-port opens TCP ports, --ingress-from (sandbox: / namespace: / cidr:) scopes who may connect. With no source given, an opened port is reachable only from sandboxes in the same namespace — never from the internet. A cidr: source does not scope in-cluster peers on Cilium: an ipBlock peer is not evaluated for pod-to-pod traffic, so such a rule leaves the opened port reachable from any sandbox in the cluster. k7 create warns when one is used; scope in-cluster access with sandbox: / namespace: instead. See docs/BACKENDS.md for the measured behaviour. --expose-port additionally publishes a port outside the cluster (NodePort with externalTrafficPolicy: Local, so the pod sees the real client IP); it requires a matching --ingress-port, and on a public node it puts the sandbox on a public IP. A cidr: allowlist there does not constrain clients on the cluster's own nodes. Egress is per-sandbox: open, blocked, CIDR allowlist, or FQDN allowlist when Cilium is the CNI (default). DNS is blocked by default when egress is locked down.
  • Sandboxes never reach the platform. With Cilium (default) a cluster-wide deny policy (k7-sandbox-platform-deny) stops every sandbox — in every egress mode, including --egress-open — from dialing the node it runs on, the other nodes, the Kubernetes API (node IP or 10.43.0.1), cloud metadata (169.254.0.0/16), and the pods of kube-system / longhorn-system (k7-api, Longhorn, ...), CoreDNS excepted. "Open" egress means the internet, not the cluster. This is deny-only and composes with the per-sandbox egress policies; it does not defend against a VM escape. Caveat: k7 install --cni flannel clusters do not get this isolation (no Cilium policy engine) — an --egress-open sandbox there can reach the node and the API server.
  • Multi-node clusters are supported (Ansible inventory; Longhorn for the QEMU/kql path). Cilium FQDN egress applies cluster-wide.
  • Root on a K3s server is full cluster control. Nodes in [k7_servers] (including the 3-node --ha example, where every host is a server) have /etc/rancher/k3s/k3s.yaml as cluster-admin (playbook --write-kubeconfig-mode 644), the join token, and etcd. The first master also has k7-api and /etc/k7/api_keys.json. Node pins and k7 nodes dedicate do not contain that: they stop two tenants sharing a k7d daemon, they do not survive a VM escape onto a master. Put tenant sandboxes on [k7_agents]; do not dedicate a tenant onto a server running k7-api. Root on an agent is not the admin kubeconfig. k7-agent has no ServiceAccount token and ingress from remote-node is denied, so that breakout owns that node's k7d (that tenant, if dedicated) — not other agents and not the Kubernetes API. Per-node isolation is the blast radius for a worker compromise; it is not useless. The shared agent token remains on every node as defence-in-depth after the CNP.

See also the docs: security model, networking, and backends comparison.

Known limitations (pre-1.0)

  • Default k7 install serves k7-api over HTTPS with a playbook-minted cluster CA, so API keys no longer travel in cleartext. There is still no rate limiting. --api-insecure-http is the escape hatch back to a plain HTTP NodePort and must be treated as such. --api-allow-cidr is defence in depth on who can connect; it is not a secure-API claim.
  • No rate limiting or abuse protection at the API layer yet.
  • API key storage is local file-backed; treat the API host as trusted. Namespace scoping is an opt-in tenancy boundary on top of that model — unscoped keys still have full cross-namespace control-plane access. Node scoping is the matching opt-in placement boundary: without --node on the key, two namespaces can still share a node's k7d daemon. Pair -n and --node, and run k7 nodes dedicate on that node, for tenant isolation on k7d. The dedicate step is a label + taint (k7.katakate.org/tenant); a hostname pin alone does not keep other sandboxes off the node. Neither contains a host compromise: root on [k7_servers] is cluster-admin.
  • Young project; no independent security audit yet.
  • The k7d backend has a different isolation trade-off for CoW sibling forks — see k7d's SECURITY.md.
  • Prefer a dedicated RBAC-restricted kubeconfig for the API rather than cluster-admin credentials in production.
  • Host compromise is cluster-wide on servers. --node / dedicate isolate k7d placement, not a root shell on [k7_servers]. The HA inventory example makes every node a server. Tenant boxes belong in [k7_agents]. k7-agent has no ServiceAccount token and no remote-node ingress, so root on an agent is that node (that tenant if dedicated), not k7-api RBAC on the rest of the cluster.

Responsible Disclosure

Do not publicly disclose vulnerabilities before we have had a reasonable time to investigate and release fixes. We appreciate coordinated disclosure and will credit reporters unless anonymity is requested.