feat(template-chart): networkPolicy.ingress.ambientMesh, plus its checked CI exception

Gitea #143. A pod cannot be reached inside Istio's ambient mesh unless its
NetworkPolicy lets ztunnel's HBONE tunnel in - ambient explicitly does not
bypass policy enforcement - and 15008 is deliberately no containerPort of the
application, so assertion 2 of ci/run-networkpolicy-test.sh rejected it.

The switch is a named intention rather than a free port list, and it renders
two things that cannot be had separately: 15008/TCP inbound, and - where
probeSources is non-empty - 169.254.7.127/32 as an additional peer, because
inside the mesh the kubelet's probes are redirected away from the worker
node's address. Whoever opens 15008 by hand has not added the probe address,
and the first narrowed probeSources then drops every probe: the pod never
becomes Ready and the symptom reads as a broken application. That is
assertion 4 of the same script, one value further along.

Assertion 2 keeps its edge. It gets ONE named exception for ONE port number,
gated on one value, and it is not granted in the main loop at all - which
renders every chart with the value at its default, so 15008 stays an ordinary
failure there. The free-port variant was design A of #143 and would have
opened the assertion for all 30 charts.

Assertion 7 holds the exception to five things on two renders per chart:
the port and the ipBlock are absent while the switch is off, 15008 is present
while it is on, 15008 is still the ONLY ingress port that is not a
containerPort, and wherever the rule carrying it restricts sources it also
carries the ipBlock. The last one is reachable because these renders apply
ci/networkpolicy/<chart>.yaml, where probeSources is non-empty.

Built into the existing script, as for #136: the exception weakens assertion 2
and belongs beside it - a separate script could go green while the exception
it is paired with is wrong, with nothing connecting the two. The job already
runs over every chart on every pull request.

Found by running it: the policy's name comes from Chart.yaml's name, not from
the directory - template-chart is called "xxx".

Not proven, and said so in values.yaml, the template header and the script's
header: that 15008 alone suffices comes from Istio's documentation, and the
probe-source half reaches no release, because probeSources is empty in every
chart here. No namespace of this cluster carries istio.io/dataplane-mode, so
there is no path on which either could be measured. What is proven is the
render and the assertions.
This commit is contained in:
jklein committed 2026-10-19 17:56:19 +00:00
1 parent 725d046dd9
commit badfaa32cb
4 files changed
+548

No files matched your search

@@ -96,6 +96,24 @@ spec this policy selects.
{{- $probeSources := $np.probeSources | default list }}
{{- $allowedNs := $np.allowedNamespaces | default list }}
{{- $ic := $np.ingressController | default dict }}
{{- /* networkPolicy.ingress - the policy's INGRESS DIRECTION, and not to be
confused with networkPolicy.ingressController one line up, nor with the
chart's top-level .Values.ingress. Three different things whose names
overlap as substrings:
.Values.ingress the Ingress OBJECT this chart renders
.Values.networkPolicy.ingress switches on the policy's inbound side
.Values.networkPolicy.ingressController
the SOURCE SELECTOR for the ingress
controller's pods, a sibling of
networkPolicy.ingress and NO PART OF
IT
Named here because a search for "ingress" matches all three, and one
round of this work already reported ingressController as an inbound
escape hatch on the strength of the substring alone. */}}
{{- $npIngress := $np.ingress | default dict }}
{{- $ambientMesh := $npIngress.ambientMesh }}
{{- $routed := or .Values.ingress.enabled .Values.httpRoute.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
@@ -129,6 +147,29 @@ spec:
- protocol: {{ .protocol | default "TCP" }}
port: {{ .containerPort }}
{{- end }}
{{- if $ambientMesh }}
{{- /* FIRST HALF OF networkPolicy.ingress.ambientMesh. 15008/TCP is
ztunnel's HBONE tunnel, the port every inbound mesh connection
arrives on once the namespace carries istio.io/dataplane-mode.
Istio's own documentation is explicit that ambient "does not and
will never bypass Kubernetes NetworkPolicy enforcement", so
without this port a pod in the mesh is simply unreachable.
It is the one port in this rule that is NOT a containerPort of
the selected pod spec - ztunnel terminates it, not the
application - so ci/run-networkpolicy-test.sh holds a NAMED
exception for exactly this number, gated on exactly this value.
Anything else that is not a containerPort still fails, and so
does 15008 itself while the value is off.
NOT MEASURED, and the chart should not pretend otherwise: that
15008 ALONE suffices comes from Istio's documentation. What has
been measured on this cluster is only which listeners ztunnel
opens. Provable at the first namespace that joins the mesh, and
no namespace carries the label yet. */}}
- protocol: TCP
port: 15008
{{- end }}
{{- if $probeSources }}
{{- /* RESTRICTED, because the operator supplied probeSources. The
kubelet's probes come from the IP of the WORKER NODE running the
@@ -168,6 +209,35 @@ spec:
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- if $ambientMesh }}
{{- /* SECOND HALF OF networkPolicy.ingress.ambientMesh, and the reason
the switch is one value instead of two. Inside the mesh the
kubelet's probes are redirected and no longer arrive from the
worker node's address: they arrive from 169.254.7.127, which is
therefore the probeSources entry nobody would think to add.
The two halves are deliberately inseparable. An operator who
only opens 15008 by hand has a policy that passes every review -
and the moment probeSources is non-empty, every probe is dropped,
the pod never becomes Ready, and the symptom reads as a broken
application rather than as a policy. That is assertion 4 of
ci/run-networkpolicy-test.sh, re-created one value further along.
One switch cannot be set halfway.
Only rendered in this branch, because this is the only branch
with a "from" list at all: with probeSources empty the rule is
open and needs no peer. The IPv6 counterpart
fd16:9254:7127:1337:ffff:ffff:ffff:ffff is deliberately absent -
this cluster is SingleStack IPv4, and an ipBlock of the wrong
family is accepted by the API server and matches nothing.
UNPROVEN IN OPERATION: probeSources is empty in every chart of
this repository (stalwart sets it to the empty list explicitly),
so no release reaches this branch today. What CI proves is its
render, through the overlay in ci/networkpolicy. */}}
- ipBlock:
cidr: 169.254.7.127/32
{{- end }}
{{- else }}
{{- /* NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only
inside the cluster, because this port carries no NodePort. The