Skip to content

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 run is outbound-only, so nothing needs to reach this Pod unless a listener (dashboard, apiServer, webhook, a2a, ...) is exposed - add the matching rule to extraIngress in that case.
  • DNS is scoped to kube-system via an immutable namespace-name label. Override networkPolicy.dns.namespaceSelector/podSelector for a distribution whose DNS runs elsewhere.
  • blockPrivateEgress: true blocks RFC1918 and the metadata endpoint while still permitting public internet egress. Set it false only if the agent must reach an in-cluster proxy through a broad allowance; prefer extraEgress with a precise namespaceSelector/podSelector instead, as this example does for LiteLLM.
  • policyTypes is fixed at [Ingress, Egress] and is not configurable - the chart's policy always isolates both directions.

Open Raw YAML

Complete overlay

charts/hermes-agent/values-networkpolicy-litellm.yaml
# 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
charts/hermes-agent/values-networkpolicy-dashboard.yaml
# 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