From c1388e694b58baa74ad58a3740be645c0fdb2eb1 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sat, 13 Jun 2026 12:57:27 +0800 Subject: [PATCH 1/3] feat: harden helm deployment defaults --- coding-agent/docs/deployment/phase2-k8s.md | 63 +++++- coding-agent/helm/templates/_helpers.tpl | 8 + coding-agent/helm/templates/deployment.yaml | 3 + coding-agent/helm/templates/httproute.yaml | 27 +++ .../helm/templates/networkpolicy.yaml | 60 ++++++ .../helm/templates/serviceaccount.yaml | 13 ++ coding-agent/helm/values.yaml | 46 ++++ coding-agent/tests/deploy/test_helm_chart.py | 200 ++++++++++++++++++ 8 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 coding-agent/helm/templates/httproute.yaml create mode 100644 coding-agent/helm/templates/networkpolicy.yaml create mode 100644 coding-agent/helm/templates/serviceaccount.yaml diff --git a/coding-agent/docs/deployment/phase2-k8s.md b/coding-agent/docs/deployment/phase2-k8s.md index cf8082167..b3b0019f5 100644 --- a/coding-agent/docs/deployment/phase2-k8s.md +++ b/coding-agent/docs/deployment/phase2-k8s.md @@ -13,6 +13,14 @@ and secrets as operator-provided values. and mounts separate `workspace` and `data` PVCs. - The default Service is `ClusterIP` on port `8080`. - HTTP bearer auth is enabled by default. The chart expects an existing Secret. +- The pod uses a dedicated ServiceAccount by default, does not mount a + ServiceAccount token, and the chart does not create RBAC bindings. +- Kubernetes service environment variable injection is disabled with + `enableServiceLinks=false`. +- NetworkPolicy is enabled by default. It allows inbound HTTP to the agent pod, + DNS to CoreDNS/kube-dns, and outbound public HTTPS while excluding private, + link-local, loopback, and carrier-grade NAT ranges. +- HTTPRoute support is optional and disabled by default. ## Required Operator Decisions @@ -31,6 +39,37 @@ Fill these before live deployment: - Kimi values: `provider=kimi-code`, `model=kimi-for-coding` - Provider Secret values, if required by the selected provider - Storage class and PVC sizes, if the cluster default is not acceptable +- CNI NetworkPolicy enforcement. Do not treat rendered NetworkPolicy objects as + an isolation boundary until the target cluster is confirmed to enforce them. +- Resource requests and limits for shared clusters. +- Gateway API and HTTPRoute parentRefs, if the chart is responsible for the + public route. + +## Shared Or Public Cluster Prerequisites + +Do not deploy the unauthenticated Orbstack values to a public or shared cluster: +they are only for local development and intentionally set +`server.allowUnauthenticated=true`. + +Before deploying to a shared cluster: + +- Confirm the CNI enforces Kubernetes NetworkPolicy. A cluster may accept + NetworkPolicy manifests without enforcing them. +- Set resource requests and limits. The default chart leaves `resources` empty + so operators can choose values that match the target node. +- Keep the generated ServiceAccount unprivileged. The chart does not create + Role, ClusterRole, RoleBinding, or ClusterRoleBinding resources. +- Keep `automountServiceAccountToken=false` unless there is a reviewed reason to + let the pod call the Kubernetes API. +- If `httpRoute.enabled=true`, ensure the Gateway API CRDs exist and TLS is + terminated by the Gateway, Traefik, or another reviewed edge component. +- If the provider endpoint, proxy, or dependency is on a private CIDR, add a + narrow `networkPolicy.extraEgress` rule for that destination. The default + public HTTPS rule intentionally excludes private address ranges, including + `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`, + `169.254.0.0/16`, and `127.0.0.0/8`. +- If the target cluster cannot enforce NetworkPolicy, do not count it as a safe + shared-cluster deployment target for this service. ## Build And Publish @@ -91,7 +130,9 @@ helm --kube-context upgrade --install coding-agent ./helm \ --namespace coding-agent \ --set image.repository='//coding-agent' \ --set image.tag='' \ - --set-json cors.origins='["https://"]' + --set-json cors.origins='["https://"]' \ + --set-json resources.requests='{"cpu":"250m","memory":"512Mi"}' \ + --set-json resources.limits='{"cpu":"1","memory":"2Gi"}' ``` Kimi provider: @@ -104,7 +145,9 @@ helm --kube-context upgrade --install coding-agent ./helm \ --set image.tag='' \ --set server.auth.enabled=true \ --set server.allowUnauthenticated=false \ - --set-json cors.origins='["https://"]' + --set-json cors.origins='["https://"]' \ + --set-json resources.requests='{"cpu":"250m","memory":"512Mi"}' \ + --set-json resources.limits='{"cpu":"1","memory":"2Gi"}' ``` If the target cluster has no default storage class, set PVC classes explicitly: @@ -114,6 +157,22 @@ If the target cluster has no default storage class, set PVC classes explicitly: --set persistence.data.storageClassName='' ``` +If the chart should render the Gateway API route, provide the parent Gateway and +hostname explicitly: + +```bash +--set httpRoute.enabled=true \ +--set-json httpRoute.parentRefs='[{"name":"","namespace":""}]' \ +--set-json httpRoute.hostnames='[""]' +``` + +If a private provider endpoint or egress proxy is required, add only that +destination: + +```bash +--set-json networkPolicy.extraEgress='[{"to":[{"ipBlock":{"cidr":""}}],"ports":[{"protocol":"TCP","port":443}]}]' +``` + ## Health Checks Wait for rollout: diff --git a/coding-agent/helm/templates/_helpers.tpl b/coding-agent/helm/templates/_helpers.tpl index 01b07945f..dd91f0794 100644 --- a/coding-agent/helm/templates/_helpers.tpl +++ b/coding-agent/helm/templates/_helpers.tpl @@ -22,3 +22,11 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "coding-agent.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} + +{{- define "coding-agent.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "coding-agent.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} diff --git a/coding-agent/helm/templates/deployment.yaml b/coding-agent/helm/templates/deployment.yaml index 7eef63673..799753f4f 100644 --- a/coding-agent/helm/templates/deployment.yaml +++ b/coding-agent/helm/templates/deployment.yaml @@ -18,6 +18,9 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} spec: + serviceAccountName: {{ include "coding-agent.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + enableServiceLinks: {{ .Values.enableServiceLinks }} {{- with .Values.podSecurityContext }} securityContext: {{- toYaml . | nindent 8 }} diff --git a/coding-agent/helm/templates/httproute.yaml b/coding-agent/helm/templates/httproute.yaml new file mode 100644 index 000000000..13b4e7792 --- /dev/null +++ b/coding-agent/helm/templates/httproute.yaml @@ -0,0 +1,27 @@ +{{- if .Values.httpRoute.enabled }} +{{- if not .Values.httpRoute.parentRefs }} +{{- fail "httpRoute.parentRefs must be set when httpRoute.enabled=true" }} +{{- end }} +{{- if not .Values.httpRoute.hostnames }} +{{- fail "httpRoute.hostnames must be set when httpRoute.enabled=true" }} +{{- end }} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "coding-agent.fullname" . }} + labels: + {{- include "coding-agent.labels" . | nindent 4 }} +spec: + parentRefs: + {{- toYaml .Values.httpRoute.parentRefs | nindent 4 }} + hostnames: + {{- toYaml .Values.httpRoute.hostnames | nindent 4 }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ .Values.httpRoute.path | quote }} + backendRefs: + - name: {{ include "coding-agent.fullname" . }} + port: {{ .Values.service.port }} +{{- end }} diff --git a/coding-agent/helm/templates/networkpolicy.yaml b/coding-agent/helm/templates/networkpolicy.yaml new file mode 100644 index 000000000..006243779 --- /dev/null +++ b/coding-agent/helm/templates/networkpolicy.yaml @@ -0,0 +1,60 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "coding-agent.fullname" . }} + labels: + {{- include "coding-agent.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "coding-agent.selectorLabels" . | nindent 6 }} + policyTypes: + {{- if .Values.networkPolicy.ingress.enabled }} + - Ingress + {{- end }} + {{- if .Values.networkPolicy.egress.enabled }} + - Egress + {{- end }} + {{- if .Values.networkPolicy.ingress.enabled }} + ingress: + - ports: + - port: {{ .Values.server.port }} + protocol: TCP + {{- with .Values.networkPolicy.ingress.from }} + from: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- range .Values.networkPolicy.extraIngress }} + - {{- toYaml . | nindent 6 }} + {{- end }} + {{- end }} + {{- if .Values.networkPolicy.egress.enabled }} + egress: + {{- if .Values.networkPolicy.egress.dns.enabled }} + - to: + - namespaceSelector: + {{- toYaml .Values.networkPolicy.egress.dns.namespaceSelector | nindent 12 }} + podSelector: + {{- toYaml .Values.networkPolicy.egress.dns.podSelector | nindent 12 }} + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + {{- end }} + {{- if .Values.networkPolicy.egress.publicHttps.enabled }} + - to: + - ipBlock: + cidr: {{ .Values.networkPolicy.egress.publicHttps.cidr | quote }} + except: + {{- toYaml .Values.networkPolicy.egress.publicHttps.except | nindent 14 }} + ports: + - port: 443 + protocol: TCP + {{- end }} + {{- range .Values.networkPolicy.extraEgress }} + - {{- toYaml . | nindent 6 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/coding-agent/helm/templates/serviceaccount.yaml b/coding-agent/helm/templates/serviceaccount.yaml new file mode 100644 index 000000000..902903039 --- /dev/null +++ b/coding-agent/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "coding-agent.serviceAccountName" . }} + labels: + {{- include "coding-agent.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/coding-agent/helm/values.yaml b/coding-agent/helm/values.yaml index 04886fd0d..4ca97ecba 100644 --- a/coding-agent/helm/values.yaml +++ b/coding-agent/helm/values.yaml @@ -30,6 +30,52 @@ service: type: ClusterIP port: 8080 +enableServiceLinks: false + +serviceAccount: + create: true + name: "" + automountServiceAccountToken: false + annotations: {} + +networkPolicy: + enabled: true + ingress: + enabled: true + from: [] + egress: + enabled: true + dns: + enabled: true + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchExpressions: + - key: k8s-app + operator: In + values: + - kube-dns + - coredns + publicHttps: + enabled: true + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 100.64.0.0/10 + - 169.254.0.0/16 + - 127.0.0.0/8 + extraIngress: [] + extraEgress: [] + +httpRoute: + enabled: false + parentRefs: [] + hostnames: [] + path: / + agent: dataDir: /var/lib/coding-agent/data extraEnv: [] diff --git a/coding-agent/tests/deploy/test_helm_chart.py b/coding-agent/tests/deploy/test_helm_chart.py index 1c44c8b91..4285e354d 100644 --- a/coding-agent/tests/deploy/test_helm_chart.py +++ b/coding-agent/tests/deploy/test_helm_chart.py @@ -62,6 +62,14 @@ def _service(docs: list[dict[str, Any]]) -> dict[str, Any]: return _object_named(docs, "Service", "coding-agent") +def _service_account(docs: list[dict[str, Any]]) -> dict[str, Any]: + return _object_named(docs, "ServiceAccount", "coding-agent") + + +def _network_policy(docs: list[dict[str, Any]]) -> dict[str, Any]: + return _object_named(docs, "NetworkPolicy", "coding-agent") + + def _objects_of_kind(docs: list[dict[str, Any]], kind: str) -> list[dict[str, Any]]: return [doc for doc in docs if doc.get("kind") == kind] @@ -131,6 +139,9 @@ def test_helm_default_runtime_contract_is_runnable() -> None: _assert_storage(docs, "/var/lib/coding-agent/data") pod_spec = _deployment(docs)["spec"]["template"]["spec"] assert pod_spec["securityContext"]["fsGroup"] == 10001 + assert pod_spec["serviceAccountName"] == "coding-agent-coding-agent" + assert pod_spec["automountServiceAccountToken"] is False + assert pod_spec["enableServiceLinks"] is False main = _container(docs) assert main["securityContext"] == { @@ -147,6 +158,195 @@ def test_helm_default_runtime_contract_is_runnable() -> None: assert "--allow-unauthenticated" not in main["command"] +def test_helm_default_creates_unprivileged_service_account_without_rbac() -> None: + docs = _render() + + service_account = _service_account(docs) + assert service_account["apiVersion"] == "v1" + assert service_account["automountServiceAccountToken"] is False + + assert not _objects_of_kind(docs, "Role") + assert not _objects_of_kind(docs, "ClusterRole") + assert not _objects_of_kind(docs, "RoleBinding") + assert not _objects_of_kind(docs, "ClusterRoleBinding") + + +def test_helm_service_account_can_be_reused_without_creation() -> None: + docs = _render( + "--set", + "serviceAccount.create=false", + "--set", + "serviceAccount.name=existing-agent-sa", + ) + + assert not _objects_of_kind(docs, "ServiceAccount") + pod_spec = _deployment(docs)["spec"]["template"]["spec"] + assert pod_spec["serviceAccountName"] == "existing-agent-sa" + assert pod_spec["automountServiceAccountToken"] is False + + +def test_helm_default_network_policy_limits_ingress_and_egress() -> None: + docs = _render() + policy = _network_policy(docs) + spec = policy["spec"] + + assert spec["podSelector"]["matchLabels"] == { + "app.kubernetes.io/name": "coding-agent", + "app.kubernetes.io/instance": "coding-agent", + } + assert spec["policyTypes"] == ["Ingress", "Egress"] + assert spec["ingress"] == [ + { + "ports": [ + { + "port": 8080, + "protocol": "TCP", + } + ] + } + ] + + dns_rule = spec["egress"][0] + assert dns_rule["to"] == [ + { + "namespaceSelector": { + "matchLabels": {"kubernetes.io/metadata.name": "kube-system"} + }, + "podSelector": { + "matchExpressions": [ + { + "key": "k8s-app", + "operator": "In", + "values": ["kube-dns", "coredns"], + } + ] + }, + } + ] + assert dns_rule["ports"] == [ + {"port": 53, "protocol": "UDP"}, + {"port": 53, "protocol": "TCP"}, + ] + + https_rule = spec["egress"][1] + assert https_rule["to"] == [ + { + "ipBlock": { + "cidr": "0.0.0.0/0", + "except": [ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "100.64.0.0/10", + "169.254.0.0/16", + "127.0.0.0/8", + ], + } + } + ] + assert https_rule["ports"] == [{"port": 443, "protocol": "TCP"}] + + +def test_helm_network_policy_can_be_disabled() -> None: + docs = _render("--set", "networkPolicy.enabled=false") + + assert not _objects_of_kind(docs, "NetworkPolicy") + + +def test_helm_network_policy_renders_extra_rules() -> None: + docs = _render( + "--set-json", + 'networkPolicy.extraIngress[0]={"from":[{"namespaceSelector":{"matchLabels":{"gateway":"public"}}}],"ports":[{"protocol":"TCP","port":8080}]}', + "--set-json", + 'networkPolicy.extraEgress[0]={"to":[{"ipBlock":{"cidr":"203.0.113.0/24"}}],"ports":[{"protocol":"TCP","port":8443}]}', + ) + policy = _network_policy(docs) + + assert policy["spec"]["ingress"][-1] == { + "from": [{"namespaceSelector": {"matchLabels": {"gateway": "public"}}}], + "ports": [{"protocol": "TCP", "port": 8080}], + } + assert policy["spec"]["egress"][-1] == { + "to": [{"ipBlock": {"cidr": "203.0.113.0/24"}}], + "ports": [{"protocol": "TCP", "port": 8443}], + } + + +def test_helm_httproute_is_disabled_by_default() -> None: + docs = _render() + + assert not _objects_of_kind(docs, "HTTPRoute") + + +@pytest.mark.parametrize( + ("args", "message"), + [ + ( + ["--set", "httpRoute.enabled=true"], + "httpRoute.parentRefs must be set when httpRoute.enabled=true", + ), + ( + [ + "--set", + "httpRoute.enabled=true", + "--set-json", + 'httpRoute.parentRefs=[{"name":"public-gateway"}]', + ], + "httpRoute.hostnames must be set when httpRoute.enabled=true", + ), + ], +) +def test_helm_httproute_requires_explicit_gateway_and_hostname( + args: list[str], message: str +) -> None: + result = subprocess.run( + [_helm(), "template", "coding-agent", str(CHART), *args], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert result.returncode != 0 + assert message in result.stderr + + +def test_helm_httproute_renders_gateway_backend_and_hostname() -> None: + docs = _render( + "--set", + "httpRoute.enabled=true", + "--set-json", + 'httpRoute.parentRefs=[{"name":"public-gateway","namespace":"traefik"}]', + "--set-json", + 'httpRoute.hostnames=["agent.example.com"]', + ) + route = _object_named(docs, "HTTPRoute", "coding-agent") + + assert route["apiVersion"] == "gateway.networking.k8s.io/v1" + assert route["spec"]["parentRefs"] == [ + {"name": "public-gateway", "namespace": "traefik"} + ] + assert route["spec"]["hostnames"] == ["agent.example.com"] + assert route["spec"]["rules"] == [ + { + "matches": [ + { + "path": { + "type": "PathPrefix", + "value": "/", + } + } + ], + "backendRefs": [ + { + "name": "coding-agent-coding-agent", + "port": 8080, + } + ], + } + ] + + def test_helm_default_configures_http_auth() -> None: docs = _render() server = _agent_toml(docs)["server"] From be339f088ab92ae1e9dd8c740b35c1f2d1f9d5b5 Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sat, 13 Jun 2026 13:06:12 +0800 Subject: [PATCH 2/3] fix: reject empty network policy directions --- .../helm/templates/networkpolicy.yaml | 3 +++ coding-agent/tests/deploy/test_helm_chart.py | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/coding-agent/helm/templates/networkpolicy.yaml b/coding-agent/helm/templates/networkpolicy.yaml index 006243779..9d8e7c30e 100644 --- a/coding-agent/helm/templates/networkpolicy.yaml +++ b/coding-agent/helm/templates/networkpolicy.yaml @@ -1,4 +1,7 @@ {{- if .Values.networkPolicy.enabled }} +{{- if and (not .Values.networkPolicy.ingress.enabled) (not .Values.networkPolicy.egress.enabled) }} +{{- fail "networkPolicy.ingress.enabled or networkPolicy.egress.enabled must be true when networkPolicy.enabled=true" }} +{{- end }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: diff --git a/coding-agent/tests/deploy/test_helm_chart.py b/coding-agent/tests/deploy/test_helm_chart.py index 4285e354d..ffab5ddb8 100644 --- a/coding-agent/tests/deploy/test_helm_chart.py +++ b/coding-agent/tests/deploy/test_helm_chart.py @@ -253,6 +253,31 @@ def test_helm_network_policy_can_be_disabled() -> None: assert not _objects_of_kind(docs, "NetworkPolicy") +def test_helm_network_policy_requires_at_least_one_direction_when_enabled() -> None: + result = subprocess.run( + [ + _helm(), + "template", + "coding-agent", + str(CHART), + "--set", + "networkPolicy.ingress.enabled=false", + "--set", + "networkPolicy.egress.enabled=false", + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + assert result.returncode != 0 + assert ( + "networkPolicy.ingress.enabled or networkPolicy.egress.enabled must be true " + "when networkPolicy.enabled=true" + ) in result.stderr + + def test_helm_network_policy_renders_extra_rules() -> None: docs = _render( "--set-json", From f4511cb252fe65edbd97630b79abc19c0822743d Mon Sep 17 00:00:00 2001 From: kina0630 Date: Sat, 13 Jun 2026 13:11:17 +0800 Subject: [PATCH 3/3] fix: preserve explicit ingress source lists --- coding-agent/docs/deployment/phase2-k8s.md | 4 ++++ coding-agent/helm/templates/networkpolicy.yaml | 4 ++-- coding-agent/helm/values.yaml | 4 +++- coding-agent/tests/deploy/test_helm_chart.py | 7 +++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/coding-agent/docs/deployment/phase2-k8s.md b/coding-agent/docs/deployment/phase2-k8s.md index b3b0019f5..c73df7032 100644 --- a/coding-agent/docs/deployment/phase2-k8s.md +++ b/coding-agent/docs/deployment/phase2-k8s.md @@ -68,6 +68,10 @@ Before deploying to a shared cluster: public HTTPS rule intentionally excludes private address ranges, including `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `100.64.0.0/10`, `169.254.0.0/16`, and `127.0.0.0/8`. +- The default `networkPolicy.ingress.from=null` allows any pod source to the + agent HTTP port. For shared clusters, prefer setting this to the Gateway or + ingress-controller namespace/pod selector; set it to `[]` only when you want + the NetworkPolicy to deny all pod-network ingress. - If the target cluster cannot enforce NetworkPolicy, do not count it as a safe shared-cluster deployment target for this service. diff --git a/coding-agent/helm/templates/networkpolicy.yaml b/coding-agent/helm/templates/networkpolicy.yaml index 9d8e7c30e..06a241591 100644 --- a/coding-agent/helm/templates/networkpolicy.yaml +++ b/coding-agent/helm/templates/networkpolicy.yaml @@ -24,9 +24,9 @@ spec: - ports: - port: {{ .Values.server.port }} protocol: TCP - {{- with .Values.networkPolicy.ingress.from }} + {{- if ne .Values.networkPolicy.ingress.from nil }} from: - {{- toYaml . | nindent 8 }} + {{- toYaml .Values.networkPolicy.ingress.from | nindent 8 }} {{- end }} {{- range .Values.networkPolicy.extraIngress }} - {{- toYaml . | nindent 6 }} diff --git a/coding-agent/helm/values.yaml b/coding-agent/helm/values.yaml index 4ca97ecba..8430e2de8 100644 --- a/coding-agent/helm/values.yaml +++ b/coding-agent/helm/values.yaml @@ -42,7 +42,9 @@ networkPolicy: enabled: true ingress: enabled: true - from: [] + # null allows any source to the HTTP port. Set [] to deny all ingress or + # provide NetworkPolicyPeer entries to allow only a gateway/controller. + from: null egress: enabled: true dns: diff --git a/coding-agent/tests/deploy/test_helm_chart.py b/coding-agent/tests/deploy/test_helm_chart.py index ffab5ddb8..e102dc9b3 100644 --- a/coding-agent/tests/deploy/test_helm_chart.py +++ b/coding-agent/tests/deploy/test_helm_chart.py @@ -278,6 +278,13 @@ def test_helm_network_policy_requires_at_least_one_direction_when_enabled() -> N ) in result.stderr +def test_helm_network_policy_preserves_explicit_empty_ingress_sources() -> None: + docs = _render("--set-json", "networkPolicy.ingress.from=[]") + policy = _network_policy(docs) + + assert policy["spec"]["ingress"][0]["from"] == [] + + def test_helm_network_policy_renders_extra_rules() -> None: docs = _render( "--set-json",