fix(paperless-ngx): the egress rule names the Tika/Gotenberg pod ports, and CI now asserts it across charts #138

Merged
jklein merged 3 commits from fix/paperless-egress-pod-ports into main 2026-10-15 22:06:05 +02:00
3 Commits
Author SHA1 Message Date
jklein 95f3e8c1a1 docs(paperless-ngx): the policy header said TWO and listed three
SonarQube Analysis / SonarQube Analysis (push) Successful in 39s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 13s
Publish Paperless-NGX Helm Chart / Publish Paperless-NGX Helm Chart (push) Successful in 13s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 20s
Validate Helm Charts / Selector match (pull_request) Successful in 9s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 11m54s
Validate Helm Charts / YAML scalars (pull_request) Successful in 10s
Validate Helm Charts / NetworkPolicy (pull_request) Successful in 11s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 6s
Validate Helm Charts / Bump level (pull_request) Successful in 4s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 20s
Install-test / Install-test paperless-ngx (pull_request) Successful in 3m13s
Validate Helm Charts / Install-test (pull_request) Successful in 3m13s
Pre-existing, and corrected here rather than left because the third item is the
mail rule this PR rewrites. values.yaml already said THREE.

Part of #136
2026-10-15 17:54:15 +00:00
jklein 1894e7d465 test(ci): assert a chart's egress ports against the POD ports of the chart it dials
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 10s
Validate Helm Charts / Selector match (pull_request) Successful in 9s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 9s
Validate Helm Charts / NetworkPolicy (pull_request) Successful in 12s
Validate Helm Charts / Bump level (pull_request) Successful in 4s
Validate Helm Charts / YAML scalars (pull_request) Successful in 10s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 6s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 20s
Install-test / Install-test paperless-ngx (pull_request) Successful in 3m2s
Validate Helm Charts / Install-test (pull_request) Successful in 3m2s
The four assertions from Gitea #5 hold ports against the pod spec the policy
SELECTS. That is right for ingress and meaningless for egress: an egress port
belongs to the destination, and for a database, a cache or the web that
destination is in no render this repository has. So the mistake behind #136 -
an egress rule naming a ClusterIP Service's port, which kube-proxy has already
translated away by the time the policy is evaluated at the pod's interface - was
not catchable within one chart.

It is catchable across two, and paperless-ngx -> apache-tika, gotenberg is the
only place in the repository where a chart dials another chart's Service (the
three other policies naming port 80 aim at the internet, where there is no DNAT
and 80 is the real port). The pairs are an explicit table rather than a
discovery, because no render can know which Service another chart dials - the
address is a value or a 1Password field - and a row naming a chart that no
longer exists fails instead of being skipped.

Each row is checked on two renders of the consumer: with the feature gate ON the
egress ports must CONTAIN every pod port behind the peers' Services, resolved
Service port -> targetPort -> containerPort, which is exactly the translation the
policy is evaluated after; with the gate OFF the requirement must not apply and
those ports must be absent, so a gated rule cannot quietly become one that
permits what the release cannot use. The evidence line prints the chain
("port 80 -> targetPort 'tika-port' -> pod port 9998"), so a reader sees the
DNAT rather than two numbers.

Built into run-networkpolicy-test.sh rather than added as a new script with its
own job: that job already runs over every chart on every PR, Helm is already the
YAML parser through ci/yaml2json, and the verdict and summary plumbing is
shared. python3 standard library only, no yq, no pyyaml, no Docker.

Six fail paths executed, not assumed: a required port removed, a wrong number in
its place, the peer chart renamed away, the policy name no longer following the
<release>-<chart>-<kind> convention, the peers rendering no Service at all (an
empty requirement reads like a satisfied one, so it fails), and the rule made
unconditional, which is the fail path of the gate-off half.

Measured: 30 charts, 210 assertions held (208 before), 0 failed, ~15s, no
cluster. README's stated figure was 206 and is corrected here.

Part of #136
2026-10-15 17:46:45 +00:00
jklein 530227405c fix(paperless-ngx): egress names the Tika/Gotenberg POD ports, not the Service port
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
2026-10-15 17:39:24 +00:00