The rule allowed outbound 80, the port the apache-tika and gotenberg Services listen on, and justified it in a comment: an egress rule names the port on the destination as the client dials it, and the client dials the Service. That is wrong. kube-proxy does a ClusterIP Service's destination NAT BEFORE the egress policy is evaluated at the pod's network interface, so the policy sees podIP:targetPort and never clusterIP:port - and those two charts forward 80 to 9998 and 3000. Deployed as 14.0.0+up3.3.1 it cut Paperless off from both services on 2026-10-15: mail import, EML, HTML and Office documents stopped, plain PDFs and images kept working, and the error was httpx.ConnectTimeout with no mention of a NetworkPolicy. The rule now names 9998, 3000 and 80. The 80 stays because it costs nothing and covers a foreign Tika that really does listen on 80. The refuted sentence is quoted rather than deleted: it reads perfectly, which is how it survived review, and the timeout-not-refused symptom is written down with it. No policy switch of its own: the rule stays gated on paperless.files.tikaGotenberg.enabled, because a second value that can drift from the feature re-arms this very outage. New networkPolicy.egress.mailImport opens IMAPS 993, off by default - paperless' mail accounts are rows in its database, so the chart cannot derive the rule. Plain IMAP (143) and POP3 stay in extraPorts, and 993 is noted as a real destination port, because a mail server outside the cluster has no DNAT in the path. Minor: one additive value, and a fix that widens a rule. Nothing breaks, no value becomes required, appVersion unchanged. Part of #136
293 lines
13 KiB
YAML
293 lines
13 KiB
YAML
{{- /*
|
|
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 UNLESS THE OPERATOR SAYS SO. Paperless' mail
|
|
accounts live in its DATABASE, configured through the web UI, so this chart
|
|
cannot see them and cannot derive the rule. networkPolicy.egress.mailImport
|
|
opens IMAPS (993) and is off by default; plain IMAP (143) and POP3 belong in
|
|
networkPolicy.egress.extraPorts. Left off on a release that does fetch mail,
|
|
the symptom 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 AND NEVER SERVICE PORTS.
|
|
A NetworkPolicy is evaluated at the pod's network interface, after kube-proxy
|
|
has already done its destination NAT - of a NodePort on the way in, and of a
|
|
ClusterIP on the way OUT. So a NodePort number in an ingress rule matches
|
|
nothing while looking strict (and, being the only rule for that port, blocks
|
|
every inbound connection instead), and a SERVICE port in an egress rule matches
|
|
nothing wherever the Service's port and its targetPort differ. The second half
|
|
is not hypothetical: it is Gitea #136, it cost an outage on 2026-10-15, and the
|
|
sentence that caused it is quoted as refuted at the Tika/Gotenberg rule below.
|
|
|
|
ci/run-networkpolicy-test.sh asserts every INGRESS port here against the
|
|
containerPorts of the pod spec this policy selects. Egress cannot be checked
|
|
that way - an egress port belongs to the DESTINATION, which for Postgres, Redis
|
|
and the web is not even in the render. The one exception is Tika and Gotenberg,
|
|
which are two charts of THIS repository: the same test renders them and asserts
|
|
that the egress rule below contains their containerPorts.
|
|
*/ -}}
|
|
{{- 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 by default and is the plausible surprise: mail. Paperless'
|
|
mail accounts are rows in its database, configured in the web UI, so the chart
|
|
cannot see them - networkPolicy.egress.mailImport opens IMAPS 993, and anything
|
|
else (143, POP3) goes into 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 THEIR POD PORTS 9998 AND 3000 - and 80 as well, for a
|
|
third case named at the end. Gitea #136: this rule used to name 80 ALONE, and
|
|
the release that shipped it cut Paperless off from both services on
|
|
2026-10-15. Mail import, EML, HTML and Office documents stopped; plain PDFs
|
|
and images kept working, which is why it did not look like a network fault.
|
|
|
|
THE SENTENCE THAT STOOD HERE IS WRONG. It is quoted rather than deleted,
|
|
because it reads perfectly and that is precisely how it survived review:
|
|
|
|
"an EGRESS rule names the port on the DESTINATION as the client dials
|
|
it, and the client dials the Service."
|
|
|
|
kube-proxy translates the destination of a ClusterIP Service (DNAT) BEFORE
|
|
the egress policy is evaluated at the pod's network interface. The policy
|
|
therefore sees podIP:targetPort and never clusterIP:port, so an egress rule
|
|
names the POD port. A Service port only ever matches by coincidence, where
|
|
port and targetPort are the same number - which is the case for 5432, 6379
|
|
and 443 above and below, and is the reason nothing else here broke.
|
|
|
|
THE SYMPTOM IS A TIMEOUT, NOT "connection refused", and that is the half to
|
|
remember: a missing service REFUSES, a dropped packet RUNS INTO THE TIMEOUT.
|
|
What this produced was httpx.ConnectTimeout out of paperless' parser, naming
|
|
the Tika URL and no NetworkPolicy at all - nothing in the error points here.
|
|
|
|
WHY 80 STAYS: it costs nothing and covers an installation whose Tika really
|
|
does listen on 80 - a foreign one behind a proxy, where 80 IS the pod port.
|
|
|
|
WHY THERE IS NO SWITCH OF ITS OWN for this rule, and that is deliberate: it
|
|
is gated on the FEATURE value, because a second value that can drift from the
|
|
feature re-arms exactly this outage. Whoever enables Tika and forgets the
|
|
policy switch lands back here, with the same message that does not point at
|
|
the policy. Rendered only while paperless.files.tikaGotenberg.enabled is
|
|
true, so a release without them still carries no rule that permits nothing.
|
|
|
|
9998 and 3000 are the containerPorts the apache-tika and gotenberg charts of
|
|
this repository declare, so they are fixed rather than guessed - and
|
|
ci/run-networkpolicy-test.sh renders both of those charts and asserts that
|
|
this rule contains them. That is the one assertion a single-chart render
|
|
cannot make, and it is why this mistake was not caught the first time.
|
|
*/}}
|
|
- ports:
|
|
- protocol: TCP
|
|
port: 9998
|
|
- protocol: TCP
|
|
port: 3000
|
|
- 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 }}
|
|
{{- if .mailImport }}
|
|
{{- /*
|
|
IMAPS for mail import, OFF BY DEFAULT - because this chart cannot know
|
|
whether a release fetches mail. Paperless' mail accounts are rows in its
|
|
DATABASE, entered in the web UI, and nothing in the values hints that they
|
|
exist. So this value is an operator's statement ("this release imports
|
|
mail") and not a derivation, which is also why it cannot be folded into
|
|
another switch.
|
|
|
|
993 ONLY. Plain IMAP (143) and POP3 (110, 995) are deliberately NOT included:
|
|
a value named mailImport that quietly opened four ports would be wider than
|
|
its name, and the plaintext variants are not what a new account uses. They go
|
|
into networkPolicy.egress.extraPorts.
|
|
|
|
AND THIS PORT IS THE REAL DESTINATION PORT, unlike the Tika rule above. A
|
|
mail server outside the cluster is reached with no Service and therefore no
|
|
DNAT in the path, so 993 is both what paperless dials and what the policy
|
|
sees. What decides which number an egress rule needs is the DNAT, not the
|
|
direction and not the protocol - that is the whole lesson of #136.
|
|
*/}}
|
|
- ports:
|
|
- protocol: TCP
|
|
port: 993
|
|
{{- end }}
|
|
{{- 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 }}
|