CNI plugin: Cilium by default
By default, K3s is installed with Flannel disabled (--flannel-backend=none --disable-network-policy) and Cilium as the CNI. Cilium runs with kubeProxyReplacement=true (no kube-proxy), and its eBPF datapath attaches to the host-side veth — including for Kata VMs, whose virtio NICs are bridged to a host veth.
CiliumNetworkPolicy for FQDN egress. On Flannel it falls back to the standard Kubernetes NetworkPolicy with CIDR-only ipBlock rules.
Sandboxes never reach the platform (always on, Cilium)
A cluster-wide, deny-onlyCiliumClusterwideNetworkPolicy named k7-sandbox-platform-deny is installed by k7 install. It stops every sandbox — in every egress mode, including --egress-open — from reaching:
- the node it runs on and the other nodes (SSH, kubelet, k3s ports),
- the Kubernetes API server (
10.43.0.1:443), - link-local / cloud metadata (
169.254.0.0/16), - the
kube-systemandlonghorn-systempods —k7-api,k7-agent, Longhorn — except CoreDNS, which stays reachable so FQDN egress keeps working.
enableDefaultDeny: false), so it never narrows what a whitelist allows beyond those targets, and it cannot turn an open sandbox into a default-deny one. Cilium-only: --cni flannel clusters do not get this isolation.
Ingress: denied by default, opt-in per sandbox
Every sandbox gets a deny-all{name}-deny-ingress NetworkPolicy. Practical implications:
- Sandbox-to-sandbox networking is denied — even within the same namespace — until you open a port.
kubectl execandk7 shellstill work — they go through the Kubernetes API, not pod networking.kube-systemtraffic is allowed — needed for cluster services (DNS, metrics, etc.).
Sources:
Anything else is a hard error at create time.
--ingress-from without --ingress-port is an error too: sources alone open nothing. --ingress-port with no --ingress-from means “other sandboxes in this namespace”, never the world — reaching a sandbox from anywhere requires --ingress-from cidr:0.0.0.0/0, typed out in full. These are stock v1 NetworkPolicy rules, so ingress works on --cni flannel as well.
A sandbox is a bare Deployment with no Service, so ingress rules only cover in-cluster traffic. To reach a sandbox from outside the cluster, expose it.
Exposing a sandbox outside the cluster
--expose-port <port> (repeatable; expose_ports in YAML / API) creates a NodePort Service named {name}-expose in the sandbox’s namespace. k7 create prints the resolved http://<node-ip>:<nodeport> for each port, and k7 list shows the allocated NodePorts.
- Every
--expose-portneeds a matching--ingress-port. k7 refuses otherwise: a public NodePort in front of a deny-all policy is a confusing no-op, and one in front of an accidentally-open policy is a breach. - The Service uses
externalTrafficPolicy: Local, so the client’s real source IP reaches the pod and--ingress-from cidr:…rules mean something. Consequence: the NodePort only answers on the node running the sandbox (k7 listshows which one). - A
cidr:allowlist does not constrain clients on the cluster’s own nodes; verify it from a genuinely external client —curlfrom a node passes no matter how the rule is written. - On a bare-metal host the node IP is public, so
--expose-portwith--ingress-from cidr:0.0.0.0/0publishes an untrusted sandbox to the internet. k7 warns on stderr on every such create. k7 deleteremoves the Service. Ak7 forkinherits the source’s ingress and egress policy but not its Service — a fork is not reachable from outside until you expose it.
Egress control
A sandbox has exactly one of three egress modes:--egress-open and --egress are mutually exclusive. Defaults are asymmetric (kept for compatibility): the CLI without flags is block-all; an API/YAML request that omits egress_whitelist is open.
How FQDN egress works under Cilium
When the whitelist contains FQDNs, k7 creates aCiliumNetworkPolicy like:
*.example.com) are supported via Cilium’s matchPattern. Bare exact names use matchName. A leading *. covers subdomains at any depth (registry.docker.com and production.cloudfront.docker.com) but not the apex docker.com — add the apex as its own entry. Under the hood k7 maps *. to Cilium’s multi-label **. matchPattern so CDN-backed registries work. The install also sets Cilium dnsProxy.minTtl=3600 so clients that cache a resolved IP longer than a short CDN TTL keep the FQDN→IP allowance for an hour.
When DNS is blocked
If the whitelist is[] or contains only CIDRs, DNS resolution is denied by the policy. To work around this:
- Pre-resolve names in
before_script(which runs before the egress policy is applied). - Embed your dependencies into the image rather than fetching them at runtime.
- Whitelist your own internal DNS server / egress proxy and resolve there.
Network policies created per sandbox
For each sandbox, k7 creates:- A
{name}-deny-ingressNetworkPolicy(always;--ingress-portadds allow rules to this same object). - Either:
- A
NetworkPolicywith CIDR-onlyipBlockegress rules (when only CIDRs are listed, or the cluster runs Flannel), or - A
CiliumNetworkPolicyfor FQDN+CIDR egress (when FQDNs are present and Cilium is installed).
- A
- A
{name}-exposeNodePortService, only with--expose-port.
k7-sandbox-platform-deny policy applies to every sandbox. All per-sandbox objects are deleted with the sandbox. A k7 fork inherits the source’s ingress and egress configuration and is rolled back if its policies cannot be created — a fork of a locked-down sandbox never comes up open.
Administrative access
kubectl exec, k7 shell, the API’s /exec endpoint, and k7 logs all bypass network policies because they ride the Kubernetes API. Pod networking restrictions never affect operator access.
Restricting who can reach k7-api
k7-api is a NodePort Service on 31007, served over HTTPS by default (see Security model). On a dedicated box the node IP is public. k7 install --api-allow-cidr 203.0.113.4/32 --api-allow-cidr 198.51.100.0/24 (repeatable, Cilium only — --cni flannel fails at the CLI) renders a CiliumNetworkPolicy k7-api-ingress on the API pod and flips the Service to externalTrafficPolicy: Local so the pod sees the real client IP. Both are skipped entirely when the flag is unset, which is the default.
This is defence in depth for operators who know their client CIDRs; it stacks with TLS and does not replace it. Things worth knowing before you debug it:
- With
Local, the NodePort answers only on the node running thek7-apipod — the first master, which is the address the playbook writes into/etc/k7/api_endpoint. - A curl from a node is not a test of the allowlist. Node-local traffic matches Cilium’s
hostentity and is SNAT’d to the pod network, so it succeeds however wrong the CIDR list is. Test from off-cluster. - A wrong CIDR only costs API access: the policy selects the
k7-apipod endpoint, never the host, so SSH,kubectl, and on-nodek7 --corekeep working. - Rollback: re-run
k7 installwithout the flag, or deleteciliumnetworkpolicy/k7-api-ingressinkube-systemand patch the Service back toexternalTrafficPolicy: Cluster.
Debugging policy drops (Hubble)
Hubble is opt-in observability, not enforcement — off by default, not a security control. Enable it withk7 install --hubble (Cilium only; --hubble --cni flannel fails loudly). It installs Hubble Relay and a pinned hubble CLI; the Hubble UI is not installed (no authentication, public node IP). Rollback: cilium hubble disable.
Policy denied DROPPED verdict with the destination still labelled world means the FQDN→identity mapping missed (wildcard too narrow, or the learned IP expired).
Multi-node considerations
- Cilium routes pod traffic across nodes natively (no Flannel VXLAN).
CiliumNetworkPolicyapplies cluster-wide regardless of which node the sandbox runs on.- Longhorn replication traffic between nodes is not affected — it goes through the host network (Cilium’s host-level rules), not the sandbox’s policy.
