feat!: NetworkPolicy in the last five charts - the sweep is complete
Validate Helm Charts / Build outline (pull_request) Successful in 8s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 12s
Validate Helm Charts / Build patchmon (pull_request) Successful in 11s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 9s
Validate Helm Charts / Build samba (pull_request) Successful in 6s
Validate Helm Charts / Build satisfactory (pull_request) Successful in 10s
Validate Helm Charts / Build searxng (pull_request) Successful in 7s
Validate Helm Charts / Build stalwart (pull_request) Successful in 7s
Validate Helm Charts / Build sure (pull_request) Successful in 6s
Validate Helm Charts / Build template-chart (pull_request) Successful in 9s
Validate Helm Charts / Build teamspeak-server (pull_request) Successful in 8s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 20s
Validate Helm Charts / Build valkey (pull_request) Successful in 10s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 10s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 9s
Validate Helm Charts / Build zipline (pull_request) Successful in 8s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 17s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 17s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 18s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 18s
Validate Helm Charts / Trivy config freesailarr (pull_request) Successful in 15s
Validate Helm Charts / Trivy config garage (pull_request) Successful in 17s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 27s
Validate Helm Charts / Trivy config homeassistant (pull_request) Successful in 18s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 15s
Validate Helm Charts / Trivy config minecraft-server (pull_request) Successful in 17s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 11s
Validate Helm Charts / Trivy config samba (pull_request) Successful in 12s
Validate Helm Charts / Trivy config satisfactory (pull_request) Successful in 14s
Validate Helm Charts / Trivy config matrix-synapse (pull_request) Successful in 20s
Validate Helm Charts / Trivy config minecraft-router (pull_request) Successful in 17s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 15s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 15s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 13s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 15s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / Trivy config stalwart (pull_request) Successful in 21s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 14s
Validate Helm Charts / Trivy config teamspeak-server (pull_request) Successful in 16s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 13s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 24s
Validate Helm Charts / Resource quantities (pull_request) Successful in 13s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 10s
Validate Helm Charts / Bump level (pull_request) Successful in 5s
Validate Helm Charts / NetworkPolicy (pull_request) Successful in 12s
Validate Helm Charts / Selector match (pull_request) Successful in 10s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 13s
Validate Helm Charts / YAML scalars (pull_request) Successful in 17s
Validate Helm Charts / Trivy config valkey (pull_request) Successful in 26s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 21s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 15s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 13s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 5s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 22s
Validate Helm Charts / Build bulwark (pull_request) Successful in 6s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 5s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 4s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 8s
Validate Helm Charts / Build freesailarr (pull_request) Successful in 6s
Validate Helm Charts / Build garage (pull_request) Successful in 7s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 6s
Validate Helm Charts / Build homeassistant (pull_request) Successful in 6s
Validate Helm Charts / Build keycloak (pull_request) Successful in 6s
Validate Helm Charts / Build matrix-synapse (pull_request) Successful in 6s
Validate Helm Charts / Build minecraft-router (pull_request) Successful in 5s
Validate Helm Charts / Build minecraft-server (pull_request) Successful in 5s
Validate Helm Charts / Build rallly (pull_request) Successful in 6s
Install-test / Install-test apache-tika (pull_request) Successful in 1m7s
Install-test / Install-test bentopdf (pull_request) Successful in 52s
Install-test / Install-test bulwark (pull_request) Successful in 1m16s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 1m45s
Install-test / Install-test cyberchef (pull_request) Successful in 52s
Install-test / Install-test freesailarr (pull_request) Successful in 3m33s
Install-test / Install-test garage (pull_request) Successful in 1m10s
Install-test / Install-test gotenberg (pull_request) Successful in 1m10s
Install-test / Install-test homeassistant (pull_request) Successful in 2m8s
Install-test / Install-test keycloak (pull_request) Successful in 2m13s
Install-test / Install-test minecraft-server (pull_request) Successful in 1m41s
Install-test / Install-test rss-bridge (pull_request) Successful in 44s
Install-test / Install-test matrix-synapse (pull_request) Successful in 1m24s
Install-test / Install-test minecraft-router (pull_request) Successful in 38s
Install-test / Install-test outline (pull_request) Successful in 2m6s
Install-test / Install-test paperless-ngx (pull_request) Successful in 3m5s
Install-test / Install-test patchmon (pull_request) Successful in 1m21s
Install-test / Install-test rallly (pull_request) Successful in 1m45s
Install-test / Install-test samba (pull_request) Successful in 1m9s
Install-test / Install-test sure (pull_request) Successful in 2m11s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m48s
Install-test / Install-test valkey (pull_request) Successful in 1m17s
Install-test / Install-test searxng (pull_request) Successful in 1m12s
Install-test / Install-test stalwart (pull_request) Successful in 1m37s
Install-test / Install-test teamspeak-server (pull_request) Successful in 1m14s
Install-test / Install-test voucher-vault (pull_request) Successful in 2m4s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 59s
Install-test / Install-test zipline (pull_request) Successful in 1m55s
Validate Helm Charts / Install-test (pull_request) Successful in 44m24s

matrix-synapse, paperless-ngx, freesailarr, valkey, stalwart. One major each.
All 29 installable charts plus template-chart now render a policy, and
ci/run-networkpolicy-test.sh passes over all 30 with 206 assertions held.

MATRIX-SYNAPSE renders TWO policies, one per workload, and the split is the
decision in that file. Synapse federates - it must be able to reach any
homeserver on the internet, so 443 and 8448 go to any destination - while the
delegation nginx serves two static JSON documents and calls nobody, so its
egress is DNS and nothing else. A single policy over both pods would have to
permit the union, handing full federation reach and the database port to an
nginx whose job is to return a constant, and it would read as one decision
rather than as a widening. networkPolicy.egress.extraPorts deliberately does
not reach the delegation policy for the same reason.

PAPERLESS-NGX names the two pods of its own release that this object does NOT
cover: the valkey subchart's pod, which carries its own labels and needs no
cover because the valkey chart ships a tighter policy of its own; and the
consume CronJob's pods, whose pod template carries NO LABELS AT ALL, so no
podSelector can match them. That second one is a real gap in the convention. It
is tolerable only because the job moves files between mounted volumes and
reaches the SMB share through a CSI volume the node mounts - the pod opens no
connection of its own - and if that ever changes the CronJob needs labels
before it can need a policy.

FREESAILARR is ten containers in one pod behind a VPN, and simpler than that
sounds: they share a network namespace, so all inter-container traffic is
localhost and no policy sees any of it. Because the namespace belongs to
gluetun, everything the *arr containers send is encapsulated before it leaves,
so three rules describe the whole outbound surface - DNS, UDP 51820 for the
tunnel, and 443 for gluetun's server-list API, which cannot go through the
tunnel because it runs before the tunnel exists. The tunnel rule is the one
that can break this chart and also the best covered: gluetun's health server
answers 200 only while the tunnel is up and all three of its probes address it,
so a blocked 51820 is a pod that never goes Ready - in the install-test, which
brings up a real tunnel.

VALKEY AND STALWART, the two charts that had a policy before this issue, are
pulled onto the convention without a single rule changing. What changed:

  - the reasoning moved out of "#" comments, which are part of the rendered
    object and travel into the cluster, into template comments. Both files
    failed the new check; that is what it is for.
  - stalwart gains networkPolicy.ingressController, which fixes a hole its own
    comment described without being able to solve: it advised putting the
    controller's namespace into allowedNamespaces, which is namespace-granular
    and would have opened the http port to every pod in kube-system.
  - stalwart's example was WRONG and is corrected. It suggested
    ["ingress-nginx"]; measured on 2026-10-14, ingress-nginx is gone from this
    cluster entirely and the controller is rke2-traefik in kube-system. A policy
    written against the old one would have been accepted, looked strict, and
    silently blocked all web traffic.

Their justified specifics are untouched: valkey stays Ingress-only, because it
opens no outbound connection and an egress rule would constrain nothing while
making the object look stricter; and it keeps 6379 restricted by source with no
probeSources at all, which is safe only because all three of its probes are
exec probes and put no packet on the network. The convention was written to
accommodate both, and homeassistant and satisfactory now invoke valkey's rule
in the other direction.

ONE CHANGE TO THE TEST, forced by a structural fact rather than by convenience:
the rendered-comment assertion now skips documents from a SUBCHART. A
dependency is fetched from the Helm registry, not from the copy in this
repository, so the policy paperless-ngx and stalwart bundle is whatever valkey
version is PUBLISHED - and nothing in the parent chart can change its rendered
comments. Holding the parent responsible would have made the test unfixable
until valkey's next release and would fail a chart for a file its own diff does
not contain. The subchart's own pull request is where it is caught, because this
test runs over every chart including valkey itself. All twelve failure paths
were re-run against the change.

Part of #5
This commit is contained in:
jklein committed 2026-10-14 14:46:11 +00:00
1 parent 28a0230ef0
commit 73c2ec704f
22 files changed
+1593 -52

No files matched your search

+17 -1
View File
@@ -704,11 +704,27 @@ for policy in policies:
# the chart's reasoning in front of everyone who diffs a release. Go template # the chart's reasoning in front of everyone who diffs a release. Go template
# comments render nothing and are the place for it. It can only be checked on # comments render nothing and are the place for it. It can only be checked on
# the raw YAML - the comments are gone once ci/yaml2json has read the stream. # the raw YAML - the comments are gone once ci/yaml2json has read the stream.
# SUBCHART DOCUMENTS ARE EXCLUDED FROM THIS ONE, and the reason is structural
# rather than lenient. A dependency is fetched from the Helm registry, not from
# the copy in this repository, so the NetworkPolicy that paperless-ngx and
# stalwart bundle is whatever version of valkey is PUBLISHED - and nothing in
# the parent chart can change its rendered comments. Holding the parent
# responsible would make this test unfixable until the subchart's next release,
# and would fail a chart for a file its own diff does not contain. The
# subchart's own pull request is where that is caught, because this test runs
# over every chart including valkey itself.
#
# Recognised from the "# Source:" header Helm writes above each document: a
# subchart's path contains "/charts/".
if policies: if policies:
current, offenders = [], [] current, offenders = [], []
for line in raw.splitlines() + ["---"]: for line in raw.splitlines() + ["---"]:
if line.strip() == "---": if line.strip() == "---":
if re.search(r"^kind:\s*NetworkPolicy\s*$", "\n".join(current), re.M): body = "\n".join(current)
source = re.search(r"^# Source:\s*(\S+)", body, re.M)
from_subchart = bool(source) and "/charts/" in source.group(1)
if (re.search(r"^kind:\s*NetworkPolicy\s*$", body, re.M)
and not from_subchart):
for raw_line in current: for raw_line in current:
stripped = raw_line.strip() stripped = raw_line.strip()
if stripped.startswith("#") and not stripped.startswith("# Source:"): if stripped.startswith("#") and not stripped.startswith("# Source:"):
+1 -1
View File
@@ -8,7 +8,7 @@ description: A Helm chart to deploy a Wireguard-tunneled media automation stack
type: application type: application
version: 4.0.0+up1.0.0 version: 5.0.0+up1.0.0
# The chart's own version marker, freely chosen and tied to no upstream. The # The chart's own version marker, freely chosen and tied to no upstream. The
# stack bundles ten independently versioned projects (nine of them on by # stack bundles ten independently versioned projects (nine of them on by
# default), so there is no single "app version" to track here. # default), so there is no single "app version" to track here.
+69
View File
@@ -418,3 +418,72 @@ MORE THAN ONE REPLICA IS NOT MEANINGFUL
config volumes, all of them fighting over the same media claims and the same config volumes, all of them fighting over the same media claims and the same
VPN credentials. VPN credentials.
{{- end }} {{- end }}
{{- if .Values.networkPolicy.enabled }}
NETWORKPOLICY IS ACTIVE — WHAT IT CAPS, AND WHAT IT DOES NOT
{{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects this workload's pods.
Inbound: the nine probed ports — gluetun's health server (9999) and the eight
web interfaces (8080, 9696, 8191, 7878, 8989, 5055, 6767, 5007) — plus
mediathekarr's newznab port (5008), which is RESTRICTED by source because
nothing probes it and its only consumer is Prowlarr in this same pod.
Outbound: DNS, UDP 51820 (the WireGuard tunnel) and TCP 443 (gluetun's
server-list API){{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }}.
THE TEN CONTAINERS TALK TO EACH OTHER OVER LOCALHOST, so none of that traffic
is governed by this object at all — they share a network namespace. And
because that namespace belongs to gluetun, everything the *arr containers
send is encapsulated into the tunnel before it leaves the pod: indexer and
tracker traffic needs no egress rule of its own.
IF THIS POD WILL NOT BECOME READY, SUSPECT THE TUNNEL RULE. gluetun's health
server answers 200 only while the tunnel is up, and all three of its probes
address it, so a blocked UDP 51820 shows up as a pod that never goes Ready
rather than as a network error. 51820 is WireGuard's and Mullvad's standard
port; Mullvad also offers 53, 123, 500 and 4500 for obfuscation, and a
release on one of those has to widen the rule in the template —
networkPolicy.egress.extraPorts is TCP only.
AND IF SEERR CANNOT REACH AN IN-CLUSTER SERVICE (Jellyfin, say), two things
have to be true, not one: its port in networkPolicy.egress.extraPorts, and
gluetun.vpn.firewallOutboundSubnets set — gluetun's own firewall blocks
cluster-internal egress before this policy is ever consulted.
Three limits belong in the same breath, because none of them is visible in
the object itself:
- EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and
selectors and cannot express a hostname, so every egress rule permits its
port to ANY address. That is a limit of the object, not a shortcut — this
cluster's CNI (Canal: Flannel plus Calico) has no FQDN rule either. An
outbound port the chart does not know about is BLOCKED: add it to
networkPolicy.egress.extraPorts, and expect to find out through an
application-level connection error rather than through this object.
{{- if .Values.networkPolicy.probeSources }}
- THE PROBE PATH IS RESTRICTED in this release, because
networkPolicy.probeSources is set to
{{ join ", " .Values.networkPolicy.probeSources }}
Probes come from the IP of the WORKER NODE running the pod. If none of those
CIDRs covers the node this pod lands on, the kubelet's probes are dropped
and THE POD NEVER BECOMES READY — the rollout then times out and the symptom
looks like a broken application. Compare
kubectl -n {{ .Release.Namespace }} describe pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-sfs
against `kubectl get node -o wide`.
{{- else }}
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so that rule carries no source selector at all and any pod the CNI
routes to can reach it. Not from the internet — but this is NOT pod-level
isolation. Narrowing it means listing the worker node IPs in
networkPolicy.probeSources, because the kubelet probes from the node's own
address and the chart cannot know it; a wrong guess there leaves the pod
permanently un-Ready.
{{- end }}
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
this object is accepted and filters nothing, so everything reads as
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
shows the object exists; only a connection attempt from an unrelated
namespace shows that it bites.
Switch it off with networkPolicy.enabled: false.
{{- end }}
@@ -0,0 +1,253 @@
{{- /*
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 }}
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 $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 }}
{{- 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 }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.", "description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"type": "object" "type": "object"
}, },
"networkPolicy": {
"description": "The chart's NetworkPolicy (Gitea #5), on by default. A ceiling on reachability and not an authentication layer: only the declared container ports are reachable at all, egress is capped by PORT (a NetworkPolicy matches CIDRs and selectors, never hostnames, so a destination host cannot be expressed), and ingress is capped by source for every port no network probe touches. Only a boundary where the cluster's CNI enforces policies - an unenforced policy is accepted by the API server and filters nothing. The template's header carries the full list of what it does not do.",
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"allowedNamespaces": {
"description": "Namespaces whose pods may reach this workload, in addition to the release's own namespace, which is always allowed. Matched on the kubernetes.io/metadata.name label Kubernetes sets on every namespace itself, so no labelling work is needed - and no typo protection either: a name that does not exist matches nothing and is silently ineffective. An entry opens the port to EVERY pod in that namespace; for the ingress controller use ingressController instead.",
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"probeSources": {
"description": "Source CIDRs allowed to reach the port the network probes address. EMPTY MEANS OPEN, not blocked: with no entry that rule is rendered without a \"from\" selector. The kubelet's probes come from the IP of the worker node running the pod, an address the chart cannot know and must not guess - a guessed CIDR that excludes the real node blocks every probe and the pod never becomes Ready, which reads as a broken application rather than as a policy. Setting this restricts the rule, which is why the chart adds the own namespace, ingressController and allowedNamespaces to the same rule.",
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"ingressController": {
"description": "The ingress controller allowed to reach the routed port once probeSources restricts it; rendered only while ingress.enabled or httpRoute.enabled is true. The two keys become ONE \"from\" entry and are therefore an AND - pods carrying podLabels IN namespace - which is the whole point: the controller's namespace in allowedNamespaces would open the port to every pod in kube-system. The default is measured on this cluster (DaemonSet rke2-traefik in kube-system, which also serves the Gateway API here), not assumed; a policy written against the wrong controller is accepted, looks strict and silently blocks all web traffic.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"namespace": {
"type": ["string", "null"]
},
"podLabels": {
"description": "Label set the controller's pods carry, rendered verbatim as a podSelector's matchLabels. Kept open because the chart does not interpret the keys.",
"type": ["object", "null"]
}
}
},
"egress": {
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"extraPorts": {
"description": "Additional outbound TCP ports, to ANY destination - ports and not destinations, because a NetworkPolicy cannot express a hostname. DNS is always allowed and has no switch. Without an entry here an outbound port the chart does not already derive is blocked, which is the cap's whole purpose and also its upgrade risk.",
"type": ["array", "null"],
"items": {
"type": "integer"
}
}
}
}
}
},
"scaleDown": { "scaleDown": {
"type": "boolean" "type": "boolean"
}, },
+98
View File
@@ -1330,3 +1330,101 @@ mediathekarr:
capabilities: capabilities:
drop: drop:
- ALL - ALL
# ---------------------------------------------------------------------------
# NetworkPolicy (Gitea #5). On by default. The reference implementation with
# the full reasoning is template-chart; the convention is in README, "Chart
# style guidelines", and the operator's view on this chart's wiki page.
# ---------------------------------------------------------------------------
networkPolicy:
# TEN CONTAINERS IN ONE POD, AND THE FIRST THING TO KNOW: they talk to each
# other over LOCALHOST. They share a network namespace, so that traffic never
# touches an interface and no NetworkPolicy sees it. This object governs only
# what enters or leaves the pod.
#
# WHAT IT ALLOWS, inbound: the nine probed ports as one rule (gluetun's
# health server 9999, plus qBittorrent 8080, Prowlarr 9696, FlareSolverr
# 8191, Radarr 7878, Sonarr 8989, Seerr 5055, Bazarr 6767, Mediathekarr
# 5007), and mediathekarr's newznab port 5008 RESTRICTED by source - nothing
# probes it, and its only consumer is Prowlarr over localhost, so restricting
# it costs nothing.
#
# OUTBOUND, and this chart is unlike every other one here: the pod's network
# namespace belongs to gluetun, so everything the *arr containers do is
# encapsulated into the WireGuard tunnel before it leaves. Three rules
# describe the whole outbound surface - DNS, UDP 51820 for the tunnel, and
# 443 for gluetun's server-list API, which cannot go through the tunnel
# because it runs before the tunnel exists. Indexer and tracker traffic needs
# no rule: it is already inside.
#
# THE TUNNEL RULE IS THE ONE THAT CAN BREAK THIS CHART, and it is also the
# best-covered: gluetun's health server returns 200 only while the tunnel is
# up, and all three of its probes address it, so a blocked 51820 means a pod
# that never becomes Ready - visibly, and in the install-test, which brings
# up a real tunnel. 51820 is WireGuard's and Mullvad's standard port; Mullvad
# also offers 53, 123, 500 and 4500 for obfuscation, and a release using one
# of those must widen the rule in the TEMPLATE, because egress.extraPorts is
# TCP only. That is a limit of the shared values shape, said plainly.
enabled: true
# Namespaces whose pods may reach this workload, in addition to the release's
# own namespace, which is always allowed. Empty means: own namespace only.
#
# Matched on the "kubernetes.io/metadata.name" label Kubernetes sets on every
# namespace itself, so no labelling work is needed on a namespace this chart
# does not own - and no typo protection either: a name that does not exist
# matches nothing and is silently ineffective. Note the granularity: an entry
# opens the port to EVERY pod in that namespace.
allowedNamespaces: []
# Source CIDRs allowed to reach the nine probed ports (gluetun's health server and the eight web interfaces), which is every inbound port except mediathekarr's newznab port.
#
# EMPTY MEANS OPEN, NOT BLOCKED. With no entry that rule is rendered WITHOUT a
# "from" selector, so the port is reachable from anywhere the CNI routes to -
# inside the cluster, since it carries no NodePort.
#
# That is the deliberate choice between two bad defaults. The kubelet's probes
# originate from the IP of the WORKER NODE running the pod, an address this
# chart cannot know and must not guess: a CIDR that excludes the real node
# blocks every probe, the pod never becomes Ready, the rollout times out, and
# the symptom reads as a broken application rather than as a policy.
#
# Narrow it by listing the worker node IPs (e.g. ["10.0.1.11/32"]) - and mind
# the companion trap: once this list is non-empty the rule IS restricted, so
# everything else that needs the port has to be in it. The chart adds the own
# namespace, the ingressController below and allowedNamespaces to the same rule for exactly that reason.
probeSources: []
# The ingress controller allowed to reach the routed port once probeSources
# restricts it. Rendered only while ingress.enabled or httpRoute.enabled is true, and only in
# the restricted branch - with the default (open) rule the controller reaches
# the port anyway. The two keys become ONE "from" entry and are therefore an
# AND: pods carrying podLabels IN namespace. That is the point of having them
# at all - the controller's namespace in allowedNamespaces would open the port
# to every pod in kube-system.
#
# MEASURED ON THIS CLUSTER, NOT ASSUMED (2026-10-14): ingress-nginx is gone,
# the only IngressClass is "traefik" and it is the default, and the controller
# is the DaemonSet rke2-traefik in kube-system. The same Traefik serves the
# Gateway API here, so this one source covers the Ingress and the HTTPRoute
# path alike. A policy written against the wrong controller is accepted, looks
# strict, and silently blocks all web traffic - and ingress.className says
# nothing about it, because a class names an implementation, not the pods the
# traffic comes from.
ingressController:
namespace: kube-system
podLabels:
app.kubernetes.io/name: rke2-traefik
egress:
# Additional outbound TCP ports. DNS (UDP and TCP 53) is always allowed and
# has no switch - a knob whose only other setting breaks every name lookup
# in the pod is a trap, not a feature.
#
# PORTS, NOT DESTINATIONS, and that is a limit of the object rather than
# laziness: a NetworkPolicy matches CIDRs and selectors and never hostnames,
# so "only to the database host" cannot be written at all. Each entry
# permits its port to ANY destination. What the cap still buys is real: a
# compromised pod cannot open arbitrary outbound connections.
#
# TCP ONLY - an alternative WireGuard UDP port cannot be added here. The
# usual entry is an in-cluster service Seerr talks to outside the tunnel
# (Jellyfin on 8096), which also needs gluetun.vpn.firewallOutboundSubnets
# set, or gluetun's own firewall blocks it before this policy is reached.
extraPorts: []
+1 -1
View File
@@ -3,7 +3,7 @@ name: matrix-synapse
description: A Helm chart to deploy a Matrix Synapse server on Kubernetes description: A Helm chart to deploy a Matrix Synapse server on Kubernetes
icon: https://raw.githubusercontent.com/element-hq/synapse/develop/docs/favicon.png icon: https://raw.githubusercontent.com/element-hq/synapse/develop/docs/favicon.png
type: application type: application
version: 7.2.0+upv1.159.0 version: 8.0.0+upv1.159.0
appVersion: "v1.159.0" appVersion: "v1.159.0"
maintainers: maintainers:
- name: jklein - name: jklein
+59
View File
@@ -396,3 +396,62 @@ matrix.env IS APPENDED, NOT MERGED
{{ .Release.Namespace }}. Note that Synapse takes its real configuration from {{ .Release.Namespace }}. Note that Synapse takes its real configuration from
homeserver.yaml, not from the environment. homeserver.yaml, not from the environment.
{{- end }} {{- end }}
{{- if .Values.networkPolicy.enabled }}
NETWORKPOLICY IS ACTIVE — TWO OBJECTS, ONE PER WORKLOAD
{{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects the Synapse pods and
{{ .Release.Name }}-{{ .Chart.Name }}-delegation-networkpolicy the delegation nginx. They are
separate because their needs are opposite: Synapse federates and must be able
to reach any homeserver on the internet, while the delegation server returns
two static JSON documents and calls nobody. One policy over both pods would
have to permit the union — handing full federation reach and the database port
to an nginx that serves a constant.
Synapse: inbound TCP 8008 only; outbound DNS, TCP 5432 (Postgres), 443 and
8448 (federation, remote media, push gateways, identity servers){{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP
{{ join ", " . }}{{ end }}{{ end }}.
Delegation nginx: inbound TCP 8080 only; outbound DNS and nothing else —
networkPolicy.egress.extraPorts deliberately does not widen it.
Three limits, none of them visible in the objects themselves:
- FEDERATION CANNOT BE NARROWED. 443 and 8448 go to ANY destination, because
a federating homeserver may contact any other homeserver there is, and a
NetworkPolicy matches CIDRs and selectors but never hostnames — with this
cluster's CNI (Canal: Flannel plus Calico) there is no FQDN rule either.
What the cap still buys is real: Synapse can open NO other outbound port,
so a compromised instance reaches no other database, no SSH and no mail
server in the cluster.
- 5432 IS THE STANDARD PORT, NOT YOUR CONFIGURED ONE. Synapse's Postgres
connection lives inside secrets.configSecretRef, which this chart never
parses. A database elsewhere is blocked — put its port in
networkPolicy.egress.extraPorts. The same applies to a TURN server for
voice, or to SMTP if this release sends mail.
{{- if .Values.networkPolicy.probeSources }}
- THE PROBE PATHS ARE RESTRICTED in this release, because
networkPolicy.probeSources is set to
{{ join ", " .Values.networkPolicy.probeSources }}
Probes come from the IP of the WORKER NODE running the pod. If none of those
CIDRs covers the node a pod lands on, its probes are dropped and THE POD
NEVER BECOMES READY — the rollout times out and the symptom looks like a
broken application.
AND CHECK FEDERATION AFTER THIS CHANGE, not just the web UI. Inbound
federation reaches Synapse through the Ingress, and every remote homeserver
that wants to federate fetches /.well-known/matrix from the delegation
server — so a wrong ingressController here stops federation and stops it
being discoverable, and that failure appears on OTHER people's servers
before it appears on yours.
{{- else }}
- BOTH PROBE PORTS ARE OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so neither rule carries a source selector and any pod the CNI routes
to can reach them. Not from the internet — neither port has a NodePort —
but this is NOT pod-level isolation. Narrowing it means listing the worker
node IPs, because the kubelet probes from the node's own address and the
chart cannot know it; a wrong guess leaves the pod permanently un-Ready.
{{- end }}
Switch both off with networkPolicy.enabled: false. And remember that a policy
is only a boundary where the CNI enforces one — unenforced, both objects are
accepted and filter nothing, so everything reads as configured.
{{- end }}
@@ -0,0 +1,269 @@
{{- /*
Two NetworkPolicies, one per workload (Gitea #5).
THIS CHART RENDERS TWO POD TEMPLATES AND THEY NEED DIFFERENT RULES, which is
why this file is written out rather than generated from one shape like most of
the sweep. A single policy selecting both would have to be the UNION of what
each needs - Synapse's federation egress granted to the delegation server, and
the delegation server's ports granted to Synapse - and a policy that is the
union of two workloads' needs is strictly weaker than two policies, while
looking like one decision.
<release>-<chart>-networkpolicy the Synapse StatefulSet
<release>-<chart>-delegation-networkpolicy the delegation nginx Deployment
The split matters most on egress. Synapse federates: it opens connections to
other people's homeservers, to push gateways and to identity servers, so its
egress rule is necessarily wide. The delegation server serves two static JSON
documents under /.well-known/matrix and calls NOTHING - so its egress is DNS
and nothing else, which is as tight as this object gets. Merging them would
have handed Synapse's reach to an nginx whose entire job is to return a
constant.
BOTH WORKLOADS SIT BEHIND THE SAME INGRESS, on different paths (/ to Synapse,
/.well-known/matrix to the delegation server), so both carry the ingress
controller in their restricted branch - and both would go dark together if it
were wrong.
AND BOTH PROBE PORTS STAY OPEN BY DEFAULT, because all three probes of each
workload address its own HTTP port. The kubelet probes from the IP of the
worker node running the pod, which no chart can know; narrowing is
networkPolicy.probeSources, and it applies to both policies at once.
WHAT THESE POLICIES DO NOT DO:
- EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and
selectors and never hostnames, so "only the homeservers we federate with"
cannot be expressed - and with Canal (Flannel plus Calico) there is no FQDN
rule to reach for. Federation therefore means 443 and 8448 to any address,
which is the honest description: Synapse may contact any homeserver on the
internet, because that is what federation is.
- THE DATABASE PORT IS A GUESS. Synapse's Postgres connection lives inside
secrets.configSecretRef, a 1Password item this chart never parses, so 5432
is the standard port and not the configured one.
- A POLICY IS ONLY A BOUNDARY WHERE THE CNI ENFORCES ONE.
*/ -}}
{{- 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 }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
{{- /*
Selects the Synapse StatefulSet's pods - NOT the delegation Deployment's,
which has its own policy below. ci/run-networkpolicy-test.sh reports which pod
templates each selector matches, so a label renamed in one of the two
workloads shows up as a finding rather than as a policy that filters nothing.
*/}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
policyTypes:
- Ingress
- Egress
ingress:
{{- /*
8008 - Synapse's client and federation API, the port the Service forwards 80
to, and the port all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 8008
{{- 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.
MIND THE FEDERATION SIDE OF THIS RULE. Inbound federation traffic arrives
through the Ingress, so the controller entry below is what keeps other
homeservers able to reach this one. Restricting this rule without it does
not merely hide a web UI: it stops federation inbound, which looks like
other servers being unreachable rather than like a local policy.
*/}}
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 }}
{{- 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.
*/}}
{{- 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. Synapse needs it more than most - every
federated server is resolved by name, including its SRV and .well-known
delegation lookups.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
PostgreSQL on the STANDARD port. The connection lives inside
secrets.configSecretRef, a 1Password item this chart never parses, so this
is the conventional port and not the configured one - a Postgres elsewhere
is blocked and belongs in networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 5432
{{- /*
FEDERATION, and this is the rule that cannot be narrowed. 443 and 8448 to
ANY destination: 8448 is the Matrix federation port, 443 is where a
delegating homeserver puts its federation API instead, and both are also
what Synapse uses to fetch remote media, to reach a push gateway and to
reach an identity server.
"Any destination" is not laziness here, it is the specification: a
federating homeserver may contact any other homeserver on the internet, and
this object cannot match a hostname in any case. What the cap still buys is
real - Synapse cannot open a connection on any OTHER port, so a compromised
instance reaches no database, no SSH and no mail server elsewhere in the
cluster.
*/}}
- ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 8448
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. 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 }}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-networkpolicy
spec:
{{- /* Selects the delegation nginx Deployment's pods only. */}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-delegation-nginx-deployment
policyTypes:
- Ingress
- Egress
ingress:
{{- /*
8080 - the only port this pod declares, the port the delegation Service
forwards 80 to, and the port all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 8080
{{- if $probeSources }}
{{- /*
RESTRICTED, because the operator supplied probeSources - the same value
governs both policies in this chart. The kubelet probes from the node's
own IP, so it has to be in the list or this pod never becomes Ready.
AND THE CONSUMERS OF THIS ONE ARE OTHER PEOPLE'S HOMESERVERS. The
delegation documents under /.well-known/matrix are fetched by every remote
server that wants to federate with this one, through the Ingress. Restrict
this rule without the controller entry and federation stops being
discoverable - a failure that shows up on OTHER servers, not here.
*/}}
from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
- 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 }}
{{- else }}
{{- /* Open inside the cluster, as above - the probe path. */}}
{{- end }}
egress:
{{- /*
DNS AND NOTHING ELSE, and this is the tightest egress rule in the chart on
purpose. This nginx serves two static JSON documents and calls nobody; it
does not touch the database, it does not federate, it has no upstream. Even
DNS is only here because the container resolves at start.
This is exactly what the two-policy split buys. Folded into Synapse's
policy, this pod would have been permitted 443, 8448 and 5432 - full
federation reach and the database - for a workload whose entire job is to
return a constant. networkPolicy.egress.extraPorts deliberately does NOT
apply here: an entry meant for Synapse's database has no business widening
this.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- end }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.", "description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"type": "object" "type": "object"
}, },
"networkPolicy": {
"description": "The chart's NetworkPolicy (Gitea #5), on by default. A ceiling on reachability and not an authentication layer: only the declared container ports are reachable at all, egress is capped by PORT (a NetworkPolicy matches CIDRs and selectors, never hostnames, so a destination host cannot be expressed), and ingress is capped by source for every port no network probe touches. Only a boundary where the cluster's CNI enforces policies - an unenforced policy is accepted by the API server and filters nothing. The template's header carries the full list of what it does not do.",
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"allowedNamespaces": {
"description": "Namespaces whose pods may reach this workload, in addition to the release's own namespace, which is always allowed. Matched on the kubernetes.io/metadata.name label Kubernetes sets on every namespace itself, so no labelling work is needed - and no typo protection either: a name that does not exist matches nothing and is silently ineffective. An entry opens the port to EVERY pod in that namespace; for the ingress controller use ingressController instead.",
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"probeSources": {
"description": "Source CIDRs allowed to reach the port the network probes address. EMPTY MEANS OPEN, not blocked: with no entry that rule is rendered without a \"from\" selector. The kubelet's probes come from the IP of the worker node running the pod, an address the chart cannot know and must not guess - a guessed CIDR that excludes the real node blocks every probe and the pod never becomes Ready, which reads as a broken application rather than as a policy. Setting this restricts the rule, which is why the chart adds the own namespace, ingressController and allowedNamespaces to the same rule.",
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"ingressController": {
"description": "The ingress controller allowed to reach the routed port once probeSources restricts it; rendered only while ingress.enabled or httpRoute.enabled is true. The two keys become ONE \"from\" entry and are therefore an AND - pods carrying podLabels IN namespace - which is the whole point: the controller's namespace in allowedNamespaces would open the port to every pod in kube-system. The default is measured on this cluster (DaemonSet rke2-traefik in kube-system, which also serves the Gateway API here), not assumed; a policy written against the wrong controller is accepted, looks strict and silently blocks all web traffic.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"namespace": {
"type": ["string", "null"]
},
"podLabels": {
"description": "Label set the controller's pods carry, rendered verbatim as a podSelector's matchLabels. Kept open because the chart does not interpret the keys.",
"type": ["object", "null"]
}
}
},
"egress": {
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"extraPorts": {
"description": "Additional outbound TCP ports, to ANY destination - ports and not destinations, because a NetworkPolicy cannot express a hostname. DNS is always allowed and has no switch. Without an entry here an outbound port the chart does not already derive is blocked, which is the cap's whole purpose and also its upgrade risk.",
"type": ["array", "null"],
"items": {
"type": "integer"
}
}
}
}
}
},
"scaleDown": { "scaleDown": {
"type": "boolean" "type": "boolean"
}, },
+96
View File
@@ -285,3 +285,99 @@ httpRoute:
# A chart-global hostname list covering both would erase that distinction. # A chart-global hostname list covering both would erase that distinction.
# Set this only when the server name itself has to move. # Set this only when the server name itself has to move.
hostnames: [] hostnames: []
# ---------------------------------------------------------------------------
# NetworkPolicy (Gitea #5). On by default. TWO objects, one per workload - see
# the template for why they are not one. The reference implementation with the
# full reasoning is template-chart; the convention is in README, "Chart style
# guidelines".
# ---------------------------------------------------------------------------
networkPolicy:
# WHAT THESE POLICIES ALLOW. Synapse: inbound 8008 only, outbound DNS plus TCP
# 5432 (Postgres), 443 and 8448 (federation, remote media, push gateways,
# identity servers). The delegation nginx: inbound 8080 only, outbound DNS and
# NOTHING else - it serves two static JSON documents and calls nobody.
#
# THAT ASYMMETRY IS THE REASON THERE ARE TWO. One policy selecting both pods
# would have to permit the union of what each needs, which would hand
# Synapse's full federation reach and its database port to an nginx whose
# whole job is to return a constant - and it would look like one decision
# rather than a widening.
#
# WHAT THEY DO NOT DO: federation cannot be narrowed. 443 and 8448 go to ANY
# destination, because a federating homeserver may contact any other
# homeserver on the internet and this object matches CIDRs and selectors but
# never hostnames. And 5432 is the STANDARD Postgres port, not the configured
# one - the connection lives inside secrets.configSecretRef, which this chart
# never parses.
#
# Both probe ports stay open inside the cluster by default (see probeSources),
# and a policy is only a boundary where the CNI enforces one.
enabled: true
# Namespaces whose pods may reach either workload, in addition to the
# release's own namespace, which is always allowed. Empty means: own namespace
# only. Matched on the "kubernetes.io/metadata.name" label Kubernetes sets on
# every namespace itself, so no labelling work is needed - and no typo
# protection either: a name that does not exist matches nothing and is
# silently ineffective. An entry opens the port to EVERY pod in that
# namespace.
allowedNamespaces: []
# Source CIDRs allowed to reach the two probe ports (Synapse's 8008 and the
# delegation server's 8080). ONE VALUE GOVERNS BOTH POLICIES.
#
# EMPTY MEANS OPEN, NOT BLOCKED. With no entry both rules are rendered WITHOUT
# a "from" selector, so the ports are reachable from anywhere the CNI routes
# to - inside the cluster, since neither carries a NodePort.
#
# The kubelet's probes originate from the IP of the WORKER NODE running the
# pod, an address this chart cannot know and must not guess: a CIDR that
# excludes the real node blocks every probe, the pod never becomes Ready, the
# rollout times out, and the symptom reads as a broken application rather
# than as a policy.
#
# AND MIND WHAT ELSE THESE TWO PORTS CARRY. Inbound federation arrives at
# 8008 through the Ingress, and every remote homeserver that wants to
# federate fetches /.well-known/matrix from 8080. Narrowing this list without
# ingressController below therefore does not merely hide a web UI: it stops
# federation, inbound and discoverable, and the failure shows up on OTHER
# people's servers before it shows up here.
probeSources: []
# The ingress controller allowed to reach both ports once probeSources
# restricts them. Rendered only while ingress.enabled or httpRoute.enabled is
# true, and only in the restricted branch - with the default (open) rules the
# controller reaches both ports anyway.
#
# The two keys become ONE "from" entry and are therefore an AND: pods carrying
# podLabels IN namespace. That is the point of having them here - the
# controller's namespace in allowedNamespaces would open both ports to every
# pod in kube-system.
#
# MEASURED ON THIS CLUSTER, NOT ASSUMED (2026-10-14): ingress-nginx is gone,
# the only IngressClass is "traefik" and it is the default, and the controller
# is the DaemonSet rke2-traefik in kube-system. The same Traefik serves the
# Gateway API here, so this one source covers the Ingress and the HTTPRoute
# path alike. A policy written against the wrong controller is accepted, looks
# strict, and silently blocks all web traffic - and ingress.className says
# nothing about it, because a class names an implementation, not the pods the
# traffic comes from.
ingressController:
namespace: kube-system
podLabels:
app.kubernetes.io/name: rke2-traefik
egress:
# Additional outbound TCP ports FOR SYNAPSE ONLY. These deliberately do NOT
# widen the delegation server's policy: an entry meant for Synapse's
# database has no business reaching a workload that serves a constant.
#
# DNS is always allowed and has no switch - a knob whose only other setting
# breaks every name lookup is a trap, not a feature, and Synapse resolves a
# name for every server it federates with.
#
# PORTS, NOT DESTINATIONS: a NetworkPolicy matches CIDRs and selectors and
# never hostnames, so each entry permits its port to ANY destination.
#
# The usual entries here: a Postgres that is not on 5432, a TURN/coturn
# server for voice (3478 and its relay range), or SMTP (587 or 465) if this
# release sends mail - Synapse's mail configuration lives inside
# secrets.configSecretRef, which this chart never reads.
extraPorts: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/dev/src-ui/s
type: application type: application
version: 13.1.0+up3.3.1 version: 14.0.0+up3.3.1
appVersion: 3.3.1 appVersion: 3.3.1
+63
View File
@@ -531,3 +531,66 @@ BACKUP IS TWO THINGS AT THE SAME TIME
paperless.env adds {{ len .Values.paperless.env }} variable(s), rendered after the ones the chart paperless.env adds {{ len .Values.paperless.env }} variable(s), rendered after the ones the chart
sets itself, so they win on a name the chart also sets. sets itself, so they win on a name the chart also sets.
{{- end }} {{- end }}
{{- if .Values.networkPolicy.enabled }}
NETWORKPOLICY IS ACTIVE — WHAT IT CAPS, AND WHAT IT DOES NOT
{{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects this workload's pods.
Inbound: TCP 8000 only. Outbound: DNS, TCP 5432 (Postgres), 6379
(Redis/Valkey), 443{{ if .Values.paperless.files.tikaGotenberg.enabled }}, 80 (the Tika and Gotenberg Services){{ end }}{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP
{{ join ", " . }}{{ end }}{{ end }}.
MAIL FETCHING IS NOT PERMITTED. Paperless' mail accounts are rows in its
database, configured in the web UI, so this chart cannot see them. A release
that fetches mail needs IMAP (993, or 143) in
networkPolicy.egress.extraPorts — otherwise a mail rule silently stops
importing, with a healthy pod behind it.
AND TWO PODS OF THIS RELEASE ARE NOT COVERED BY THIS OBJECT:
- the valkey subchart's pod, which carries its own labels. It needs no
cover from here: the valkey chart ships its own NetworkPolicy, and a
tighter one.
- the consume CronJob's pods, whose pod template carries NO labels at all,
so no podSelector can match them. They move files between mounted
volumes and reach the SMB share through a CSI volume the NODE mounts, so
the pod opens no connection of its own — but the gap is real and is named
here rather than left to be discovered.
Three limits belong in the same breath, because none of them is visible in
the object itself:
- EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and
selectors and cannot express a hostname, so every egress rule permits its
port to ANY address. That is a limit of the object, not a shortcut — this
cluster's CNI (Canal: Flannel plus Calico) has no FQDN rule either. An
outbound port the chart does not know about is BLOCKED: add it to
networkPolicy.egress.extraPorts, and expect to find out through an
application-level connection error rather than through this object.
{{- if .Values.networkPolicy.probeSources }}
- THE PROBE PATH IS RESTRICTED in this release, because
networkPolicy.probeSources is set to
{{ join ", " .Values.networkPolicy.probeSources }}
Probes come from the IP of the WORKER NODE running the pod. If none of those
CIDRs covers the node this pod lands on, the kubelet's probes are dropped
and THE POD NEVER BECOMES READY — the rollout then times out and the symptom
looks like a broken application. Compare
kubectl -n {{ .Release.Namespace }} describe pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-sfs
against `kubectl get node -o wide`.
{{- else }}
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so that rule carries no source selector at all and any pod the CNI
routes to can reach it. Not from the internet — but this is NOT pod-level
isolation. Narrowing it means listing the worker node IPs in
networkPolicy.probeSources, because the kubelet probes from the node's own
address and the chart cannot know it; a wrong guess there leaves the pod
permanently un-Ready.
{{- end }}
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
this object is accepted and filters nothing, so everything reads as
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
shows the object exists; only a connection attempt from an unrelated
namespace shows that it bites.
Switch it off with networkPolicy.enabled: false.
{{- end }}
@@ -0,0 +1,223 @@
{{- /*
NetworkPolicy for paperless-ngx (Gitea #5).
Paperless reaches a Postgres and a Redis, and - when
paperless.files.tikaGotenberg.enabled is true - the Tika and Gotenberg services,
which in this repository are two other charts whose Services listen on port 80.
That last rule is derived from the switch rather than always rendered, so a
release without Tika does not carry a rule that permits nothing.
TWO THINGS THIS POLICY DOES NOT COVER, and both are real rather than
theoretical:
1. THE VALKEY SUBCHART'S POD. This chart declares valkey as an optional
dependency, and the subchart renders its own StatefulSet with its own
labels. It is not covered by THIS policy - but it does not need to be: the
valkey chart ships a NetworkPolicy of its own, and it is the tighter one
(Ingress-only, 6379 restricted by source, because its probes are exec probes
and no kubelet traffic has to be let through). This is the one subchart in
the repository that brings its own policy.
2. THE CONSUME CRONJOB'S PODS. paperless-consume.cronjob.yaml renders a pod
template with NO LABELS AT ALL, so no podSelector can match it and no policy
in this chart can restrict it. That is a gap in the convention, named here
rather than glossed. What makes it tolerable: the CronJob's whole job is to
move files between two mounted volumes, and the SMB share reaches it as a
CSI volume - mounted by the NODE, not opened by the pod - so the pod itself
opens no connection that a policy would have anything to say about. If that
ever changes, the CronJob needs labels before it can need a policy.
3. MAIL FETCHING IS NOT PERMITTED. Paperless' mail accounts live in its
DATABASE, configured through the web UI, so this chart cannot see them. A
release that fetches mail needs IMAP (993 or 143) in
networkPolicy.egress.extraPorts, and the symptom otherwise is a mail rule
that silently stops importing.
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 }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
{{- /*
Selects the paperless server's StatefulSet pods. Note that this render may
contain the valkey subchart's pods as well - this selector must not match them,
and ci/run-networkpolicy-test.sh reports which pod templates it does match so
that is visible rather than assumed.
*/}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
policyTypes:
- Ingress
{{- /*
Egress is listed because there is a lot to restrict and most of it is known.
What is NOT permitted and is the plausible surprise: IMAP. Paperless' mail
accounts are rows in its database, configured in the web UI, so the chart
cannot see them - see networkPolicy.egress.extraPorts.
*/}}
- Egress
ingress:
{{- /*
8000 - the only port this pod declares, the one the Service forwards 80 to, and
the port all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 8000
{{- 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, or the Ingress goes dark the
moment this list is filled.
*/}}
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 }}
{{- 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 }}
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
{{- /*
PostgreSQL on the STANDARD port. The connection details live in a 1Password
item this chart never parses, so this is the conventional port and not the
configured one; a Postgres elsewhere is blocked and belongs in
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 5432
{{- /*
Redis/Valkey on the STANDARD port, same reasoning. This covers the bundled
valkey subchart as well as an external one - the subchart's Service uses 6379
too.
*/}}
- ports:
- protocol: TCP
port: 6379
{{- if .Values.paperless.files.tikaGotenberg.enabled }}
{{- /*
TIKA AND GOTENBERG, on port 80 - which is what their Services listen on in this
repository, even though their containers use 9998 and 3000. That distinction is
the whole reason this rule says 80: an EGRESS rule names the port on the
DESTINATION as the client dials it, and the client dials the Service.
Rendered only while paperless.files.tikaGotenberg.enabled is true, so a release
without them carries no rule that permits nothing. Endpoints on another port -
the containers' own 9998 and 3000, if addressed directly rather than through
their Services - need networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 80
{{- end }}
{{- /*
Outbound HTTPS, for the trained classifier's model downloads and for any
remote resource a consumed document pulls. To any destination, because those
are hostnames.
*/}}
- 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 }}
+51
View File
@@ -51,6 +51,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.", "description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"type": "object" "type": "object"
}, },
"networkPolicy": {
"description": "The chart's NetworkPolicy (Gitea #5), on by default. A ceiling on reachability and not an authentication layer: only the declared container ports are reachable at all, egress is capped by PORT (a NetworkPolicy matches CIDRs and selectors, never hostnames, so a destination host cannot be expressed), and ingress is capped by source for every port no network probe touches. Only a boundary where the cluster's CNI enforces policies - an unenforced policy is accepted by the API server and filters nothing. The template's header carries the full list of what it does not do.",
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"allowedNamespaces": {
"description": "Namespaces whose pods may reach this workload, in addition to the release's own namespace, which is always allowed. Matched on the kubernetes.io/metadata.name label Kubernetes sets on every namespace itself, so no labelling work is needed - and no typo protection either: a name that does not exist matches nothing and is silently ineffective. An entry opens the port to EVERY pod in that namespace; for the ingress controller use ingressController instead.",
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"probeSources": {
"description": "Source CIDRs allowed to reach the port the network probes address. EMPTY MEANS OPEN, not blocked: with no entry that rule is rendered without a \"from\" selector. The kubelet's probes come from the IP of the worker node running the pod, an address the chart cannot know and must not guess - a guessed CIDR that excludes the real node blocks every probe and the pod never becomes Ready, which reads as a broken application rather than as a policy. Setting this restricts the rule, which is why the chart adds the own namespace, ingressController and allowedNamespaces to the same rule.",
"type": ["array", "null"],
"items": {
"type": "string"
}
},
"ingressController": {
"description": "The ingress controller allowed to reach the routed port once probeSources restricts it; rendered only while ingress.enabled or httpRoute.enabled is true. The two keys become ONE \"from\" entry and are therefore an AND - pods carrying podLabels IN namespace - which is the whole point: the controller's namespace in allowedNamespaces would open the port to every pod in kube-system. The default is measured on this cluster (DaemonSet rke2-traefik in kube-system, which also serves the Gateway API here), not assumed; a policy written against the wrong controller is accepted, looks strict and silently blocks all web traffic.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"namespace": {
"type": ["string", "null"]
},
"podLabels": {
"description": "Label set the controller's pods carry, rendered verbatim as a podSelector's matchLabels. Kept open because the chart does not interpret the keys.",
"type": ["object", "null"]
}
}
},
"egress": {
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"extraPorts": {
"description": "Additional outbound TCP ports, to ANY destination - ports and not destinations, because a NetworkPolicy cannot express a hostname. DNS is always allowed and has no switch. Without an entry here an outbound port the chart does not already derive is blocked, which is the cap's whole purpose and also its upgrade risk.",
"type": ["array", "null"],
"items": {
"type": "integer"
}
}
}
}
}
},
"scaleDown": { "scaleDown": {
"type": "boolean" "type": "boolean"
}, },
+97
View File
@@ -540,3 +540,100 @@ valkey:
# And one rule that is about this repository rather than about an installation: # And one rule that is about this repository rather than about an installation:
# changes to valkey/ must not share a pull request with changes to this chart. # changes to valkey/ must not share a pull request with changes to this chart.
# Chart.yaml says why; README and the wiki's valkey page carry the long form. # Chart.yaml says why; README and the wiki's valkey page carry the long form.
# ---------------------------------------------------------------------------
# NetworkPolicy (Gitea #5). On by default. The reference implementation with
# the full reasoning is template-chart; the convention is in README, "Chart
# style guidelines", and the operator's view on this chart's wiki page.
# ---------------------------------------------------------------------------
networkPolicy:
# WHAT THIS POLICY ALLOWS: inbound 8000 only, and outbound DNS plus TCP 5432
# (Postgres), 6379 (Redis/Valkey), 443, and - only while
# paperless.files.tikaGotenberg.enabled is true - 80 for the Tika and
# Gotenberg Services.
#
# NOTE WHY THAT RULE SAYS 80 AND NOT 9998/3000: an egress rule names the port
# on the DESTINATION as the client dials it, and paperless dials those
# charts' SERVICES, which listen on 80 and forward to the containers' own
# ports.
#
# THREE THINGS IT DOES NOT COVER:
# - The VALKEY SUBCHART'S POD. It carries its own labels, so this policy
# does not select it - and it does not need to: the valkey chart ships a
# NetworkPolicy of its own, and a tighter one.
# - The CONSUME CRONJOB'S PODS. Their pod template carries NO LABELS, so no
# podSelector can match them and nothing here restricts them. Tolerable
# only because the job moves files between mounted volumes and the SMB
# share arrives as a CSI volume mounted by the NODE - the pod opens no
# connection of its own. If that changes, the CronJob needs labels first.
# - MAIL FETCHING. Paperless' mail accounts are rows in its database,
# configured in the web UI, so this chart cannot see them. A release that
# fetches mail needs IMAP in egress.extraPorts, or a mail rule silently
# stops importing.
#
# And the usual two: 5432 and 6379 are the STANDARD ports rather than the
# configured ones, and a policy is only a boundary where the CNI enforces
# one.
enabled: true
# Namespaces whose pods may reach this workload, in addition to the release's
# own namespace, which is always allowed. Empty means: own namespace only.
#
# Matched on the "kubernetes.io/metadata.name" label Kubernetes sets on every
# namespace itself, so no labelling work is needed on a namespace this chart
# does not own - and no typo protection either: a name that does not exist
# matches nothing and is silently ineffective. Note the granularity: an entry
# opens the port to EVERY pod in that namespace.
allowedNamespaces: []
# Source CIDRs allowed to reach port 8000, which is the port all three probes address.
#
# EMPTY MEANS OPEN, NOT BLOCKED. With no entry that rule is rendered WITHOUT a
# "from" selector, so the port is reachable from anywhere the CNI routes to -
# inside the cluster, since it carries no NodePort.
#
# That is the deliberate choice between two bad defaults. The kubelet's probes
# originate from the IP of the WORKER NODE running the pod, an address this
# chart cannot know and must not guess: a CIDR that excludes the real node
# blocks every probe, the pod never becomes Ready, the rollout times out, and
# the symptom reads as a broken application rather than as a policy.
#
# Narrow it by listing the worker node IPs (e.g. ["10.0.1.11/32"]) - and mind
# the companion trap: once this list is non-empty the rule IS restricted, so
# everything else that needs the port has to be in it. The chart adds the own
# namespace, the ingressController below and allowedNamespaces to the same rule for exactly that reason.
probeSources: []
# The ingress controller allowed to reach the routed port once probeSources
# restricts it. Rendered only while ingress.enabled or httpRoute.enabled is true, and only in
# the restricted branch - with the default (open) rule the controller reaches
# the port anyway. The two keys become ONE "from" entry and are therefore an
# AND: pods carrying podLabels IN namespace. That is the point of having them
# at all - the controller's namespace in allowedNamespaces would open the port
# to every pod in kube-system.
#
# MEASURED ON THIS CLUSTER, NOT ASSUMED (2026-10-14): ingress-nginx is gone,
# the only IngressClass is "traefik" and it is the default, and the controller
# is the DaemonSet rke2-traefik in kube-system. The same Traefik serves the
# Gateway API here, so this one source covers the Ingress and the HTTPRoute
# path alike. A policy written against the wrong controller is accepted, looks
# strict, and silently blocks all web traffic - and ingress.className says
# nothing about it, because a class names an implementation, not the pods the
# traffic comes from.
ingressController:
namespace: kube-system
podLabels:
app.kubernetes.io/name: rke2-traefik
egress:
# Additional outbound TCP ports. DNS (UDP and TCP 53) is always allowed and
# has no switch - a knob whose only other setting breaks every name lookup
# in the pod is a trap, not a feature.
#
# PORTS, NOT DESTINATIONS, and that is a limit of the object rather than
# laziness: a NetworkPolicy matches CIDRs and selectors and never hostnames,
# so "only to the database host" cannot be written at all. Each entry
# permits its port to ANY destination. What the cap still buys is real: a
# compromised pod cannot open arbitrary outbound connections.
#
# IMAP (993, or 143 for the plaintext case) is the entry this chart needs
# most, for mail accounts configured in the web UI. Also a Postgres or
# Redis on a non-standard port, or a Tika/Gotenberg addressed on its
# container port (9998 / 3000) instead of through its Service.
extraPorts: []
+1 -1
View File
@@ -28,7 +28,7 @@ type: application
# ci/stalwart/test-values.yaml as it stands on origin/main - measured, both # ci/stalwart/test-values.yaml as it stands on origin/main - measured, both
# empty diffs. New optional values with unchanged defaults is exactly the minor # empty diffs. New optional values with unchanged defaults is exactly the minor
# case (README, "Versioning"). # case (README, "Versioning").
version: 1.3.0+upv0.16.25 version: 2.0.0+upv0.16.25
appVersion: "v0.16.25" appVersion: "v0.16.25"
maintainers: maintainers:
+98 -31
View File
@@ -1,15 +1,41 @@
{{- /* {{- /*
One NetworkPolicy per node group. One NetworkPolicy per node group (Gitea #5).
THIS CHART AND valkey ARE THE TWO THAT HAD A POLICY BEFORE #5, so the
convention was derived from them rather than imposed on them. Three things
changed here; everything else is the original and is right.
- THE REASONING MOVED OUT OF "#" COMMENTS into template comments. A "#"
comment is part of the RENDERED object: it travels into the cluster, shows
up in `helm get manifest` and `kubectl get -o yaml`, and lands in every
release diff. ci/run-networkpolicy-test.sh now fails on a rendered "#", and
this file was one of the two that failed it.
- networkPolicy.ingressController ARRIVED, and it fixes a real hole this
file's own comment described without being able to solve: it used to say
that setting probeSources means putting the ingress controller's namespace
into allowedNamespaces "or the Ingress goes dark". That works, but
allowedNamespaces is namespace-granular, so it would have opened the http
port to EVERY pod in the controller's namespace - kube-system, here. The
new value renders a namespaceSelector and a podSelector in ONE from-entry,
which is an AND, and narrows the source to the controller's pods.
- THE EXAMPLE IN values.yaml WAS WRONG AND IS CORRECTED. It suggested
allowedNamespaces: ["ingress-nginx"]. Measured on 2026-10-14: ingress-nginx
is GONE from this cluster - no DaemonSet, no pods - the only IngressClass is
traefik, and the controller is the DaemonSet rke2-traefik in kube-system. A
policy written against the old controller would have been accepted, would
have looked strict, and would have silently blocked all web traffic.
WHAT DID NOT CHANGE, because it is the reason this file was the model:
This object is the CEILING ON REACHABILITY, and that is its real job here: it This object is the CEILING ON REACHABILITY, and that is its real job here: it
holds no matter which ports the process actually bound. A node whose holds no matter which ports the process actually bound. A node whose ClusterRole
ClusterRole is missing from the database fail-opens into a full node and binds is missing from the database fail-opens into a full node and binds every
every listener - including IMAP and the admin interface on a group meant to listener - including IMAP and the admin interface on a group meant to take SMTP
take SMTP only - while staying Running with a green probe. The per-port only - while staying Running with a green probe. The per-port Services publish
Services publish only what was asked for, and this policy is the second, only what was asked for, and this policy is the second, independent limit. See
independent limit. See values.yaml for the source-level detail. values.yaml for the source-level detail.
!!! THE PORT NUMBERS HERE ARE POD PORTS (25/465/993/8080), NOT NODEPORTS !!! THE PORT NUMBERS HERE ARE POD PORTS (25/465/993/8080), NOT NODEPORTS.
This is the easiest mistake to make in this chart, so it is spelled out. A This is the easiest mistake to make in this chart, so it is spelled out. A
NetworkPolicy is evaluated at the POD's network interface, after kube-proxy has NetworkPolicy is evaluated at the POD's network interface, after kube-proxy has
@@ -17,6 +43,8 @@ already translated the NodePort to the targetPort. Writing 30025 here would
match nothing: the packet arrives at the pod on 25. The policy would then look match nothing: the packet arrives at the pod on 25. The policy would then look
strict, be accepted by the API server, and silently block every inbound strict, be accepted by the API server, and silently block every inbound
connection - or, if it is the only rule, block the lot. connection - or, if it is the only rule, block the lot.
ci/run-networkpolicy-test.sh now asserts this mechanically, against the
containerPorts of the pod spec each policy selects.
Egress is restricted BY PORT and not by destination, because a NetworkPolicy Egress is restricted BY PORT and not by destination, because a NetworkPolicy
matches CIDRs and selectors and never hostnames - so dataStore.postgres.host matches CIDRs and selectors and never hostnames - so dataStore.postgres.host
@@ -32,6 +60,7 @@ unenforced policy is accepted and filters nothing.
{{- $egress := .Values.networkPolicy.egress -}} {{- $egress := .Values.networkPolicy.egress -}}
{{- $probeSources := .Values.networkPolicy.probeSources | default list -}} {{- $probeSources := .Values.networkPolicy.probeSources | default list -}}
{{- $allowedNs := .Values.networkPolicy.allowedNamespaces | default list -}} {{- $allowedNs := .Values.networkPolicy.allowedNamespaces | default list -}}
{{- $ic := .Values.networkPolicy.ingressController | default dict -}}
{{- range $group, $spec := fromYaml (include "stalwart.nodes" .) }} {{- range $group, $spec := fromYaml (include "stalwart.nodes" .) }}
{{- $name := printf "%s-%s" (include "stalwart.fullname" $root) $group }} {{- $name := printf "%s-%s" (include "stalwart.fullname" $root) $group }}
--- ---
@@ -49,9 +78,11 @@ spec:
- Ingress - Ingress
- Egress - Egress
ingress: ingress:
{{- /* The public mail ports: open to any source. They have to be - the {{- /*
senders are the whole internet, and after NodePort masquerading the The public mail ports: open to any source. They have to be - the senders are
source address is a control-plane node anyway. */}} the whole internet, and after NodePort masquerading the source address is a
control-plane node anyway.
*/}}
{{- $publicPorts := list }} {{- $publicPorts := list }}
{{- range $portName := $spec.exposedPorts }} {{- range $portName := $spec.exposedPorts }}
{{- $port := index $ports $portName }} {{- $port := index $ports $portName }}
@@ -61,7 +92,10 @@ spec:
- ports: - ports:
{{- range $portName := $publicPorts }} {{- range $portName := $publicPorts }}
{{- $port := index $ports $portName }} {{- $port := index $ports $portName }}
# {{ $portName }}: published on nodePort {{ int $port.nodePort }}, reaching the pod on {{ int $port.containerPort }} {{- /*
{{ $portName }}: published on a nodePort, reaching the pod on
{{ int $port.containerPort }} - which is the number that belongs here.
*/}}
- protocol: {{ $port.protocol | default "TCP" }} - protocol: {{ $port.protocol | default "TCP" }}
port: {{ int $port.containerPort }} port: {{ int $port.containerPort }}
{{- end }} {{- end }}
@@ -72,17 +106,38 @@ spec:
- protocol: {{ $http.protocol | default "TCP" }} - protocol: {{ $http.protocol | default "TCP" }}
port: {{ int $http.containerPort }} port: {{ int $http.containerPort }}
{{- if or $probeSources $allowedNs }} {{- if or $probeSources $allowedNs }}
# Restricted because networkPolicy.probeSources or .allowedNamespaces is {{- /*
# set. MIND THE KUBELET: probes come from the IP of the worker node Restricted because networkPolicy.probeSources or .allowedNamespaces is
# running the pod, so that address has to be in probeSources or the pod set. MIND THE KUBELET: probes come from the IP of the worker node running
# never becomes Ready - and a pod that is never Ready reads as a broken the pod, so that address has to be in probeSources or the pod never
# application rather than as a policy. becomes Ready - and a pod that is never Ready reads as a broken
application rather than as a policy.
*/}}
from: from:
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:
# Set by Kubernetes itself on every namespace (>= 1.21), so this {{- /*
# needs no labelling work. Set by Kubernetes itself on every namespace (>= 1.21), so this
needs no labelling work.
*/}}
kubernetes.io/metadata.name: {{ $root.Release.Namespace }} kubernetes.io/metadata.name: {{ $root.Release.Namespace }}
{{- if $ic.namespace }}
{{- /*
The ingress controller, as ONE from-entry with TWO selectors - which is
an AND: those pods in that namespace. Written as two list entries it
would be an OR and would open the http port to every pod in the
controller's namespace. This is what replaced the old advice of putting
kube-system into allowedNamespaces.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }} {{- range $allowedNs }}
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:
@@ -93,12 +148,18 @@ spec:
cidr: {{ . | quote }} cidr: {{ . | quote }}
{{- end }} {{- end }}
{{- else }} {{- else }}
# NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside {{- /*
# the cluster: http carries no nodePort, so nothing outside reaches it NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
# through this chart. The alternative was to guess the worker node CIDRs, cluster: http carries no nodePort, so nothing outside reaches it through
# and a wrong guess blocks every kubelet probe. Narrow it by setting this chart. The alternative was to guess the worker node CIDRs, and a
# networkPolicy.probeSources (and then .allowedNamespaces for the wrong guess blocks every kubelet probe. Narrow it by setting
# ingress controller, or the Ingress goes dark). networkPolicy.probeSources; the ingress controller is then added from
networkPolicy.ingressController, so the Ingress does not go dark.
Note that an empty "from: []" would be worse than no key at all: it also
allows everything, but while reading as a restriction.
ci/run-networkpolicy-test.sh rejects that form.
*/}}
{{- end }} {{- end }}
egress: egress:
{{- if $egress.dns }} {{- if $egress.dns }}
@@ -109,10 +170,12 @@ spec:
port: 53 port: 53
{{- end }} {{- end }}
{{- if $egress.smtp }} {{- if $egress.smtp }}
# Outbound delivery: port 25 on any host on the internet. Uniform for every {{- /*
# group on purpose - the chart does not know which groups run the Outbound delivery: port 25 on any host on the internet. Uniform for every
# outboundMta task, the value would drift away from the database, and the group on purpose - the chart does not know which groups run the outboundMta
# failure mode of getting it wrong is mail silently not leaving. task, the value would drift away from the database, and the failure mode of
getting it wrong is mail silently not leaving.
*/}}
- ports: - ports:
- protocol: TCP - protocol: TCP
port: 25 port: 25
@@ -125,8 +188,12 @@ spec:
{{- range $root.Values.networkPolicy.egress.extraPorts }} {{- range $root.Values.networkPolicy.egress.extraPorts }}
{{- $outPorts = append $outPorts (int .) }} {{- $outPorts = append $outPorts (int .) }}
{{- end }} {{- end }}
# The data store, the blob store, the in-memory store and anything added {{- /*
# through networkPolicy.egress.extraPorts. Ports only - see the header. The data store, the blob store, the in-memory store and anything added
through networkPolicy.egress.extraPorts. Ports only - see the header. Unlike
most charts in this sweep these are the CONFIGURED ports rather than the
standard ones, because this chart holds them as values.
*/}}
- ports: - ports:
{{- range $outPorts | uniq }} {{- range $outPorts | uniq }}
- protocol: TCP - protocol: TCP
+14
View File
@@ -304,6 +304,20 @@
"description": "On by default. Only a real boundary where the cluster's CNI enforces NetworkPolicies; an unenforced policy is accepted by the API server and filters nothing.", "description": "On by default. Only a real boundary where the cluster's CNI enforces NetworkPolicies; an unenforced policy is accepted by the API server and filters nothing.",
"type": "boolean" "type": "boolean"
}, },
"ingressController": {
"description": "The ingress controller allowed to reach the http port once probeSources or allowedNamespaces restricts it; with neither set the http rule carries no source selector and the controller reaches the port anyway. The two keys become ONE \"from\" entry and are therefore an AND - pods carrying podLabels IN namespace - which is why this is separate from the namespace-granular allowedNamespaces. The default is measured on this cluster (DaemonSet rke2-traefik in kube-system, which also serves the Gateway API here), not assumed; a policy written against the wrong controller is accepted, looks strict and silently blocks all web traffic.",
"type": ["object", "null"],
"additionalProperties": false,
"properties": {
"namespace": {
"type": ["string", "null"]
},
"podLabels": {
"description": "Label set the controller's pods carry, rendered verbatim as a podSelector's matchLabels. Kept open because the chart does not interpret the keys.",
"type": ["object", "null"]
}
}
},
"probeSources": { "probeSources": {
"description": "Source CIDRs allowed to reach the http port, where the kubelet's probe traffic has to be permitted - probes come from the IP of the worker node running the pod. EMPTY MEANS OPEN (the rule renders without a 'from' selector), which is the deliberate choice: a guessed CIDR that misses the real node addresses blocks every probe, and a pod that never becomes Ready reads as a broken application rather than as a policy.", "description": "Source CIDRs allowed to reach the http port, where the kubelet's probe traffic has to be permitted - probes come from the IP of the worker node running the pod. EMPTY MEANS OPEN (the rule renders without a 'from' selector), which is the deliberate choice: a guessed CIDR that misses the real node addresses blocks every probe, and a pod that never becomes Ready reads as a broken application rather than as a policy.",
"type": ["array", "null"], "type": ["array", "null"],
+33 -6
View File
@@ -504,16 +504,43 @@ networkPolicy:
# (e.g. ["10.0.1.11/32", "10.0.1.12/32"]). # (e.g. ["10.0.1.11/32", "10.0.1.12/32"]).
# #
# And mind the companion trap: once this list is non-empty the http rule is # And mind the companion trap: once this list is non-empty the http rule is
# restricted, which cuts off the ingress controller too unless its namespace # restricted, which cuts off everything else that needs the port. The chart
# is in allowedNamespaces below. # adds the release's own namespace, ingressController below and
# allowedNamespaces to the same rule for exactly that reason.
probeSources: [] probeSources: []
# Namespaces allowed to reach the http port, matched on the # Namespaces allowed to reach the http port, matched on the
# kubernetes.io/metadata.name label Kubernetes sets on every namespace # kubernetes.io/metadata.name label Kubernetes sets on every namespace
# itself. The release's own namespace is always allowed. Put the ingress # itself. The release's own namespace is always allowed. A name that does not
# controller's namespace here (e.g. ["ingress-nginx"]) as soon as # exist matches nothing and is silently ineffective.
# probeSources is set. A name that does not exist matches nothing and is #
# silently ineffective. # NOT FOR THE INGRESS CONTROLLER - use ingressController below. This value is
# namespace-granular, so naming the controller's namespace here would open
# the http port to EVERY pod in it (kube-system, in this cluster). The
# earlier version of this comment advised exactly that, with
# ["ingress-nginx"] as the example, and both halves of that advice are now
# wrong: see ingressController.
allowedNamespaces: [] allowedNamespaces: []
# The ingress controller allowed to reach the http port once probeSources or
# allowedNamespaces restricts it. With neither set the http rule carries no
# source selector at all and the controller reaches the port anyway, so this
# value only becomes load-bearing at the moment one of them is filled - which
# is also the moment the Ingress would otherwise go dark.
#
# The two keys are rendered as ONE "from" entry, which makes them an AND:
# pods carrying podLabels IN namespace. That is the whole point of having
# them separate from allowedNamespaces.
#
# MEASURED ON THIS CLUSTER, NOT ASSUMED (2026-10-14): ingress-nginx is GONE -
# no DaemonSet, no pods - the only IngressClass is "traefik" and it is the
# default, and the controller is the DaemonSet rke2-traefik in kube-system.
# The same Traefik serves the Gateway API here as well. A policy written
# against the wrong controller is accepted, looks strict, and silently blocks
# all web traffic; ingress.className says nothing about it, because a class
# names an implementation and not the pods the traffic comes from.
ingressController:
namespace: kube-system
podLabels:
app.kubernetes.io/name: rke2-traefik
# Egress is restricted BY PORT, not by destination, and the reason is a limit # Egress is restricted BY PORT, not by destination, and the reason is a limit
# of the object rather than laziness: a NetworkPolicy matches ipBlock CIDRs # of the object rather than laziness: a NetworkPolicy matches ipBlock CIDRs
# and pod/namespace selectors, never hostnames — so dataStore.postgres.host # and pod/namespace selectors, never hostnames — so dataStore.postgres.host
+1 -1
View File
@@ -26,7 +26,7 @@ type: application
# running image with no change in this repository and nothing reports it. The # running image with no change in this repository and nothing reports it. The
# remedy if that ever matters is pinDigests for this one image - see the valkey # remedy if that ever matters is pinDigests for this one image - see the valkey
# block in renovate.json5. # block in renovate.json5.
version: 1.0.0+up9.1.2.alpine version: 2.0.0+up9.1.2.alpine
appVersion: "9.1.2-alpine" appVersion: "9.1.2-alpine"
maintainers: maintainers:
+46 -9
View File
@@ -1,27 +1,64 @@
{{- /*
NetworkPolicy for valkey (Gitea #5).
THIS CHART AND stalwart ARE THE TWO THAT HAD A POLICY BEFORE #5, so the
convention was derived from them rather than imposed on them. What changed here
is the FORM and not a single rule: the reasoning moved out of "#" comments,
which are part of the rendered object and travel into the cluster, into template
comments, which render nothing. ci/run-networkpolicy-test.sh now fails on a
rendered "#", and this file was one of the two that failed it.
TWO THINGS THAT DELIBERATELY DID NOT CHANGE, because they are right and the
convention was written to accommodate them:
1. INGRESS ONLY. This instance opens no outbound connection of its own - no
replication, no cluster bus - so an egress rule here would constrain nothing
while making the object look stricter than it is. policyTypes names only
what there is something to restrict. homeassistant and satisfactory invoke
the same rule in the other direction, for outbound traffic nobody can
enumerate.
2. 6379 IS RESTRICTED BY SOURCE, with no probeSources value anywhere in this
chart - and that combination is only safe because of a detail worth knowing:
ALL THREE PROBES HERE ARE EXEC PROBES. An exec probe runs inside the
container through the kubelet's runtime and puts no packet on the network, so
there is no kubelet traffic to let through and nothing to accommodate. A
chart with an httpGet or tcpSocket probe on this port could NOT restrict it
this way - it would need the worker node's IP, which no chart can know, and
the pod would never become Ready without it. minecraft-server is tight for
the same reason; every web app in the sweep is not.
So this policy is what the rest of the rollout aims at and mostly cannot reach:
a port that is genuinely closed to everything but the namespaces named below.
*/ -}}
{{- if .Values.networkPolicy.enabled }} {{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: NetworkPolicy kind: NetworkPolicy
metadata: metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec: spec:
podSelector: podSelector:
matchLabels: matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
# Ingress only. Egress is deliberately unrestricted: this instance opens no
# outbound connection of its own (no replication, no cluster bus), so an
# egress rule here would constrain nothing while making the object look
# stricter than it is.
policyTypes: policyTypes:
- Ingress - Ingress
ingress: ingress:
# One rule with a list of sources. Note the shape: an empty "from" would {{- /*
# allow EVERYTHING, so the namespaces are always spelled out - the release's One rule with a list of sources. Note the shape: an empty "from" would allow
# own namespace first, then whatever networkPolicy.allowedNamespaces adds. EVERYTHING, so the namespaces are always spelled out - the release's own
namespace first, then whatever networkPolicy.allowedNamespaces adds. An
empty "from: []" is worse still, because it allows everything while reading
as a restriction; ci/run-networkpolicy-test.sh rejects that form outright.
*/}}
- from: - from:
- namespaceSelector: - namespaceSelector:
matchLabels: matchLabels:
# Set by Kubernetes itself on every namespace (>= 1.21), so this {{- /*
# needs no labelling work on the target namespaces. Set by Kubernetes itself on every namespace (>= 1.21), so this
needs no labelling work on the target namespaces.
*/}}
kubernetes.io/metadata.name: {{ .Release.Namespace }} kubernetes.io/metadata.name: {{ .Release.Namespace }}
{{- range .Values.networkPolicy.allowedNamespaces }} {{- range .Values.networkPolicy.allowedNamespaces }}
- namespaceSelector: - namespaceSelector: