Skip to content

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:

Terminal window
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.

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 and holds the container’s full default capability set, including CAP_NET_RAW. Treat a sandbox pod as fully compromised 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.

HopWhat rides it
Ingress controller → APIEvery request, including the login password
Ingress controller → frontendThe session cookie on every page load
Frontend → APIThe same session cookie, on server-rendered requests
API → PostgreSQLDatabase credentials (config.database.useSsl is false by default)
API → ClickHouseDatabase credentials (config.clickhouse.protocol is http by default)
API → MinIOThe MinIO root account, not a scoped user
Bucket bootstrap Job → MinIOThe MinIO root account again, at install time
API → sandbox serverThe sandbox server’s shared API key
API → LiteLLMLITELLM_MASTER_KEY, the key that mints every per-sandbox model key
LiteLLM → PostgreSQLDatabase credentials, for its own schema
Sandbox → APIA full platform API key, plus the runtime token
Sandbox → LiteLLMA per-sandbox model proxy key
Sandbox → MinIOScoped, short-lived object storage credentials

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.

Terminal window
kubectl create namespace netpol-check
kubectl label namespace netpol-check pod-security.kubernetes.io/enforce=privileged
kubectl -n netpol-check run server --image=nginx:alpine --port=80
kubectl -n netpol-check expose pod server --port=80
kubectl -n netpol-check wait --for=condition=Ready pod/server --timeout=60s
kubectl -n netpol-check apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
EOF
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-check

Embedded 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:

Terminal window
# 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 established
kubectl 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 namespace
kubectl get pods --all-namespaces -o wide

Repeat 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:

StorePortsClients
ClickHouse8123, 9000The API
PostgreSQL5432The API, including its migration init container; the LiteLLM proxy when enabled, which keeps its own schema
MinIO9000, and 9001 with the console enabledThe API; the bucket bootstrap Job; sandbox pods; your ingress controller

MinIO is the one to get right, because sandboxes reach it by two different routes. Presigned URLs for datasets, models, and capability bundles are minted against the in-cluster service, so agent code fetching those connects pod-to-pod. Workspace and artifact sync through the SDK’s storage layer uses scoped credentials whose endpoint is the external storage.<your-domain> address, so that traffic leaves the sandbox, reaches your ingress controller, and arrives at MinIO from there. A policy that allows only the sandbox pod selector breaks the second route.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: clickhouse-platform-only
spec:
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/v1
kind: NetworkPolicy
metadata:
name: postgresql-platform-only
spec:
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/v1
kind: NetworkPolicy
metadata:
name: minio-platform-and-sandboxes
spec:
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
# Agent code fetching datasets, models, and capability bundles
- podSelector:
matchExpressions:
- key: opensandbox.io/id
operator: Exists
# Workspace sync and browser traffic arriving through the ingress.
# 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.

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: Exists

The 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/v1
kind: NetworkPolicy
metadata:
name: sandbox-deny-cloud-metadata
spec:
podSelector:
matchExpressions:
- key: opensandbox.io/id
operator: Exists
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32

The 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.
  • ipBlock handling 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 PostgreSQL and ClickHouse is better done from the other side, with the ingress policies above — neither has a sandbox client, so restricting them cannot break agent code. MinIO is different: sandboxes read datasets, models, and capability bundles from it, so denying them object storage breaks tasks rather than contains them.

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.

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 and protocol-level egress control for agent code belongs to an egress proxy that terminates and inspects the connection. Use NetworkPolicy for the coarse question of which pods can reach which services, and treat it as the floor beneath that proxy rather than a replacement for it.

For the service topology these policies assume, see Architecture. For the controls that constrain a sandbox from the inside, see Sandbox runtime.