mirror of
https://github.com/Katakate/k7.git
synced 2026-09-21 17:43:20 +00:00
13fafe0997
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.
11 KiB
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).k7d— Katakate/k7d (runtimeClassName: k7); CoW sibling-fork isolation differs — see k7d'sSECURITY.md.k7d-fc(runtimeClassName: k7-fc) — same daemon and shim; stock Firecracker under the stock jailer. What that buys (and does not) is k7d'sSECURITY.mdFirecracker 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 explicitnode_name). Namespace scope is the control-plane tenancy boundary; node scope is the placement boundary (API-key--nodeis a Kubernetes node name fromk7 nodes list/kubectl get nodes— the Linux hostname K3s registered — joined via the kubelet-stampedkubernetes.io/hostnamelabel; 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 IDwritesk7.katakate.org/tenantas 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 withk7 config set api.ca,--api-ca, orK7_API_CA.--api-hostname(Let's Encrypt via Caddy) and--api-tls-cert/--api-tls-keyare the other tracks;--api-insecure-httprestores a plain HTTP NodePort and must be treated as such (verify=Falseon anhttps://URL is an error).k7 install --api-allow-cidr <cidr>(repeatable, Cilium only) restricts who can reach it: aCiliumNetworkPolicyon thek7-apipod allows the operator's CIDRs plus the reservedhost/remote-node/health/kube-apiserverpeers (the kubelet probe path), and the Service switches toexternalTrafficPolicy: Localso 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-ingressundoes it). AfromCIDRpeer 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 viaK7_REGISTRY_ALLOWLIST) and rejects any host that resolves to loopback/private/link-local/metadata/reserved addresses. Redirect following is disabled. The previouslocalhost→httpdowngrade path has been removed. - Ingress to sandboxes is denied by default (NetworkPolicy) and opt-in
per sandbox:
--ingress-portopens 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. Acidr:source does not scope in-cluster peers on Cilium: anipBlockpeer is not evaluated for pod-to-pod traffic, so such a rule leaves the opened port reachable from any sandbox in the cluster.k7 createwarns when one is used; scope in-cluster access withsandbox:/namespace:instead. Seedocs/BACKENDS.mdfor the measured behaviour.--expose-portadditionally publishes a port outside the cluster (NodePort withexternalTrafficPolicy: 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. Acidr: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 or10.43.0.1), cloud metadata (169.254.0.0/16), and the pods ofkube-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 flannelclusters do not get this isolation (no Cilium policy engine) — an--egress-opensandbox there can reach the node and the API server. - Multi-node clusters are supported (Ansible inventory; Longhorn for
the QEMU/
kqlpath). Cilium FQDN egress applies cluster-wide. - Root on a K3s server is full cluster control. Nodes in
[k7_servers](including the 3-node--haexample, where every host is a server) have/etc/rancher/k3s/k3s.yamlas cluster-admin (playbook--write-kubeconfig-mode 644), the join token, and etcd. The first master also hask7-apiand/etc/k7/api_keys.json. Node pins andk7 nodes dedicatedo 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 runningk7-api. Root on an agent is not the admin kubeconfig.k7-agenthas no ServiceAccount token and ingress fromremote-nodeis 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 installservesk7-apiover HTTPS with a playbook-minted cluster CA, so API keys no longer travel in cleartext. There is still no rate limiting.--api-insecure-httpis the escape hatch back to a plain HTTP NodePort and must be treated as such.--api-allow-cidris 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
--nodeon the key, two namespaces can still share a node's k7d daemon. Pair-nand--node, and runk7 nodes dedicateon 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
k7dbackend has a different isolation trade-off for CoW sibling forks — see k7d'sSECURITY.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/dedicateisolate 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-agenthas no ServiceAccount token and noremote-nodeingress, 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.