Network Policy Design — ICN K3s Cluster
Status: Design only — not yet implemented Reason for deferral: Keep flexible for active development Revisit when: Moving toward a pilot / hardening sprint Last reviewed: 2026-03-02
Current State
The icn namespace has four policies (deployed at cluster creation):
| Policy | Effect |
|---|---|
default-deny-ingress |
Block all inbound by default |
allow-dns-egress |
Permit UDP 53 to kube-dns |
allow-icn-mesh |
Allow mesh traffic between ICN daemon pods |
allow-pilot-ui |
Allow external access to Pilot UI port 3000 |
All other namespaces (icn-coop-*, monitoring, registry) have no policies — full open mesh.
Port Map (from live cluster inspection)
| Namespace | QUIC (UDP) | RPC (TCP) | Metrics (TCP) | Health (TCP) |
|---|---|---|---|---|
icn (main daemon) |
7777 | 5601 | 9100 | 8080 |
icn-coop-alpha |
7827 | 5651 | 9150 | 8080 |
icn-coop-beta |
7834 | 5658 | 9157 | 8080 |
icn-coop-gamma |
7825 | 5649 | 9148 | 8080 |
icn-coop-delta |
7831 | 5655 | 9154 | 8080 |
Each coop also has a NodePort service exposing QUIC and RPC externally for P2P federation.
mDNS note: All coop configs have mdns_enabled = true. mDNS uses multicast UDP to 224.0.0.251:5353. Kubernetes NetworkPolicies operate on unicast only — multicast bypasses them entirely. Do not rely on NetworkPolicy to control mDNS; it will still broadcast within the node's subnet regardless.
Design: icn-coop-* Namespaces
Apply the same policy template to all four coop namespaces. Use a label selector approach so the same manifests work across all coops.
What traffic to allow
Ingress:
- From other
icn-coop-*namespaces → QUIC (UDP) + RPC (TCP) ports (P2P ICN mesh: coops form trust relationships with each other) - From
icnnamespace → QUIC + RPC (Main daemon participates in the same mesh) - From
monitoringnamespace → metrics port only (TCP 9150/9157/9148/9154) (Prometheus scraping) - From
kube-systemnamespace → any (kubelet probes, health checks)
Egress:
- To
kube-system(kube-dns) → UDP 53 - To other
icn-coop-*namespaces → QUIC + RPC ports - To
icnnamespace → QUIC + RPC - To
0.0.0.0/0→ QUIC ports (UDP) (External P2P federation — coops need to reach peers outside the cluster)
Do NOT restrict:
- Egress to external internet on QUIC ports — ICN is a P2P system, coops federate with nodes outside the cluster. Blocking external egress breaks real-world federation.
Sketch
# Template — apply per-coop with appropriate port numbers
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: icn-coop-alpha # repeat per namespace
spec:
podSelector: {}
policyTypes: [Ingress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-icn-mesh-ingress
namespace: icn-coop-alpha
spec:
podSelector:
matchLabels:
app: icn
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
icn-mesh: "true" # label all coop + icn namespaces with this
ports:
- port: 7827 # QUIC — varies per coop
protocol: UDP
- port: 5651 # RPC — varies per coop
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-metrics-scrape
namespace: icn-coop-alpha
spec:
podSelector:
matchLabels:
app: icn
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- port: 9150 # metrics — varies per coop
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: icn-coop-alpha
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-icn-mesh-egress
namespace: icn-coop-alpha
spec:
podSelector:
matchLabels:
app: icn
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
icn-mesh: "true"
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # block intra-cluster non-mesh traffic via external path
- 192.168.0.0/16
ports:
- port: 7825 # QUIC range covering all coops + main daemon
protocol: UDP
- port: 7827
protocol: UDP
- port: 7831
protocol: UDP
- port: 7834
protocol: UDP
- port: 7777
protocol: UDP
Implementation note: Because each coop has a different port number, the QUIC ingress rule needs to be per-coop (no shared port). Consider standardising all coops to the same port in a future refactor — that would make a single policy template work cleanly.
Design: monitoring Namespace
Prometheus needs to scrape all namespaces — its egress must remain fully open. The main concern is controlling inbound access to Grafana and Prometheus UIs.
Ingress:
- NodePort access is handled at the node/iptables level, not by NetworkPolicy — no policy needed for NodePort ingress
- Intra-namespace: Prometheus → Alertmanager, Grafana → Prometheus — allow freely
- From
kube-system→ any (kubelet probes)
Egress:
- To all namespaces → any port (Prometheus must scrape everything)
- To external internet → TCP 443 (for remote write, alert webhooks if added)
Assessment: Restricting monitoring egress is high friction for low security gain in a homelab. Recommend leaving monitoring egress unrestricted indefinitely. Only add ingress default-deny if/when Grafana gets real user auth.
Design: registry Namespace
The in-cluster registry (10.8.30.40:30500) is accessed two ways:
- ci-runner (external, 10.8.30.46) → NodePort 30500 (Docker push)
- K3s containerd on each node → NodePort 30500 (image pull via
registries.yaml)
Both paths go through NodePort/iptables at the node level, not through pod-to-pod networking. The registry pod IP (10.43.x.x) is only directly accessed by in-cluster actors (e.g., kubectl commands against the registry API).
Ingress:
- From
icnnamespace → TCP 5000 (registry API, for any in-cluster push tooling) - From
kube-system→ any (kubelet) - NodePort access: not controlled by NetworkPolicy
Egress:
- DNS only (
kube-systemUDP 53) - Registry is stateless for egress — it only serves content from its PVC
Assessment: Default-deny ingress on the registry namespace is safe and has no development impact, since dev workflow (ci-runner → NodePort → containerd) bypasses pod networking entirely.
Implementation Order (when ready)
- Label namespaces — add
icn-mesh: "true"toicn,icn-coop-alpha/beta/gamma/delta - Registry — lowest risk, no dev impact. Add default-deny + allow-dns-egress
- icn-coop-* — medium risk. Requires careful port mapping. Test P2P formation after applying
- monitoring — low priority. Egress must stay open; only add ingress deny if Grafana gets auth
Before implementing, verify
# Confirm coops can see each other via mDNS (multicast — won't be affected by NP)
kubectl exec -n icn-coop-alpha deploy/icn-alpha -- icnctl peer list
# Confirm Prometheus is scraping all coop metrics endpoints
curl -s http://10.8.30.40:30090/api/v1/targets | jq '.data.activeTargets | length'
# After applying any policy, immediately recheck mesh formation
kubectl exec -n icn-coop-alpha deploy/icn-alpha -- icnctl peer list
Decision: Why Not Now
During active development:
- Developers need
kubectl exec/kubectl port-forwardinto any pod freely - Feature branches may add new ports or communication patterns before they're documented
- Broken NetworkPolicy is subtle — a misconfigured egress rule silently drops gossip messages with no error visible in app logs
- The cluster is on a private VLAN (10.8.30.0/24) behind OPNsense — the perimeter is already controlled
Re-evaluate when: First external pilot participant connects, OR when coop namespaces start holding real identity/ledger data.