NetworkPolicy
| Required secret | Overlay |
|---|---|
OPENAI_API_KEY |
values-networkpolicy-litellm.yaml |
When to use it¶
The agent runs its own shell/code execution inside its own Pod - the Pod
itself is the sandbox. Without a NetworkPolicy that sandbox has no network
boundary: lateral movement to other in-cluster Services, and reachability of
the cloud metadata endpoint (169.254.169.254 / the IPv6 equivalent within
fd00::/8), which on most managed clusters hands out node IAM credentials.
Use this whenever the cluster's CNI enforces NetworkPolicy (most managed
Kubernetes offerings do; some local dev CNIs, notably kind's default, do not).
Install¶
helm upgrade --install hermes-agent ./charts/hermes-agent \
--namespace hermes-agent --create-namespace \
-f charts/hermes-agent/values-networkpolicy-litellm.yaml \
--set-string env.OPENAI_API_KEY='sk-<your-litellm-proxy-key>' --wait
Adapt before deploying¶
networkPolicy.enabled: false by default - existing installs are unaffected
until you opt in. Once enabled:
- Ingress is denied entirely by default.
hermes gateway runis outbound-only, so nothing needs to reach this Pod unless a listener (dashboard,apiServer,webhook,a2a, ...) is exposed - add the matching rule toextraIngressin that case. - DNS is scoped to
kube-systemvia an immutable namespace-name label. OverridenetworkPolicy.dns.namespaceSelector/podSelectorfor a distribution whose DNS runs elsewhere. blockPrivateEgress: trueblocks RFC1918 and the metadata endpoint while still permitting public internet egress. Set itfalseonly if the agent must reach an in-cluster proxy through a broad allowance; preferextraEgresswith a precisenamespaceSelector/podSelectorinstead, as this example does for LiteLLM.policyTypesis fixed at[Ingress, Egress]and is not configurable - the chart's policy always isolates both directions.
Complete overlay¶
# values-networkpolicy-litellm.yaml
#
# Egress-locked install talking to an in-cluster LiteLLM proxy (see
# values-litellm-k8s.yaml). blockPrivateEgress denies the rest of RFC1918 and
# the cloud metadata endpoint while still permitting the LiteLLM Service
# specifically, via extraEgress - a precise allowlist instead of opening all
# of RFC1918 just to reach one in-cluster Service.
#
# Adjust the namespaceSelector/podSelector below to match your LiteLLM
# install; the labels here match the upstream berriai/litellm-helm chart's
# defaults (Service "litellm" in namespace "litellm", port 4000).
#
# helm upgrade --install hermes-agent ./charts/hermes-agent \
# --namespace hermes-agent --create-namespace \
# -f charts/hermes-agent/values-networkpolicy-litellm.yaml \
# --set-string env.OPENAI_API_KEY='sk-<your-litellm-proxy-key>' --wait
config:
providers:
litellm:
base_url: http://litellm.litellm.svc.cluster.local:4000/v1
key_env: OPENAI_API_KEY
discover_models: true
model:
provider: litellm
default: openai/gpt-oss-120b
terminal:
backend: local
env:
OPENAI_API_KEY: "sk-DUMMY_replace_me_000000000000000000000000"
networkPolicy:
enabled: true
blockPrivateEgress: true
extraEgress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: litellm
podSelector:
matchLabels:
app.kubernetes.io/name: litellm
ports:
- protocol: TCP
port: 4000
Dashboard behind an Ingress¶
Turning the policy on silently cuts an Ingress off from the dashboard: the pod
stays Ready (the kubelet probe is not affected) but the controller cannot reach
it. Add a rule for port 9119 to networkPolicy.extraIngress. What to allow depends
on how the controller reaches the pod. Measured on MicroK8s with Calico (VXLAN) and
a host-network ingress-nginx with one controller per node, the dashboard pod on one
of them:
| Rule | Controller on the same node | Controller on another node |
|---|---|---|
| None | Allowed | Blocked |
| Pod and namespace selector of the ingress | Allowed | Blocked |
ipBlock of the node network only |
Allowed | Blocked |
ipBlock of the node network and the pod network |
Allowed | Allowed |
A host-network controller is not a pod as far as the policy is concerned, so a
selector never matches it, and Calico always admitted traffic from the pod's own node
in this test. Traffic from another node reaches the dashboard from that node's tunnel
address, which lies inside the pod network. Allow both networks and set the same two
CIDRs in dashboard.trustedProxies; otherwise sign-in works but the session cookies
lose Secure depending on which node the request enters. A controller that runs as
an ordinary pod can be matched with a namespace and pod selector instead, which is
narrower; that variant was not measured. Use bounded CIDRs rather than a pod IP,
which changes when the controller pod is recreated.
helm upgrade --install hermes-agent ./charts/hermes-agent \
--namespace hermes-agent --create-namespace \
-f charts/hermes-agent/values-networkpolicy-dashboard.yaml --wait
# values-networkpolicy-dashboard.yaml
#
# The dashboard behind an Ingress while the chart's NetworkPolicy is on.
#
# The NetworkPolicy denies all inbound traffic by default, so turning it on
# silently cuts the Ingress off from the dashboard: the pod stays Ready (the
# kubelet probe is not affected) but the controller cannot reach it. This
# overlay adds the one rule that is needed, for the dashboard port only.
#
# What to allow depends on how the controller reaches the pod. Measured on
# MicroK8s with Calico (VXLAN) and a host-network ingress-nginx, one controller
# per node (the dashboard pod on one of them):
#
# rule controller on controller on
# the same node another node
# no rule allowed BLOCKED
# podSelector/namespaceSelector of the ingress allowed BLOCKED
# ipBlock: the node network only allowed BLOCKED
# ipBlock: the node network + the pod network allowed allowed
#
# A host-network controller is not a pod as far as the policy is concerned, so a
# selector never matches it. Traffic from another node reaches the dashboard from
# that node's tunnel address, which is inside the pod network. Allow BOTH
# networks, and set the same two CIDRs in `dashboard.trustedProxies`, or the
# session cookies lose `Secure` depending on which node the request enters.
#
# Replace the two CIDRs below with your own:
# node network: the addresses of the nodes running the controller, for
# example `kubectl get nodes -o wide`
# pod network: `kubectl get ippools.crd.projectcalico.org` (Calico) or
# `kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}'`
# A bounded CIDR is preferred over a pod IP, which changes when the controller
# pod is recreated. A controller that runs as an ordinary pod (not host-network)
# can be matched with a namespaceSelector and podSelector instead, which is
# narrower; that variant was not measured here.
#
# Dummy values: override at install time. Never commit real credentials.
#
# helm upgrade --install hermes-agent ./charts/hermes-agent \
# --namespace hermes-agent --create-namespace \
# -f charts/hermes-agent/values-networkpolicy-dashboard.yaml \
# --set-string env.OPENAI_API_KEY='sk-<real>' \
# --set-string env.HERMES_DASHBOARD_BASIC_AUTH_PASSWORD='<strong password>' \
# --set-string env.HERMES_DASHBOARD_BASIC_AUTH_SECRET="$(openssl rand -base64 32)" \
# --wait
#
# Changing `dashboard.*` or the hosts later needs `--set bootstrap.overwrite=true`
# for that upgrade (see the README, "Expose the dashboard").
config:
model:
provider: openai-api
default: gpt-4o-mini
terminal:
backend: local
dashboard:
enabled: true
# Same two networks as the NetworkPolicy rule below.
trustedProxies:
- "10.0.4.0/24"
- "10.1.0.0/16"
auth:
provider: basic
env:
OPENAI_API_KEY: "sk-DUMMY_replace_me_000000000000000000000000"
HERMES_DASHBOARD_BASIC_AUTH_USERNAME: "admin"
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD: "DUMMY_replace_me"
HERMES_DASHBOARD_BASIC_AUTH_SECRET: "DUMMY_replace_me_with_openssl_rand_base64_32"
service:
enabled: true
ingress:
enabled: true
className: nginx
annotations: {}
# cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: hermes-agent.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: hermes-agent-tls
hosts:
- hermes-agent.example.com
networkPolicy:
enabled: true
extraIngress:
- from:
- ipBlock:
cidr: "10.0.4.0/24"
- ipBlock:
cidr: "10.1.0.0/16"
ports:
- protocol: TCP
port: 9119