Network trust boundaries
What Dreadnode assumes about your cluster network, which credentials cross it in the clear, and how to segment it with NetworkPolicies you control.
The chart creates no NetworkPolicy resources. On a fresh install:
kubectl get networkpolicy -n <namespace># No resources found in <namespace> namespace.Dreadnode expects your ingress controller to terminate TLS, and treats the pod network behind it as
a trusted transport domain. Every hop between platform services is plain HTTP. A default install is
plain HTTP at the edge too — global.scheme is http until you set it to https alongside
global.tls.secretName, which TLS certificates covers.
Segmentation of the pod network is yours to define, because only you know what else runs in the cluster, which CNI enforces policy, and which internal hosts your agents are supposed to reach.
Two trust zones
Section titled “Two trust zones”Platform services are one trusted zone. The API, frontend, docs site, sandbox server, sandbox gateway, sandbox controller, the three data stores, and the LiteLLM proxy when you enable it all share the pod network and talk over plain HTTP. The control is that only first-party workloads run there. Dreadnode ships no service mesh and no in-cluster mTLS, and adding one is not required for a supported install.
Sandbox pods are outside that zone. Sandboxes run agent code that is untrusted by design. The
runtime image grants passwordless sudo, so agent code takes uid 0 on demand. Where egress
enforcement is on it gives up CAP_NET_ADMIN and CAP_NET_RAW; where it is off, agent code holds
the container’s full default capability set. Treat a sandbox pod as fully compromised either way and
put your controls at the pod boundary rather than inside it.
What crosses the cluster network in the clear
Section titled “What crosses the cluster network in the clear”These are the hops a security review asks about. Each carries a credential over plain HTTP between pods, so anything that can sniff the pod network or reach the service directly sees it.
| Hop | What rides it |
|---|---|
| Ingress controller → API | Every request, including the login password |
| Ingress controller → frontend | The session cookie on every page load |
| Frontend → API | The same session cookie, on server-rendered requests |
| API → PostgreSQL | Database credentials (config.database.useSsl is false by default) |
| API → ClickHouse | Database credentials (config.clickhouse.protocol is http by default) |
| API → MinIO | The MinIO root account, not a scoped user |
| Bucket bootstrap Job → MinIO | The MinIO root account again, at install time |
| API → sandbox server | The sandbox server’s shared API key |
| API → LiteLLM | LITELLM_MASTER_KEY, the key that mints every per-sandbox model key |
| LiteLLM → PostgreSQL | Database credentials, for its own schema |
| Sandbox → API | A full platform API key, plus the runtime token |
| Sandbox → LiteLLM | A per-sandbox model proxy key |
| Ingress controller → MinIO | Scoped object storage credentials, for browser and sandbox reads |
The LiteLLM rows exist only when you enable the bundled proxy. Treat the table as the set worth reviewing rather than a proof of completeness — the section below derives the real one from your own cluster.
Confirm your cluster enforces NetworkPolicy
Section titled “Confirm your cluster enforces NetworkPolicy”A NetworkPolicy on a cluster whose CNI does not implement it is silently inert. The API server
accepts the object, kubectl get networkpolicy lists it, no event or error is produced, and nothing
is filtered. Verify enforcement first, or every policy below is decoration.
kubectl create namespace netpol-checkkubectl label namespace netpol-check pod-security.kubernetes.io/enforce=privilegedkubectl -n netpol-check run server --image=nginx:alpine --port=80kubectl -n netpol-check expose pod server --port=80kubectl -n netpol-check wait --for=condition=Ready pod/server --timeout=60s
kubectl -n netpol-check apply -f - <<'EOF'apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: deny-all-ingressspec: podSelector: {} policyTypes: - IngressEOF
kubectl -n netpol-check run probe --rm -i --restart=Never --image=busybox \ -- wget -qO- -T 5 http://server# Enforcing: wget: download timed out, or "can't connect to remote host: Connection refused"# on a CNI that rejects rather than drops# Not enforcing: the nginx welcome page
kubectl delete namespace netpol-checkEmbedded Cluster installs run k0s with Calico, which enforces NetworkPolicy. On your own cluster it depends on the CNI: Calico and Cilium enforce by default, Amazon’s VPC CNI needs the network policy agent enabled on the addon, and a plain flannel install does not enforce at all. Substitute images already present in your registry if the cluster has no public egress.
Two rules that govern every policy you write
Section titled “Two rules that govern every policy you write”A pod matched by no policy is unrestricted. Selecting a pod is what constrains it. Until a
policy’s podSelector matches a pod, that pod accepts and originates anything.
Every rule is an allow rule. There is no deny. Once a pod is selected for a policy type, only traffic matching some rule is permitted, and any additional policy can only widen what is allowed. To block one destination you must write a rule that allows everything else, and that block survives only while no other policy selects the same pods for the same direction.
Derive the client set from your own cluster
Section titled “Derive the client set from your own cluster”The policies below name the clients a default install has. Yours differs — you may run the LiteLLM proxy, external data stores, a backup tool, or your own workloads against the same services. Read the live connections before you write a rule, rather than trusting the tables on this page:
# Peer addresses of every established connection into PostgreSQL.# If the image has no `ss`, attach one that does:# kubectl debug -n <namespace> -it <release>-postgresql-0 \# --image=nicolaka/netshoot --target=postgresql -- ss -Hnt state establishedkubectl exec -n <namespace> <release>-postgresql-0 -- \ sh -c "ss -Hnt state established '( sport = :5432 )'" | awk '{print $4}'
# Map those peer IPs back to pods across every namespacekubectl get pods --all-namespaces -o wideRepeat for ClickHouse (:8123, :9000) and MinIO (:9000), and do it while the system is doing
real work — a running evaluation, an agent task reading a dataset, a backup — not on an idle
cluster. Anything you see and do not allow, you are about to break.
Restrict the data stores to their real clients
Section titled “Restrict the data stores to their real clients”Only ClickHouse has a single client. The other two do not, and the difference is what makes a data-store policy break an install:
| Store | Ports | Clients |
|---|---|---|
| ClickHouse | 8123, 9000 | The API |
| PostgreSQL | 5432 | The API, including its migration init container; the LiteLLM proxy when enabled, which keeps its own schema |
| MinIO | 9000, and 9001 with the console enabled | The API; the bucket bootstrap Job; your ingress controller |
MinIO is the one to get right, and the surprise is that sandboxes are not on that list. Object
storage credentials and presigned URLs both carry the external storage.<your-domain> address, so
agent code reading a dataset, pulling a model, or installing a capability bundle leaves the pod,
reaches your ingress controller, and arrives at MinIO from there. Browsers downloading artifacts take
the same path. A policy that omits the ingress controller breaks every one of them.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: clickhouse-platform-onlyspec: podSelector: matchLabels: app.kubernetes.io/name: clickhouse app.kubernetes.io/instance: <release> policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app.kubernetes.io/name: dreadnode-api app.kubernetes.io/instance: <release> ports: - { port: 8123, protocol: TCP } - { port: 9000, protocol: TCP }---apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: postgresql-platform-onlyspec: podSelector: matchLabels: app.kubernetes.io/name: postgresql app.kubernetes.io/instance: <release> policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app.kubernetes.io/instance: <release> matchExpressions: - key: app.kubernetes.io/name operator: In values: [dreadnode-api, dreadnode-litellm] ports: - { port: 5432, protocol: TCP }---apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: minio-platform-and-sandboxesspec: podSelector: matchLabels: app.kubernetes.io/name: minio app.kubernetes.io/instance: <release> app.kubernetes.io/component: storage policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app.kubernetes.io/name: dreadnode-api app.kubernetes.io/instance: <release> # The bucket bootstrap Job also carries name=minio, so match its component - podSelector: matchLabels: app.kubernetes.io/name: minio app.kubernetes.io/instance: <release> app.kubernetes.io/component: bootstrap # Browser downloads and sandbox object-store reads both arrive here. # Replace with the namespace and labels your controller actually uses. - namespaceSelector: matchLabels: kubernetes.io/metadata.name: traefik ports: - { port: 9000, protocol: TCP }Apply them with -n <namespace>; they carry no namespace field so the same file works against any
release.
Two more clients the chart cannot know about: your backup tooling, which usually reaches
PostgreSQL and MinIO directly, and anything of your own you have pointed at these services. On
strict CNIs, note that MinIO is the only data store with an httpGet readiness probe — kubelet
probe traffic originates from the node rather than a pod, and is permitted only because Calico and
Cilium special-case it. If you point the platform at an external data store, these policies stop
applying to it; the traffic leaves the cluster and your network controls take over.
Restrict sandbox pods
Section titled “Restrict sandbox pods”Sandbox pods are created at runtime by the sandbox server rather than rendered by the chart, so they
carry their own labels. Every sandbox pod has opensandbox.io/id, with a value unique per sandbox,
so select on key existence:
podSelector: matchExpressions: - key: opensandbox.io/id operator: ExistsThe sandbox server reserves the opensandbox.io/ prefix and rejects it in request metadata, so
agent code cannot relabel its way out of a policy.
Blocking the cloud metadata service is the cheapest restriction worth making. Agent code with root can otherwise query it for the node’s instance credentials:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: sandbox-deny-cloud-metadataspec: podSelector: matchExpressions: - key: opensandbox.io/id operator: Exists policyTypes: - Egress egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 169.254.169.254/32The single rule permits every destination and port except the metadata endpoint, DNS and pod-to-pod traffic included. Three things to know before relying on it:
- It holds only while no other Egress policy selects sandbox pods. Add one that allows everything, and metadata reach comes back with no error and no signal.
ipBlockhandling of in-cluster destinations varies by CNI. Run a real agent task against it before you trust it, as the enforcement check above notes.- Skip it if your agents are meant to assess cloud metadata services as targets.
Keeping sandboxes off the data stores is better done from the other side, with the ingress policies
above — none of the three takes a direct connection from a sandbox pod, so restricting them cannot
break agent code. What a sandbox does need is the route to storage.<your-domain>, which runs
through your ingress controller rather than the pod network.
Sandboxes land in the release namespace by default. dreadnode-sandbox-server.kubernetes.namespace
pins them elsewhere, which makes the boundary easier to name — but a from: podSelector peer only
matches pods in the policy’s own namespace, so pinning means adding a namespaceSelector to the
MinIO policy above or object storage access stops working.
Where NetworkPolicy stops
Section titled “Where NetworkPolicy stops”NetworkPolicy operates on IP addresses and ports. It cannot express a hostname, inspect a TLS SNI, or distinguish two sites behind the same CDN address. Any rule that tries to allow “the model provider” or “the package index” collapses into allowing all outbound HTTPS, which restricts nothing.
Destination-level egress control for agent code belongs somewhere that can resolve a name. For sandbox pods, that is the egress sidecar: it filters by hostname and wildcard as well as by address, and it runs inside the sandbox pod’s own network namespace, so it does not depend on your CNI implementing NetworkPolicy at all. See Restrict sandbox egress.
That leaves NetworkPolicy the coarse question of which pods can reach which services. The two compose: the sidecar governs what a sandbox can reach outbound, and the policies on this page govern what can reach your data stores inbound. Neither replaces the other, and a sandbox pod is covered by both.
For the service topology these policies assume, see Architecture. For the controls that constrain a sandbox from the inside, see Sandbox runtime.