Gitea #143. Additive, default false, so the render is byte-identical to the previous release - proven by diffing both renders against origin/main rather than asserted. Switched on it adds 15008/TCP, ztunnel's HBONE tunnel, to the rule carrying the probe port, and - inside that rule's source-restricted branch - 169.254.7.127/32, the address the kubelet's probes arrive from once the pod is in the mesh. Ambient does not bypass NetworkPolicy enforcement, so without the port a pod whose namespace has joined the mesh is unreachable. The two halves are one value because the second is the one nobody thinks of: 15008 added by hand leaves a policy that passes review and drops every probe the first time probeSources is narrowed, and a pod that never becomes Ready reads as a broken application rather than as a policy. Minor, not major: additive value, unchanged default, unchanged render.
306 lines
14 KiB
YAML
306 lines
14 KiB
YAML
{{- /*
|
|
NetworkPolicy for freesailarr (Gitea #5).
|
|
|
|
TEN CONTAINERS IN ONE POD, BEHIND A VPN, AND THE POLICY IS SIMPLER THAN THAT
|
|
SOUNDS - for one reason worth stating first: the containers talk to each other
|
|
over LOCALHOST. They share a network namespace, so none of that traffic ever
|
|
reaches a network interface and NO NetworkPolicy has anything to say about it.
|
|
Prowlarr reaching Radarr, Recyclarr reaching Sonarr, every one of those hops is
|
|
invisible here. What this policy governs is only what enters or leaves the pod.
|
|
|
|
INBOUND is the eight published web interfaces plus gluetun's health server, and
|
|
almost all of them are probe ports - the kubelet opens an HTTP connection to
|
|
each container's own interface - so they are one rule on the probe path. The
|
|
newznab port of mediathekarr is the exception: nothing probes it, and its only
|
|
consumer is Prowlarr in this same pod over localhost, so it is restricted by
|
|
source and that costs nothing.
|
|
|
|
OUTBOUND IS WHERE THIS CHART IS UNLIKE EVERY OTHER ONE HERE, because the pod's
|
|
network namespace belongs to gluetun. Every connection the *arr containers make
|
|
goes into the WireGuard tunnel and leaves the pod as UDP to the VPN endpoint -
|
|
so a NetworkPolicy sees the OUTER packets and almost nothing else. Three rules
|
|
cover it: DNS, UDP 51820 for the tunnel itself, and 443 for gluetun's own
|
|
server-list API. Indexer and tracker traffic needs no rule of its own; it is
|
|
already inside the tunnel.
|
|
|
|
AND THE TUNNEL RULE IS THE ONE THAT CAN BREAK THIS CHART, which is also why it
|
|
is well covered: gluetun's health server returns 200 only while the tunnel is
|
|
up, and all three of its probes address it. A blocked UDP 51820 therefore means
|
|
a pod that never becomes Ready - visibly, in the install-test, which brings up
|
|
a real tunnel. 51820 is WireGuard's standard port and Mullvad's; Mullvad also
|
|
offers 53, 123, 500 and 4500 for obfuscation, and a release using one of those
|
|
needs networkPolicy.egress.extraPorts. Note that extraPorts is TCP only, so an
|
|
alternative UDP port needs the rule here widened instead - said plainly,
|
|
because it is a limit of the shared values shape and not an oversight.
|
|
|
|
WHAT THIS OBJECT IS: a ceiling on reachability, not an authentication layer.
|
|
Only the ports declared below are reachable at all - whatever else the process
|
|
binds, now or after the next app update, is blocked without anyone having to
|
|
notice it exists.
|
|
|
|
WHAT IT IS NOT:
|
|
- EGRESS IS COARSE. A NetworkPolicy matches CIDRs and selectors and NEVER
|
|
hostnames, so "only to the database host" cannot be expressed at all. This
|
|
cluster's CNI is Canal (Flannel plus Calico for policy) and has no FQDN
|
|
rule; Calico's GlobalNetworkSet could express one but would bind this chart
|
|
to Calico, the same argument that kept implementation-specific objects out
|
|
of the Gateway-API sweep (#114). Each egress rule therefore permits its
|
|
PORT to any destination.
|
|
- NOT A BOUNDARY WHERE THE CNI DOES NOT ENFORCE ONE. An unenforced policy is
|
|
accepted by the API server and filters nothing, so everything reads as
|
|
configured while nothing is filtered.
|
|
- THE REASONING IS IN TEMPLATE COMMENTS, not in "#" comments: those are part
|
|
of the rendered object, travel into the cluster and appear in every release
|
|
diff. ci/run-networkpolicy-test.sh fails on a rendered "#".
|
|
|
|
THE PORT NUMBERS BELOW ARE POD PORTS, NEVER NODEPORTS. A NetworkPolicy is
|
|
evaluated at the pod's network interface, after kube-proxy has already
|
|
translated a NodePort to the targetPort, so a NodePort number here would match
|
|
nothing while looking strict - and, being the only rule for that port, would
|
|
block every inbound connection instead. ci/run-networkpolicy-test.sh asserts
|
|
every port here against the containerPorts of the pod spec this policy
|
|
selects.
|
|
*/ -}}
|
|
{{- if .Values.networkPolicy.enabled }}
|
|
{{- $np := .Values.networkPolicy }}
|
|
{{- $ns := .Release.Namespace }}
|
|
{{- $probeSources := $np.probeSources | default list }}
|
|
{{- $allowedNs := $np.allowedNamespaces | default list }}
|
|
{{- $ic := $np.ingressController | default dict }}
|
|
{{- $routed := or .Values.ingress.enabled .Values.httpRoute.enabled }}
|
|
{{- /* networkPolicy.ingress - the policy's INBOUND side, and not the same thing
|
|
as networkPolicy.ingressController (the SOURCE SELECTOR for the ingress
|
|
controller's pods, a SIBLING of it and no part of it) nor as the
|
|
top-level .Values.ingress (the Ingress object). All three match a search
|
|
for "ingress"; one round of this work already mistook the second for an
|
|
inbound escape hatch on the strength of the substring. */}}
|
|
{{- $npIngress := $np.ingress | default dict }}
|
|
{{- $ambientMesh := $npIngress.ambientMesh }}
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata:
|
|
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
|
|
labels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
|
|
spec:
|
|
{{- /*
|
|
Selects the StatefulSet's pods. ci/run-networkpolicy-test.sh fails if
|
|
this matches no pod spec of the same render - which is the mistake that
|
|
would otherwise leave a policy in the namespace filtering nothing, and
|
|
its usual cause is a label renamed in the workload template.
|
|
*/}}
|
|
podSelector:
|
|
matchLabels:
|
|
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
|
|
policyTypes:
|
|
- Ingress
|
|
{{- /*
|
|
Egress is listed, and for this chart it is unusually meaningful: because the
|
|
pod's network namespace belongs to gluetun, the only things that leave it are
|
|
the tunnel, DNS and gluetun's control-plane calls. Everything the applications
|
|
do is encapsulated. So three narrow rules really do describe the pod's whole
|
|
outbound surface - which is rare here.
|
|
*/}}
|
|
- Egress
|
|
ingress:
|
|
{{- /*
|
|
THE PROBE PATH, nine ports of it. gluetun's health server (9999) plus the eight
|
|
web interfaces the kubelet probes directly: qBittorrent (8080), Prowlarr
|
|
(9696), FlareSolverr (8191), Radarr (7878), Sonarr (8989), Seerr (5055), Bazarr
|
|
(6767) and Mediathekarr (5007). Every one of them is addressed by at least one
|
|
httpGet probe, so none of them can be restricted by source without the worker
|
|
node's IP.
|
|
|
|
Recyclarr is absent from this list on purpose: it declares no port and its
|
|
probes are exec probes, so there is nothing to allow.
|
|
*/}}
|
|
- ports:
|
|
- protocol: TCP
|
|
port: 9999
|
|
- protocol: TCP
|
|
port: 8080
|
|
- protocol: TCP
|
|
port: 9696
|
|
- protocol: TCP
|
|
port: 8191
|
|
- protocol: TCP
|
|
port: 7878
|
|
- protocol: TCP
|
|
port: 8989
|
|
- protocol: TCP
|
|
port: 5055
|
|
- protocol: TCP
|
|
port: 6767
|
|
- protocol: TCP
|
|
port: 5007
|
|
{{- if $ambientMesh }}
|
|
{{- /* FIRST HALF OF networkPolicy.ingress.ambientMesh (Gitea #143). 15008/TCP
|
|
is ztunnel's HBONE tunnel, the port every inbound connection arrives on
|
|
once the namespace carries istio.io/dataplane-mode - ambient "does not
|
|
and will never bypass Kubernetes NetworkPolicy enforcement", so without
|
|
it a pod in the mesh is simply unreachable.
|
|
|
|
It is the one port here that is NOT a containerPort of the selected pod
|
|
spec: ztunnel terminates it, not the application. ci/run-networkpolicy-
|
|
test.sh therefore holds a NAMED exception for exactly this number, gated
|
|
on exactly this value - and assertion 7 of that script proves, on a
|
|
second render, that 15008 stays the ONLY such port and that it is absent
|
|
while the value is false.
|
|
|
|
NOT MEASURED: that 15008 alone suffices comes from Istio's
|
|
documentation. Measured on this cluster is only which listeners ztunnel
|
|
opens. Provable at the first namespace to join the mesh; none has. */}}
|
|
- 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 pod, so that address has to be
|
|
in the list or the pod never becomes Ready - and a pod that is never Ready
|
|
reads as a broken application rather than as a policy.
|
|
|
|
The ingress controller is added to the same rule - this chart renders TWO
|
|
Ingresses, the main one and qBittorrent's, and both go dark the moment this
|
|
list is filled without it.
|
|
*/}}
|
|
from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
{{- /*
|
|
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
|
|
labelling work on a namespace this chart does not own.
|
|
*/}}
|
|
kubernetes.io/metadata.name: {{ $ns }}
|
|
{{- if and $routed $ic.namespace }}
|
|
{{- /*
|
|
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
|
|
IN that namespace. Written as two list entries it would be an OR, and would
|
|
open the port to every pod in kube-system.
|
|
*/}}
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
|
|
{{- with $ic.podLabels }}
|
|
podSelector:
|
|
matchLabels:
|
|
{{- toYaml . | nindent 14 }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- range $allowedNs }}
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: {{ . | quote }}
|
|
{{- end }}
|
|
{{- range $probeSources }}
|
|
- ipBlock:
|
|
cidr: {{ . | quote }}
|
|
{{- end }}
|
|
{{- if $ambientMesh }}
|
|
{{- /* SECOND HALF OF networkPolicy.ingress.ambientMesh, and the reason it is
|
|
one value and not 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.
|
|
|
|
An operator who only opens 15008 by hand writes a policy that passes
|
|
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 one value further along. A switch whose
|
|
halves can only be had together cannot be set halfway.
|
|
|
|
Only in this branch, because this is the only branch with a "from" list:
|
|
with no sources restricted the rule is open and needs no peer. The IPv6
|
|
counterpart is deliberately absent - this cluster is SingleStack IPv4,
|
|
and an ipBlock of the wrong family is accepted and matches nothing.
|
|
|
|
UNPROVEN IN OPERATION: probeSources is empty in every chart here, so no
|
|
release reaches this branch. CI proves its render, through the overlays
|
|
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 alternative was to guess
|
|
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
|
|
stays un-Ready, the rollout times out, and the cause looks like the
|
|
application. An empty "from: []" would be worse than either - it allows
|
|
everything too, but while reading as a restriction. Narrow it with
|
|
networkPolicy.probeSources.
|
|
*/}}
|
|
{{- end }}
|
|
{{- /*
|
|
MEDIATHEKARR'S NEWZNAB PORT, and the one inbound port here that can be
|
|
restricted: no probe addresses it. Its only consumer is Prowlarr, which runs in
|
|
THIS pod and reaches it over localhost - traffic no NetworkPolicy sees at all -
|
|
so restricting this rule to the release's own namespace plus allowedNamespaces
|
|
costs nothing and removes a port from the cluster's reach.
|
|
*/}}
|
|
- ports:
|
|
- protocol: TCP
|
|
port: 5008
|
|
from:
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
{{- /*
|
|
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
|
|
labelling work on a namespace this chart does not own.
|
|
*/}}
|
|
kubernetes.io/metadata.name: {{ $ns }}
|
|
{{- range $allowedNs }}
|
|
- namespaceSelector:
|
|
matchLabels:
|
|
kubernetes.io/metadata.name: {{ . | quote }}
|
|
{{- end }}
|
|
egress:
|
|
{{- /*
|
|
DNS, unconditionally. There is no value to switch this off, and that is
|
|
deliberate: a knob whose only other setting breaks every name lookup in
|
|
the pod is a trap and not a feature.
|
|
*/}}
|
|
- ports:
|
|
- protocol: UDP
|
|
port: 53
|
|
- protocol: TCP
|
|
port: 53
|
|
{{- /*
|
|
THE WIREGUARD TUNNEL, and the rule this chart cannot live without. Everything
|
|
the *arr containers do leaves the pod inside this tunnel, so blocking it
|
|
blocks all of them at once - and gluetun's health server, which all three of
|
|
its probes address, returns 200 only while the tunnel is up. The pod would
|
|
never become Ready.
|
|
|
|
51820 is WireGuard's standard port and Mullvad's. Mullvad also offers 53, 123,
|
|
500 and 4500 for obfuscation; a release using one of those must widen THIS rule,
|
|
because networkPolicy.egress.extraPorts is TCP only.
|
|
*/}}
|
|
- ports:
|
|
- protocol: UDP
|
|
port: 51820
|
|
{{- /*
|
|
Outbound HTTPS, for gluetun's own control-plane traffic: it fetches and caches
|
|
the provider's server list over the API before the tunnel exists, so this
|
|
cannot go through the tunnel. Indexer and tracker traffic does NOT need a rule
|
|
here - it is already inside the tunnel by the time it leaves the pod.
|
|
*/}}
|
|
- ports:
|
|
- protocol: TCP
|
|
port: 443
|
|
{{- with $np.egress }}
|
|
{{- with .extraPorts }}
|
|
{{- /*
|
|
Ports only, to any destination - see the header. This is the operator's
|
|
escape hatch from the egress cap, and the only alternative to switching
|
|
the whole policy off.
|
|
*/}}
|
|
- ports:
|
|
{{- range . }}
|
|
- protocol: TCP
|
|
port: {{ int . }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|