feat!: NetworkPolicy in six more charts, on by default
Install-test / Install-test apache-tika (pull_request) Successful in 1m21s
Install-test / Install-test bentopdf (pull_request) Successful in 1m16s
Install-test / Install-test cyberchef (pull_request) Successful in 1m6s
Install-test / Install-test gotenberg (pull_request) Successful in 1m32s
Install-test / Install-test outline (pull_request) Failing after 1m16s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m1s
Install-test / Install-test searxng (pull_request) Successful in 1m13s
Validate Helm Charts / Install-test (pull_request) Failing after 9m57s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 1m11s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Failing after 18s
Validate Helm Charts / HTTPRoute match (pull_request) Failing after 13s
Validate Helm Charts / Selector match (pull_request) Failing after 17s
Validate Helm Charts / Bump level (pull_request) Successful in 8s
Validate Helm Charts / YAML scalars (pull_request) Failing after 16s
Validate Helm Charts / NetworkPolicy (pull_request) Failing after 9s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 5s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 13s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 5s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 6s
Validate Helm Charts / Build outline (pull_request) Successful in 5s
Validate Helm Charts / Build template-chart (pull_request) Successful in 5s
Validate Helm Charts / Trivy config outline (pull_request) Failing after 12s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 15s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 14s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 5s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 5s
Validate Helm Charts / Build searxng (pull_request) Successful in 5s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 5s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 14s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 13s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 13s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 14s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 15s

bentopdf, apache-tika, gotenberg, rss-bridge, searxng, ytdlp-web-player. One
major each, networkPolicy.enabled defaults to true.

Three of them differ from the simple case in ways worth naming:

GOTENBERG gets DNS-only egress as a deliberate, documented trade. It drives a
headless Chromium, and 3000 is its probe port - so it is reachable from the
whole cluster by default, and an egress rule for 80/443 would turn a service
anyone can POST to into a way of fetching arbitrary URLs. The price is the
remote-asset half of HTML conversion: inline content converts, images the page
would have fetched silently do not. That degrades a PDF instead of failing a
pod, so nothing alerts on it, which is why it is in the template header, in
values.yaml and in NOTES.txt, with extraPorts: [80, 443] named as the knowing
opt-in.

RSS-BRIDGE, SEARXNG and YTDLP-WEB-PLAYER get the open internet on their web
ports, because reaching other people's servers IS the application and this
object cannot name a host. Said plainly in each, so none of them reads
stricter than it is. What the cap still buys is real: no other outbound port,
so a compromised pod reaches no database, no mail server and no SSH anywhere.

APACHE-TIKA and GOTENBERG render no Ingress - their consumers are other pods,
in other namespaces here - so neither gets an ingressController value and both
say in NOTES.txt that narrowing probeSources means listing the consumers'
namespaces in allowedNamespaces in the same change, or extraction and
conversion stop.

YTDLP-WEB-PLAYER names the gap this convention has: its oauth2-proxy subchart
renders its own Deployment with its own labels and ships no policy, so with the
proxy enabled the pod that actually faces the Ingress is unrestricted in both
directions. Nothing in this chart can change that - an upstream chart is
consumed as it comes - so it is written down instead.

ci/outline/test-values.yaml gains egress.extraPorts: [5556]. The CI dex runs
there rather than on 443, and Outline fetches the discovery document at boot -
so this is the chart's documented escape hatch used for exactly the case it was
written for, and the one place in the repo where the cap's upgrade risk is
actually exercised.

Part of #5
This commit is contained in:
jklein committed 2026-10-14 14:20:39 +00:00
1 parent de995d0f53
commit ee9ae8a724
31 files changed
+2044 -6

No files matched your search

+1 -1
View File
@@ -5,5 +5,5 @@ icon: https://www.apache.org/logos/res/tika/default.png
type: application type: application
version: 10.0.2+up3.3.1.0.full version: 11.0.0+up3.3.1.0.full
appVersion: "3.3.1.0-full" appVersion: "3.3.1.0-full"
+54
View File
@@ -58,3 +58,57 @@ THE JVM ONLY GETS A QUARTER OF THE MEMORY LIMIT
cheap regardless; the heap is what matters for parsers that have to build a cheap regardless; the heap is what matters for parsers that have to build a
whole document in memory, and for OCR. whole document in memory, and for OCR.
{{- 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 9998 only. Outbound: DNS{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }} plus TCP {{ join ", " . }}{{ end }}{{ end }} — Tika is handed documents and
fetches nothing.
THIS CHART HAS NO INGRESS AND ITS CONSUMERS ARE OTHER PODS. While
networkPolicy.probeSources is empty the 9998 rule has no source selector, so
any pod in the cluster — including a paperless-ngx release in another
namespace — reaches it. Narrowing probeSources restricts that rule, and the
consumers' namespaces must then be listed in networkPolicy.allowedNamespaces
in the same change, or document extraction stops with a connection error on
the paperless side and nothing at all on this one.
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 }}-deployment
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,149 @@
{{- /*
NetworkPolicy for apache-tika (Gitea #5).
Apache Tika here is a cluster-internal service: the chart creates no Ingress at
all, and its only consumers are other pods - paperless-ngx points at
http://<service>.<namespace>.svc.cluster.local for text extraction.
THAT MAKES THE SOURCE QUESTION THE INTERESTING ONE, AND IT HAS AN AWKWARD
ANSWER. 9998 is the port all three probes address, so it cannot be restricted
by source without the worker node's IP - and this chart's consumers live in
OTHER namespaces (paperless-cmk, paperless-jk, paperless-tb in this cluster).
So an operator who narrows probeSources has to list those namespaces in
allowedNamespaces in the same breath, or every document extraction stops. That
is written into values.yaml and NOTES.txt rather than left to be discovered.
Egress is DNS only. Tika extracts text from documents it is handed; it fetches
nothing, and a text extractor that can reach the internet is a worse thing to
hand an untrusted document to.
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 there is something real to restrict: Tika extracts
text from documents it is handed and fetches nothing of its own, so DNS is the
only rule. A text extractor that can reach the internet is a worse thing to
hand an untrusted document to.
*/}}
- Egress
ingress:
{{- /*
9998 - 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: 9998
{{- 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 }}
{{- 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 }}
+37
View File
@@ -9,6 +9,43 @@
"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"
}
},
"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"
}, },
+60
View File
@@ -70,3 +70,63 @@ tika:
cpu: 0.1 cpu: 0.1
memory: 512Mi memory: 512Mi
ephemeralStorage: 10Mi ephemeralStorage: 10Mi
# ---------------------------------------------------------------------------
# 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 9998 only - the single port this pod
# declares - and outbound nothing but DNS. Tika is handed documents and
# fetches nothing, which is a property worth keeping: a text extractor that
# can reach the internet is a worse thing to hand an untrusted document to.
#
# WHAT IT DOES NOT DO, and read this before narrowing anything: 9998 is the
# probe port, so it is NOT restricted by source in the default. This chart
# has no Ingress and its consumers are other pods - paperless-ngx releases,
# which in this cluster live in their own namespaces. The moment probeSources
# is set, the 9998 rule is restricted and those namespaces must be in
# allowedNamespaces, or every document extraction stops.
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 9998, 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 and allowedNamespaces to the same rule for exactly that reason.
probeSources: []
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.
#
# Nothing is needed here for Tika itself. Add a port only if a parser is
# configured that fetches remote resources - and consider whether that is
# wanted at all.
extraPorts: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/alam00000/bentopdf/main/public/images/fa
type: application type: application
version: 1.1.0+up2.8.8 version: 2.0.0+up2.8.8
appVersion: "2.8.8" appVersion: "2.8.8"
maintainers: maintainers:
+46
View File
@@ -153,3 +153,49 @@ bentopdf.env HAS NO EFFECT ON THE APP — AND THREE VALUES BREAK THE POD
starts. Using them means also setting starts. Using them means also setting
securityContext.container.readOnlyRootFilesystem: false. securityContext.container.readOnlyRootFilesystem: false.
{{- 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 8080 only. Outbound: DNS{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }} plus TCP {{ join ", " . }}{{ end }}{{ end }} — the documents are
processed in the browser, so the pod needs nothing more.
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 }}-deployment
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,157 @@
{{- /*
NetworkPolicy for bentopdf (Gitea #5).
BentoPDF is a browser-side PDF tool: nginx serves the bundle, the documents are
processed in the visitor's browser, and the chart has no secrets and no
volumes. So the pod holds nothing and calls nothing, and the policy is as tight
as this object gets - inbound 8080, outbound DNS.
The ingress direction is not restricted by source in the default, because 8080
is the port all three probes address. See the rule below.
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 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 the restriction is close to total and therefore worth
having: a static file server needs name resolution and nothing else, so DNS is
the only rule.
*/}}
- Egress
ingress:
{{- /*
8080 - the image's EXPOSE and the port its nginx.conf listens on, the one the
Service targets, and the one all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 8080
{{- 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 and the
HTTPRoute both go 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
{{- 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"
}, },
+76
View File
@@ -299,3 +299,79 @@ httpRoute:
# chart has no OnePasswordItem, and why the checksum limitation for # chart has no OnePasswordItem, and why the checksum limitation for
# operator-filled secrets does not apply here: the only thing mounted is the # operator-filled secrets does not apply here: the only thing mounted is the
# ConfigMap, and that one is covered by checksum/config. # ConfigMap, and that one is covered by checksum/config.
# ---------------------------------------------------------------------------
# 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 8080 only, outbound nothing but DNS.
# BentoPDF processes documents in the visitor's browser and the chart has no
# secrets and no volumes, so the pod has no reason to open an outbound
# connection at all.
#
# WHAT IT DOES NOT DO: the 8080 rule is not restricted by source in the
# default, because 8080 is the probe port - see probeSources. So this is a
# port and egress ceiling, not pod-level isolation. And it is only a boundary
# where the CNI enforces policies.
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 8080, 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.
#
# Nothing is needed here for BentoPDF itself.
extraPorts: []
+17
View File
@@ -1,6 +1,23 @@
secrets: secrets:
configSecretRef: op://ci/outline-config configSecretRef: op://ci/outline-config
redisSecretRef: op://ci/outline-redis redisSecretRef: op://ci/outline-redis
# The egress cap of the chart's NetworkPolicy (Gitea #5) allows 443 for the
# OIDC provider, because that is where an OIDC provider normally listens. The
# CI one does not: fixtures.yaml runs dex at
# http://dex.default.svc.cluster.local:5556/dex, and Outline fetches the
# issuer's discovery document at boot.
#
# So this entry is not a workaround for the test - it is the chart's documented
# escape hatch, used for exactly the case values.yaml names, and the fixture is
# the one place in the repository where that case is actually exercised. Remove
# it and the install-test shows what the cap does to a dependency on a
# non-standard port, which is the risk this convention carries into every
# release.
networkPolicy:
egress:
extraPorts:
- 5556
dbSecretRef: op://ci/outline-db dbSecretRef: op://ci/outline-db
ingress: ingress:
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://gotenberg.dev/img/logo.png
type: application type: application
version: 8.0.3+up8.37.0 version: 9.0.0+up8.37.0
appVersion: "8.37.0" appVersion: "8.37.0"
maintainers: maintainers:
+59
View File
@@ -80,3 +80,62 @@ REQUESTS TIME OUT AFTER 30 SECONDS
cannot be changed here. This is the ceiling large conversions actually meet: cannot be changed here. This is the ceiling large conversions actually meet:
an 18 MB HTML needs about 22 seconds, and a 25 MB one does not finish inside an 18 MB HTML needs about 22 seconds, and a 25 MB one does not finish inside
the 30. Callers that need longer must split the document instead. the 30. Callers that need longer must split the document instead.
{{- 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 3000 only. Outbound: DNS{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }} plus TCP {{ join ", " . }}{{ end }}{{ end }}.
EGRESS IS DNS ONLY ON PURPOSE, AND IT COSTS SOMETHING. Gotenberg drives a
headless Chromium, so an egress rule for 80/443 would turn a service the
whole cluster can POST to into a way of fetching arbitrary URLs. The price is
that HTML conversion loses remote assets: inline content converts, images the
page would have fetched silently do not. That degrades a PDF instead of
failing a pod, so nothing will alert on it. If this release converts URLs or
needs remote assets, set networkPolicy.egress.extraPorts: [80, 443] — and
know what it opens.
THIS CHART HAS NO INGRESS AND ITS CONSUMERS ARE OTHER PODS. While
probeSources is empty the 3000 rule has no source selector; narrowing it
means listing the consumers' namespaces in allowedNamespaces in the same
change, or every conversion stops.
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 }}-deployment
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,146 @@
{{- /*
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, 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 inline content
and loses 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; the fix for a deployment that needs it is
networkPolicy.egress.extraPorts: [80, 443], chosen knowingly.
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 }}
{{- 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 }}
+37
View File
@@ -9,6 +9,43 @@
"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"
}
},
"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"
}, },
+63
View File
@@ -92,3 +92,66 @@ gotenberg:
limits: limits:
memory: 3Gi memory: 3Gi
ephemeralStorage: 256Mi ephemeralStorage: 256Mi
# ---------------------------------------------------------------------------
# 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 3000 only and outbound nothing but DNS.
#
# THE EGRESS CAP IS THE POINT OF THIS ONE. Gotenberg drives a headless
# Chromium, so allowing 80/443 outbound would let anything that can POST to
# this service fetch arbitrary URLs through it - and 3000 is the probe port,
# so the whole cluster can POST to it by default. The cost is the
# remote-asset half of HTML conversion: a converted page keeps its inline
# content and silently loses images it would have fetched. That is a degraded
# PDF, not a dead pod, which is exactly why it is written down here. A
# deployment that needs remote assets sets egress.extraPorts: [80, 443]
# knowingly.
#
# And like every chart here: 3000 is not restricted by source in the default
# (probe port), 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 3000, 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 and allowedNamespaces to the same rule for exactly that reason.
probeSources: []
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.
#
# [80, 443] is the entry to add if this release must fetch remote assets or
# convert URLs - and see the SSRF note above before adding it. A
# paperless-ngx that only converts mail bodies does not need it.
extraPorts: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/RSS-Bridge/rss-bridge/master/static/logo
type: application type: application
version: 2.1.1+up2026.09.30 version: 3.0.0+up2026.09.30
appVersion: "2026-09-30" appVersion: "2026-09-30"
maintainers: maintainers:
+52
View File
@@ -114,3 +114,55 @@ Useful to know:
WARNING: scaleDown is true, so this release is running with 0 replicas. WARNING: scaleDown is true, so this release is running with 0 replicas.
Set scaleDown=false to bring it back up. Set scaleDown=false to bring it back up.
{{- 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 80 only. Outbound: DNS plus TCP 80 and 443 to ANY destination{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }},
plus TCP {{ join ", " . }}{{ end }}{{ end }}.
THE EGRESS RULE IS THE OPEN INTERNET ON TWO PORTS, and it has to be: fetching
other people's websites is what RSS-Bridge does, and a NetworkPolicy matches
CIDRs and selectors but never hostnames, so "only the configured bridges"
cannot be written. What the cap buys is that this pod can open NO other
outbound port — not a database, not SMTP, not SSH.
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 }}-deployment
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,178 @@
{{- /*
NetworkPolicy for rss-bridge (Gitea #5).
RSS-Bridge's whole job is to fetch other people's websites and turn them into
feeds, so the egress cap can only ever be coarse here: 80 and 443 to ANY
destination. A NetworkPolicy matches CIDRs and selectors and never hostnames,
so "only the sites in the bridge list" is not expressible, and with Canal there
is no FQDN rule to reach for. What the cap still buys is narrow but real: this
pod cannot open a connection on any OTHER port - no SSH, no database, no mail -
so a compromised bridge cannot be used to reach the rest of the cluster on the
ports that matter.
Said plainly, because it would otherwise look stricter than it is: this chart's
egress rule permits the open internet on the two web ports.
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 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 there IS something to restrict: this workload
needs DNS and the ports named below, and nothing else. A chart whose
workload opens no outbound connection of its own must NOT list Egress -
valkey is the example - because a rule that constrains nothing makes the
object look stricter than it is.
*/}}
- Egress
ingress:
{{- /*
80 - the container's port (the port NAME rss-bridge-port is load-bearing and is
what the Service and the probes address). It is the probe path. Note that this
is the POD's 80, not the Service's: they happen to be the same number here,
which is exactly the kind of coincidence that makes the NodePort mistake easy
elsewhere.
*/}}
- ports:
- protocol: TCP
port: 80
{{- 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 and the
HTTPRoute both go 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
{{- /*
The web, on both ports, to any destination. This IS the application: a feed
bridge that cannot reach the sites it bridges does nothing. 80 as well as 443,
because plenty of the bridged sources are still plain HTTP. Ports only - a
hostname cannot appear in this object, so this rule cannot be narrowed to the
configured bridges.
*/}}
- ports:
- protocol: TCP
port: 80
- 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
@@ -10,6 +10,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"
}, },
+81
View File
@@ -352,3 +352,84 @@ httpRoute:
# - rss-bridge.example.com # - rss-bridge.example.com
# - feeds.example.com # - feeds.example.com
hostnames: [] hostnames: []
# ---------------------------------------------------------------------------
# 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 80 only (the container's port), and
# outbound DNS plus TCP 80 and 443 to ANY destination.
#
# WHAT IT DOES NOT DO, and this one needs saying out loud: the egress rule
# permits the open internet on both web ports, because that IS the
# application - a feed bridge that cannot reach the sites it bridges does
# nothing, and a NetworkPolicy cannot express "only these hosts". What the
# cap still buys is real but narrow: this pod cannot open any OTHER outbound
# port, so a compromised bridge cannot reach a database, a mail server or an
# SSH port anywhere in the cluster.
#
# Inbound 80 is not restricted by source in the default (it is the probe
# port), 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 80, 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.
#
# 80 and 443 are already allowed, so most bridges need nothing here. Add a
# port only for a source that serves feeds somewhere else.
extraPorts: []
+1 -1
View File
@@ -9,7 +9,7 @@ type: application
# (2026.8.22-9fea41204) and "latest" moves daily. appVersion is therefore the # (2026.8.22-9fea41204) and "latest" moves daily. appVersion is therefore the
# exact tag, and the +up suffix normalizes the dash to a dot as the versioning # exact tag, and the +up suffix normalizes the dash to a dot as the versioning
# scheme requires - the same shape rss-bridge and minecraft-server use. # scheme requires - the same shape rss-bridge and minecraft-server use.
version: 1.1.1+up2026.10.13.22c64b8d3 version: 2.0.0+up2026.10.13.22c64b8d3
appVersion: "2026.10.13-22c64b8d3" appVersion: "2026.10.13-22c64b8d3"
maintainers: maintainers:
+51
View File
@@ -149,3 +149,54 @@ NOTHING TO BACK UP
the signing key from the Secret, and the SQLite caches under /tmp plus the signing key from the Secret, and the SQLite caches under /tmp plus
/var/cache/searxng are emptyDirs that refill on their own. App-version /var/cache/searxng are emptyDirs that refill on their own. App-version
upgrades and rollbacks therefore carry no data risk. upgrades and rollbacks therefore carry no data risk.
{{- 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 8080 only. Outbound: DNS, TCP 443 (the search engines) and 6379
(external Valkey){{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }}.
443 GOES TO ANY DESTINATION, because querying search engines is the
application and this object cannot name a host. 6379 is the STANDARD Valkey
port: the URL sits in a 1Password item the chart never parses, so a Valkey on
another port is blocked — put it in networkPolicy.egress.extraPorts.
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 }}-deployment
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,178 @@
{{- /*
NetworkPolicy for searxng (Gitea #5).
SearXNG is a metasearch engine, so like rss-bridge its egress can only be
coarse: 443 to ANY destination, because querying search engines is the
application and a NetworkPolicy cannot name a host. 6379 is allowed as well,
for the EXTERNAL Valkey the limiter needs - this chart ships none of its own,
and the URL lives in a 1Password item the chart never parses, so 6379 is the
standard port rather than the configured one.
What the cap buys here is the same narrow but real thing: no other outbound
port, so a compromised instance cannot reach a database or a mail server.
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 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 there IS something to restrict: this workload
needs DNS and the ports named below, and nothing else. A chart whose
workload opens no outbound connection of its own must NOT list Egress -
valkey is the example - because a rule that constrains nothing makes the
object look stricter than it is.
*/}}
- Egress
ingress:
{{- /*
8080 - the image's own GRANIAN_PORT and its EXPOSE, the port the Service
targets, and the one all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 8080
{{- 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 and the
HTTPRoute both go 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
{{- /*
HTTPS to any destination. This IS the application: a metasearch engine that
cannot reach the engines returns nothing. Cannot be narrowed - the engine list
is hostnames, and this object matches CIDRs and selectors only.
*/}}
- ports:
- protocol: TCP
port: 443
{{- /*
Valkey/Redis for the limiter, on the STANDARD port. This chart deploys no
Valkey; secrets.valkeySecretRef points at an external one through a URL the
chart never parses, so a Valkey on another port needs
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 6379
{{- 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"
}, },
+80
View File
@@ -399,3 +399,83 @@ httpRoute:
# with it: signed image-proxy URLs and the OpenSearch description keep naming # with it: signed image-proxy URLs and the OpenSearch description keep naming
# ingress.domain. # ingress.domain.
hostnames: [] hostnames: []
# ---------------------------------------------------------------------------
# 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 8080 only, and outbound DNS plus TCP 443
# (the search engines - this IS the application) and 6379 (the external
# Valkey the limiter uses).
#
# WHAT IT DOES NOT DO: 443 goes to ANY destination, because the engine list
# is hostnames and this object matches CIDRs and selectors only. And 6379 is
# the STANDARD Valkey port, not the configured one - the URL lives in a
# 1Password item this chart never parses, so a Valkey elsewhere is blocked
# and belongs in egress.extraPorts.
#
# Inbound 8080 is not restricted by source in the default (probe port), 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 8080, 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.
#
# Add 80 if an engine or a proxy in use is plain HTTP, or the real Valkey
# port if it is not 6379.
extraPorts: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/matszwe02/ytdlp_web_player/main/src/stat
type: application type: application
version: 1.1.0+upv1.2.0 version: 2.0.0+upv1.2.0
appVersion: "v1.2.0" appVersion: "v1.2.0"
maintainers: maintainers:
+53
View File
@@ -324,3 +324,56 @@ WHAT THIS CHART DELIBERATELY DOES NOT DO
and there is no value to make them work. No PersistentVolume. No mirror of and there is no value to make them work. No PersistentVolume. No mirror of
the upstream's twenty environment switches as chart values — they live in the upstream's twenty environment switches as chart values — they live in
ytdlpWebPlayer.env, so an upstream release cannot make this chart lie. ytdlpWebPlayer.env, so an upstream release cannot make this chart lie.
{{- 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 5000 only. Outbound: DNS plus TCP 80 and 443 to ANY destination{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }},
plus TCP {{ join ", " . }}{{ end }}{{ end }} — fetching media is the application.
THE OAUTH2-PROXY SUBCHART IS NOT COVERED BY THIS POLICY. It selects this
chart's own workload by label; the subchart's Deployment carries its own
labels and ships no policy of its own. With the proxy enabled, the pod that
faces the Ingress is therefore unrestricted in both directions. Nothing in
this chart can change that — an upstream chart is consumed as it comes — so
do not read "this chart has a NetworkPolicy" as "this release is covered".
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 }}-deployment
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,177 @@
{{- /*
NetworkPolicy for ytdlp-web-player (Gitea #5).
Downloading media is this application's purpose, so egress 80 and 443 to any
destination is the rule and cannot be narrowed - a NetworkPolicy matches CIDRs
and selectors, never hostnames.
AND ONE THING THIS POLICY DOES NOT COVER AT ALL, which matters more here than
in any other chart of the sweep: THE OAUTH2-PROXY SUBCHART'S PODS. This policy
selects this chart's own workload by its app label; the subchart renders its own
Deployment with its own labels and brings no policy of its own. So with
oauth2-proxy enabled, the proxy pod - the component that actually faces the
Ingress - is unrestricted in both directions. That is a gap in the convention
and not a decision of this chart: an upstream chart is consumed as it comes
(README, "Chart dependencies"), and patching a subchart's templates is not done
here. Said out loud so nobody reads "the chart has a NetworkPolicy" as "the
release is covered".
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 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 there IS something to restrict: this workload
needs DNS and the ports named below, and nothing else. A chart whose
workload opens no outbound connection of its own must NOT list Egress -
valkey is the example - because a rule that constrains nothing makes the
object look stricter than it is.
*/}}
- Egress
ingress:
{{- /*
5000 - the port from ytdlpWebPlayer.ports named "http", which is the single
place it is configured: the chart derives PORT from it, the Service targets it
by that name, and all three probes address it by name. The probe path.
*/}}
- ports:
- protocol: TCP
port: 5000
{{- 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. With oauth2-proxy enabled the
traffic arrives from the PROXY POD instead, which allowedNamespaces cannot
express at pod granularity - another reason the subchart gap above matters.
*/}}
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
{{- /*
The web on both ports, to any destination: fetching media is the application.
Not narrowable to the sites in use, because this object cannot name a host.
*/}}
- ports:
- protocol: TCP
port: 80
- 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 - and note that with a subchart present this is no longer hypothetical: whatever is set here reaches oauth2-proxy too.", "description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it - and note that with a subchart present this is no longer hypothetical: whatever is set here reaches oauth2-proxy too.",
"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"
}, },
+83
View File
@@ -455,3 +455,86 @@ oauth2-proxy:
# it is spelled out here because switching it on looks like an upgrade. # it is spelled out here because switching it on looks like an upgrade.
alphaConfig: alphaConfig:
enabled: false enabled: false
# ---------------------------------------------------------------------------
# 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 5000 only, and outbound DNS plus TCP 80
# and 443 to ANY destination - fetching media is the application, and this
# object cannot name a host.
#
# WHAT IT DOES NOT COVER: THE OAUTH2-PROXY SUBCHART'S PODS. This policy
# selects this chart's own workload by its app label. The subchart renders
# its own Deployment with its own labels and ships no policy, so with the
# proxy enabled the pod that actually faces the Ingress is unrestricted in
# both directions. That is a gap in the convention, not a choice of this
# chart: an upstream chart is consumed as it comes (README, "Chart
# dependencies"). Do not read "this chart has a NetworkPolicy" as "this
# release is covered".
#
# Inbound 5000 is not restricted by source in the default (probe port), 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 the port named "http" (5000), which 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.
#
# 80 and 443 are already allowed. Add a port only for a source that serves
# media elsewhere.
extraPorts: []