{{- /* NetworkPolicy for gotenberg (Gitea #5). Gotenberg here is a cluster-internal service: no Ingress, and its consumers are other pods (paperless-ngx converts mail HTML to PDF through it). EGRESS IS DNS ONLY BY DEFAULT, AND THAT IS A DELIBERATE, VISIBLE TRADE rather than an oversight. Gotenberg drives a headless Chromium, so an egress rule for 80 and 443 would let anything that can POST to this service fetch arbitrary URLs through it - a textbook SSRF pivot, and the pod is reachable from the whole cluster by default because 3000 is the probe port. Blocking it costs the remote-asset half of HTML conversion: a converted page keeps its EMBEDDED content and loses the images it would have FETCHED, which degrades a PDF rather than failing a pod. The degradation is silent, which is why it is named here, in values.yaml and in NOTES.txt. A deployment that needs those images sets networkPolicy.egress.remoteAssets: true - a named switch rather than a line in extraPorts, so the trade is a decision someone made and not a port number nobody can place. Its default is off, because restrictive-by-default is the convention and the deployment is where reality is accommodated. The long form of what it buys, and of the one thing about it that was NOT verified, is on the rule itself. 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 }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy labels: app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy spec: {{- /* Selects the Deployment'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 }}-deployment policyTypes: - Ingress {{- /* Egress is listed because this is the chart where the cap matters most: a headless Chromium that can reach the internet turns a service anyone in the cluster can POST to into an SSRF pivot. DNS is the only rule. */}} - Egress ingress: {{- /* 3000 - the only port this pod declares, the one the Service forwards 80 to, and the one all three probes address. The probe path. */}} - ports: - protocol: TCP port: 3000 {{- 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. NO ingress controller is added here, because this chart renders no Ingress: its consumers are other pods, and they have to be named in allowedNamespaces. */}} 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 }} {{- 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 {{- with $np.egress }} {{- if .remoteAssets }} {{- /* REMOTE ASSETS: the web, on both ports, to any destination. Off by default, and the switch exists because the cost of leaving it off is invisible while the cost of removing it is not. WHAT IT BUYS, measured on the live cluster rather than assumed. paperless-ngx splits a mail into two steps with two separate endpoints, both cluster-internal: the .eml goes to TIKA for parsing (PAPERLESS_TIKA_ENDPOINT) and the resulting HTML comes here for rendering (PAPERLESS_TIKA_GOTENBERG_ENDPOINT). Of the images in that HTML: - EMBEDDED images are MIME parts INSIDE the file. Neither Tika nor Gotenberg needs the network for them, which is why apache-tika stays DNS-only and why this switch is not needed for a mail that carries its pictures with it. - REMOTE images ( in the mail body) are fetched by CHROMIUM, here, at render time. With this switch off they are silently dropped: the PDF is produced, it is just missing them. Nothing fails, nothing is logged by this chart, and no probe notices. NOT CHECKED, and not claimed: whether paperless-ngx re-embeds the images Tika extracted before handing the HTML over. If it does not, embedded images would be lost as well - but that would be pre-existing paperless behaviour and not this policy, so do not read the switch as a fix for it. WHY IT IS A SWITCH AND NOT THE DEFAULT. Gotenberg drives a headless Chromium and 3000 is its probe port, so the service is reachable from the whole cluster by default. Allowing 80/443 outbound therefore turns anything that can POST here into a way of fetching arbitrary URLs - an SSRF pivot with the cluster's whole internal address space behind it. That argument is unchanged by the switch; the switch only makes the trade a NAMED decision with a default of "no" instead of a line in extraPorts that reads like nothing in particular. */}} - ports: - protocol: TCP port: 80 - protocol: TCP port: 443 {{- end }} {{- end }} {{- 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 }}