Namespace isolation
Cluster operations — Reference for the administrators and operators who run Nautilus.
Pods don’t accept connections from pods in other namespaces unless a NetworkPolicy in their namespace or a global rule lets them. The global rules are Calico policies in the tenant-isolation directory of the prp/calico repository. What users see is described in Exposing to other namespaces.
Who decides what
- Namespace administrators decide which other namespaces may reach their pods, with ordinary NetworkPolicies in their own namespace. When admins open a tenant service to another namespace on an owner’s behalf, they do it the same way: a NetworkPolicy in the destination namespace, labeled
nrp.ai/policy=cross-ns-allow, with the reason in thenrp.ai/reasonannotation. The owner can change or remove it. - The global rules in
prp/calicohold only what namespace administrators mustn’t control: the baseline, services kept to their namespace, exceptions into system namespaces (labelnautilus.io/system-namespace=true), the platform services that reach every pod, and services that are public anyway.
The policies
- Tier
tenant-isolation(tier.yaml): evaluated after the default tier, where Kubernetes NetworkPolicies live. A pod that an ingress NetworkPolicy selects is decided by its namespace’s NetworkPolicies alone; the rules below apply only to pods that no NetworkPolicy selects. - Baseline
tenant-isolation.baseline: a Calico NetworkPolicy in every namespace that denies ingress from pods in other namespaces. It doesn’t match traffic from outside the cluster, from nodes or from host-network pods. - Global rules, checked before the baseline, first match wins:
- Services that don’t authenticate are kept to their own namespace: a
Passfrom that namespace, then aDenyfrom every other source, including the shared services, host-network pods and nodes. Stalwart’s FoundationDB and Redis are kept this way. shared-services.yaml(user namespaces) andsystem-cross-namespace-allow.yaml(system namespaces) pass connections from the shared-service namespaces (ingress controllers, monitoring and the operators that connect to the pods they manage) and the portal, and, in system namespaces, to specific services such as Stalwart’s mail ports or the TURN REST service.internet-visible.yamlpasses every namespace to what the internet already reaches: the TCP ports behind public LoadBalancer VIPs, and Prometheus and Thanos, whose Ingresses are public. An in-cluster client of a VIP keeps its pod address, so without this rule it would be blocked while the internet gets through.
- Services that don’t authenticate are kept to their own namespace: a
A namespace belongs in the shared-service list only if its pods connect to pods in other namespaces, since anyone who can run a pod there reaches every pod in the cluster. The same holds for namespaces exempt from the psp-no-host-network Gatekeeper constraint: their host-network pods count as nodes and aren’t isolated. Keep both lists short.
RBAC keeps namespace administrators from creating, changing or deleting Calico policies, and the Gatekeeper constraint protect-tenant-isolation-networkpolicy (prp/gatekeeper) reserves the baseline for cluster administrators.
New namespaces
The portal creates the baseline with each namespace. For namespaces made any other way, the CronJob kube-system/tenant-isolation-baseline (tenant-isolation/baseline-reconciler.yaml, applied by hand with kubectl apply) creates missing baselines every minute. It only creates: it never changes or deletes a baseline, and Gatekeeper lets its service account do nothing else. A failed run shows as a failed Job:
kubectl -n kube-system get jobs -l app=tenant-isolation-baselineChanging the rules
Edit the files in prp/calico and merge to master; CI applies them with calicoctl apply.
- CI runs for every pushed branch, so push work in progress with
git push -o ci.skip. - A push that changes
.gitlab-ci.ymldoesn’t deploy; push the files it applies in a later commit. calicoctl applydoesn’t delete: remove a policy that leaves the repository withcalicoctl delete globalnetworkpolicy <name>in thecalicoctlpod inkube-system.
To keep a service to its namespace, copy the Stalwart pair at the top of system-cross-namespace-allow.yaml. After adding or removing a LoadBalancer Service, regenerate the internet-visible rules and commit the result:
kubectl get svc -A -o json | tenant-isolation/generate-internet-visible.py > tenant-isolation/internet-visible.yamlChecking a connection
List the policies that apply, then probe from a short-lived pod in the source namespace. A dropped connection times out; nc prints nothing and exits non-zero.
kubectl get globalnetworkpolicies.crd.projectcalico.org | grep tenant-isolationkubectl -n <namespace> get networkpolicies.crd.projectcalico.org,networkpolicies
kubectl -n <source-namespace> apply -f - <<'EOF'apiVersion: v1kind: Podmetadata: {name: netprobe}spec: restartPolicy: Never activeDeadlineSeconds: 600 containers: - name: probe image: alpine command: [sleep, '500'] resources: {requests: {cpu: 50m, memory: 32Mi}, limits: {cpu: 50m, memory: 32Mi}}EOFkubectl -n <source-namespace> wait --for=condition=Ready pod/netprobekubectl -n <source-namespace> exec netprobe -- nc -z -w 4 <pod-ip> <port> && echo open || echo blockedkubectl -n <source-namespace> delete pod netprobeA probe can also look blocked when the source pod’s own namespace has an egress NetworkPolicy that selects it.

This work was supported in part by National Science Foundation (NSF) awards CNS-1730158, ACI-1540112, ACI-1541349, OAC-1826967, OAC-2112167, CNS-2100237, CNS-2120019.