feat!: NetworkPolicy in nine more charts, on by default
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 11s
Install-test / Install-test patchmon (pull_request) Successful in 1m41s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
Validate Helm Charts / Selector match (pull_request) Successful in 16s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / NetworkPolicy (pull_request) Failing after 11s
Validate Helm Charts / YAML scalars (pull_request) Successful in 10s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 6s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 17s
Validate Helm Charts / Build bulwark (pull_request) Successful in 16s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 7s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 13s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 4s
Validate Helm Charts / Build homeassistant (pull_request) Successful in 5s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 6s
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 sure (pull_request) Successful in 5s
Validate Helm Charts / Build template-chart (pull_request) Successful in 5s
Validate Helm Charts / Build outline (pull_request) Successful in 5s
Validate Helm Charts / Build patchmon (pull_request) Successful in 5s
Validate Helm Charts / Build rallly (pull_request) Successful in 5s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 15s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 7s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 7s
Install-test / Install-test rallly (pull_request) Successful in 1m41s
Validate Helm Charts / Build zipline (pull_request) Successful in 7s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 13s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 18s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 13s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 12s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 16s
Validate Helm Charts / Trivy config homeassistant (pull_request) Successful in 12s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 14s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 12s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 13s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 20s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 24s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 15s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 14s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 17s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 15s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 11s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 24s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m9s
Install-test / Install-test searxng (pull_request) Successful in 1m9s
Install-test / Install-test sure (pull_request) Successful in 2m24s
Install-test / Install-test apache-tika (pull_request) Successful in 1m23s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m44s
Install-test / Install-test bentopdf (pull_request) Successful in 59s
Install-test / Install-test bulwark (pull_request) Successful in 1m23s
Install-test / Install-test voucher-vault (pull_request) Successful in 1m58s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 1m55s
Install-test / Install-test cyberchef (pull_request) Successful in 1m6s
Install-test / Install-test gotenberg (pull_request) Successful in 1m19s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 59s
Install-test / Install-test zipline (pull_request) Successful in 1m52s
Install-test / Install-test homeassistant (pull_request) Successful in 2m26s
Validate Helm Charts / Install-test (pull_request) Successful in 27m21s
Install-test / Install-test outline (pull_request) Successful in 2m7s

bulwark, calibre-web-automated, patchmon, rallly, urlaubsverwaltung,
voucher-vault, zipline, sure, homeassistant. One major each.

These are the charts with real outbound dependencies, and they all share one
limitation that is now written into every one of them: THE EGRESS PORTS ARE THE
STANDARD ONES, NOT THE CONFIGURED ONES. Databases, caches and OIDC issuers
arrive as connection URLs or as fields inside 1Password items that the charts
never parse, so a chart cannot know where its dependency listens and cannot
validate a guess. A dependency somewhere else is blocked, and
networkPolicy.egress.extraPorts is the escape hatch. That is the upgrade risk
of this rollout, stated per chart instead of discovered per release.

Four of them differ in ways that mattered:

HOMEASSISTANT gets policyTypes: [Ingress] and NO egress section at all, and
that is the decision in the file rather than an omission. Its outbound traffic
is the union of every integration an operator enables - mDNS and SSDP
discovery, MQTT, a Zigbee or Z-Wave bridge on whatever port its firmware chose,
ESPHome, printers, speakers, inverters, cloud APIs, NTP - which no chart can
know. A cap would protect little, since the devices sit on the node's own LAN,
and would break integrations one at a time, months apart, each looking like a
flaky device rather than like a policy. So valkey's rule applies in the other
direction: policyTypes names only what there is something to restrict.

URLAUBSVERWALTUNG allows BOTH 5432 and 3306, deliberately. It is handed a full
JDBC URL inside a 1Password item and never parses it, so it cannot tell
PostgreSQL from MariaDB - and this cluster runs both. Allowing one would break
half the possible deployments on upgrade, with the symptom buried in a Java
stack trace. A small widening against a large and silent failure.

RALLLY derives its SMTP rule from rallly.smtp.enabled rather than always
rendering it: with mail switched off there is nothing to permit, and a rule
that permits nothing makes the object look stricter than it is.

BULWARK is the chart where the cap can fail most quietly. Its probes test its
own port and say nothing about the JMAP server, so a jmapServerUrl on a port
the policy does not allow yields a Ready pod serving an empty webmail. 443 is
allowed, Stalwart's in-cluster 8080 is not, and both halves of that are in
values.yaml and NOTES.txt.

PATCHMON and HOMEASSISTANT select their pods by the BARE release-and-chart
label, with no -deployment or -sfs suffix. Copying a selector from a
neighbouring chart would have matched nothing and left a policy that filters
nothing - which is precisely the first assertion of the render test.

Five CI fixtures gain networkPolicy.egress.extraPorts, and they are the only
place in the repository where the cap's cost is actually exercised: the CI dex
runs on 5556 rather than 443 (patchmon, rallly, urlaubsverwaltung,
voucher-vault) and the CI JMAP server on plain 8080 (bulwark).

Part of #5
This commit is contained in:
jklein committed 2026-10-14 14:26:25 +00:00
1 parent 6bde72192f
commit e1adcef71f
50 files changed
+3242 -9

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ type: application
# duplicated here. The short version: eight tag shapes, six of them things
# nobody should pin, and plain `docker` versioning excludes all six - so that
# block deliberately carries NO versioningTemplate.
version: 1.1.0+up1.12.0
version: 2.0.0+up1.12.0
appVersion: "1.12.0"
maintainers:
+53
View File
@@ -327,3 +327,56 @@ WHAT THIS CHART DOES NOT RENDER
platform objects and live in cluster-config; this chart renders an
HTTPRoute at most, which is the part that looks the same on every Gateway
implementation.
{{- 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 and TCP 443 — the JMAP server and the
OIDC provider{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }}.
CHECK THE JMAP PORT. 443 is where a published JMAP endpoint lives. A
bulwark.jmapServerUrl pointing at Stalwart's in-cluster HTTP port (8080), or
at anything else, is BLOCKED by this policy — and because Bulwark's probes
only test its own port, the pod becomes Ready and then serves an empty,
broken webmail with nothing in the events to explain it. Add the real port to
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 }}-sfs
against `kubectl get node -o wide`.
{{- else }}
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so that rule carries no source selector at all and any pod the CNI
routes to can reach it. Not from the internet — but this is NOT pod-level
isolation. Narrowing it means listing the worker node IPs in
networkPolicy.probeSources, because the kubelet probes from the node's own
address and the chart cannot know it; a wrong guess there leaves the pod
permanently un-Ready.
{{- end }}
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
this object is accepted and filters nothing, so everything reads as
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
shows the object exists; only a connection attempt from an unrelated
namespace shows that it bites.
Switch it off with networkPolicy.enabled: false.
{{- end }}
@@ -0,0 +1,175 @@
{{- /*
NetworkPolicy for bulwark (Gitea #5).
Bulwark is a JMAP webmail CLIENT: it holds no mail, has no database, no cache
and no broker. Everything it shows lives on a Stalwart server reached over
bulwark.jmapServerUrl, and that single outbound dependency is what shapes the
egress rules.
AND IT IS THE ONE CHART WHERE THE EGRESS CAP CAN FAIL QUIETLY. Bulwark's probes
address its own HTTP port and say nothing about whether the JMAP server is
reachable - so a release whose JMAP URL sits on a port this policy does not
allow comes up Ready and then shows an empty, broken webmail. 443 is allowed
because that is where a published JMAP endpoint lives; a URL pointing straight
at Stalwart's in-cluster HTTP port (8080) is blocked and needs
networkPolicy.egress.extraPorts. That asymmetry is in values.yaml and NOTES.txt
because nothing else will report it.
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 := .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
{{- /*
Selects the StatefulSet's pods. ci/run-networkpolicy-test.sh fails if
this matches no pod spec of the same render - which is the mistake that
would otherwise leave a policy in the namespace filtering nothing, and
its usual cause is a label renamed in the workload template.
*/}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
policyTypes:
- Ingress
{{- /*
Egress is listed 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:
{{- /*
3000 - the only port this pod declares, targeted by both the regular and the
headless Service, and the port 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.
The ingress controller is added to the same rule, or the Ingress goes dark the
moment this list is filled.
*/}}
from:
- namespaceSelector:
matchLabels:
{{- /*
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
labelling work on a namespace this chart does not own.
*/}}
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
{{- /*
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
IN that namespace. Written as two list entries it would be an OR, and would
open the port to every pod in kube-system.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
{{- range $probeSources }}
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- else }}
{{- /*
NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
cluster, because this port carries no NodePort. The alternative was to guess
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
stays un-Ready, the rollout times out, and the cause looks like the
application. An empty "from: []" would be worse than either - it allows
everything too, but while reading as a restriction. Narrow it with
networkPolicy.probeSources.
*/}}
{{- end }}
egress:
{{- /*
DNS, unconditionally. There is no value to switch this off, and that is
deliberate: a knob whose only other setting breaks every name lookup in
the pod is a trap and not a feature.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
Outbound HTTPS. This covers both of Bulwark's outbound dependencies: the JMAP
server named by bulwark.jmapServerUrl, and the OAuth/OIDC provider. Ports only
- the URLs are hostnames, which this object cannot match. A JMAP server reached
on its in-cluster HTTP port instead (Stalwart's 8080) is NOT covered and needs
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 443
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. This is the operator's
escape hatch from the egress cap, and the only alternative to switching
the whole policy off.
*/}}
- ports:
{{- range . }}
- protocol: TCP
port: {{ int . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"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": {
"description": "true renders the StatefulSet with replicas: 0 and takes precedence over bulwark.replicas. The PVC, Secrets, Service and Ingress stay in place.",
"type": "boolean"
+82
View File
@@ -816,3 +816,85 @@ httpRoute:
# unregistered origin is rejected at the provider, which says nothing about
# this chart.
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 3000 only, and outbound DNS plus TCP 443 -
# which covers Bulwark's two outbound dependencies, the JMAP server
# (bulwark.jmapServerUrl) and the OIDC provider.
#
# WHAT IT DOES NOT DO, and this is the one to check before upgrading: 443 is
# where a PUBLISHED JMAP endpoint lives. If jmapServerUrl points straight at
# Stalwart's in-cluster HTTP port (8080) or anywhere else, that traffic is
# BLOCKED - and Bulwark's probes only test its own port, so the pod comes up
# Ready and serves a broken, empty webmail with nothing in the events to say
# why. Put the real port in egress.extraPorts below.
#
# Inbound 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, 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 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.
#
# 8080 is the entry this chart needs most often: a jmapServerUrl pointing at
# Stalwart's in-cluster HTTP Service rather than at its published HTTPS
# name.
extraPorts: []
+1 -1
View File
@@ -5,7 +5,7 @@ name: calibre-web-automated
description: A Helm chart to deploy calibere-web-automated
icon: https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/main/README_images/cwa-logo-round-dark.png
version: 7.1.0+upv4.0.6
version: 8.0.0+upv4.0.6
appVersion: v4.0.6
maintainers:
+48
View File
@@ -217,3 +217,51 @@ TIME, AND THE ENVIRONMENT THE CHART ALREADY OCCUPIES
variable to disable WAL: NETWORK_SHARE_MODE=true" — so a library claim backed
by NFS or SMB, which is what cwa.storageClass decides, runs into exactly the
locking problems that setting exists to avoid.
{{- 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 8083 only. Outbound: DNS and TCP 443 (metadata providers){{ with .Values.networkPolicy.egress }}{{ with .extraPorts }},
plus TCP {{ join ", " . }}{{ end }}{{ end }}. A send-to-Kindle or e-mail setup needs its SMTP port
(587 or 465) in networkPolicy.egress.extraPorts — without it the send fails
silently from this side.
Three limits belong in the same breath, because none of them is visible in
the object itself:
- EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and
selectors and cannot express a hostname, so every egress rule permits its
port to ANY address. That is a limit of the object, not a shortcut — this
cluster's CNI (Canal: Flannel plus Calico) has no FQDN rule either. An
outbound port the chart does not know about is BLOCKED: add it to
networkPolicy.egress.extraPorts, and expect to find out through an
application-level connection error rather than through this object.
{{- if .Values.networkPolicy.probeSources }}
- THE PROBE PATH IS RESTRICTED in this release, because
networkPolicy.probeSources is set to
{{ join ", " .Values.networkPolicy.probeSources }}
Probes come from the IP of the WORKER NODE running the pod. If none of those
CIDRs covers the node this pod lands on, the kubelet's probes are dropped
and THE POD NEVER BECOMES READY — the rollout then times out and the symptom
looks like a broken application. Compare
kubectl -n {{ .Release.Namespace }} describe pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-sfs
against `kubectl get node -o wide`.
{{- else }}
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so that rule carries no source selector at all and any pod the CNI
routes to can reach it. Not from the internet — but this is NOT pod-level
isolation. Narrowing it means listing the worker node IPs in
networkPolicy.probeSources, because the kubelet probes from the node's own
address and the chart cannot know it; a wrong guess there leaves the pod
permanently un-Ready.
{{- end }}
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
this object is accepted and filters nothing, so everything reads as
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
shows the object exists; only a connection attempt from an unrelated
namespace shows that it bites.
Switch it off with networkPolicy.enabled: false.
{{- end }}
@@ -0,0 +1,168 @@
{{- /*
NetworkPolicy for calibre-web-automated (Gitea #5).
Calibre-Web-Automated keeps its library on a PVC and has no database and no
cache of its own, so the only outbound traffic that matters is metadata
lookups: cover art and book details fetched from public providers over HTTPS.
That is why 443 is allowed and nothing else is. The failure mode if it were
not: metadata lookups return nothing, which looks like a provider being down
rather than like a policy - so the rule is here rather than left to
extraPorts.
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 := .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
{{- /*
Selects the StatefulSet's pods. ci/run-networkpolicy-test.sh fails if
this matches no pod spec of the same render - which is the mistake that
would otherwise leave a policy in the namespace filtering nothing, and
its usual cause is a label renamed in the workload template.
*/}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
policyTypes:
- Ingress
{{- /*
Egress is listed 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:
{{- /*
8083 - the only port this pod declares, the one the Service forwards 80 to, and
the port all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 8083
{{- if $probeSources }}
{{- /*
RESTRICTED, because the operator supplied probeSources. The kubelet's probes
come from the IP of the WORKER NODE running the pod, so that address has to be
in the list or the pod never becomes Ready - and a pod that is never Ready
reads as a broken application rather than as a policy.
The ingress controller is added to the same rule, or the Ingress goes dark the
moment this list is filled.
*/}}
from:
- namespaceSelector:
matchLabels:
{{- /*
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
labelling work on a namespace this chart does not own.
*/}}
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
{{- /*
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
IN that namespace. Written as two list entries it would be an OR, and would
open the port to every pod in kube-system.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
{{- range $probeSources }}
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- else }}
{{- /*
NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
cluster, because this port carries no NodePort. The alternative was to guess
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
stays un-Ready, the rollout times out, and the cause looks like the
application. An empty "from: []" would be worse than either - it allows
everything too, but while reading as a restriction. Narrow it with
networkPolicy.probeSources.
*/}}
{{- end }}
egress:
{{- /*
DNS, unconditionally. There is no value to switch this off, and that is
deliberate: a knob whose only other setting breaks every name lookup in
the pod is a trap and not a feature.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
Outbound HTTPS for metadata providers (covers, book details). To any
destination, because the providers are hostnames and this object matches CIDRs
and selectors only.
*/}}
- ports:
- protocol: TCP
port: 443
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. This is the operator's
escape hatch from the egress cap, and the only alternative to switching
the whole policy off.
*/}}
- ports:
{{- range . }}
- protocol: TCP
port: {{ int . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"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": {
"type": "boolean"
},
+75
View File
@@ -189,3 +189,78 @@ httpRoute:
# CWA is told its address by nothing, so a second name needs no second value
# changed with it.
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 8083 only, and outbound DNS plus TCP 443
# for metadata lookups (covers and book details from public providers).
#
# WHAT IT DOES NOT DO: 443 goes to any destination - the providers are
# hostnames, which this object cannot match. Inbound 8083 is not restricted
# by source in the default, because 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 8083, 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 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 a port only for a metadata provider or an e-mail/send-to-Kindle setup
# that does not use 443 - SMTP submission would be 587 or 465.
extraPorts: []
+12
View File
@@ -160,3 +160,15 @@ ingress:
# also the annotation this chart's NOTES.txt tells operators to add, so the
# fixture is the shape a real install has.
nginx.ingress.kubernetes.io/proxy-body-size: "25m"
# The egress cap of this chart's NetworkPolicy (Gitea #5) allows the STANDARD
# ports of its dependencies. The CI JMAP server does not: fixtures.yaml serves it over plain HTTP on 8080 rather than over HTTPS on 443 - which is exactly the asymmetry values.yaml warns about for an in-cluster jmapServerUrl.
#
# So this is not a workaround for the test: it is the chart's documented escape
# hatch, used for exactly the case values.yaml names. The fixtures are the one
# place in the repository where that case is exercised, and removing this entry
# shows what the cap costs a dependency on a non-standard port.
networkPolicy:
egress:
extraPorts:
- 8080
+12
View File
@@ -7,3 +7,15 @@ secrets:
ingress:
clusterIssuer: ci
domain: patchmon.ci.local
# The egress cap of this chart's NetworkPolicy (Gitea #5) allows the STANDARD
# ports of its dependencies. The CI OIDC provider does not: fixtures.yaml runs dex on 5556 rather than on 443.
#
# So this is not a workaround for the test: it is the chart's documented escape
# hatch, used for exactly the case values.yaml names. The fixtures are the one
# place in the repository where that case is exercised, and removing this entry
# shows what the cap costs a dependency on a non-standard port.
networkPolicy:
egress:
extraPorts:
- 5556
+12
View File
@@ -10,3 +10,15 @@ rallly:
ingress:
clusterIssuer: ci
domain: rallly.ci.local
# The egress cap of this chart's NetworkPolicy (Gitea #5) allows the STANDARD
# ports of its dependencies. The CI OIDC provider does not: fixtures.yaml runs dex on 5556 rather than on 443.
#
# So this is not a workaround for the test: it is the chart's documented escape
# hatch, used for exactly the case values.yaml names. The fixtures are the one
# place in the repository where that case is exercised, and removing this entry
# shows what the cap costs a dependency on a non-standard port.
networkPolicy:
egress:
extraPorts:
- 5556
+12
View File
@@ -7,3 +7,15 @@ ingress:
# clusterIssuer is strictly required (no default); the Ingress just has to
# apply - there is no cert-manager in the CI cluster.
clusterIssuer: ci
# The egress cap of this chart's NetworkPolicy (Gitea #5) allows the STANDARD
# ports of its dependencies. The CI OIDC provider does not: fixtures.yaml runs dex on 5556 rather than on 443 - and this application has no login path besides OIDC.
#
# So this is not a workaround for the test: it is the chart's documented escape
# hatch, used for exactly the case values.yaml names. The fixtures are the one
# place in the repository where that case is exercised, and removing this entry
# shows what the cap costs a dependency on a non-standard port.
networkPolicy:
egress:
extraPorts:
- 5556
+12
View File
@@ -6,3 +6,15 @@ secrets:
ingress:
clusterIssuer: ci
domain: vv.ci.local
# The egress cap of this chart's NetworkPolicy (Gitea #5) allows the STANDARD
# ports of its dependencies. The CI OIDC provider does not: fixtures.yaml runs dex on 5556 rather than on 443.
#
# So this is not a workaround for the test: it is the chart's documented escape
# hatch, used for exactly the case values.yaml names. The fixtures are the one
# place in the repository where that case is exercised, and removing this entry
# shows what the cap costs a dependency on a non-standard port.
networkPolicy:
egress:
extraPorts:
- 5556
+1 -1
View File
@@ -4,7 +4,7 @@ description: A Helm chart to deploy Home Assistant on Kubernetes
icon: https://www.home-assistant.io/images/home-assistant-logo.svg
type: application
version: 6.2.0+up2026.8.3
version: 7.0.0+up2026.8.3
appVersion: "2026.8.3"
+52
View File
@@ -372,3 +372,55 @@ env IS APPENDED, AND HOME ASSISTANT WILL NOT READ IT ON ITS OWN
value: a value is stored in the pod spec and readable by anyone who can read
pods in {{ .Release.Namespace }}.
{{- 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 8123 only — anything else this image or an add-on binds is not
reachable. THERE IS NO EGRESS RESTRICTION AT ALL, deliberately: outbound
traffic here is the union of every integration you enable (mDNS and SSDP
discovery, MQTT, a Zigbee or Z-Wave bridge, ESPHome, printers, speakers,
inverters, cloud APIs, NTP), a chart cannot know that set, and a cap would
break integrations one at a time — months apart, each looking like a flaky
device rather than like a policy. An installation that wants one should write
it at release level, against its own integration list.
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 }}"
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,124 @@
{{- /*
NetworkPolicy for homeassistant (Gitea #5).
INGRESS ONLY, AND THE MISSING EGRESS SECTION IS THE DECISION IN THIS FILE.
Home Assistant's outbound traffic is the union of every integration an operator
enables, and that set is unknowable to a chart: mDNS and SSDP discovery on
5353/UDP and 1900/UDP, MQTT on 1883 or 8883, a Zigbee or Z-Wave bridge on
whatever port its firmware chose, ESPHome on 6053, printers, speakers, TVs,
inverters, cloud APIs on 443, an NTP client on 123/UDP. A port cap here would
not protect anything meaningful - the devices are on the same LAN as the node -
and it would break integrations ONE AT A TIME, months apart, each failure
looking like a flaky device rather than like this object. There is no version
of this list that stays correct.
So the rule from valkey applies, in the other direction: policyTypes names only
what there is something to restrict, because an egress rule that has to be
widened until it permits everything makes the object look stricter than it is
while costing real outages to get there. If a deployment wants an egress cap,
it belongs in a release-level policy written against that installation's actual
integrations - not in a chart that cannot see them.
What the Ingress half still buys is real and worth having on its own: only 8123
is reachable, so anything else this image or an add-on binds is not.
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 this chart's workload. Note the label: the bare release-and-chart name,
QUOTED, with no -sfs suffix - unlike most charts here. Copying a selector from
a neighbouring chart would match nothing and leave this policy filtering
nothing, which is what ci/run-networkpolicy-test.sh holds it against.
*/}}
podSelector:
matchLabels:
app: "{{ .Release.Name }}-{{ .Chart.Name }}"
policyTypes:
- Ingress
ingress:
{{- /*
8123 - the only port this pod declares, the one the Service forwards 80 to, and
the port all three probes address. The probe path.
No ingress-controller entry: this chart keeps its flexible upstream ingress
style and renders no Ingress of its own (a documented exception in README),
so the sources to name are namespaces, through allowedNamespaces.
*/}}
- ports:
- protocol: TCP
port: 8123
{{- 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.
*/}}
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 }}
{{- end }}
+24
View File
@@ -9,6 +9,30 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"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"
}
}
}
},
"scaleDown": {
"type": "boolean"
},
+53
View File
@@ -235,3 +235,56 @@ core:
# kubectl rollout restart statefulset <release>-homeassistant-sfs
secrets:
configSecretRef: null
# ---------------------------------------------------------------------------
# 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 8123 only. THERE IS NO EGRESS SECTION, and
# that is the decision in this chart rather than an omission.
#
# Home Assistant's outbound traffic is the union of every integration enabled
# in a release: mDNS/SSDP discovery (5353/UDP, 1900/UDP), MQTT (1883/8883), a
# Zigbee or Z-Wave bridge on whatever port its firmware chose, ESPHome (6053),
# printers, speakers, TVs, inverters, cloud APIs on 443, NTP on 123/UDP. A
# chart cannot know that set, a cap would protect little - the devices sit on
# the node's own LAN - and it would break integrations one at a time, months
# apart, each looking like a flaky device rather than like a policy. So
# policyTypes names Ingress only, for the same reason valkey names Ingress
# only: a rule that has to be widened until it permits everything makes the
# object look stricter than it is.
#
# An installation that wants an egress cap should write one against its own
# integrations, at release level, where they are visible.
#
# Inbound 8123 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 8123, 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: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/PatchMon/PatchMon/main/frontend/public/a
type: application
version: 7.1.0+up2.1.3
version: 8.0.0+up2.1.3
appVersion: "2.1.3"
maintainers:
+51
View File
@@ -147,3 +147,54 @@ SCALED TO ZERO
retrying, so nothing is lost; they simply report nothing until you set it
back and upgrade again.
{{- 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 3000 only. Outbound: DNS, TCP 5432 (Postgres), 6379 (Redis) and
443 (OIDC){{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }}.
THOSE ARE THE STANDARD PORTS, NOT YOUR CONFIGURED ONES — all three
dependencies arrive as connection details inside 1Password items this chart
never parses. A dependency elsewhere is blocked; add its port to
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 }}
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,188 @@
{{- /*
NetworkPolicy for patchmon (Gitea #5).
PatchMon has three outbound dependencies and all three arrive as connection
details inside 1Password items the chart never parses: Postgres, Redis and an
OIDC provider. So the egress rules name the STANDARD ports (5432, 6379, 443)
and nothing else, and a deployment whose dependency sits elsewhere is blocked
until networkPolicy.egress.extraPorts says so.
Note the pod label this policy selects: the bare release-and-chart name,
without the -deployment suffix most charts here use.
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 := .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
{{- /*
Selects this chart's workload. Note the label: it is the bare
release-and-chart name with NO -deployment suffix, unlike most charts
here - copying a selector from a neighbouring chart would match nothing
and leave this policy filtering nothing at all, which is why
ci/run-networkpolicy-test.sh holds it against the render.
*/}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}
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:
{{- /*
3000 - the only port this pod declares, the one the Service forwards 80 to, and
the port all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 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.
The ingress controller is added to the same rule, or the Ingress goes dark the
moment this list is filled.
*/}}
from:
- namespaceSelector:
matchLabels:
{{- /*
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
labelling work on a namespace this chart does not own.
*/}}
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
{{- /*
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
IN that namespace. Written as two list entries it would be an OR, and would
open the port to every pod in kube-system.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
{{- range $probeSources }}
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- else }}
{{- /*
NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
cluster, because this port carries no NodePort. The alternative was to guess
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
stays un-Ready, the rollout times out, and the cause looks like the
application. An empty "from: []" would be worse than either - it allows
everything too, but while reading as a restriction. Narrow it with
networkPolicy.probeSources.
*/}}
{{- end }}
egress:
{{- /*
DNS, unconditionally. There is no value to switch this off, and that is
deliberate: a knob whose only other setting breaks every name lookup in
the pod is a trap and not a feature.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
PostgreSQL on the STANDARD port. The connection details live in a 1Password
item the chart never parses, so this is the conventional port and not the
configured one; a Postgres anywhere else is blocked and belongs in
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 5432
{{- /*
Redis/Valkey on the STANDARD port, same reasoning as Postgres above: the URL
sits in a secret this chart does not read.
*/}}
- ports:
- protocol: TCP
port: 6379
{{- /*
Outbound HTTPS, for the OIDC provider: the chart fetches the issuer's
configuration and exchanges tokens over it, and the issuer URL lives inside a
1Password item the chart never parses. 443 to any destination is as narrow as
this object gets for that - a hostname cannot appear in a NetworkPolicy. An
issuer on another port (a CI dex on 5556) needs
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 443
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. This is the operator's
escape hatch from the egress cap, and the only alternative to switching
the whole policy off.
*/}}
- ports:
{{- range . }}
- protocol: TCP
port: {{ int . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"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": {
"type": "boolean"
},
+78
View File
@@ -224,3 +224,81 @@ secrets:
postgresSecretRef: null
oidcSecretRef: null
patchmonSecretRef: null
# ---------------------------------------------------------------------------
# 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 DNS plus TCP 5432
# (Postgres), 6379 (Redis) and 443 (the OIDC provider).
#
# WHAT IT DOES NOT DO: those three are the STANDARD ports, not the configured
# ones. All three dependencies arrive as connection details inside 1Password
# items this chart never parses, so it cannot know where they listen. A
# dependency on another port is BLOCKED and belongs in egress.extraPorts.
#
# Inbound 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, 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 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 the real port whenever Postgres, Redis or the OIDC issuer is not on
# 5432 / 6379 / 443 - a self-hosted issuer on 8443, for instance.
extraPorts: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/lukevella/rallly/main/apps/landing/publi
type: application
version: 8.2.0+up4.15.4
version: 9.0.0+up4.15.4
appVersion: "4.15.4"
maintainers:
+53
View File
@@ -244,3 +244,56 @@ SCALED TO ZERO
until you set it back and upgrade again. Secrets and Service{{ if .Values.ingress.enabled }}, and the Ingress
with its certificate,{{ else if $httpRoute.enabled }}, and the HTTPRoute,{{ end }} stay in place.
{{- 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 3000 only. Outbound: DNS, TCP 5432 (Postgres) and 443 (OIDC){{ if .Values.rallly.smtp.enabled }},
plus 587 and 465 for mail submission (rallly.smtp.enabled is true){{ else }}.
No SMTP rule is rendered, because rallly.smtp.enabled is false{{ end }}{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus
TCP {{ join ", " . }}{{ end }}{{ end }}.
THOSE ARE THE STANDARD PORTS, NOT YOUR CONFIGURED ONES. A mail server on 25
or 2525, or an OIDC issuer that is not on 443, is blocked until its port is in
networkPolicy.egress.extraPorts — and a blocked mail server shows up as
invitations that are never delivered, with a Ready pod behind it.
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 }}
+197
View File
@@ -0,0 +1,197 @@
{{- /*
NetworkPolicy for rallly (Gitea #5).
Rallly reaches a Postgres, an OIDC provider and - when rallly.smtp.enabled is
true - a mail server. The SMTP rule is the one thing in this sweep that is
DERIVED from an existing value rather than always rendered: with SMTP switched
off there is no mail traffic to permit, and a rule that permits nothing makes
the object look stricter than it is.
The ports are the standard ones, because every one of these dependencies
arrives as connection details inside a 1Password item the chart never parses.
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 := .Values.ingress.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:
{{- /*
3000 - the only port this pod declares, the one the Service forwards 80 to, and
the port all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 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.
The ingress controller is added to the same rule, or the Ingress goes dark the
moment this list is filled.
*/}}
from:
- namespaceSelector:
matchLabels:
{{- /*
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
labelling work on a namespace this chart does not own.
*/}}
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
{{- /*
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
IN that namespace. Written as two list entries it would be an OR, and would
open the port to every pod in kube-system.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
{{- range $probeSources }}
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- else }}
{{- /*
NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
cluster, because this port carries no NodePort. The alternative was to guess
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
stays un-Ready, the rollout times out, and the cause looks like the
application. An empty "from: []" would be worse than either - it allows
everything too, but while reading as a restriction. Narrow it with
networkPolicy.probeSources.
*/}}
{{- end }}
egress:
{{- /*
DNS, unconditionally. There is no value to switch this off, and that is
deliberate: a knob whose only other setting breaks every name lookup in
the pod is a trap and not a feature.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
PostgreSQL on the STANDARD port. The connection details live in a 1Password
item the chart never parses, so this is the conventional port and not the
configured one; a Postgres anywhere else is blocked and belongs in
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 5432
{{- /*
Outbound HTTPS, for the OIDC provider: the chart fetches the issuer's
configuration and exchanges tokens over it, and the issuer URL lives inside a
1Password item the chart never parses. 443 to any destination is as narrow as
this object gets for that - a hostname cannot appear in a NetworkPolicy. An
issuer on another port (a CI dex on 5556) needs
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 443
{{- if .Values.rallly.smtp.enabled }}
{{- /*
SMTP submission, both conventional ports - 587 (STARTTLS) and 465 (implicit
TLS) - because the chart cannot tell which one the configured server uses: the
host and port live in secrets.smtpSecretRef, which this chart never parses.
RENDERED ONLY WHILE rallly.smtp.enabled IS TRUE. With mail switched off there
is nothing to permit, and a rule that permits nothing makes the object look
stricter than it is. A mail server on 25 or 2525 needs
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 587
- protocol: TCP
port: 465
{{- end }}
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. This is the operator's
escape hatch from the egress cap, and the only alternative to switching
the whole policy off.
*/}}
- ports:
{{- range . }}
- protocol: TCP
port: {{ int . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+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.",
"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": {
"type": "boolean"
},
+80
View File
@@ -186,3 +186,83 @@ httpRoute:
# the provider is built from ingress.domain - a new name needs both sides
# updated or logins fail on a redirect-uri mismatch while the pod stays green.
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 3000 only, and outbound DNS plus TCP 5432
# (Postgres), 443 (the OIDC provider) and - only while rallly.smtp.enabled is
# true - 587 and 465 for mail submission.
#
# WHAT IT DOES NOT DO: all of those are STANDARD ports, not configured ones.
# The database, the issuer and the mail server all arrive as connection
# details inside 1Password items this chart never parses, so a dependency
# anywhere else is BLOCKED - a mail server on 25 or 2525 above all. Put it in
# egress.extraPorts.
#
# Inbound 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, 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 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.
#
# The usual entries here are a mail server on 25 or 2525, or an OIDC issuer
# that is not on 443.
extraPorts: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/we-promise/sure/main/public/android-chro
type: application
version: 1.1.0+up0.7.4
version: 2.0.0+up0.7.4
appVersion: "0.7.4"
maintainers:
+53
View File
@@ -213,3 +213,56 @@ THE CHART CONFIGURES NO MAIL AND NO AI
An entry repeating a name the chart already sets wins at install time and is
silently lost on the next helm upgrade — read it back off the StatefulSet.
{{- 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 3000 only, declared by the web container. Outbound: DNS, TCP
5432 (Postgres), 6379 (Redis) and 443{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }} — and those egress
rules apply to the WORKER container as well, because a NetworkPolicy selects
pods and not containers.
5432 AND 6379 ARE THE STANDARD PORTS, NOT YOUR CONFIGURED ONES: the Postgres
host and port are fields of the 1Password item, passed through without being
read. A dependency elsewhere is blocked; add its port to
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 }}-sfs
against `kubectl get node -o wide`.
{{- else }}
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so that rule carries no source selector at all and any pod the CNI
routes to can reach it. Not from the internet — but this is NOT pod-level
isolation. Narrowing it means listing the worker node IPs in
networkPolicy.probeSources, because the kubelet probes from the node's own
address and the chart cannot know it; a wrong guess there leaves the pod
permanently un-Ready.
{{- end }}
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
this object is accepted and filters nothing, so everything reads as
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
shows the object exists; only a connection attempt from an unrelated
namespace shows that it bites.
Switch it off with networkPolicy.enabled: false.
{{- end }}
+190
View File
@@ -0,0 +1,190 @@
{{- /*
NetworkPolicy for sure (Gitea #5).
Sure runs two containers in one pod - a web container and a worker - and the
policy covers both, because a NetworkPolicy selects PODS and not containers.
That is worth knowing here: the worker declares no port at all and its probes
are exec probes, so it contributes nothing to the ingress rules while being
fully subject to the egress ones.
Its dependencies are a Postgres (host and port are FIELDS of the 1Password
item, which the chart passes through without reading) and a Redis. 443 is
allowed for outbound HTTPS, which the worker needs for the external data this
application fetches.
One asymmetry worth noting: liveness uses /up, which touches neither Postgres
nor Redis - deliberately, so a database outage does not restart the pod. The
startup and readiness probes do more. Either way the probe port is 3000 and
the probe path is open by default.
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 := .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
{{- /*
Selects the StatefulSet's pods. ci/run-networkpolicy-test.sh fails if
this matches no pod spec of the same render - which is the mistake that
would otherwise leave a policy in the namespace filtering nothing, and
its usual cause is a label renamed in the workload template.
*/}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
policyTypes:
- Ingress
{{- /*
Egress is listed because there is something real to restrict, and it applies to
BOTH containers of this pod: a NetworkPolicy selects pods, not containers, so
the worker is covered by these rules even though it declares no port and
contributes nothing to the ingress side.
*/}}
- Egress
ingress:
{{- /*
3000 - declared by the web container (the worker declares no port at all), the
port the Service forwards 80 to, and the one the 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.
The ingress controller is added to the same rule, or the Ingress goes dark the
moment this list is filled.
*/}}
from:
- namespaceSelector:
matchLabels:
{{- /*
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
labelling work on a namespace this chart does not own.
*/}}
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
{{- /*
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
IN that namespace. Written as two list entries it would be an OR, and would
open the port to every pod in kube-system.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
{{- range $probeSources }}
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- else }}
{{- /*
NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
cluster, because this port carries no NodePort. The alternative was to guess
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
stays un-Ready, the rollout times out, and the cause looks like the
application. An empty "from: []" would be worse than either - it allows
everything too, but while reading as a restriction. Narrow it with
networkPolicy.probeSources.
*/}}
{{- end }}
egress:
{{- /*
DNS, unconditionally. There is no value to switch this off, and that is
deliberate: a knob whose only other setting breaks every name lookup in
the pod is a trap and not a feature.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
PostgreSQL on the STANDARD port. Here the host and the port are FIELDS of
secrets.dbSecretRef, which the chart passes through to the application without
reading - so it still cannot know the port, and a Postgres elsewhere is blocked
until networkPolicy.egress.extraPorts names it.
*/}}
- ports:
- protocol: TCP
port: 5432
{{- /*
Redis/Valkey on the STANDARD port, same reasoning as Postgres above: the URL
sits in a secret this chart does not read.
*/}}
- ports:
- protocol: TCP
port: 6379
{{- /*
Outbound HTTPS, which the worker container needs for the external data this
application fetches. To any destination - hostnames cannot appear here.
*/}}
- ports:
- protocol: TCP
port: 443
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. This is the operator's
escape hatch from the egress cap, and the only alternative to switching
the whole policy off.
*/}}
- ports:
{{- range . }}
- protocol: TCP
port: {{ int . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"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": {
"type": "boolean"
},
+83
View File
@@ -374,3 +374,86 @@ httpRoute:
# docs/hosting/webauthn.md). APP_DOMAIN and WEBAUTHN_ALLOWED_ORIGINS follow
# ingress.domain as well.
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 3000 only (declared by the web container;
# the worker declares no port), and outbound DNS plus TCP 5432 (Postgres),
# 6379 (Redis) and 443 (the external data the worker fetches).
#
# IT COVERS BOTH CONTAINERS. A NetworkPolicy selects pods, not containers, so
# the worker is subject to the egress rules too - which is where it does all
# of its work.
#
# WHAT IT DOES NOT DO: 5432 and 6379 are the STANDARD ports. The Postgres
# host and port are FIELDS of the 1Password item, which the chart passes
# through without reading, so it cannot know them. A dependency elsewhere is
# BLOCKED and belongs in egress.extraPorts.
#
# Inbound 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 the web container's 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 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 the real port whenever Postgres or Redis is not on 5432 / 6379, or
# for an SMTP server (587 or 465) if mail is configured.
extraPorts: []
+1 -1
View File
@@ -4,5 +4,5 @@ description: A Helm chart to deploy urlaubsverwaltung on Kubernetes
icon: https://raw.githubusercontent.com/urlaubsverwaltung/urlaubsverwaltung/main/src/main/resources/static/favicons/apple-touch-icon.png
type: application
version: 10.1.1+up6.17.0
version: 11.0.0+up6.17.0
appVersion: 6.17.0
+51
View File
@@ -416,3 +416,54 @@ urlaubsverwaltung.env IS ADDITIVE ONLY, AND A COLLISION IS LOST ON UPGRADE
urlaubsverwaltung.env is empty in this release, so the placeholders above are
live exactly as printed.
{{- 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, TCP 5432 and 3306 (the database — both
conventional ports, because the chart never parses the JDBC URL and cannot
tell which engine is behind it) and 443 (OIDC){{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }}.
A database on any other port is blocked, and the symptom is a connection
timeout inside a Java stack trace rather than anything naming this object.
Put the real port 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,187 @@
{{- /*
NetworkPolicy for urlaubsverwaltung (Gitea #5).
Urlaubsverwaltung takes a full JDBC URL from a 1Password item - "jdbc-url",
e.g. jdbc:postgresql://host:5432/urlaubsverwaltung - and an OIDC provider,
which it REQUIRES: there is no login path besides OIDC.
BOTH DATABASE PORTS ARE ALLOWED, 5432 AND 3306, AND THAT IS A DELIBERATE
LOOSENING. The chart cannot parse the JDBC URL to find out which engine is
behind it, and this cluster runs both a Postgres and a MariaDB; allowing only
one would mean a policy that breaks half the possible deployments on upgrade,
with the symptom appearing as a connection timeout inside a Java stack trace.
Two ports instead of one is a small widening against a large and silent
failure, and it is written down rather than left to look like carelessness.
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 := .Values.ingress.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 only port this pod declares, the one the Service forwards 80 to, and
the port all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 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 goes dark the
moment this list is filled.
*/}}
from:
- namespaceSelector:
matchLabels:
{{- /*
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
labelling work on a namespace this chart does not own.
*/}}
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
{{- /*
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
IN that namespace. Written as two list entries it would be an OR, and would
open the port to every pod in kube-system.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
{{- range $probeSources }}
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- else }}
{{- /*
NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
cluster, because this port carries no NodePort. The alternative was to guess
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
stays un-Ready, the rollout times out, and the cause looks like the
application. An empty "from: []" would be worse than either - it allows
everything too, but while reading as a restriction. Narrow it with
networkPolicy.probeSources.
*/}}
{{- end }}
egress:
{{- /*
DNS, unconditionally. There is no value to switch this off, and that is
deliberate: a knob whose only other setting breaks every name lookup in
the pod is a trap and not a feature.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
The database, on BOTH conventional ports: 5432 for PostgreSQL and 3306 for
MySQL/MariaDB. The chart receives a full JDBC URL inside a 1Password item and
never parses it, so it cannot know which engine or port is in use - and this
cluster runs both. Allowing one of the two would break half the possible
deployments on upgrade, as a connection timeout buried in a Java stack trace.
A database on any other port needs networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 5432
- protocol: TCP
port: 3306
{{- /*
Outbound HTTPS, for the OIDC provider: the chart fetches the issuer's
configuration and exchanges tokens over it, and the issuer URL lives inside a
1Password item the chart never parses. 443 to any destination is as narrow as
this object gets for that - a hostname cannot appear in a NetworkPolicy. An
issuer on another port (a CI dex on 5556) needs
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 443
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. This is the operator's
escape hatch from the egress cap, and the only alternative to switching
the whole policy off.
*/}}
- ports:
{{- range . }}
- protocol: TCP
port: {{ int . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"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": {
"type": "boolean"
},
+81
View File
@@ -195,3 +195,84 @@ urlaubsverwaltung:
cpu: 0.1
memory: 768Mi
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 8080 only, and outbound DNS plus TCP 5432
# AND 3306 (the database - see below) and 443 (the OIDC provider, which this
# application requires: there is no other login path).
#
# WHY BOTH DATABASE PORTS: the chart is handed a full JDBC URL inside a
# 1Password item and never parses it, so it cannot tell PostgreSQL from
# MariaDB - and this cluster runs both. Allowing only one would break half the
# possible deployments on upgrade, and the symptom would be a connection
# timeout inside a Java stack trace. A deliberate, small widening against a
# large and silent failure.
#
# 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 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 the real port for a database that is on neither 5432 nor 3306, or for
# an OIDC issuer that is not on 443.
extraPorts: []
+1 -1
View File
@@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/l4rm4nd/VoucherVault/main/myapp/static/a
type: application
version: 6.2.1+up1.30.3
version: 7.0.0+up1.30.3
appVersion: "1.30.3"
maintainers:
+50
View File
@@ -219,3 +219,53 @@ EXPIRY NOTIFICATIONS STAY SILENT UNTIL A USER ADDS AN APPRISE URL
schedule runs daily at 09:00 in the container's time zone
(TZ={{ index $env "TZ" | default "Europe/Berlin" }}) and is created ONLY while an empty database is initialized —
delete it in the admin and no later start brings it back.
{{- if .Values.networkPolicy.enabled }}
NETWORKPOLICY IS ACTIVE — WHAT IT CAPS, AND WHAT IT DOES NOT
{{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects this workload's pods.
Inbound: TCP 8000 only. Outbound: DNS, TCP 5432 (Postgres), 6379 (Redis) and
443 (OIDC){{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }}.
THOSE ARE THE STANDARD PORTS, NOT YOUR CONFIGURED ONES — the connection
details live in 1Password items this chart never parses. A dependency
elsewhere is blocked; add its port to 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 }}-sfs
against `kubectl get node -o wide`.
{{- else }}
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so that rule carries no source selector at all and any pod the CNI
routes to can reach it. Not from the internet — but this is NOT pod-level
isolation. Narrowing it means listing the worker node IPs in
networkPolicy.probeSources, because the kubelet probes from the node's own
address and the chart cannot know it; a wrong guess there leaves the pod
permanently un-Ready.
{{- end }}
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
this object is accepted and filters nothing, so everything reads as
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
shows the object exists; only a connection attempt from an unrelated
namespace shows that it bites.
Switch it off with networkPolicy.enabled: false.
{{- end }}
@@ -0,0 +1,183 @@
{{- /*
NetworkPolicy for voucher-vault (Gitea #5).
Voucher-Vault reaches a Postgres, a Redis and an OIDC provider, all three
configured through 1Password items the chart never parses - so the egress rules
name the standard ports (5432, 6379, 443) and a dependency elsewhere is blocked
until networkPolicy.egress.extraPorts says otherwise.
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 := .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
{{- /*
Selects the StatefulSet's pods. ci/run-networkpolicy-test.sh fails if
this matches no pod spec of the same render - which is the mistake that
would otherwise leave a policy in the namespace filtering nothing, and
its usual cause is a label renamed in the workload template.
*/}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
policyTypes:
- Ingress
{{- /*
Egress is listed 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:
{{- /*
8000 - the only port this pod declares, the one the Service forwards 80 to, and
the port all three probes address. The probe path.
*/}}
- ports:
- protocol: TCP
port: 8000
{{- if $probeSources }}
{{- /*
RESTRICTED, because the operator supplied probeSources. The kubelet's probes
come from the IP of the WORKER NODE running the pod, so that address has to be
in the list or the pod never becomes Ready - and a pod that is never Ready
reads as a broken application rather than as a policy.
The ingress controller is added to the same rule, or the Ingress goes dark the
moment this list is filled.
*/}}
from:
- namespaceSelector:
matchLabels:
{{- /*
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
labelling work on a namespace this chart does not own.
*/}}
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
{{- /*
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
IN that namespace. Written as two list entries it would be an OR, and would
open the port to every pod in kube-system.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
{{- range $probeSources }}
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- else }}
{{- /*
NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
cluster, because this port carries no NodePort. The alternative was to guess
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
stays un-Ready, the rollout times out, and the cause looks like the
application. An empty "from: []" would be worse than either - it allows
everything too, but while reading as a restriction. Narrow it with
networkPolicy.probeSources.
*/}}
{{- end }}
egress:
{{- /*
DNS, unconditionally. There is no value to switch this off, and that is
deliberate: a knob whose only other setting breaks every name lookup in
the pod is a trap and not a feature.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
PostgreSQL on the STANDARD port. The connection details live in a 1Password
item the chart never parses, so this is the conventional port and not the
configured one; a Postgres anywhere else is blocked and belongs in
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 5432
{{- /*
Redis/Valkey on the STANDARD port, same reasoning as Postgres above: the URL
sits in a secret this chart does not read.
*/}}
- ports:
- protocol: TCP
port: 6379
{{- /*
Outbound HTTPS, for the OIDC provider: the chart fetches the issuer's
configuration and exchanges tokens over it, and the issuer URL lives inside a
1Password item the chart never parses. 443 to any destination is as narrow as
this object gets for that - a hostname cannot appear in a NetworkPolicy. An
issuer on another port (a CI dex on 5556) needs
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 443
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. This is the operator's
escape hatch from the egress cap, and the only alternative to switching
the whole policy off.
*/}}
- ports:
{{- range . }}
- protocol: TCP
port: {{ int . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"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": {
"type": "boolean"
},
+78
View File
@@ -274,3 +274,81 @@ httpRoute:
# 127.0.0.1 on purpose. So a second hostname needs ingress.domain changed in
# the same step.
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 8000 only, and outbound DNS plus TCP 5432
# (Postgres), 6379 (Redis) and 443 (the OIDC provider).
#
# WHAT IT DOES NOT DO: those are the STANDARD ports, not the configured ones -
# all three dependencies arrive as connection details inside 1Password items
# this chart never parses. A dependency on another port is BLOCKED and belongs
# in egress.extraPorts.
#
# Inbound 8000 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 8000, which is the port all three probes address.
#
# EMPTY MEANS OPEN, NOT BLOCKED. With no entry that rule is rendered WITHOUT a
# "from" selector, so the port is reachable from anywhere the CNI routes to -
# inside the cluster, since it carries no NodePort.
#
# That is the deliberate choice between two bad defaults. The kubelet's probes
# originate from the IP of the WORKER NODE running the pod, an address this
# chart cannot know and must not guess: a CIDR that excludes the real node
# blocks every probe, the pod never becomes Ready, the rollout times out, and
# the symptom reads as a broken application rather than as a policy.
#
# Narrow it by listing the worker node IPs (e.g. ["10.0.1.11/32"]) - and mind
# the companion trap: once this list is non-empty the rule IS restricted, so
# everything else that needs the port has to be in it. The chart adds the own
# namespace, the ingressController below and allowedNamespaces to the same rule for exactly that reason.
probeSources: []
# The ingress controller allowed to reach the routed port once probeSources
# restricts it. Rendered only while ingress.enabled 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 the real port whenever Postgres, Redis or the OIDC issuer is not on
# 5432 / 6379 / 443.
extraPorts: []
+1 -1
View File
@@ -16,7 +16,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 7.2.0+up4.7.0
version: 8.0.0+up4.7.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
+52
View File
@@ -209,3 +209,55 @@ zipline.env IS APPENDED, NOT MERGED
belongs in valueFrom.secretKeyRef: a plain "value" is stored in the pod spec
and readable by everyone who can read pods in {{ .Release.Namespace }}.
{{- 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 3000 only. Outbound: DNS, TCP 5432 (Postgres) and 443 (optional
OAuth providers, and an external S3 endpoint){{ with .Values.networkPolicy.egress }}{{ with .extraPorts }}, plus TCP {{ join ", " . }}{{ end }}{{ end }}.
5432 IS THE STANDARD PORT, NOT YOUR CONFIGURED ONE: the connection URL lives
in a 1Password item this chart never parses. A Postgres elsewhere is blocked —
and since all three probes hit this app's own port and not the database, that
shows up as an application error rather than as an un-Ready pod. Add the real
port to 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 }}-sfs
against `kubectl get node -o wide`.
{{- else }}
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so that rule carries no source selector at all and any pod the CNI
routes to can reach it. Not from the internet — but this is NOT pod-level
isolation. Narrowing it means listing the worker node IPs in
networkPolicy.probeSources, because the kubelet probes from the node's own
address and the chart cannot know it; a wrong guess there leaves the pod
permanently un-Ready.
{{- end }}
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
this object is accepted and filters nothing, so everything reads as
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
shows the object exists; only a connection attempt from an unrelated
namespace shows that it bites.
Switch it off with networkPolicy.enabled: false.
{{- end }}
@@ -0,0 +1,177 @@
{{- /*
NetworkPolicy for zipline (Gitea #5).
Zipline keeps its uploads on a PVC, so its only hard outbound dependency is
Postgres. 443 is allowed in addition, because Zipline's optional OAuth
providers (Discord, GitHub, Google, or an OIDC issuer) are configured through
env and all of them are HTTPS - and a login path that silently cannot reach its
provider is a worse failure than a slightly wider rule.
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 := .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy
spec:
{{- /*
Selects the StatefulSet's pods. ci/run-networkpolicy-test.sh fails if
this matches no pod spec of the same render - which is the mistake that
would otherwise leave a policy in the namespace filtering nothing, and
its usual cause is a label renamed in the workload template.
*/}}
podSelector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
policyTypes:
- Ingress
{{- /*
Egress is listed 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:
{{- /*
3000 - the only port this pod declares (named zipline-server), the one the
Service forwards 80 to, and the port 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.
The ingress controller is added to the same rule, or the Ingress goes dark the
moment this list is filled.
*/}}
from:
- namespaceSelector:
matchLabels:
{{- /*
Set by Kubernetes itself on every namespace (>= 1.21), so this needs no
labelling work on a namespace this chart does not own.
*/}}
kubernetes.io/metadata.name: {{ $ns }}
{{- if and $routed $ic.namespace }}
{{- /*
ONE from-entry with TWO selectors, which is an AND: pods carrying these labels
IN that namespace. Written as two list entries it would be an OR, and would
open the port to every pod in kube-system.
*/}}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ $ic.namespace | quote }}
{{- with $ic.podLabels }}
podSelector:
matchLabels:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- end }}
{{- range $allowedNs }}
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ . | quote }}
{{- end }}
{{- range $probeSources }}
- ipBlock:
cidr: {{ . | quote }}
{{- end }}
{{- else }}
{{- /*
NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the
cluster, because this port carries no NodePort. The alternative was to guess
the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod
stays un-Ready, the rollout times out, and the cause looks like the
application. An empty "from: []" would be worse than either - it allows
everything too, but while reading as a restriction. Narrow it with
networkPolicy.probeSources.
*/}}
{{- end }}
egress:
{{- /*
DNS, unconditionally. There is no value to switch this off, and that is
deliberate: a knob whose only other setting breaks every name lookup in
the pod is a trap and not a feature.
*/}}
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- /*
PostgreSQL on the STANDARD port. The connection details live in a 1Password
item the chart never parses, so this is the conventional port and not the
configured one; a Postgres anywhere else is blocked and belongs in
networkPolicy.egress.extraPorts.
*/}}
- ports:
- protocol: TCP
port: 5432
{{- /*
Outbound HTTPS, for Zipline's optional OAuth providers (Discord, GitHub,
Google, an OIDC issuer) - all of them HTTPS, all configured through env, none
of them known to the chart. Also covers an external S3 endpoint if one is
configured that way. Ports only: these are hostnames, which this object cannot
match.
*/}}
- ports:
- protocol: TCP
port: 443
{{- with $np.egress }}
{{- with .extraPorts }}
{{- /*
Ports only, to any destination - see the header. This is the operator's
escape hatch from the egress cap, and the only alternative to switching
the whole policy off.
*/}}
- ports:
{{- range . }}
- protocol: TCP
port: {{ int . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+51
View File
@@ -9,6 +9,57 @@
"description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.",
"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": {
"type": "boolean"
},
+79
View File
@@ -167,3 +167,82 @@ httpRoute:
# CORE_DEFAULT_DOMAIN with it: every share link Zipline hands out keeps
# naming ingress.domain until that value is changed too.
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 3000 only, and outbound DNS plus TCP 5432
# (Postgres) and 443 (optional OAuth providers, and an external S3 endpoint
# if one is configured).
#
# WHAT IT DOES NOT DO: 5432 is the STANDARD Postgres port - the connection
# URL lives in a 1Password item this chart never parses - so a database
# elsewhere is BLOCKED and belongs in egress.extraPorts. 443 goes to any
# destination, because the providers are hostnames.
#
# Inbound 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, 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 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.
#
# The usual entry here is a Postgres that is not on 5432, or an S3 endpoint
# (MinIO, garage) on a non-standard port.
extraPorts: []