NetworkPolicies in every chart (Gitea #5): the convention, the gate, and what it does not do
29 chart pages plus Chart-Style-Guide, CI-CD, Charts and Lessons-Learned. Chart-Style-Guide gains section 15, and its core is the FOUR PORT CLASSES - the part that is actually hard to get right. A port a network probe addresses stays open until probeSources is set, because the kubelet probes from the worker node's IP and no chart can know it. A NodePort-published port stays open because the source is already masqueraded. Everything else can be and is restricted by source, which is why the charts with a real inbound restriction are exactly those whose probes use a different port. A replica-to-replica bus gets a bare podSelector - and, since policyTypes lists Egress, its outbound side needs a rule of its own or the cluster never forms while every node looks healthy. The exec-probe distinction is written down as load-bearing rather than pedantic: it is the whole reason valkey and minecraft-server are the tightest policies in the repository and every HTTP-probed web app is looser. CI-CD gains the networkpolicy job as section 8 (the later two renumbered), and it ends with the measurement rather than a claim. Two experiments ran on the PR and were reverted: cyberchef's probe port restricted to an address no probe can come from stayed GREEN at 1m9s, and outline with its DNS egress rule removed went RED at 5m59s. Together those say k3s exempts the kubelet's probe traffic while enforcing everything else - so the install-test's net is an EGRESS net, and a wrong ingress port is caught by the render assertion or not at all. Every chart page gets a networkPolicy values table and an operating note naming that chart's own trap, not a boilerplate one: - keycloak carries the blast radius: it is the cluster's identity provider, its http rule is the only source-restricted inbound rule in the repo, and nothing in CI can catch a wrong controller - so the page asks for a real login and a look at consumers' issuer URLs after the first upgrade. - garage asks the reader to check their S3 consumers, because anything in another namespace is blocked until it is in allowedNamespaces. - bulwark names the quietest failure in the sweep: its probes test its own port, so a jmapServerUrl on a blocked port yields a Ready pod serving an empty webmail. - gotenberg explains what its DNS-only cap COSTS - HTML conversion silently loses remote assets - rather than only what it buys. - satisfactory and homeassistant each argue their own Ingress-only policy; satisfactory's deciding half is that it is the one chart with no install-test at all, so an egress cap there would be the only one with no empirical check behind it. - paperless-ngx names the two pods of its release that the object does NOT cover, including the consume CronJob whose pod template carries no labels. - freesailarr explains why ten containers behind a VPN need only three egress rules, and that extraPorts cannot help with an alternative WireGuard port because it is TCP-only. valkey and stalwart are corrected rather than extended. valkey's page now says why having no probeSources is what makes it the tightest policy here and not an incomplete one. stalwart's carries a dated correction: this page and the chart both advised putting the ingress controller's namespace into allowedNamespaces with ["ingress-nginx"] as the example, and both halves are wrong - ingress-nginx is gone from the cluster entirely, and allowedNamespaces would have opened http to every pod in that namespace anyway. Charts drops the claim that valkey is "the first chart with a NetworkPolicy on by default" and says instead that every chart has one, with the two shapes a reader needs before opening any chart page. Lessons-Learned gains four, all transferable: - the net that existed for a different failure than the one it was built for, and why both experiments were needed before either could be read; - a comment that could not describe its own syntax (a template comment must not spell out its own closing delimiter; the render then fails pointing at the template, not at the sentence); - three values that reached no rule and would have read as controls - a convention about value NAMES is not a convention about which values exist; - the subchart whose comments the parent could not fix, and the rule it produced: before making a check a gate, ask which failures the author of this diff can actually fix.
34 files changed
+1118
-6
No files matched your search
+32
-2
@@ -211,7 +211,37 @@ The defect has a **loud and a silent half**, like `quantity-render`'s, and the s
|
|||||||
|
|
||||||
Measured: **30 charts, 408 scalars, 0 failed**, no cluster. The fail path was exercised in all three shapes against a throwaway copy of the chart as it stood on `main` — the bare schedule aborting the render, the star-valued `env` entry aborting it on the StatefulSet once the schedule was quoted, and finally the bool-looking value on a render that **succeeds**, where the type assertion named the variable, the container and the API server's own message. **Six charts still render their `env` block bare** (`matrix-synapse`, `minecraft-router`, `minecraft-server`, `rallly`, `satisfactory`, `teamspeak-server`): they pass only because no fixture of theirs sets a hostile value, and bringing one over is a major bump for that chart.
|
Measured: **30 charts, 408 scalars, 0 failed**, no cluster. The fail path was exercised in all three shapes against a throwaway copy of the chart as it stood on `main` — the bare schedule aborting the render, the star-valued `env` entry aborting it on the StatefulSet once the schedule was quoted, and finally the bool-looking value on a render that **succeeds**, where the type assertion named the variable, the container and the API server's own message. **Six charts still render their `env` block bare** (`matrix-synapse`, `minecraft-router`, `minecraft-server`, `rallly`, `satisfactory`, `teamspeak-server`): they pass only because no fixture of theirs sets a hostile value, and bringing one over is a major bump for that chart.
|
||||||
|
|
||||||
### 8. `trivy-config` (reporting only)
|
### 8. `networkpolicy` (every chart, every PR)
|
||||||
|
|
||||||
|
The sixth job that ignores the changed-chart matrix and the fifth that renders every chart (Gitea #5). `ci/run-networkpolicy-test.sh` holds **every `NetworkPolicy` a chart renders against the pod specs, probes and Services of the same render**. It exists because #5 put a policy into all 29 installable charts at once, on by default — 29 objects whose every failure mode is silent, since the API server accepts a policy that filters **nothing** exactly as readily as one that filters **everything**.
|
||||||
|
|
||||||
|
| Assertion | What goes wrong without it | Who notices today |
|
||||||
|
|---|---|---|
|
||||||
|
| the `podSelector` matches a pod spec of the same render | the object sits in the namespace, is listed by `kubectl`, reads as a boundary and filters nothing — usually because a label was renamed in the workload template, a different file and a different diff | nobody, ever: nothing is denied, so there is no symptom to trace back |
|
||||||
|
| every **ingress** port is a `containerPort` of the selected pods | a NodePort number where a pod port belongs. The policy is evaluated at the pod's interface, *after* kube-proxy translated the port, so `30025` matches nothing while looking strict — and being the only rule for that port, it blocks every inbound connection instead | nobody in CI (see below), and in production as a service that went mute |
|
||||||
|
| no ingress rule allows every source by accident | `from: []` allows everything, identically to no `from` at all, while reading in a diff and in `kubectl` output as a restriction — the one mistake here that makes a policy *weaker* while looking *stricter* | nobody |
|
||||||
|
| every port a **network** probe addresses is either unrestricted, or the chart offers `probeSources` **and** says in `NOTES.txt` what a wrong CIDR costs | the kubelet probes from the worker node's IP, which no chart can know. A restricted probe port without it drops every probe: the pod never becomes `Ready` and the symptom reads as a broken application | the install-test, under production's CNI — **but not in CI**, see below |
|
||||||
|
|
||||||
|
Egress ports are **deliberately not** held against the pod's `containerPort`s: an egress port is a port on the *destination*, and that comparison would fail every chart that reaches a database.
|
||||||
|
|
||||||
|
- **Two guards beyond the four, both earned.** A chart that renders **no** policy fails — that is how this issue decayed in the first place, since `garage` and `keycloak` were created after it was filed and arrived without one. And a **`#` comment inside a rendered policy** fails, because a `#` comment is part of the *object*: it ships to the cluster, appears in `helm get manifest`, and lands in every release diff. Both precedent charts failed that check the first time it ran, which is what it is for.
|
||||||
|
- **Assertion 3 is stricter than it reads.** A *missing* `from` is the documented open form and both precedent charts use it on purpose, so an outright ban was not available. It is accepted only where the render itself justifies it: every port of the rule is probed, or is published by a `NodePort` Service in the same render. Anything else is an allow-all nobody chose.
|
||||||
|
- **Subchart documents are skipped by the comment assertion**, structurally rather than leniently: a dependency is fetched from the registry, not from the copy in this repo, so the policy `paperless-ngx` and `stalwart` bundle is whatever `valkey` version is *published* — and nothing in the parent can change its rendered comments. The subchart's own PR catches it, because this job runs over `valkey` too.
|
||||||
|
- **Anti-vacuum guard.** `ci/networkpolicy/<chart>.yaml` is a render-only overlay applied **last**, whose job is to render the **source-restricted probe rule** — a branch no chart's own defaults reach, and the one holding the expensive mistake: a `namespaceSelector` and a `podSelector` as two list entries instead of one would be an OR, opening the port to every pod in `kube-system`. A chart with an overlay that renders no restricted **probe** rule fails. The counter is specifically the probe rule, because `keycloak` restricts two rules in its own defaults and would have satisfied a looser guard while the branch went unrendered.
|
||||||
|
- **Helm is the YAML parser**, through `ci/yaml2json`, for the same reason as the render tests above.
|
||||||
|
|
||||||
|
Measured: **30 charts, 206 assertions held, 0 failed**, no cluster, a few seconds. All twelve fail paths were exercised rather than assumed, and one earned its keep immediately: the `probeSources` check was a substring test, so a chart whose key had been **deleted** while the comment above it still discussed the value passed. It now looks for the key.
|
||||||
|
|
||||||
|
**What no part of this pipeline covers, and it was measured rather than assumed.** On the PR for #5 two experiments ran and were then reverted:
|
||||||
|
|
||||||
|
| Experiment | Result | What it means |
|
||||||
|
|---|---|---|
|
||||||
|
| `cyberchef`'s probe port restricted to `192.0.2.11/32`, an address no probe can come from | **install-test green, 1m9s** | **k3s exempts the kubelet's probe traffic from policy.** A blocked probe path does **not** turn the install-test red |
|
||||||
|
| `outline`'s DNS egress rule removed, so `postgres` and `redis` do not resolve and `/_health` cannot pass | **install-test red, 5m59s** | policies **are** enforced in CI. The net is real |
|
||||||
|
|
||||||
|
So the install-test's net is an **egress** net — it catches anything the application itself needs, not a blocked probe path. Two consequences worth keeping: the probe-path trap is not reachable from the defaults of this convention (the rule is open until an operator sets `probeSources`, which is what the fourth assertion and the `NOTES.txt` sentence are for); and a **wrong ingress port is caught by assertion 2 or not at all**, since probes are exempt and no ingress controller runs in CI either. That the path from Traefik to a pod works is proven **nowhere** in this pipeline.
|
||||||
|
|
||||||
|
### 9. `trivy-config` (reporting only)
|
||||||
|
|
||||||
Renders each chart in the scan matrix and lists what `trivy config` finds. **It never fails the job** (`TRIVY_CONFIG_MODE: report`). Deliberately not a gate yet: the repo has several documented deviations, and a check that goes red on those every time is one people learn to scroll past. Switching the mode to `gate` is the intended step once the seccomp gap is closed.
|
Renders each chart in the scan matrix and lists what `trivy config` finds. **It never fails the job** (`TRIVY_CONFIG_MODE: report`). Deliberately not a gate yet: the repo has several documented deviations, and a check that goes red on those every time is one people learn to scroll past. Switching the mode to `gate` is the intended step once the seccomp gap is closed.
|
||||||
|
|
||||||
@@ -219,7 +249,7 @@ Trivy is installed from its release tarball, not via `aquasecurity/setup-trivy`
|
|||||||
|
|
||||||
Exceptions live in `ci/trivy/trivyignore.yaml`, each with a mandatory `statement` — the script refuses to run without one and prints every suppressed finding together with its reason. **The trusted-registry list for AVD-KSV-0125 is a separate file on purpose**, `ci/trivy/data/ksv0125.yaml`: it is *configured* rather than suppressed, so the check keeps being able to say something about an image from a genuinely foreign source. A new registry therefore means an entry there, not an ignore — `quay.io` joined it with [keycloak](keycloak), the first chart in the repo using it. Note what the check does *not* forgive: an unqualified Docker Hub image puts the organisation in the registry position and is still reported, which is why every chart writes its image fully qualified.
|
Exceptions live in `ci/trivy/trivyignore.yaml`, each with a mandatory `statement` — the script refuses to run without one and prints every suppressed finding together with its reason. **The trusted-registry list for AVD-KSV-0125 is a separate file on purpose**, `ci/trivy/data/ksv0125.yaml`: it is *configured* rather than suppressed, so the check keeps being able to say something about an image from a genuinely foreign source. A new registry therefore means an entry there, not an ignore — `quay.io` joined it with [keycloak](keycloak), the first chart in the repo using it. Note what the check does *not* forgive: an unqualified Docker Hub image puts the organisation in the registry position and is still reported, which is why every chart writes its image fully qualified.
|
||||||
|
|
||||||
### 9. `install-test` (per changed, non-excluded chart)
|
### 10. `install-test` (per changed, non-excluded chart)
|
||||||
|
|
||||||
`ci/run-install-test.sh <chart>` against a throwaway [k3d](https://k3d.io) cluster: apply the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install` with the CI values, and wait for every Deployment and StatefulSet to roll out. Since probes and hardening are active, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
|
`ci/run-install-test.sh <chart>` against a throwaway [k3d](https://k3d.io) cluster: apply the stub `OnePasswordItem` CRD plus the chart's fixtures, `helm install` with the CI values, and wait for every Deployment and StatefulSet to roll out. Since probes and hardening are active, "Ready" means the app actually answers. On failure the job dumps pod descriptions, events and container logs.
|
||||||
|
|
||||||
|
|||||||
@@ -330,3 +330,58 @@ And the one that bites hardest: **everything under the subchart's values key is
|
|||||||
- **An erased `condition:` key switches a subchart ON.** Setting the switch to `null` **deletes** the key from the coalesced values, Helm then cannot resolve the `condition:` path, and an unresolvable condition counts as *enabled*. So the one block in a chart that must **not** be null-safe is the dependency switch — the opposite of [§12](#12-values-that-must-not-lie)'s rule for every other block, and in the worst direction, since the subchart comes up unconfigured. Require the key in the schema. An empty block (`{}`) is harmless: Helm merges it with the chart default.
|
- **An erased `condition:` key switches a subchart ON.** Setting the switch to `null` **deletes** the key from the coalesced values, Helm then cannot resolve the `condition:` path, and an unresolvable condition counts as *enabled*. So the one block in a chart that must **not** be null-safe is the dependency switch — the opposite of [§12](#12-values-that-must-not-lie)'s rule for every other block, and in the worst direction, since the subchart comes up unconfigured. Require the key in the schema. An empty block (`{}`) is harmless: Helm merges it with the chart default.
|
||||||
|
|
||||||
`template-chart` remains dependency-free: the scaffold should not carry a `dependencies:` block a new chart has to remember to delete.
|
`template-chart` remains dependency-free: the scaffold should not carry a `dependencies:` block a new chart has to remember to delete.
|
||||||
|
|
||||||
|
## 15. NetworkPolicies
|
||||||
|
|
||||||
|
**Every chart renders a `NetworkPolicy`, and `networkPolicy.enabled` defaults to `true`** (Gitea #5). Reference implementation: [`template-chart`](template-chart#networkpolicy). Adding one to an existing chart is a **major** bump.
|
||||||
|
|
||||||
|
The default is the decision the convention rests on. A policy that ships switched off is reached only when somebody remembers to switch it on — and the issue is then closed while nothing is filtered. That is not hypothetical: `garage` and `keycloak` were both created *after* #5 was filed and both arrived with no policy at all, `keycloak` with its unauthenticated management surface on 9000. `ci/run-networkpolicy-test.sh` fails a chart that renders none, which is what keeps that from happening again.
|
||||||
|
|
||||||
|
### What the object is, and what it is not
|
||||||
|
|
||||||
|
A **ceiling on reachability**, not an authentication layer. Three limits are real:
|
||||||
|
|
||||||
|
1. **Only the declared ports 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. For most charts here this is where nearly all of the value sits.
|
||||||
|
2. **Egress is capped by port**, so a compromised pod cannot open arbitrary outbound connections.
|
||||||
|
3. **Ingress is capped by source for every port no network probe touches.**
|
||||||
|
|
||||||
|
### The four port classes — the part to get right first
|
||||||
|
|
||||||
|
Every port falls into one of four, and the class decides the rule:
|
||||||
|
|
||||||
|
| Class | Rule | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| **probe path** — a port a *network* probe addresses | **no `from`** by default; restricted once `probeSources` is set | the kubelet probes from the IP of the **worker node running the pod**, which no chart can know. A CIDR that misses it drops every probe: the pod never becomes `Ready`, the rollout times out, and the symptom reads as a broken application |
|
||||||
|
| **public** — published by a `NodePort` Service | **no `from`** | the senders are on the internet and, after masquerading, the source is a cluster node anyway. A selector could only be a list of node IPs — a restriction in appearance, a maintenance trap in fact |
|
||||||
|
| **closed** — no probe, not published outside | namespace + `allowedNamespaces` (+ `ingressController` for HTTP) | the only class that yields a **real** inbound restriction. The charts that have one are exactly those whose probes use a *different* port: `keycloak`'s http, `garage`'s s3/web, `minecraft-server`'s RCON, `teamspeak-server`'s query port |
|
||||||
|
| **self** — a bus between a release's own replicas | bare `podSelector`, no `namespaceSelector` | the narrowest rule the object can express. And `policyTypes: Egress` means the **outbound** side needs its own rule, or the bus is open one way and the cluster never forms while every node looks healthy |
|
||||||
|
|
||||||
|
**`exec` probes are not network probes**, and that is load-bearing rather than pedantic: an `exec` probe runs inside the container through the kubelet's runtime and puts **no packet on the network**. It is the entire reason [`valkey`](valkey#networkpolicy) and [`minecraft-server`](minecraft-server#networkpolicy) have the tightest policies in the repository — every port restricted, no `probeSources` at all — and every HTTP-probed web app has a looser one.
|
||||||
|
|
||||||
|
### Values, and shipping only the live ones
|
||||||
|
|
||||||
|
`networkPolicy.enabled`, `.allowedNamespaces`, `.probeSources`, `.ingressController`, `.egress.extraPorts`. A chart ships **only the ones that reach a rule**: a value that silently does nothing is worse than one that is absent, because it reads as a control. `samba` and `satisfactory` carry `enabled` alone; `teamspeak-server` has `allowedNamespaces` but no `probeSources`.
|
||||||
|
|
||||||
|
`ingressController` is its own two-key value rather than a line in `allowedNamespaces` for one reason: **its two selectors render as one `from` entry, which is an AND** — those pods in that namespace. As two list entries they would be an OR, and `kube-system` in `allowedNamespaces` would open the port to every pod in the cluster's own namespace. Its default is **measured, not assumed** (2026-10-14): `ingress-nginx` is gone from this cluster, the only `IngressClass` is `traefik`, and the controller is the DaemonSet `rke2-traefik` in `kube-system` — which also serves the Gateway API here, so one source covers the Ingress and the HTTPRoute path alike. `ingress.className` says nothing about it: a class names an implementation, not the pods the traffic comes from.
|
||||||
|
|
||||||
|
### What to write down per chart, because the object will not say it
|
||||||
|
|
||||||
|
- **Egress is coarse: by port, never by destination.** A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, so "only to the database host" is inexpressible. This cluster's CNI is Canal (Flannel plus Calico) and has no FQDN rule; Calico's `GlobalNetworkSet` could express one but would bind every chart to Calico — the same argument that kept implementation-specific objects out of the Gateway-API sweep — so it is **not used**.
|
||||||
|
- **Most dependency ports are the standard ones, not the configured ones.** Databases, caches and OIDC issuers arrive as connection URLs or fields inside 1Password items the charts never parse. A dependency elsewhere is **blocked**; `egress.extraPorts` is the escape hatch. The exceptions derive theirs from values: `keycloak` (`keycloak.db.port`), `stalwart`, and `minecraft-router` from its own routing table.
|
||||||
|
- **The probed port stays open inside the cluster by default** — for a single-port web app the policy is a port and egress ceiling, not pod-level isolation. Say so rather than letting the object imply otherwise.
|
||||||
|
- **A subchart's pods are not covered.** [`ytdlp-web-player`](ytdlp-web-player#networkpolicy)'s oauth2-proxy is the sharp case: with the proxy on, the pod that actually faces the Ingress is unrestricted both ways, and an upstream chart is consumed as it comes ([§14](#14-chart-dependencies)). [`paperless-ngx`](paperless-ngx#networkpolicy) is the opposite — its `valkey` brings its own, tighter policy. Name which case the chart is in.
|
||||||
|
- **A policy is only a boundary where the CNI enforces one.** Unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. Verify by connecting from an unrelated namespace.
|
||||||
|
|
||||||
|
### Where "Ingress only" is the right answer
|
||||||
|
|
||||||
|
`policyTypes` names only what there is something to restrict. `valkey` established that for a workload that opens **no** outbound connection. Three charts now invoke the same rule in the other direction, for outbound traffic nobody can enumerate — and each argues it in its own file rather than inheriting a blanket excuse: [`homeassistant`](homeassistant#networkpolicy) (the union of every integration an operator enables), [`satisfactory`](satisfactory#networkpolicy) (Steam's content delivery, *and* the one chart with no install-test at all). [`gotenberg`](gotenberg#networkpolicy) is the instructive contrast: it **keeps** a DNS-only cap deliberately, because it drives a headless Chromium on its probe port, and pays for it with HTML conversions that silently lose remote assets.
|
||||||
|
|
||||||
|
### Mechanics
|
||||||
|
|
||||||
|
- **Pod ports, never NodePorts.** A policy is evaluated at the pod's interface, *after* kube-proxy translated the port. `30025` where the pod listens on `25` matches nothing: accepted, strict-looking, and blocking every inbound connection instead.
|
||||||
|
- **Protocol as well as number.** `teamspeak-server`'s voice port is UDP and `satisfactory`'s game port is both — a TCP-only rule on either is accepted, looks complete, and drops the traffic that arrives.
|
||||||
|
- **Never `from: []`.** It allows everything, identically to having no key, while reading as a restriction. Write no `from` where open is meant and say so in a template comment.
|
||||||
|
- **Reasoning in Go template comments, never `#`.** A `#` comment is part of the rendered *object*: it ships to the cluster and lands in every release diff. And such a comment must not spell out its own closing delimiter — the parser ends it early and the rest is emitted as manifest content, failing the render with `did not find expected node content` while pointing at the template rather than at the sentence.
|
||||||
|
- **A conditional `containerPort` needs a conditional rule.** `minecraft-server`'s RCON port renders only when `server.rcon.enabled`; a rule without the same gate names a port the pod does not declare.
|
||||||
|
|
||||||
|
See [CI/CD](CI-CD#networkpolicy-render-test) for the gate and for what it provably does **not** cover.
|
||||||
+3
-1
@@ -12,6 +12,8 @@ grep -E '^(version|appVersion):' <chart>/Chart.yaml
|
|||||||
|
|
||||||
**The "Ingress" column describes the Ingress mode only.** Since the Gateway-API sweep finished (Gitea #114), **every chart with a routing object also offers `httpRoute.enabled` as the mutually exclusive alternative** — 21 charts, the one exception being [rss-bridge](rss-bridge), which is frozen for changes. So that column says how a chart publishes *while the Ingress is the active mode*; it is not a claim that the Ingress is the only mode. The Gateway shapes differ more than the column suggests — two objects for [keycloak](keycloak) (public + admin) and [matrix-synapse](matrix-synapse) (homeserver + delegation), one per host entry for [homeassistant](homeassistant), one per app or one shared for [freesailarr](freesailarr) — and what each mode costs its chart is on the chart's own page under "Routing: Ingress or Gateway API". The convention is [style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api).
|
**The "Ingress" column describes the Ingress mode only.** Since the Gateway-API sweep finished (Gitea #114), **every chart with a routing object also offers `httpRoute.enabled` as the mutually exclusive alternative** — 21 charts, the one exception being [rss-bridge](rss-bridge), which is frozen for changes. So that column says how a chart publishes *while the Ingress is the active mode*; it is not a claim that the Ingress is the only mode. The Gateway shapes differ more than the column suggests — two objects for [keycloak](keycloak) (public + admin) and [matrix-synapse](matrix-synapse) (homeserver + delegation), one per host entry for [homeassistant](homeassistant), one per app or one shared for [freesailarr](freesailarr) — and what each mode costs its chart is on the chart's own page under "Routing: Ingress or Gateway API". The convention is [style guide §4](Chart-Style-Guide#4-ingress-and-gateway-api).
|
||||||
|
|
||||||
|
**Every chart ships a `NetworkPolicy`, on by default** (Gitea #5), so that is not listed per row either — it would be the same word thirty times. What differs per chart is which of its ports can be restricted and what its egress rule costs, and that is on each chart's own page under `networkPolicy`. Two shapes are worth knowing before reading any of them: a port a **network** probe addresses stays open inside the cluster (the kubelet probes from the worker node's IP, which no chart can know), and `policyTypes` names only what there is something to restrict — so [valkey](valkey), [homeassistant](homeassistant) and [satisfactory](satisfactory) are Ingress-only, each for its own reason. The convention is [style guide §15](Chart-Style-Guide#15-networkpolicies).
|
||||||
|
|
||||||
| Chart | Deploys | Shape | Ingress | Notable |
|
| Chart | Deploys | Shape | Ingress | Notable |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| [apache-tika](apache-tika) | Apache Tika | Deployment + Service | no | content extraction for [paperless-ngx](paperless-ngx) |
|
| [apache-tika](apache-tika) | Apache Tika | Deployment + Service | no | content extraction for [paperless-ngx](paperless-ngx) |
|
||||||
@@ -41,7 +43,7 @@ grep -E '^(version|appVersion):' <chart>/Chart.yaml
|
|||||||
| [teamspeak-server](teamspeak-server) | TeamSpeak server | StatefulSet | no | probes address the TCP port by name; the voice service is UDP and unprobeable |
|
| [teamspeak-server](teamspeak-server) | TeamSpeak server | StatefulSet | no | probes address the TCP port by name; the voice service is UDP and unprobeable |
|
||||||
| [template-chart](template-chart) | — | scaffold | — | **not installable**; the reference implementation of every convention |
|
| [template-chart](template-chart) | — | scaffold | — | **not installable**; the reference implementation of every convention |
|
||||||
| [urlaubsverwaltung](urlaubsverwaltung) | Urlaubsverwaltung | Deployment | yes | readiness names its health-group members explicitly after a silent outage |
|
| [urlaubsverwaltung](urlaubsverwaltung) | Urlaubsverwaltung | Deployment | yes | readiness names its health-group members explicitly after a silent outage |
|
||||||
| [valkey](valkey) | Valkey | StatefulSet + validating init container | no | one instance per application instead of the shared `cluster-redis`; the password is a finished config line from the vault, `maxmemory` is derived from the memory limit, and the first chart with a NetworkPolicy on by default |
|
| [valkey](valkey) | Valkey | StatefulSet + validating init container | no | one instance per application instead of the shared `cluster-redis`; the password is a finished config line from the vault, `maxmemory` is derived from the memory limit, and — with [stalwart](stalwart) — one of the two charts whose NetworkPolicy predates Gitea #5, so the convention was derived from them. Still the **tightest** policy in the repository, because its probes are `exec` probes and 6379 can therefore be closed by source with no escape-hatch value at all |
|
||||||
| [voucher-vault](voucher-vault) | VoucherVault | StatefulSet | yes | DB and OIDC secrets |
|
| [voucher-vault](voucher-vault) | VoucherVault | StatefulSet | yes | DB and OIDC secrets |
|
||||||
| [ytdlp-web-player](ytdlp-web-player) | ytdlp_web_player | Deployment | yes | **the only chart with a dependency**: an optional oauth2-proxy subchart, off by default |
|
| [ytdlp-web-player](ytdlp-web-player) | ytdlp_web_player | Deployment | yes | **the only chart with a dependency**: an optional oauth2-proxy subchart, off by default |
|
||||||
| [zipline](zipline) | Zipline | StatefulSet | yes | liveness deliberately avoids the database — the positive example of [§12.3](Chart-Style-Guide#12-values-that-must-not-lie) |
|
| [zipline](zipline) | Zipline | StatefulSet | yes | liveness deliberately avoids the database — the positive example of [§12.3](Chart-Style-Guide#12-values-that-must-not-lie) |
|
||||||
|
|||||||
@@ -835,3 +835,52 @@ Two consequences. **Write the exceptions, or write a test** — a count in prose
|
|||||||
The companion issue reported that the chart had *no* way to set an arbitrary `PAPERLESS_*` variable. It had one — the standard additive `env` block, in the schema and rendering after the fixed entries. The report had read `helm show values` of a **deployed, older** release and listed only the *top-level* keys; the block sits one level in.
|
The companion issue reported that the chart had *no* way to set an arbitrary `PAPERLESS_*` variable. It had one — the standard additive `env` block, in the schema and rendering after the fixed entries. The report had read `helm show values` of a **deployed, older** release and listed only the *top-level* keys; the block sits one level in.
|
||||||
|
|
||||||
The reported pain was real anyway, because the block could not be trusted with an arbitrary value. **So the right move was neither "works as designed" nor building what was asked for** (an `extraEnv`, which would have been a second name beside `env`) — it was fixing what was actually broken about the existing one and naming the deviation from the issue's title in the pull request. Check a "the chart cannot do X" report against the chart on `main` before designing X; and where the finding differs, say so instead of quietly delivering something else.
|
The reported pain was real anyway, because the block could not be trusted with an arbitrary value. **So the right move was neither "works as designed" nor building what was asked for** (an `extraEnv`, which would have been a second name beside `env`) — it was fixing what was actually broken about the existing one and naming the deviation from the issue's title in the pull request. Check a "the chart cannot do X" report against the chart on `main` before designing X; and where the finding differs, say so instead of quietly delivering something else.
|
||||||
|
|
||||||
|
## The net that existed, for a different failure than the one it was built for
|
||||||
|
|
||||||
|
The scope of Gitea #5 — a `NetworkPolicy` in 29 charts at once, on by default — rested on one claim: that the install-test **enforces** policies, so a wrong one goes red in CI rather than silently in production. The workflow creates its k3d cluster without `--disable-network-policy`, so k3s brings its policy controller, and the plan concluded that *a policy blocking the probe path makes the pod never `Ready` and the install-test red*.
|
||||||
|
|
||||||
|
That was read off a flag, so it was measured instead. Two experiments on the PR, both reverted afterwards and both kept in the history:
|
||||||
|
|
||||||
|
| Experiment | Result |
|
||||||
|
|---|---|
|
||||||
|
| `cyberchef`'s probe port restricted to `192.0.2.11/32` — an RFC 5737 documentation address that **cannot** be the k3d node's | install-test **green**, 1m9s |
|
||||||
|
| `outline`'s DNS egress rule removed, so `postgres` and `redis` do not resolve and `/_health` cannot pass | install-test **red**, 5m59s |
|
||||||
|
|
||||||
|
Both results were needed, because one alone is ambiguous. Green on the first could have meant *no enforcement at all* — in which case there was no net and the scope had to be renegotiated. Red on the second rules that out: policies **are** enforced. So the two together say something more precise than either: **k3s exempts the kubelet's probe traffic from policy**, while enforcing everything else.
|
||||||
|
|
||||||
|
The lesson is not "the net was missing". It is that **the net was real and covered a different failure than the one it was signed off for** — and that the difference only became visible because the claim was tested in both directions. Three things followed from it, and none of them would have been found by reading documentation:
|
||||||
|
|
||||||
|
- The convention's default never restricts a probe port, so the trap CI cannot see is not reachable from the rollout itself — only from an operator narrowing `probeSources` deliberately, which is why that value carries a warning in `values.yaml` *and* a required sentence in `NOTES.txt`.
|
||||||
|
- A **wrong ingress port** — the NodePort mistake — is therefore caught by the render assertion or **not at all**: probes are exempt, and no ingress controller runs in CI either. That raised the render test from a convenience to the load-bearing check.
|
||||||
|
- And it says nothing about production, whose CNI is Calico. There probe traffic **is** subject to policy, which is precisely why the default has to stay open.
|
||||||
|
|
||||||
|
**The transferable part:** when a check is the justification for a large change, test it in both directions before relying on it. A green result on a deliberately-broken input does not mean "the check is broken" — it may mean the check is sound and the *claim about it* was wrong, and those need different fixes.
|
||||||
|
|
||||||
|
## A comment that could not describe its own syntax
|
||||||
|
|
||||||
|
The convention for #5 says reasoning belongs in Go template comments rather than `#` comments, because a `#` comment is part of the rendered object and ships to the cluster. The header of the reference implementation said so — and spelled the delimiters out literally to make the point.
|
||||||
|
|
||||||
|
The render then failed with `error converting YAML to JSON: yaml: did not find expected node content`, pointing at the template. The Go template parser had ended the comment at the **first** occurrence of the closing delimiter inside the prose, and emitted the rest of the sentence as manifest content.
|
||||||
|
|
||||||
|
It is a small thing with a sharp edge, because the error names neither the sentence nor the cause, and the obvious reading of it — "my YAML is malformed" — is about the wrong half of the file. **Describe the delimiters, never quote them**, inside the construct they close. The same applies to any templating language whose comment has no escape for its own terminator.
|
||||||
|
|
||||||
|
## Three values that reached no rule, and would have read as controls
|
||||||
|
|
||||||
|
The #5 rollout settled on five value names so every chart's policy looks the same: `enabled`, `allowedNamespaces`, `probeSources`, `ingressController`, `egress.extraPorts`. Uniformity is worth something — an operator who has read one chart's block has read them all.
|
||||||
|
|
||||||
|
Then the verification harness reported, for `samba` and `satisfactory`, that setting `allowedNamespaces` and `probeSources` changed **nothing in the render**. Both charts publish every port as a NodePort, so every inbound rule is open to any source by design; there was no rule for a source list to attach to. `teamspeak-server` had the same problem with `probeSources` alone.
|
||||||
|
|
||||||
|
Uniformity lost, and it should have. A value that silently does nothing is **worse** than a value that is absent, because it reads as a control: someone narrowing a policy would have set it, seen no error, and believed the port was restricted. Those charts now ship only the values that reach a rule — `samba` and `satisfactory` carry `enabled` alone — and each says in `values.yaml` why the others are missing.
|
||||||
|
|
||||||
|
**The transferable part**, and it generalises past this issue: a convention about *value names* is not a convention about *which values exist*. Check that each one changes the render in the chart you are adding it to; a harness that renders both branches finds it in seconds, and a review almost never does.
|
||||||
|
|
||||||
|
## The subchart whose comments the parent could not fix
|
||||||
|
|
||||||
|
The #5 render test fails a chart whose rendered `NetworkPolicy` contains a `#` comment. `valkey` and `stalwart` both failed it on the first run — correctly, and both were fixed.
|
||||||
|
|
||||||
|
`paperless-ngx` then kept failing, for a policy that was not in its diff. Its `valkey` dependency is resolved from the **published Helm index**, not from the copy in this repository, so the subchart it bundles is whatever version is on the registry — and that one still had the old comments. Nothing in the parent chart could change them, and nothing would until `valkey`'s fix was merged **and published**, which cannot happen in the same pull request ([style guide §14](Chart-Style-Guide#14-chart-dependencies): never change a chart and a chart that depends on it in the same PR).
|
||||||
|
|
||||||
|
So the assertion now skips documents whose `# Source:` path contains `/charts/`. Not out of leniency: holding the parent responsible would have made the test **unfixable within the PR** and would fail a chart for a file its own diff does not contain. The subchart's own PR is where it is caught, because the job runs over every chart including `valkey`.
|
||||||
|
|
||||||
|
**The transferable part:** a check that runs over a rendered tree is running over code from more than one release. Before making one a gate, ask which failures the author of *this* diff can actually fix — and scope it to those. The rest belongs to whoever owns the other artifact.
|
||||||
+29
@@ -85,6 +85,17 @@ An erased block (`tika.securityContext:` with no children, a `null` probe, a `nu
|
|||||||
|
|
||||||
This chart has no `ingress` section at all: there is no `ingress.domain` and no `ingress.clusterIssuer` to set.
|
This chart has no `ingress` section at all: there is no `ingress.domain` and no `ingress.clusterIssuer` to set.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **9998** only. Outbound **DNS and nothing else**: Tika is handed documents and fetches nothing — a property worth keeping, because a text extractor that can reach the internet is a worse thing to hand an untrusted document to. There is no `ingressController` value, because this chart renders no Ingress.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### No authentication — and it parses whatever it is sent
|
### No authentication — and it parses whatever it is sent
|
||||||
@@ -107,6 +118,24 @@ Text extraction streams rather than buffering, so ordinary documents stay cheap
|
|||||||
|
|
||||||
A 200 with empty text is the normal shape of every failure mode here: a missing parser, a tag without `-full`, a scanned file without OCR. `curl /parsers` against the running pod lists what it actually has; `/version` confirms which image is behind the Service.
|
A 200 with empty text is the normal shape of every failure mode here: a missing parser, a tag without `-full`, a scanned file without OCR. `curl /parsers` against the running pod lists what it actually has; `/version` confirms which image is behind the Service.
|
||||||
|
|
||||||
|
### This chart has no Ingress, and its consumers are other namespaces
|
||||||
|
|
||||||
|
Tika is cluster-internal: its consumers are pods, and in this cluster they are `paperless-ngx` releases in their own namespaces. While `probeSources` is empty the 9998 rule has no source selector, so they reach it. **Narrowing `probeSources` restricts that rule**, and those namespaces must go into `networkPolicy.allowedNamespaces` in the same change — otherwise every document extraction stops, with a connection error on the paperless side and nothing at all on this one.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
Nothing to back up and nothing to migrate: no volume, no database, no state that survives a pod. An app-version bump is an image swap, and `helm rollback` carries no data risk. The one thing to check after a bump is that the new tag still carries `-full`, and that `/parsers` still lists what the consumers rely on.
|
Nothing to back up and nothing to migrate: no volume, no database, no state that survives a pod. An app-version bump is an image swap, and `helm rollback` carries no data risk. The one thing to check after a bump is that the new tag still carries `-full`, and that `/parsers` still lists what the consumers rely on.
|
||||||
|
|||||||
+26
@@ -112,6 +112,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-bentopdf-service` on port **80**.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-bentopdf-service` on port **80**.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **8080** only. Outbound **DNS and nothing else** — BentoPDF processes documents in the browser and the chart has no secrets and no volumes, so the pod needs nothing more.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -198,6 +210,20 @@ Almost everything else about BentoPDF is a **build-time** argument and therefore
|
|||||||
|
|
||||||
The image is based on `quay.io/nginx/nginx-unprivileged`: nginx runs as uid/gid 101 and listens on 8080, so the workload is fully non-root with no sysctl tweaks (same situation as `cyberchef`). Two emptyDirs keep the root filesystem read-only: `/etc/nginx/tmp` (the pid file) and `/var/cache/nginx` (**not** optional — nginx creates its temp directories at startup and the image ships the directory without them, so the master process aborts with `mkdir() "/var/cache/nginx/client_temp" failed`). `/var/run` is deliberately not mounted, unlike in the upstream chart. The container port name `http` is load-bearing: Service and all three probes select it by name.
|
The image is based on `quay.io/nginx/nginx-unprivileged`: nginx runs as uid/gid 101 and listens on 8080, so the workload is fully non-root with no sysctl tweaks (same situation as `cyberchef`). Two emptyDirs keep the root filesystem read-only: `/etc/nginx/tmp` (the pid file) and `/var/cache/nginx` (**not** optional — nginx creates its temp directories at startup and the image ships the directory without them, so the master process aborts with `mkdir() "/var/cache/nginx/client_temp" failed`). `/var/run` is deliberately not mounted, unlike in the upstream chart. The container port name `http` is load-bearing: Service and all three probes select it by name.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
Nothing to back up and nothing to migrate. No database, no volumes, no uploads — documents never leave the visitor's device, so app-version upgrades and `helm rollback` carry no data risk. Renovate tracks `ghcr.io/alam00000/bentopdf-simple` and pins the suffix-free tag family (`2.8.8`, not `v2.8.8` or `v2.8.8-amd64`), because that is the form `appVersion` can carry unchanged — see [Renovate](Renovate).
|
Nothing to back up and nothing to migrate. No database, no volumes, no uploads — documents never leave the visitor's device, so app-version upgrades and `helm rollback` carry no data risk. Renovate tracks `ghcr.io/alam00000/bentopdf-simple` and pins the suffix-free tag family (`2.8.8`, not `v2.8.8` or `v2.8.8-amd64`), because that is the form `appVersion` can carry unchanged — see [Renovate](Renovate).
|
||||||
|
|||||||
+34
@@ -243,6 +243,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
One rule, at `/`, with **no `URLRewrite` filter** — the application cannot be served under a sub-path at all, so there is nothing to strip.
|
One rule, at `/`, with **no `URLRewrite` filter** — the application cannot be served under a sub-path at all, so there is nothing to strip.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **3000** only. Outbound **DNS and TCP 443**, which covers both of Bulwark's outbound dependencies: the JMAP server named by `bulwark.jmapServerUrl`, and the OAuth/OIDC provider.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -277,6 +289,28 @@ So the limit in force becomes **the Gateway's**, whatever it happens to be, and
|
|||||||
|
|
||||||
**Set the equivalent next to the Gateway before pointing DNS at it**, and size it to what the **mail server** accepts ([stalwart](stalwart)) rather than to what looks generous here — a webmail client that accepts a 40 MB attachment the mail server will refuse has only moved the failure later. The Gateway branch of `NOTES.txt` prints this.
|
**Set the equivalent next to the Gateway before pointing DNS at it**, and size it to what the **mail server** accepts ([stalwart](stalwart)) rather than to what looks generous here — a webmail client that accepts a 40 MB attachment the mail server will refuse has only moved the failure later. The Gateway branch of `NOTES.txt` prints this.
|
||||||
|
|
||||||
|
### Check the JMAP port — this is where the cap fails most quietly
|
||||||
|
|
||||||
|
443 is where a **published** JMAP endpoint lives. If `bulwark.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 then serves an **empty, broken webmail**, with nothing in the events to say why. Put the real port in `networkPolicy.egress.extraPorts`; 8080 is the entry this chart needs most often.
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Traps
|
## Traps
|
||||||
|
|
||||||
### The volume beats the chart, for every value, silently
|
### The volume beats the chart, for every value, silently
|
||||||
|
|||||||
@@ -140,6 +140,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-calibre-web-automated-service` on port **80** — the port **number**, because `backendRefs` cannot name a port the way the Ingress backend does.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-calibre-web-automated-service` on port **80** — the port **number**, because `backendRefs` cannot name a port the way the Ingress backend does.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **8083** only. Outbound **DNS and TCP 443** for metadata lookups (covers and book details from public providers).
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -221,6 +233,26 @@ All three probes `GET /`, and CWA answers that with its login page no matter wha
|
|||||||
kubectl -n <namespace> logs <release>-calibre-web-automated-sfs-0 -f
|
kubectl -n <namespace> logs <release>-calibre-web-automated-sfs-0 -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### A send-to-Kindle or e-mail setup needs its SMTP port
|
||||||
|
|
||||||
|
Only 443 is allowed outbound, so a mail or send-to-Kindle configuration needs its submission port (587 or 465) in `networkPolicy.egress.extraPorts`. Without it the send fails from this side with nothing naming the policy.
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up the library claim before an app-version bump.** It carries the books, `metadata.db` and the application state together; there is no external database to fall back on.
|
- **Back up the library claim before an app-version bump.** It carries the books, `metadata.db` and the application state together; there is no external database to fall back on.
|
||||||
|
|||||||
+26
@@ -114,6 +114,18 @@ The Gateway-API alternative, off by default. Added with the Gateway-API pilot (G
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-cyberchef-service` on port **80**.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-cyberchef-service` on port **80**.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **8080** only — the single port this pod declares. Outbound **DNS and nothing else**: CyberChef's recipes run in the visitor's browser, so the pod has no reason to open an outbound connection at all, which makes this the tightest egress rule in the repository.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Routing modes
|
## Routing modes
|
||||||
|
|
||||||
The two modes are exclusive by design, and three of the four rules exist because of a failure that reports nothing:
|
The two modes are exclusive by design, and three of the four rules exist because of a failure that reports nothing:
|
||||||
@@ -159,6 +171,20 @@ Since app version 11.0.0 the upstream image is built on `nginxinc/nginx-unprivil
|
|||||||
|
|
||||||
With `scaleDown: true` (or `cyberchef.replicas: 0`) the Deployment runs at zero and the URL stops answering, while Service and Ingress remain — the certificate is not re-issued when you scale back up. In Gateway mode the Service and the `HTTPRoute` remain in the same way, so the route keeps pointing at a Service with no endpoints and the Gateway answers with an error of its own rather than with nothing.
|
With `scaleDown: true` (or `cyberchef.replicas: 0`) the Deployment runs at zero and the URL stops answering, while Service and Ingress remain — the certificate is not re-issued when you scale back up. In Gateway mode the Service and the `HTTPRoute` remain in the same way, so the route keeps pointing at a Service with no endpoints and the Gateway answers with an error of its own rather than with nothing.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
There is nothing to back up and nothing to migrate. The pod holds no state, so an app-version bump is a plain image swap and `helm rollback` carries no data risk. Renovate follows `ghcr.io/gchq/cyberchef` through this chart's `customManagers` block with default docker versioning — no suffix, no override — see [Renovate](Renovate).
|
There is nothing to back up and nothing to migrate. The pod holds no state, so an app-version bump is a plain image swap and `helm rollback` carries no data risk. Renovate follows `ghcr.io/gchq/cyberchef` through this chart's `customManagers` block with default docker versioning — no suffix, no override — see [Renovate](Renovate).
|
||||||
|
|||||||
+43
@@ -238,6 +238,21 @@ Setting **both** `networkInterface` and `savePath` to `null` removes the init co
|
|||||||
| `mediathekarr.startGate.settleSeconds` | int | `10` | Extra pause after gluetun is healthy. Not a measured requirement, a cheap buffer; `0` disables it. |
|
| `mediathekarr.startGate.settleSeconds` | int | `10` | Extra pause after gluetun is healthy. Not a measured requirement, a cheap buffer; `0` disables it. |
|
||||||
| `mediathekarr.downloads.directory` | string | `mediathekarr` | Sub-directory on the shared downloads claim. The chart derives `DOWNLOAD_INCOMPLETE_PATH=/downloads/<dir>/incomplete` and `DOWNLOAD_COMPLETE_PATH=/downloads/<dir>/complete` from it. Do **not** set either variable under `env` — the chart always sets both. |
|
| `mediathekarr.downloads.directory` | string | `mediathekarr` | Sub-directory on the shared downloads claim. The chart derives `DOWNLOAD_INCOMPLETE_PATH=/downloads/<dir>/incomplete` and `DOWNLOAD_COMPLETE_PATH=/downloads/<dir>/complete` from it. Do **not** set either variable under `env` — the chart always sets both. |
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
**Ten containers in one pod, behind a VPN — and the first thing to know is that they talk to each other over `localhost`.** They share a network namespace, so none of that traffic touches an interface and **no `NetworkPolicy` sees any of it**. This object governs only what enters or leaves the pod.
|
||||||
|
|
||||||
|
- **Inbound**: the nine probed ports as one rule — gluetun's health server (9999) plus qBittorrent (8080), Prowlarr (9696), FlareSolverr (8191), Radarr (7878), Sonarr (8989), Seerr (5055), Bazarr (6767) and Mediathekarr (5007) — and mediathekarr's newznab port (5008) **restricted** by source, because nothing probes it and its only consumer is Prowlarr in this same pod.
|
||||||
|
- **Outbound**: DNS, **UDP 51820** (the WireGuard tunnel) and TCP 443 (gluetun's server-list API, which cannot go through the tunnel because it runs before the tunnel exists). Indexer and tracker traffic needs no rule of its own: it is already inside the tunnel by the time it leaves the pod.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Nothing is Ready until the tunnel is up
|
### Nothing is Ready until the tunnel is up
|
||||||
@@ -510,6 +525,34 @@ kubectl -n <namespace> rollout restart statefulset/<release>-freesailarr-sfs
|
|||||||
|
|
||||||
The chart declares the enabled apps' ports (plus 9999) to gluetun's firewall, but inside a pod network those rules never match: gluetun detects the pod's own subnet and inserts an `ACCEPT` rule ahead of them, and the packet counters on the `--dport` rules stayed at 0. The declaration is kept because that subnet detection is gluetun's behaviour, not a guarantee of the chart's — a CNI handing out a `/32`, or a future gluetun dropping the rule, would make these the only thing keeping probes and ingress alive.
|
The chart declares the enabled apps' ports (plus 9999) to gluetun's firewall, but inside a pod network those rules never match: gluetun detects the pod's own subnet and inserts an `ACCEPT` rule ahead of them, and the packet counters on the `--dport` rules stayed at 0. The declaration is kept because that subnet detection is gluetun's behaviour, not a guarantee of the chart's — a CNI handing out a `/32`, or a future gluetun dropping the rule, would make these the only thing keeping probes and ingress alive.
|
||||||
|
|
||||||
|
### If the pod will not become Ready, suspect the tunnel rule
|
||||||
|
|
||||||
|
gluetun's health server answers 200 **only while the tunnel is up**, and all three of its probes address it — so a blocked UDP 51820 shows up as a pod that never goes `Ready` rather than as a network error. That also makes it the best-covered rule in the chart: the install-test brings up a **real** tunnel, so getting this wrong goes red in CI.
|
||||||
|
|
||||||
|
51820 is WireGuard's standard port and Mullvad's. Mullvad also offers **53, 123, 500 and 4500** for obfuscation, and a release on one of those has to widen the rule **in the template** — `networkPolicy.egress.extraPorts` is **TCP only**. That is a limit of the shared values shape rather than an oversight, and it is said plainly here because nothing else would say it.
|
||||||
|
|
||||||
|
### If Seerr cannot reach an in-cluster service, two things must be true
|
||||||
|
|
||||||
|
Its port in `networkPolicy.egress.extraPorts`, **and** `gluetun.vpn.firewallOutboundSubnets` set — gluetun's own firewall blocks cluster-internal egress before this policy is ever consulted. Jellyfin on 8096 is the usual case.
|
||||||
|
|
||||||
|
### Because the namespace belongs to gluetun, the egress cap is unusually meaningful
|
||||||
|
|
||||||
|
Everything the *arr containers send is encapsulated before it leaves, so three narrow rules really do describe the pod's whole outbound surface — which is rare in this repository, where most egress rules are a guess at a standard port.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Ten upstreams, no app version
|
## Ten upstreams, no app version
|
||||||
|
|
||||||
A documented exception to two repo-wide rules ([Versioning](Versioning), [Chart style guide](Chart-Style-Guide)):
|
A documented exception to two repo-wide rules ([Versioning](Versioning), [Chart style guide](Chart-Style-Guide)):
|
||||||
|
|||||||
+48
@@ -252,6 +252,54 @@ What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `Path
|
|||||||
|
|
||||||
**This is the one chart in the repo whose `backendRefs` port is rendered from a *value*.** `garage.ingress.yaml` references the Service port **by name** (`s3`) and is therefore immune to the number changing; `backendRefs` takes a `PortNumber` and cannot name a port, so a literal `3900` in the route would become a lie the moment `garage.ports.s3` is changed. The route renders the number from the same `garage.settings` helper the Service renders its port from, so the two cannot drift. The **admin** port is deliberately not routed in either mode: with this chart's configuration it serves `/health` and `/metrics` unauthenticated and answers everything else 401/403, which is a monitoring surface for the cluster, not something to publish.
|
**This is the one chart in the repo whose `backendRefs` port is rendered from a *value*.** `garage.ingress.yaml` references the Service port **by name** (`s3`) and is therefore immune to the number changing; `backendRefs` takes a `PortNumber` and cannot name a port, so a literal `3900` in the route would become a lie the moment `garage.ports.s3` is changed. The route renders the number from the same `garage.settings` helper the Service renders its port from, so the two cannot drift. The **admin** port is deliberately not routed in either mode: with this chart's configuration it serves `/health` and `/metrics` unauthenticated and answers everything else 401/403, which is a monitoring surface for the cluster, not something to publish.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Garage's four ports split three ways, which makes this the clearest illustration of the convention after `keycloak`:
|
||||||
|
|
||||||
|
- **s3** (3900) and **web** (3902): **restricted by source** — this namespace, `allowedNamespaces`, and the ingress controller. No probe touches either, which is what makes the restriction possible.
|
||||||
|
- **admin** (3903): the port all three probes address (`tcpSocket` twice and a `httpGet /health`), so it stays **open inside the cluster** — and it is also the **admin API**.
|
||||||
|
- **rpc** (3901): this release's **own pods only**. It has no Service at all.
|
||||||
|
|
||||||
|
Outbound: DNS and the rpc bus, nothing else. All four numbers are derived from `garage.settings`, so moving a port in values moves the policy with it.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
|
### Check your S3 consumers — this is the rule that will bite
|
||||||
|
|
||||||
|
Anything in **another namespace** that talks to this endpoint on its in-cluster Service address — a backup job, a Zipline storing uploads, a Stalwart blob store — is **blocked** until that namespace is in `networkPolicy.allowedNamespaces`. Consumers using the published Ingress name arrive through the controller and are already covered.
|
||||||
|
|
||||||
|
The failure is an application-level error on the *consumer's* side, with nothing pointing at this object.
|
||||||
|
|
||||||
|
### The admin API is the probed port
|
||||||
|
|
||||||
|
Key management and cluster layout live on 3903, and all three probes address it — so the cap is *reachable from inside the cluster*, not *closed*. `networkPolicy.probeSources` closes the rest, at the usual price of needing the worker node IPs.
|
||||||
|
|
||||||
|
### The rpc bus needs a rule in both directions
|
||||||
|
|
||||||
|
`policyTypes` lists `Egress`, so a node **dialling** its peer is subject to this policy too. With only the inbound rule the bus would be permitted one way and the layout would never converge, while every node looked healthy.
|
||||||
|
|
||||||
|
Not permitted, and the plausible surprise: **outbound HTTPS**. Garage does not need it to serve S3, but remote replication or an external metrics sink does — `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Traps
|
## Traps
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
|
|||||||
+33
@@ -80,6 +80,17 @@ curl http://localhost:3000/health
|
|||||||
|
|
||||||
An erased block (`gotenberg.securityContext:` with no children, a `null` probe, a `null` `resources`) falls back to the chart's own defaults instead of disappearing — [style guide §12.2](Chart-Style-Guide#12-values-that-must-not-lie).
|
An erased block (`gotenberg.securityContext:` with no children, a `null` probe, a `null` `resources`) falls back to the chart's own defaults instead of disappearing — [style guide §12.2](Chart-Style-Guide#12-values-that-must-not-lie).
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **3000** only. Outbound **DNS and nothing else** — and that is a deliberate, costly trade rather than an oversight; see the operating note. There is no `ingressController` value, because this chart renders no Ingress.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### No authentication, and this chart cannot add any
|
### No authentication, and this chart cannot add any
|
||||||
@@ -113,6 +124,28 @@ Since both limits are set explicitly, raising the matching request no longer dra
|
|||||||
|
|
||||||
The repo's resource-quantity render test injects a bare integer memory request into every `resources` block and checks the multiplied limit. Gotenberg sets **every computable limit explicitly in its fallback defaults**, and since a `null` never deletes a default there, no override can free them — nothing is multiplied, so nothing is proven. The job therefore lists this chart under **"helper not reached"** rather than counting it as a green tick for a code path that was never entered. That is the intended outcome, not a defect; see [CI/CD](CI-CD).
|
The repo's resource-quantity render test injects a bare integer memory request into every `resources` block and checks the multiplied limit. Gotenberg sets **every computable limit explicitly in its fallback defaults**, and since a `null` never deletes a default there, no override can free them — nothing is multiplied, so nothing is proven. The job therefore lists this chart under **"helper not reached"** rather than counting it as a green tick for a code path that was never entered. That is the intended outcome, not a defect; see [CI/CD](CI-CD).
|
||||||
|
|
||||||
|
### Egress is DNS-only on purpose, and it costs the remote-asset half of HTML conversion
|
||||||
|
|
||||||
|
Gotenberg drives a **headless Chromium**, and 3000 is the port the probes address — so it is reachable from the whole cluster by default. An egress rule for 80/443 would therefore turn a service anyone can POST to into a way of fetching arbitrary URLs: a textbook SSRF pivot.
|
||||||
|
|
||||||
|
The price of blocking it is real and silent. HTML conversion keeps inline content and **loses images the page would have fetched** — a degraded PDF, not a failed pod, so nothing will alert on it. A `paperless-ngx` that only converts mail bodies does not need them; a release that converts URLs or needs remote assets sets `networkPolicy.egress.extraPorts: [80, 443]` **knowingly**.
|
||||||
|
|
||||||
|
Same as Tika: this chart has no Ingress, so narrowing `probeSources` means putting the consumers' namespaces in `allowedNamespaces` in the same change, or every conversion stops.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
Nothing to back up and nothing to migrate: no volume, no database, no state that survives a pod. An app-version bump is an image swap, and `helm rollback` carries no data risk. After a bump, `curl /health` is the one check worth running — it names the engine that did not come back. Renovate follows `gotenberg/gotenberg` through this chart's `customManagers` block with default docker versioning; it was one of the two charts whose PRs proved the Gitea-side Renovate instance really reads our config — see [Renovate](Renovate).
|
Nothing to back up and nothing to migrate: no volume, no database, no state that survives a pod. An app-version bump is an image swap, and `helm rollback` carries no data risk. After a bump, `curl /health` is the one check worth running — it names the engine that did not come back. Renovate follows `gotenberg/gotenberg` through this chart's `customManagers` block with default docker versioning; it was one of the two charts whose PRs proved the Gitea-side Renovate instance really reads our config — see [Renovate](Renovate).
|
||||||
|
|||||||
+32
@@ -182,6 +182,16 @@ These five are declared in `values.yaml` and in `values.schema.json` (the schema
|
|||||||
| `tolerations` | list | `[]` | Not rendered. |
|
| `tolerations` | list | `[]` | Not rendered. |
|
||||||
| `affinity` | object | `{}` | Not rendered. |
|
| `affinity` | object | `{}` | Not rendered. |
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **8123** only — anything else this image or an add-on binds is not reachable. **There is no egress section at all**, and that is the decision in this chart rather than an omission; see the operating note. There is no `ingressController` value either, because this chart keeps its flexible upstream ingress style and renders no Ingress of its own.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### The Ingress exception: nothing here requests a certificate
|
### The Ingress exception: nothing here requests a certificate
|
||||||
@@ -303,6 +313,28 @@ Every pod of the StatefulSet mounts the **same** RWO claim at `/config` — ther
|
|||||||
|
|
||||||
This is the only chart in the repository with a `templates/tests/` directory, but its `test-connection.yaml` is **entirely commented out** — the whole manifest sits inside `{{/* … */}}`. Nothing is rendered, no `helm.sh/hook: test` object exists, and `helm test <release>` finds nothing to run. The file is a placeholder from the upstream scaffold, not a working smoke test.
|
This is the only chart in the repository with a `templates/tests/` directory, but its `test-connection.yaml` is **entirely commented out** — the whole manifest sits inside `{{/* … */}}`. Nothing is rendered, no `helm.sh/hook: test` object exists, and `helm test <release>` finds nothing to run. The file is a placeholder from the upstream scaffold, not a working smoke test.
|
||||||
|
|
||||||
|
### Why there is no egress restriction
|
||||||
|
|
||||||
|
Home Assistant's outbound traffic is the **union of every integration an operator enables**: 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. There is no version of that list which stays correct.
|
||||||
|
|
||||||
|
A cap would also protect very little — 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.
|
||||||
|
|
||||||
|
So the rule `valkey` established 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. An installation that wants a cap should write one at **release** level, against its own integration list.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **`/config` is the whole of the state.** The owner account, every long-lived token, all UI-created automations and dashboards, every integration's stored credentials and the recorder database live there — on `<release>-homeassistant-pvc`, inside its subPath `hass-config`. Only the three files from the vault are reproducible from the chart's values; nothing else is.
|
- **`/config` is the whole of the state.** The owner account, every long-lived token, all UI-created automations and dashboards, every integration's stored credentials and the recorder database live there — on `<release>-homeassistant-pvc`, inside its subPath `hass-config`. Only the three files from the vault are reproducible from the chart's values; nothing else is.
|
||||||
|
|||||||
+43
-1
@@ -15,7 +15,7 @@ What it costs is the thing to know before the first upgrade: **Keycloak supports
|
|||||||
| Routing | **two mutually exclusive modes**, Ingress (default) or Gateway API (`httpRoute.enabled`) — and **two objects in either mode**: the public hostname and the admin hostname (switchable off), **plus one object per `keycloak.realmHosts` entry** — empty by default — which gives a realm a hostname of its own (see [One hostname per realm](#one-hostname-per-realm)). The admin one is **not** an access control — see [the first trap](#the-admin-hostname-is-not-an-access-control). In Gateway mode the **proxy-buffer annotations this installation depends on are gone** and have no counterpart — see [Routing: Ingress or Gateway API](#routing-ingress-or-gateway-api) |
|
| Routing | **two mutually exclusive modes**, Ingress (default) or Gateway API (`httpRoute.enabled`) — and **two objects in either mode**: the public hostname and the admin hostname (switchable off), **plus one object per `keycloak.realmHosts` entry** — empty by default — which gives a realm a hostname of its own (see [One hostname per realm](#one-hostname-per-realm)). The admin one is **not** an access control — see [the first trap](#the-admin-hostname-is-not-an-access-control). In Gateway mode the **proxy-buffer annotations this installation depends on are gone** and have no counterpart — see [Routing: Ingress or Gateway API](#routing-ingress-or-gateway-api) |
|
||||||
| External dependencies | an external PostgreSQL. Nothing else |
|
| External dependencies | an external PostgreSQL. Nothing else |
|
||||||
| Secrets | 2 × `OnePasswordItem`, four fields in total |
|
| Secrets | 2 × `OnePasswordItem`, four fields in total |
|
||||||
| Network | ClusterIP Service on 8080 (`http`) and a **separate** ClusterIP Service on 9000 (`management`). The management port serves `/health/*` and `/metrics` **unauthenticated** and is in no Ingress. JGroups uses 7800 pod-to-pod; no NetworkPolicy yet |
|
| Network | ClusterIP Service on 8080 (http) + a second on 9000 (management), up to four Ingresses, plus a **NetworkPolicy on by default** — the only one in the repo with a source-restricted http rule |
|
||||||
| Disruption | `PodDisruptionBudget`, `minAvailable: 1` — the **first** in this repo, and now a convention ([style guide §11](Chart-Style-Guide#11-scaling)) |
|
| Disruption | `PodDisruptionBudget`, `minAvailable: 1` — the **first** in this repo, and now a convention ([style guide §11](Chart-Style-Guide#11-scaling)) |
|
||||||
| Install-test | `ci/keycloak/` — fixtures + test values, **with two replicas** and a throwaway PostgreSQL |
|
| Install-test | `ci/keycloak/` — fixtures + test values, **with two replicas** and a throwaway PostgreSQL |
|
||||||
|
|
||||||
@@ -331,6 +331,24 @@ The Gateway-API alternative to the two Ingresses, **off by default**. Added with
|
|||||||
| `httpRoute.admin.parentRefs` | list | `[]` | The **admin** route's own parent. Empty falls back to `httpRoute.parentRefs` — the exact counterpart of `ingress.admin.className` falling back to `ingress.className`, because "which data path" is a property of the **Gateway** in this API rather than of the route. An internal Gateway here is what an internal ingress class is in the other mode, and still not the access control the first trap is about. |
|
| `httpRoute.admin.parentRefs` | list | `[]` | The **admin** route's own parent. Empty falls back to `httpRoute.parentRefs` — the exact counterpart of `ingress.admin.className` falling back to `ingress.className`, because "which data path" is a property of the **Gateway** in this API rather than of the route. An internal Gateway here is what an internal ingress class is in the other mode, and still not the access control the first trap is about. |
|
||||||
| `httpRoute.admin.hostnames` | list | `[]` | Empty means `[ingress.admin.domain]`. |
|
| `httpRoute.admin.hostnames` | list | `[]` | Empty means `[ingress.admin.domain]`. |
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
**The richest policy in the repository, and the one with the largest blast radius.** Keycloak is the only chart whose three ports fall into three different classes, so it is where the convention is visible in full:
|
||||||
|
|
||||||
|
- **http** (`keycloak.ports.http`, 8080): **restricted by source** — this namespace, `allowedNamespaces`, and the ingress controller. The only genuinely source-restricted inbound rule in the whole sweep, and possible *only* because no probe uses this port.
|
||||||
|
- **management** (9000): the port all three probes address, so it stays **open inside the cluster**. This is the surface that carries health and metrics **unauthenticated**.
|
||||||
|
- **jgroups** (7800): this release's **own pods only** — a `podSelector` with no `namespaceSelector`, the narrowest rule the object can express.
|
||||||
|
|
||||||
|
Outbound: DNS, the **configured** database port (`keycloak.db.port` — this chart does not have to guess), the cluster bus, and 443 for identity brokering.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -533,6 +551,30 @@ Discovery runs through the database, but once the peers are known JGroups opens
|
|||||||
|
|
||||||
This chart renders **no** NetworkPolicy. If one is ever added, it has to allow 7800 between the replicas, or the cluster quietly becomes two islands with two green pods.
|
This chart renders **no** NetworkPolicy. If one is ever added, it has to allow 7800 between the replicas, or the cluster quietly becomes two islands with two green pods.
|
||||||
|
|
||||||
|
### Two things to check after the first upgrade, because nothing else will
|
||||||
|
|
||||||
|
**1. Log in.** This release is the cluster's identity provider, and the http rule is the only source-restricted inbound rule in the repository. If the `ingressController` source is wrong for this cluster, **every login everywhere stops** — and no part of CI can see it: no ingress controller runs in the install-test, and the probes use the management port, so the pod becomes `Ready` either way. The default was measured against the live cluster (DaemonSet `rke2-traefik` in `kube-system`, which serves the Gateway API here too), but measured labels are not proven traffic.
|
||||||
|
|
||||||
|
**2. Check your consumers' issuer URLs.** An application that reaches Keycloak on its **in-cluster Service address** rather than its public URL is a pod in another namespace, and the http rule blocks it. Those namespaces go into `networkPolicy.allowedNamespaces`. Consumers using the public issuer URL arrive through Traefik and are already covered.
|
||||||
|
|
||||||
|
### The management port is the probed one, which is unfortunate here specifically
|
||||||
|
|
||||||
|
9000 carries the health and readiness endpoints **and the metrics**, unauthenticated, and before this object existed the only thing protecting it was that no Ingress pointed at it. Because all three probes address it, it cannot be restricted by source without the worker node's IP — so the policy improves it from *anything that can route to the pod* to *anything inside the cluster*, and `networkPolicy.probeSources` is what closes the rest.
|
||||||
|
|
||||||
|
### The cluster bus needs a rule in both directions
|
||||||
|
|
||||||
|
`policyTypes` lists `Egress`, so a replica **dialling** its peer is subject to this policy too. The chart therefore renders jgroups inbound *and* outbound — without the second one the bus would be permitted one way only, and the result is a cluster that never forms while both nodes look healthy. Infinispan discovery here is `jdbc-ping`, so a blocked database costs the cluster as well as the data.
|
||||||
|
|
||||||
|
### Mail is not allowed
|
||||||
|
|
||||||
|
Keycloak's SMTP server is configured **per realm**, which this chart never sees, so verification and password-reset mail needs its port (587 or 465) in `networkPolicy.egress.extraPorts`. The symptom otherwise is mail that is never delivered, with a healthy pod behind it.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Patch updates within a minor line** are the normal case and arrive as Renovate app-version PRs. Run the compatibility check anyway — see the unresolved doc contradiction in [Minor upgrades](#minor-upgrades-need-the-cluster-down).
|
- **Patch updates within a minor line** are the normal case and arrive as Renovate app-version PRs. Run the compatibility check anyway — see the unresolved doc contradiction in [Minor upgrades](#minor-upgrades-need-the-cluster-down).
|
||||||
|
|||||||
@@ -157,6 +157,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
| `httpRoute.delegation.parentRefs` | list | `[]` | The delegation route's own parent. Empty falls back to `httpRoute.parentRefs`. Worth setting, because the server name is usually an apex domain published by a **different** Gateway — somebody else's website normally owns it. |
|
| `httpRoute.delegation.parentRefs` | list | `[]` | The delegation route's own parent. Empty falls back to `httpRoute.parentRefs`. Worth setting, because the server name is usually an apex domain published by a **different** Gateway — somebody else's website normally owns it. |
|
||||||
| `httpRoute.delegation.hostnames` | list | `[]` | Empty means `[matrix.delegation.serverUrl]`, and it deliberately does **not** fall back to `httpRoute.hostnames`: the server name is what *remote* homeservers resolve and is a different thing from the homeserver's own hostname. The migration order depends on telling them apart — move the homeserver host first, confirm it answers, move the server name last. A chart-global list covering both would erase that distinction. |
|
| `httpRoute.delegation.hostnames` | list | `[]` | Empty means `[matrix.delegation.serverUrl]`, and it deliberately does **not** fall back to `httpRoute.hostnames`: the server name is what *remote* homeservers resolve and is a different thing from the homeserver's own hostname. The migration order depends on telling them apart — move the homeserver host first, confirm it answers, move the server name last. A chart-global list covering both would erase that distinction. |
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
**Two objects, one per workload.** Synapse: inbound **8008** only; outbound DNS, TCP **5432** (Postgres), **443 and 8448** (federation, remote media, push gateways, identity servers). The delegation nginx: inbound **8080** only; outbound **DNS and nothing else**. One `probeSources` and one `ingressController` govern both; `egress.extraPorts` deliberately reaches **only** Synapse.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -275,6 +287,34 @@ Everything else the process writes goes to `/tmp`, an emptyDir that counts again
|
|||||||
|
|
||||||
Every pod of the StatefulSet mounts the same RWO claim and the same `homeserver.yaml` — there is no `volumeClaimTemplate` and no per-pod configuration. Additional pods either stay `Pending` on a node without the volume, or start as a second full homeserver on the same data. Scaling Synapse means running workers with distinct configurations, which this chart does not deploy.
|
Every pod of the StatefulSet mounts the same RWO claim and the same `homeserver.yaml` — there is no `volumeClaimTemplate` and no per-pod configuration. Additional pods either stay `Pending` on a node without the volume, or start as a second full homeserver on the same data. Scaling Synapse means running workers with distinct configurations, which this chart does not deploy.
|
||||||
|
|
||||||
|
### Why two policies and not one
|
||||||
|
|
||||||
|
The two workloads' needs are opposite. Synapse federates and must be able to reach any homeserver there is; the delegation nginx returns two static JSON documents and calls nobody. A single policy selecting both pods would have to permit the **union** — handing full federation reach and the database port to an nginx whose entire job is to return a constant — and it would read as one decision rather than as a widening.
|
||||||
|
|
||||||
|
`networkPolicy.egress.extraPorts` does not reach the delegation policy for the same reason: an entry meant for Synapse's database has no business widening that.
|
||||||
|
|
||||||
|
### Federation cannot be narrowed, and narrowing the probe ports can stop it
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination. So 443 and 8448 go to **any** destination — which is not laziness but the specification: a federating homeserver may contact any other one. What the cap still buys is that Synapse can open **no other** outbound port.
|
||||||
|
|
||||||
|
And mind what the two inbound ports carry. Inbound federation reaches Synapse through the Ingress, and every remote homeserver that wants to federate fetches `/.well-known/matrix` from the delegation server. Setting `probeSources` without a correct `ingressController` therefore does not merely hide a web UI: it stops federation **and stops it being discoverable**, and that failure appears on **other people's servers** before it appears on yours.
|
||||||
|
|
||||||
|
5432 is the standard Postgres port, not the configured one — the connection lives inside `secrets.configSecretRef`, which this chart never parses.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up `/data` before every app-version bump.** Schema migrations run at start and are one-way; a `helm rollback` returns the old image to a database the new one has already rewritten.
|
- **Back up `/data` before every app-version bump.** Schema migrations run at start and are one-way; a `helm rollback` returns the old image to a database the new one has already rewritten.
|
||||||
|
|||||||
@@ -86,6 +86,20 @@ mappings:
|
|||||||
|
|
||||||
Probe, `securityContext` and `resources` blocks may be erased (`null`) — the chart then falls back to exactly these defaults instead of rendering an unhardened, unprobed or unaccounted container. `mappings` is deliberately **not** null-safe: it fails instead.
|
Probe, `securityContext` and `resources` blocks may be erased (`null`) — the chart then falls back to exactly these defaults instead of rendering an unhardened, unprobed or unaccounted container. `mappings` is deliberately **not** null-safe: it fails instead.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound: the **proxy port** (`server.port`) **open** — it is published as a NodePort and the senders are players on the internet — and the **API port** (`server.apiPort`), which is the probe path. Outbound: DNS plus the backend ports **derived from the routing table itself** — `server.backendPort` and the `port` override of every `mappings` entry that sets one.
|
||||||
|
|
||||||
|
That makes this one of the few egress caps here that follows the configuration instead of guessing at it: add a backend on a non-standard port and the policy moves with it.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### The hostname routes, not the port
|
### The hostname routes, not the port
|
||||||
@@ -133,6 +147,32 @@ mc-router keeps no state between connections and every replica mounts the same C
|
|||||||
|
|
||||||
The chart passes `--port` and `--api-binding` as command-line arguments, and those take precedence over the matching environment variables — `PORT` or `API_BINDING` set in `server.env` change nothing. Use `server.port` and `server.apiPort`. Everything else mc-router accepts as a flag is settable here (`LOG_LEVEL`, `BACKEND_DIAL_TIMEOUT`, `CONNECTION_RATE_LIMIT`), because it parses its flags with go-flagsfiller. A plain `value` is stored in the pod spec in clear text; anything sensitive belongs in `valueFrom.secretKeyRef`.
|
The chart passes `--port` and `--api-binding` as command-line arguments, and those take precedence over the matching environment variables — `PORT` or `API_BINDING` set in `server.env` change nothing. Use `server.port` and `server.apiPort`. Everything else mc-router accepts as a flag is settable here (`LOG_LEVEL`, `BACKEND_DIAL_TIMEOUT`, `CONNECTION_RATE_LIMIT`), because it parses its flags with go-flagsfiller. A plain `value` is stored in the pod spec in clear text; anything sensitive belongs in `valueFrom.secretKeyRef`.
|
||||||
|
|
||||||
|
### The backend rule is by port, not by destination
|
||||||
|
|
||||||
|
`mappings` name **hosts** (`internalUrl`), and A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination. — so the rule permits those ports to **any** address, which is wider than your routing table. Anything else listening on a backend port is reachable through it too.
|
||||||
|
|
||||||
|
The upside is that adding a backend needs no change here: the policy reads the same value the router does.
|
||||||
|
|
||||||
|
### The policy names pod ports, never the NodePorts
|
||||||
|
|
||||||
|
A `NetworkPolicy` is evaluated at the **pod's** network interface, *after* kube-proxy has already translated the NodePort to the target port. So a rule naming the NodePort matches nothing there: the object would be accepted, would read as strict, and — being the only rule for that port — would block **every** inbound connection instead.
|
||||||
|
|
||||||
|
This is the easiest mistake to make in a NodePort chart, which is why `ci/run-networkpolicy-test.sh` asserts every ingress port against the `containerPort`s of the pod spec the policy selects, and flags a number in the NodePort range explicitly.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **There is nothing to back up.** The release creates no `OnePasswordItem`, mounts no Secret and owns no volume; the ConfigMap is rendered entirely from `mappings`. `helm uninstall` destroys nothing that is not already in your values file — the worlds live in the backend releases.
|
- **There is nothing to back up.** The release creates no `OnePasswordItem`, mounts no Secret and owns no volume; the ConfigMap is rendered entirely from `mappings`. `helm uninstall` destroys nothing that is not already in your values file — the worlds live in the backend releases.
|
||||||
|
|||||||
@@ -168,6 +168,22 @@ All three run the image's own `mc-health` script — a real Minecraft server-lis
|
|||||||
|
|
||||||
A change to either of the last two rolls the pod (`checksum/config` on the StatefulSet), which is what makes it take effect at all — `init.sh` is mounted with `subPath` and only read at pod start. The restart is not free: ~124 s measured for ftb-skies on Longhorn.
|
A change to either of the last two rolls the pod (`checksum/config` on the StatefulSet), which is what makes it take effect at all — `init.sh` is mounted with `subPath` and only read at pod start. The restart is not free: ~124 s measured for ftb-skies on Longhorn.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
**The tightest policy in the repository**, and the reason is its probes: all three are `exec` probes, which run inside the container and put **no packet on the network**. So there is no kubelet traffic to accommodate on any port, every port can be restricted by source, and **there is no `probeSources` value at all**.
|
||||||
|
|
||||||
|
- the **game port** (`server.port`): **open** when `server.service.type` is `NodePort` (players arrive from the internet, already masqueraded), and **restricted** to this namespace plus `allowedNamespaces` otherwise — which is the default. Both branches read the same value the Service does, so they cannot disagree.
|
||||||
|
- **RCON** (`server.rcon.port`, rendered only while `server.rcon.enabled`) and **metrics** (19565): always restricted by source.
|
||||||
|
|
||||||
|
Outbound: DNS and TCP 443 (Mojang's session server for online-mode, plus resource packs and mods).
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### The Java line is the chart's central fact
|
### The Java line is the chart's central fact
|
||||||
@@ -296,6 +312,22 @@ kubectl -n <namespace> logs <release>-minecraft-server-sfs-0 -c download-serverp
|
|||||||
|
|
||||||
Its closing `chown` reads the same pod security context helper as the pod spec (they used to be able to disagree in silence) and is explicitly non-fatal: an unprivileged process cannot chown foreign files, and under `set -e` a failure there would keep the whole pod from starting.
|
Its closing `chown` reads the same pod security context helper as the pod spec (they used to be able to disagree in silence) and is explicitly non-fatal: an unprivileged process cannot chown foreign files, and under `set -e` a failure there would keep the whole pod from starting.
|
||||||
|
|
||||||
|
### With a ClusterIP Service, the router's namespace must be allowed
|
||||||
|
|
||||||
|
In this cluster players reach the server through **`minecraft-router`**, which runs in its own namespace. With `server.service.type` left at `ClusterIP` the game port is restricted to this namespace, so that namespace has to be in `networkPolicy.allowedNamespaces` — otherwise the server is unreachable and the symptom is a **connection timeout in a game client with a perfectly healthy pod behind it**. The same applies to a Prometheus scraping 19565 from elsewhere.
|
||||||
|
|
||||||
|
### RCON is the rule with the most security value here, and it costs nothing
|
||||||
|
|
||||||
|
RCON is full administrative control of the server. It has no Service and no probe, so restricting it to this namespace is free — and it is gated on `server.rcon.enabled`, exactly as the `containerPort` is: a rule for a port the pod does not declare is one nobody can verify, and the render test rejects it.
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up the world before uninstalling, and before a line change.** The PVC is a chart object, so `helm uninstall` takes it with the release.
|
- **Back up the world before uninstalling, and before a line change.** The PVC is a chart object, so `helm uninstall` takes it with the release.
|
||||||
|
|||||||
+34
@@ -135,6 +135,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-outline-service` on port **80** — the port **number**, because `backendRefs` cannot name a port the way the Ingress backend does. One path covers UI, API and the collaborative WebSocket; the upgrade passes through a plain route, with no annotation involved in either mode.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-outline-service` on port **80** — the port **number**, because `backendRefs` cannot name a port the way the Ingress backend does. One path covers UI, API and the collaborative WebSocket; the upgrade passes through a plain route, with no annotation involved in either mode.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **3000** only. Outbound **DNS, TCP 5432** (Postgres), **6379** (Redis) and **443** (the OIDC provider's discovery document, fetched at boot).
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -193,6 +205,28 @@ No `SMTP_*` variable is set, so invitations, notifications and digest mails are
|
|||||||
|
|
||||||
Pinned next to the connection URL. That fits a Postgres reached over the cluster network; anything less trusted needs TLS asked for explicitly, and the chart has no value for it — use `outline.env`.
|
Pinned next to the connection URL. That fits a Postgres reached over the cluster network; anything less trusted needs TLS asked for explicitly, and the chart has no value for it — use `outline.env`.
|
||||||
|
|
||||||
|
### The egress ports are the standard ones, not your configured ones
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
Worse, the chart does not even know the port: the connection details arrive inside 1Password items it never parses. So the rules name the **conventional** ports, and a dependency listening anywhere else is **blocked**. Put the real port in `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
All three probes hit `/_health`, which checks **Postgres and Redis** — so a blocked data store here does not show up as a connection error but as a pod that **never becomes `Ready`**. That is also what made this chart the one used to prove the install-test enforces policies at all: with its DNS egress rule removed, the CI job failed after 5m59s instead of going green.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up the database before every app-version bump.** Migrations are one-way; a `helm rollback` returns the old image to a database the new one has already rewritten.
|
- **Back up the database before every app-version bump.** Migrations are one-way; a `helm rollback` returns the old image to a database the new one has already rewritten.
|
||||||
|
|||||||
+47
@@ -258,6 +258,18 @@ The first draft of that check was a `fail()` in `_helpers.tpl` called from a ren
|
|||||||
|
|
||||||
**`scaleDown` and `checksum/config` stop at the subchart boundary.** `scaleDown: true` scales paperless to zero and leaves the Valkey running — which is the right way round for a queue, because the backlog stays where it is. And a new valkey chart release reaches a cluster only when **paperless-ngx** is republished.
|
**`scaleDown` and `checksum/config` stop at the subchart boundary.** `scaleDown: true` scales paperless to zero and leaves the Valkey running — which is the right way round for a queue, because the backlog stays where it is. And a new valkey chart release reaches a cluster only when **paperless-ngx** is republished.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **8000** only. Outbound **DNS, TCP 5432** (Postgres), **6379** (Redis/Valkey), **443**, and — only while `paperless.files.tikaGotenberg.enabled` is true — **80** for the Tika and Gotenberg **Services**.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -496,6 +508,41 @@ Nothing about OIDC is exercised before someone actually signs in, so a wrong dis
|
|||||||
|
|
||||||
The same mechanism is the reason `httpRoute.hostnames` must not disagree with `ingress.domain`: a route published under a name Django does not trust produces exactly this picture — pages load, logins fail — except that nothing about it looks like a port-forward. See [Routing](#routing-ingress-or-gateway-api).
|
The same mechanism is the reason `httpRoute.hostnames` must not disagree with `ingress.domain`: a route published under a name Django does not trust produces exactly this picture — pages load, logins fail — except that nothing about it looks like a port-forward. See [Routing](#routing-ingress-or-gateway-api).
|
||||||
|
|
||||||
|
### Why that rule says 80 and not 9998/3000
|
||||||
|
|
||||||
|
An **egress** rule names the port on the **destination as the client dials it**, and paperless dials those charts' *Services*, which listen on 80 and forward to the containers' own 9998 and 3000. Addressing a container port directly instead needs that port in `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
### Mail fetching is not permitted
|
||||||
|
|
||||||
|
Paperless' mail accounts are **rows in its database**, configured in the web UI, so this chart cannot see them. A release that fetches mail needs IMAP (993, or 143) in `networkPolicy.egress.extraPorts` — otherwise a mail rule silently stops importing, with a healthy pod behind it.
|
||||||
|
|
||||||
|
### Two pods of this release are not covered by this object
|
||||||
|
|
||||||
|
- **The `valkey` subchart's pod** carries its own labels, so this policy does not select it. It needs no cover from here: the `valkey` chart ships its own NetworkPolicy, and a *tighter* one — Ingress-only with 6379 restricted by source, which it can do because its probes are `exec` probes.
|
||||||
|
- **The consume CronJob's pods** have a pod template with **no labels at all**, so no `podSelector` can match them and nothing here restricts them. That gap is real. It is tolerable only because the job moves files between mounted volumes and reaches the SMB share through a **CSI volume the node mounts** — the pod opens no connection of its own. If that ever changes, the CronJob needs labels before it can need a policy.
|
||||||
|
|
||||||
|
### The egress ports are the standard ones, not your configured ones
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
Worse, the chart does not even know the port: the connection details arrive inside 1Password items it never parses. So the rules name the **conventional** ports, and a dependency listening anywhere else is **blocked**. Put the real port in `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up before every app-version bump, and back up both halves.** Postgres holds the metadata, the tags and one row per document; `<release>-paperless-ngx-volume-claim` holds the files under `data/`, `media/` and `export/`. Restoring one without the other gives an archive full of references to documents that are not there.
|
- **Back up before every app-version bump, and back up both halves.** Postgres holds the metadata, the tags and one row per document; `<release>-paperless-ngx-volume-claim` holds the files under `data/`, `media/` and `export/`. Restoring one without the other gives an archive full of references to documents that are not there.
|
||||||
|
|||||||
+34
@@ -155,6 +155,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-patchmon-service` on port **80**. One path covers everything, for the same reason the Ingress needs only one.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-patchmon-service` on port **80**. One path covers everything, for the same reason the Ingress needs only one.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **3000** only. Outbound **DNS, TCP 5432** (Postgres), **6379** (Redis) and **443** (the OIDC provider).
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -232,6 +244,28 @@ Rotating `jwt-secret` or `session-secret` invalidates existing sessions. Rotatin
|
|||||||
|
|
||||||
With `patchmon.guacd.enabled: true` the daemon runs beside the server and is reached at `127.0.0.1:4822`; it listens on the pod only and nothing exposes 4822 outside. The image is pinned separately from `appVersion` and needs its own review at upgrade time — see [Renovate](Renovate), which tracks it through the built-in `helm-values` manager.
|
With `patchmon.guacd.enabled: true` the daemon runs beside the server and is reached at `127.0.0.1:4822`; it listens on the pod only and nothing exposes 4822 outside. The image is pinned separately from `appVersion` and needs its own review at upgrade time — see [Renovate](Renovate), which tracks it through the built-in `helm-values` manager.
|
||||||
|
|
||||||
|
### The egress ports are the standard ones, not your configured ones
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
Worse, the chart does not even know the port: the connection details arrive inside 1Password items it never parses. So the rules name the **conventional** ports, and a dependency listening anywhere else is **blocked**. Put the real port in `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
All three of this chart's dependencies arrive that way, so all three ports are conventions rather than configuration.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up the database before every app-version bump.** Migrations run at boot and are one-way; a `helm rollback` returns the old image to a database the new one has already rewritten. The chart itself holds no state at all, so the database plus the four 1Password items *are* the backup.
|
- **Back up the database before every app-version bump.** Migrations run at boot and are one-way; a `helm rollback` returns the old image to a database the new one has already rewritten. The chart itself holds no state at all, so the database plus the four 1Password items *are* the backup.
|
||||||
|
|||||||
+40
@@ -151,6 +151,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-rallly-service` on port **80**. The Next.js server serves the app, its API routes and `/api/auth/*` from one process.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-rallly-service` on port **80**. The Next.js server serves the app, its API routes and `/api/auth/*` from one process.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **3000** only. Outbound **DNS, TCP 5432** (Postgres), **443** (the OIDC provider) and — **only while `rallly.smtp.enabled` is true** — 587 and 465 for mail submission.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -217,6 +229,34 @@ The cache is an emptyDir on purpose, not a PVC: its entries are derived data tie
|
|||||||
|
|
||||||
The container context drops all capabilities and adds none — and the `add:` key is left out entirely rather than written empty. An empty list item under it renders as a null entry, which is what Trivy reported as `AVD-KSV-0022` for the older chart version deployed in the cluster (#84). Do not reintroduce it.
|
The container context drops all capabilities and adds none — and the `add:` key is left out entirely rather than written empty. An empty list item under it renders as a null entry, which is what Trivy reported as `AVD-KSV-0022` for the older chart version deployed in the cluster (#84). Do not reintroduce it.
|
||||||
|
|
||||||
|
### The SMTP rule is derived from `rallly.smtp.enabled`
|
||||||
|
|
||||||
|
With mail switched off no SMTP rule is rendered at all. That is deliberate: a rule that permits nothing makes the object look stricter than it is, so `policyTypes` and the rules name only what there is something to restrict.
|
||||||
|
|
||||||
|
With it on, **both** conventional submission ports are allowed — 587 (STARTTLS) and 465 (implicit TLS) — because the host and port live in `secrets.smtpSecretRef`, which the chart never parses, so it cannot tell which one the server uses. A mail server on **25 or 2525 is blocked**, and the symptom is invitations that are never delivered with a healthy pod behind them.
|
||||||
|
|
||||||
|
### The egress ports are the standard ones, not your configured ones
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
Worse, the chart does not even know the port: the connection details arrive inside 1Password items it never parses. So the rules name the **conventional** ports, and a dependency listening anywhere else is **blocked**. Put the real port in `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **All state is in the external Postgres.** The release itself owns no volume, so `helm uninstall` loses nothing but the Kubernetes objects — and a database backup is the only backup there is. Take one before an app-version bump: a `helm rollback` returns the old image, never the old database.
|
- **All state is in the external Postgres.** The release itself owns no volume, so `helm uninstall` loses nothing but the Kubernetes objects — and a database backup is the only backup there is. Take one before an app-version bump: a `helm rollback` returns the old image, never the old database.
|
||||||
|
|||||||
+32
@@ -126,6 +126,18 @@ The Gateway-API alternative, off by default. `rss-bridge` was the **last** chart
|
|||||||
|
|
||||||
Like every other block this chart dereferences, `httpRoute` is null-safe: an erased `httpRoute:` is YAML null and means "chart defaults apply", which collapses to "no `HTTPRoute`" rather than aborting with a nil pointer.
|
Like every other block this chart dereferences, `httpRoute` is null-safe: an erased `httpRoute:` is YAML null and means "chart defaults apply", which collapses to "no `HTTPRoute`" rather than aborting with a nil pointer.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **80** (the container's own port) only. Outbound **DNS plus TCP 80 and 443 to any destination** — fetching other people's websites *is* the application.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing modes
|
### Routing modes
|
||||||
@@ -252,6 +264,26 @@ The cache is deliberately lost on every restart. RSS-Bridge refills it on demand
|
|||||||
|
|
||||||
The template quotes every `value` because a value like `"5"` or `"true"` renders unquoted as a YAML number or bool, and the API server then rejects the whole manifest with `cannot unmarshal number into Go struct field EnvVar.value of type string`. RSS-Bridge takes numeric settings this way (`RSSBRIDGE_http_timeout`), so this is reachable with the very first env entry someone adds. The schema accepts strings only.
|
The template quotes every `value` because a value like `"5"` or `"true"` renders unquoted as a YAML number or bool, and the API server then rejects the whole manifest with `cannot unmarshal number into Go struct field EnvVar.value of type string`. RSS-Bridge takes numeric settings this way (`RSSBRIDGE_http_timeout`), so this is reachable with the very first env entry someone adds. The schema accepts strings only.
|
||||||
|
|
||||||
|
### The egress rule is the open internet on two ports, and it has to be
|
||||||
|
|
||||||
|
A feed bridge that cannot reach the sites it bridges does nothing, and A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination. So "only the configured bridges" cannot be written at all, and the rule permits 80 and 443 to **any** address. 80 as well as 443, because plenty of bridged sources are still plain HTTP.
|
||||||
|
|
||||||
|
Said plainly so the object does not read stricter than it is. What the cap still buys is narrow but real: this pod can open **no other** outbound port — no database, no SMTP, no SSH anywhere in the cluster.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
Nothing to back up: no database, no volumes, no user data. The feed cache is disposable by design, so `helm rollback` and app-version bumps carry no data risk.
|
Nothing to back up: no database, no volumes, no user data. The feed cache is disposable by design, so `helm rollback` and app-version bumps carry no data risk.
|
||||||
|
|||||||
+34
@@ -112,6 +112,16 @@ The whole `resources` and `securityContext` blocks, and each probe, may also be
|
|||||||
|
|
||||||
There are no `ingress.*` values: this chart renders no Ingress.
|
There are no `ingress.*` values: this chart renders no Ingress.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **445 and 139** — the **pod** ports — open to any source, and outbound **DNS and nothing else**.
|
||||||
|
|
||||||
|
**`enabled` is the only value here, and that is deliberate.** Both inbound rules are open to any source, so `allowedNamespaces` and `probeSources` would reach no rule at all — and a value that silently does nothing is worse than one that is absent, because it reads as a control.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Access is a node port, not 445
|
### Access is a node port, not 445
|
||||||
@@ -220,6 +230,30 @@ With `samba.shares` empty, `smb.conf` carries the `[global]` section and not a s
|
|||||||
|
|
||||||
Every replica mounts the same ReadWriteOnce claims; there is no per-pod volume. Unless the storage class hands those claims to several nodes at once, the extra pods never start, and where they do, two independent `smbd` instances write the same files with separate lock databases — which is how SMB corrupts data. The Service would hand clients to either pod at random.
|
Every replica mounts the same ReadWriteOnce claims; there is no per-pod volume. Unless the storage class hands those claims to several nodes at once, the extra pods never start, and where they do, two independent `smbd` instances write the same files with separate lock databases — which is how SMB corrupts data. The Service would hand clients to either pod at random.
|
||||||
|
|
||||||
|
### Both inbound rules are open on purpose
|
||||||
|
|
||||||
|
The ports are published as NodePorts (`samba.smbNodePort`, `samba.netbiosNodePort`) and the clients are machines on the LAN. After NodePort masquerading the source address is a cluster node, so a source selector could only ever be a list of node IPs — a restriction in appearance and a maintenance trap in fact. 445 is additionally the port all three probes open a socket against.
|
||||||
|
|
||||||
|
What the policy still buys: **only** 445 and 139 are reachable, so anything else this image binds is not.
|
||||||
|
|
||||||
|
Not covered, and it does not matter here: the NetBIOS **name-service** broadcasts on 137/138 UDP are not published by this chart's Service at all, and clients reach the share by address rather than by browse list.
|
||||||
|
|
||||||
|
### A domain-joined Samba needs more
|
||||||
|
|
||||||
|
Kerberos (88) and LDAP (389 or 636) in `networkPolicy.egress.extraPorts`. A standalone one needs nothing.
|
||||||
|
|
||||||
|
### The policy names pod ports, never the NodePorts
|
||||||
|
|
||||||
|
A `NetworkPolicy` is evaluated at the **pod's** network interface, *after* kube-proxy has already translated the NodePort to the target port. So a rule naming the NodePort matches nothing there: the object would be accepted, would read as strict, and — being the only rule for that port — would block **every** inbound connection instead.
|
||||||
|
|
||||||
|
This is the easiest mistake to make in a NodePort chart, which is why `ci/run-networkpolicy-test.sh` asserts every ingress port against the `containerPort`s of the pod spec the policy selects, and flags a number in the NodePort range explicitly.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back the share volumes up before an uninstall or a share rename** — both delete claims, and the chart sets no resource policy that would hold them.
|
- **Back the share volumes up before an uninstall or a share rename** — both delete claims, and the chart sets no resource policy that would hold them.
|
||||||
|
|||||||
+38
@@ -128,6 +128,16 @@ Whether it is still the right trade is argued in **issue #39**, not here. The ar
|
|||||||
|
|
||||||
Probe, `securityContext`, `game` and `resources` blocks may be erased (`null`) — the chart then falls back to exactly these defaults instead of rendering an unhardened, unprobed or unaccounted container. Since **7.0.1** that holds for **every** chart-owned block, `dev` and `server` included, so no erased block aborts the render any more. The one key that stays un-defaulted is [`server.storage`](#the-storage-exception-is-deliberate), and it is marked as an exception rather than left to look like an oversight.
|
Probe, `securityContext`, `game` and `resources` blocks may be erased (`null`) — the chart then falls back to exactly these defaults instead of rendering an unhardened, unprobed or unaccounted container. Since **7.0.1** that holds for **every** chart-owned block, `dev` and `server` included, so no erased block aborts the render any more. The one key that stays un-defaulted is [`server.storage`](#the-storage-exception-is-deliberate), and it is marked as an exception rather than left to look like an oversight.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound: the three declared game ports only — TCP **and** UDP on `server.gamePort`, and TCP on `server.messagingPort` — open to any source, because they are NodePorts and the clients are players on the internet. **There is no egress section at all.**
|
||||||
|
|
||||||
|
**`enabled` is the only value**, and deliberately: the single inbound rule is open to any source and there is no egress section, so `allowedNamespaces` and `probeSources` would both reach no rule — and a value that does nothing reads as a control it is not.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### The node ports are pinned, so a second release collides
|
### The node ports are pinned, so a second release collides
|
||||||
@@ -188,6 +198,34 @@ Two mechanics come with the fix, and they generalize beyond this chart ([style g
|
|||||||
|
|
||||||
Back up `/config` before setting `game.experimental: true`. A save opened by an experimental build may not open on the stable build again, and setting the flag back re-downloads the stable files but does not undo what happened to the save. With `SKIPUPDATE` overridden the branch switch does not take effect at all — the two settings contradict each other.
|
Back up `/config` before setting `game.experimental: true`. A save opened by an experimental build may not open on the stable build again, and setting the flag back re-downloads the stable files but does not undo what happened to the save. With `SKIPUPDATE` overridden the branch switch does not take effect at all — the two settings contradict each other.
|
||||||
|
|
||||||
|
### Why there is no egress restriction, and why that decision goes this way
|
||||||
|
|
||||||
|
Two reasons, and they compound.
|
||||||
|
|
||||||
|
**First**, what this server needs outbound is Steam's content delivery, and that is not a port list anyone can pin down: SteamCMD downloads and **updates** the game over TCP 80 and 443 plus the 27015–27050 range, and the master servers are reached over UDP 27000–27100 and 4380. Valve changes that set, the server re-downloads on every update, and a cap that is wrong leaves a server that cannot start — or worse, one that starts and never appears in the server browser.
|
||||||
|
|
||||||
|
**Second, and this is the deciding half**: `satisfactory` is the **one chart in this repository with no install-test at all** (`ci/excluded-charts.txt` — the image is multiple GB and the first start pulls over 10GB of game files). An egress cap here would be the only policy in the sweep with **no empirical check whatsoever** behind it, on the chart whose correct port set is least knowable. Review is the only net this chart has, and a review cannot verify a Steam port range.
|
||||||
|
|
||||||
|
So `policyTypes` names only what there is something to restrict, exactly as `valkey` does for the opposite reason. An installation that wants an egress cap should write one at **release** level, where Steam's behaviour can actually be observed.
|
||||||
|
|
||||||
|
What the Ingress half still buys is real: only the three declared game ports are reachable, so anything else the image binds is not.
|
||||||
|
|
||||||
|
### The game port is allowed as both TCP and UDP
|
||||||
|
|
||||||
|
The server binds it as both. A TCP-only rule would be accepted, would look complete, and would drop half the game traffic.
|
||||||
|
|
||||||
|
### The policy names pod ports, never the NodePorts
|
||||||
|
|
||||||
|
A `NetworkPolicy` is evaluated at the **pod's** network interface, *after* kube-proxy has already translated the NodePort to the target port. So a rule naming the NodePort matches nothing there: the object would be accepted, would read as strict, and — being the only rule for that port — would block **every** inbound connection instead.
|
||||||
|
|
||||||
|
This is the easiest mistake to make in a NodePort chart, which is why `ci/run-networkpolicy-test.sh` asserts every ingress port against the `containerPort`s of the pod spec the policy selects, and flags a number in the NodePort range explicitly.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up the volume before every app-version bump and before flipping `game.experimental`.** The save is the only copy of the world *and* of the admin password.
|
- **Back up the volume before every app-version bump and before flipping `game.experimental`.** The save is the only copy of the world *and* of the admin password.
|
||||||
|
|||||||
+32
@@ -146,6 +146,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-searxng-service` on port **80**. SearXNG serves its UI, `/search` and `/healthz` from one worker.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-searxng-service` on port **80**. SearXNG serves its UI, `/search` and `/healthz` from one worker.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **8080** only. Outbound **DNS, TCP 443** (the search engines — this *is* the application) and **6379** (the external Valkey the limiter uses).
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -248,6 +260,26 @@ The emptyDir at `/var/cache/searxng` is owned `root:977` (`fsGroup` only sets th
|
|||||||
|
|
||||||
and carries on. It cannot fix the ownership as a non-root process — which is the point — and does not need to: the directory is group-writable through `fsGroup`. Not a fault.
|
and carries on. It cannot fix the ownership as a non-root process — which is the point — and does not need to: the directory is group-writable through `fsGroup`. Not a fault.
|
||||||
|
|
||||||
|
### 443 goes anywhere, and 6379 is the standard port rather than yours
|
||||||
|
|
||||||
|
Querying search engines is what SearXNG does, and A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination. — so the 443 rule permits any destination and cannot be narrowed to an engine list.
|
||||||
|
|
||||||
|
6379 is the **conventional** Valkey port. This chart deploys no Valkey; `secrets.valkeySecretRef` points at an external one through a URL the chart never parses, so a Valkey anywhere else is blocked and belongs in `networkPolicy.egress.extraPorts`. Add 80 there too if an engine or proxy in use is plain HTTP.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Nothing to back up.** `settings.yml` comes from the ConfigMap, the signing key from the Secret, and both writable paths are emptyDirs that refill on their own. App-version upgrades and rollbacks carry no data risk.
|
- **Nothing to back up.** `settings.yml` comes from the ConfigMap, the signing key from the Secret, and both writable paths are emptyDirs that refill on their own. App-version upgrades and rollbacks carry no data risk.
|
||||||
|
|||||||
+5
-2
@@ -264,12 +264,15 @@ A **map**, not a list, so that an override never depends on a position. The key
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `networkPolicy.enabled` | `true` | Only a boundary where the CNI enforces one |
|
| `networkPolicy.enabled` | `true` | Only a boundary where the CNI enforces one |
|
||||||
| `networkPolicy.probeSources` | `[]` | CIDRs allowed to reach `http`. **Empty means open** — [why](#empty-probesources-means-open-and-that-is-the-safer-default) |
|
| `networkPolicy.probeSources` | `[]` | CIDRs allowed to reach `http`. **Empty means open** — [why](#empty-probesources-means-open-and-that-is-the-safer-default) |
|
||||||
| `networkPolicy.allowedNamespaces` | `[]` | Extra namespaces for `http`; the ingress controller belongs here once `probeSources` is set |
|
| `networkPolicy.allowedNamespaces` | `[]` | Extra namespaces for `http`. **Not for the ingress controller** — use `ingressController` below; this value is namespace-granular, so naming the controller's namespace here would open `http` to every pod in it |
|
||||||
|
| `networkPolicy.ingressController` | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach `http` once `probeSources` or `allowedNamespaces` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is the whole reason it is separate from `allowedNamespaces`. Default measured on the cluster |
|
||||||
| `networkPolicy.egress.dns` | `true` | UDP+TCP 53 to anywhere |
|
| `networkPolicy.egress.dns` | `true` | UDP+TCP 53 to anywhere |
|
||||||
| `networkPolicy.egress.smtp` | `true` | TCP 25 to anywhere — outbound delivery |
|
| `networkPolicy.egress.smtp` | `true` | TCP 25 to anywhere — outbound delivery |
|
||||||
| `networkPolicy.egress.extraPorts` | `[]` | Extra outbound TCP ports; add `443` for DNS-01 provider APIs or AI endpoints |
|
| `networkPolicy.egress.extraPorts` | `[]` | Extra outbound TCP ports; add `443` for DNS-01 provider APIs or AI endpoints |
|
||||||
|
|
||||||
Egress is restricted **by port, not by destination**: a NetworkPolicy matches CIDRs and selectors, never hostnames, so no host value from this chart can appear in it. What it still buys is that a compromised pod cannot open arbitrary outbound ports.
|
Egress is restricted **by port, not by destination**: a NetworkPolicy matches CIDRs and selectors, never hostnames, so no host value from this chart can appear in it. What it still buys is that a compromised pod cannot open arbitrary outbound ports. Unlike most charts in the repository these are the **configured** ports and not a guess at the standard ones, because this chart holds them as values (`dataStore.postgres.port`, `externalServices.*.port`).
|
||||||
|
|
||||||
|
> **Correction, 2026-10-14 (Gitea #5).** This page and the chart's own `values.yaml` used to advise putting the ingress controller's namespace into `allowedNamespaces`, with `["ingress-nginx"]` as the example. **Both halves of that are now wrong.** Measured on the live cluster: `ingress-nginx` is **gone** — no DaemonSet, no pods — the only `IngressClass` is `traefik` and it is the default, and the controller is the DaemonSet `rke2-traefik` in `kube-system`. And even with the right name, `allowedNamespaces` would have opened `http` to **every** pod in that namespace. `networkPolicy.ingressController` replaces the advice and narrows the source to the controller's pods. A policy written against the old controller would have been accepted, would have looked strict, and would have silently blocked all web traffic.
|
||||||
|
|
||||||
### `externalServices` — Kubernetes only, these do not configure Stalwart
|
### `externalServices` — Kubernetes only, these do not configure Stalwart
|
||||||
|
|
||||||
|
|||||||
+34
@@ -154,6 +154,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-sure-service` on port **80**. The web container serves everything; the Sidekiq worker has no HTTP port and is never routed to.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-sure-service` on port **80**. The web container serves everything; the Sidekiq worker has no HTTP port and is never routed to.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **3000** only, declared by the **web** container — the worker declares no port at all. Outbound **DNS, TCP 5432** (Postgres), **6379** (Redis) and **443**, and those egress rules apply to **both** containers.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -271,6 +283,28 @@ Modelling the AI options as chart values was considered and rejected: they are a
|
|||||||
|
|
||||||
The registry carries releases (`0.7.4`), prereleases (`-alpha.N`, `-rc.N`, `-hotfix.N`), the moving names `stable`, `latest` (which upstream calls the alpha channel) and `nightly`, and one `sha-<commit>` tag per build. The chart pins the release tag, and [Renovate](Renovate) uses `versioning: semver` for it: prereleases are unstable and never offered, the moving names and `sha-` tags are unparseable and never selected.
|
The registry carries releases (`0.7.4`), prereleases (`-alpha.N`, `-rc.N`, `-hotfix.N`), the moving names `stable`, `latest` (which upstream calls the alpha channel) and `nightly`, and one `sha-<commit>` tag per build. The chart pins the release tag, and [Renovate](Renovate) uses `versioning: semver` for it: prereleases are unstable and never offered, the moving names and `sha-` tags are unparseable and never selected.
|
||||||
|
|
||||||
|
### It covers both containers, because a policy selects pods and not containers
|
||||||
|
|
||||||
|
This chart runs a web container and a worker in one pod. The worker declares no port and its probes are `exec` probes, so it contributes nothing to the ingress rules — but it is fully subject to the **egress** ones, which is where it does all of its work. One object governs both.
|
||||||
|
|
||||||
|
The Postgres host and port are **fields of the 1Password item**, passed through to the application without the chart reading them, so 5432 is still a convention rather than configuration.
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up the database before every app-version bump.** Migrations are one-way; a `helm rollback` returns the old image to a database the new one has already rewritten.
|
- **Back up the database before every app-version bump.** Migrations are one-way; a `helm rollback` returns the old image to a database the new one has already rewritten.
|
||||||
|
|||||||
@@ -107,6 +107,23 @@ Deliberately **not** null-safe: an erased `storage` block yields an invalid PVC
|
|||||||
|
|
||||||
An erased block (`securityContext:` with no children, a `null` probe, a `null` `resources`) falls back to the chart's own defaults instead of disappearing — [style guide §12.2](Chart-Style-Guide#12-values-that-must-not-lie).
|
An erased block (`securityContext:` with no children, a `null` probe, a `null` `resources`) falls back to the chart's own defaults instead of disappearing — [style guide §12.2](Chart-Style-Guide#12-values-that-must-not-lie).
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Three ports, three treatments:
|
||||||
|
|
||||||
|
- **voice** (`server.voicePort`, **UDP**): NodePort, **open** — the clients are on the internet.
|
||||||
|
- **file transfer** (`server.fileTransferPort`, TCP): NodePort, open, and also the port all three probes open a socket against.
|
||||||
|
- **web query** (10080): **restricted** to this namespace plus `allowedNamespaces`. No Service, no probe, and administrative — this is the rule with real value here.
|
||||||
|
|
||||||
|
Outbound: DNS and TCP 443. There is **no `probeSources`** value: the probed port is open for an independent reason (it is a NodePort facing the internet), so the value would reach no rule.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Why the probes name `file-transfer`
|
### Why the probes name `file-transfer`
|
||||||
@@ -186,6 +203,26 @@ The container runs non-root as `9987:9987` with `readOnlyRootFilesystem` and all
|
|||||||
|
|
||||||
Entries land **after** the three variables the chart always sets — `TSSERVER_LICENSE_ACCEPTED`, `TSSERVER_DEFAULT_PORT`, `TSSERVER_FILE_TRANSFER_PORT`. Kubernetes keeps the last value for a repeated name, so redefining either port variable here wins inside the container while the Service, the NodePort and the containerPort still follow `server.*`. That splits the two apart with nothing reporting it — set the ports under `server`, never through `env`. Values are rendered through `tpl`, so anything sensitive belongs in `valueFrom.secretKeyRef`: a plain `value` is stored in the pod spec and readable by everyone who can read pods in the namespace.
|
Entries land **after** the three variables the chart always sets — `TSSERVER_LICENSE_ACCEPTED`, `TSSERVER_DEFAULT_PORT`, `TSSERVER_FILE_TRANSFER_PORT`. Kubernetes keeps the last value for a repeated name, so redefining either port variable here wins inside the container while the Service, the NodePort and the containerPort still follow `server.*`. That splits the two apart with nothing reporting it — set the ports under `server`, never through `env`. Values are rendered through `tpl`, so anything sensitive belongs in `valueFrom.secretKeyRef`: a plain `value` is stored in the pod spec and readable by everyone who can read pods in the namespace.
|
||||||
|
|
||||||
|
### If the server refuses to start or to register, widen the egress rule first
|
||||||
|
|
||||||
|
TeamSpeak's accounting service has historically used ports of its own — TCP 2008 and UDP 2010 on the 3.x line — and this chart cannot know what the 6.x beta does. Put them in `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
The install-test covers this chart, so a startup that depends on a blocked port shows up there rather than in production. A check that merely **degrades** would not.
|
||||||
|
|
||||||
|
### The protocol matters as much as the number
|
||||||
|
|
||||||
|
Voice is **UDP**. A rule written for TCP on the same number would be accepted, would look complete, and would drop every voice packet — which is why the render test checks the protocol against the `containerPort`'s and not just the number.
|
||||||
|
|
||||||
|
### A coincidence worth not copying
|
||||||
|
|
||||||
|
This chart happens to use the **same number** for the pod port and the NodePort, so the usual NodePort mistake is invisible here: the number is right either way. That is a property of these values, not a rule — do not carry the pattern to a chart where the two differ.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **The data volume is the server.** Everything lives in `/var/tsserver`: `tsserver.sqlitedb` (the virtual server, its port, all permissions, identities, channels and ServerQuery accounts), `files/` (file-transfer uploads), `logs/` (including the privilege key) and `query_ip_*list.txt`.
|
- **The data volume is the server.** Everything lives in `/var/tsserver`: `tsserver.sqlitedb` (the virtual server, its port, all permissions, identities, channels and ServerQuery accounts), `files/` (file-transfer uploads), `logs/` (including the privilege key) and `query_ip_*list.txt`.
|
||||||
|
|||||||
@@ -134,6 +134,24 @@ The only thing to adapt when copying the file is the **backend**: the Service na
|
|||||||
|
|
||||||
Note also what the scaffold demonstrates on purpose: `ingress.domain` **stays required in Gateway mode here**, because the `env` block reads it for `NORMAL_ENV`. That is a property of this chart, not of the convention — check it per chart ([§12.1](Chart-Style-Guide#12-values-that-must-not-lie)).
|
Note also what the scaffold demonstrates on purpose: `ingress.domain` **stays required in Gateway mode here**, because the `env` block reads it for `NORMAL_ENV`. That is a property of this chart, not of the convention — check it per chart ([§12.1](Chart-Style-Guide#12-values-that-must-not-lie)).
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
The reference implementation of the convention from Gitea #5: `templates/xxx.networkpolicy.yaml`, plus the values block, the schema entry and the `NOTES.txt` section that go with it. `networkPolicy.enabled` defaults to **`true`**, so a chart copied from here ships a policy without anyone having to remember one — which is the whole point: a policy that ships switched off is reached only when somebody switches it on, and then the issue is closed while nothing is filtered.
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces allowed in addition to the release's own. Namespace-granular: an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked.** |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed in once `probeSources` restricts the rule. Its two keys render as **one** `from` entry, i.e. an AND. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound TCP ports, to any destination. |
|
||||||
|
|
||||||
|
**What to adapt when copying, and it is more than renaming `xxx`:** classify every port of your workload first, because the class decides the rule — a port a **network** probe addresses stays open, a NodePort-published port stays open, anything else is restricted by source, and a replica-to-replica bus gets a bare `podSelector`. The scaffold shows only the first of those four, because it has only one port. `keycloak` and `garage` are the worked examples of all of them; the four classes are in [style guide §15](Chart-Style-Guide#networkpolicies).
|
||||||
|
|
||||||
|
Then **drop the values your chart does not use.** A chart whose every inbound rule is open to any source has no use for `allowedNamespaces` or `probeSources`, and a value that silently reaches no rule is worse than one that is absent, because it reads as a control — `samba` and `satisfactory` ship `enabled` alone for exactly that reason. The scaffold cannot know which of them your chart needs, so it ships all of them.
|
||||||
|
|
||||||
|
And note which of the two comment styles this file uses: **every** comment in it is a Go template comment. A `#` comment is part of the *rendered object* — it travels into the cluster, appears in `helm get manifest`, and lands in every release diff — and `ci/run-networkpolicy-test.sh` fails on one. The header of the file also carries the second-order trap found while writing it: prose inside a template comment must not spell out the comment's own closing delimiter, or the parser ends the comment early and the remaining prose is emitted as manifest content.
|
||||||
|
|
||||||
## Traps the scaffold carries into the copy
|
## Traps the scaffold carries into the copy
|
||||||
|
|
||||||
### Why 8080 and not 80
|
### Why 8080 and not 80
|
||||||
|
|||||||
@@ -136,6 +136,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-urlaubsverwaltung-service` on port **80**. The Spring Boot application serves the UI, `/login` and `/actuator` from one port.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-urlaubsverwaltung-service` on port **80**. The Spring Boot application serves the UI, `/login` and `/actuator` from one port.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **8080** only. Outbound **DNS, TCP 5432 *and* 3306** (the database — both conventional ports, see the operating note) and **443** (the OIDC provider, which this application requires: there is no other login path).
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -250,6 +262,26 @@ Helm never sees a `OnePasswordItem`'s content, so it cannot hash it ([style guid
|
|||||||
kubectl -n <namespace> rollout restart deployment/<release>-urlaubsverwaltung-deployment
|
kubectl -n <namespace> rollout restart deployment/<release>-urlaubsverwaltung-deployment
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Both database ports are allowed, and that is a deliberate loosening
|
||||||
|
|
||||||
|
This chart is handed a **full JDBC URL** inside a 1Password item (`jdbc-url`) 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 buried in a Java stack trace.
|
||||||
|
|
||||||
|
So 5432 and 3306 are both permitted: a small widening against a large and silent failure. A database on any other port still needs `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up the database before every app-version bump.** Liquibase migrates in place on every start; `helm rollback` puts the previous image back, but the schema does not come back with it. There is no volume in this release to back up — the database is the whole of the state.
|
- **Back up the database before every app-version bump.** Liquibase migrates in place on every start; `helm rollback` puts the previous image back, but the schema does not come back with it. There is no volume in this release to back up — the database is the whole of the state.
|
||||||
|
|||||||
+4
@@ -139,6 +139,10 @@ redis://:<password>@<release>-valkey-service.<namespace>.svc.cluster.local:6379/
|
|||||||
|
|
||||||
There is **no `ingress` section**. A Valkey whose only authentication is one password does not belong behind a public hostname, so the values that would allow it are absent rather than switched off.
|
There is **no `ingress` section**. A Valkey whose only authentication is one password does not belong behind a public hostname, so the values that would allow it are absent rather than switched off.
|
||||||
|
|
||||||
|
There is also no `probeSources` and no `ingressController`, and **that is what makes this the tightest policy in the repository rather than an incomplete one.** Since Gitea #5 every chart here ships a policy, and almost all of them have to leave their probed port open: the kubelet probes from the IP of the worker node running the pod, an address no chart can know. This chart does not, because **all three of its probes are `exec` probes** — they run inside the container through the kubelet's runtime and put **no packet on the network at all**. So there is no probe traffic to accommodate, 6379 can be closed to everything but the namespaces above, and no escape-hatch value is needed. `minecraft-server` is tight for the same reason; every HTTP-probed web app in the repo is not.
|
||||||
|
|
||||||
|
`policyTypes` naming **`Ingress` only** is the other half, and the convention now cites it in both directions: an egress rule here would constrain nothing while making the object look stricter than it is, and `homeassistant` and `satisfactory` invoke the same rule for outbound traffic nobody can enumerate. See [Chart-Style-Guide](Chart-Style-Guide#networkpolicies) for the four port classes this chart is the reference for.
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Persistence and eviction are a pair, not two switches
|
### Persistence and eviction are a pair, not two switches
|
||||||
|
|||||||
+34
@@ -150,6 +150,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-voucher-vault-service` on port **80**. Django serves the UI, `/oidc/*` and the admin from one process.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-voucher-vault-service` on port **80**. Django serves the UI, `/oidc/*` and the admin from one process.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **8000** only. Outbound **DNS, TCP 5432** (Postgres), **6379** (Redis) and **443** (the OIDC provider).
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -268,6 +280,28 @@ kubectl -n <namespace> rollout restart statefulset/<release>-voucher-vault-sfs
|
|||||||
|
|
||||||
Rotating `secretKey` logs every user out.
|
Rotating `secretKey` logs every user out.
|
||||||
|
|
||||||
|
### The egress ports are the standard ones, not your configured ones
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
Worse, the chart does not even know the port: the connection details arrive inside 1Password items it never parses. So the rules name the **conventional** ports, and a dependency listening anywhere else is **blocked**. Put the real port in `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
All three of this chart's dependencies arrive that way, so all three ports are conventions rather than configuration.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Upgrades migrate, rollbacks do not unmigrate.** Every single start runs `makemigrations` and `migrate` before the server comes up, so an app-version bump changes the schema the moment the new pod boots. `helm rollback` puts the old image back but leaves the migrated schema in place, and the old code then meets columns it does not know.
|
- **Upgrades migrate, rollbacks do not unmigrate.** Every single start runs `makemigrations` and `migrate` before the server comes up, so an app-version bump changes the schema the moment the new pod boots. `helm rollback` puts the old image back but leaves the migrated schema in place, and the old code then meets columns it does not know.
|
||||||
|
|||||||
@@ -171,6 +171,18 @@ Reaching the key from a template needs `index .Values "oauth2-proxy"` — `.Valu
|
|||||||
| `oauth2-proxy.config.configFile` | string | `email_domains = ["*"]`, `upstreams = ["http://{{ .Release.Name }}-ytdlp-web-player-service:80"]`, `reverse_proxy = true`, `session_cookie_minimal = true` | The proxy's whole configuration. **Add your provider settings here.** See [Three load-bearing lines in `configFile`](#three-load-bearing-lines-in-configfile). |
|
| `oauth2-proxy.config.configFile` | string | `email_domains = ["*"]`, `upstreams = ["http://{{ .Release.Name }}-ytdlp-web-player-service:80"]`, `reverse_proxy = true`, `session_cookie_minimal = true` | The proxy's whole configuration. **Add your provider settings here.** See [Three load-bearing lines in `configFile`](#three-load-bearing-lines-in-configfile). |
|
||||||
| `oauth2-proxy.alphaConfig.enabled` | bool | `false` | **Leave this off.** With it on the subchart generates a configuration *without* upstreams and ignores `config.configFile` silently — the login succeeds and the app answers 404. It is off in the subchart's defaults too; it is spelled out here because switching it on looks like an upgrade. |
|
| `oauth2-proxy.alphaConfig.enabled` | bool | `false` | **Leave this off.** With it on the subchart generates a configuration *without* upstreams and ignores `config.configFile` silently — the login succeeds and the app answers 404. It is off in the subchart's defaults too; it is spelled out here because switching it on looks like an upgrade. |
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **5000** only. Outbound **DNS plus TCP 80 and 443 to any destination** — fetching media is the application.
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Without the proxy, this release is open to everyone who knows the URL
|
### Without the proxy, this release is open to everyone who knows the URL
|
||||||
@@ -299,6 +311,26 @@ Each pod keeps its own `/data` emptyDir, so a video fetched by one is invisible
|
|||||||
|
|
||||||
HLS playback **through** the proxy. Rendering and wiring are verified; a long stream through proxy and ingress is not. Three places to look first if playback stalls where a direct ingress works: proxy-buffering in ingress-nginx, its 60-second proxy-read/send timeouts, and the proxy's own upstream timeout. Browser extension and PWA are unverified too — an expired session answers with a redirect to the provider, not a 401, and a `fetch()` sees only an opaque redirect.
|
HLS playback **through** the proxy. Rendering and wiring are verified; a long stream through proxy and ingress is not. Three places to look first if playback stalls where a direct ingress works: proxy-buffering in ingress-nginx, its 60-second proxy-read/send timeouts, and the proxy's own upstream timeout. Browser extension and PWA are unverified too — an expired session answers with a redirect to the provider, not a 401, and a `fetch()` sees only an opaque redirect.
|
||||||
|
|
||||||
|
### The oauth2-proxy subchart is not covered by this policy
|
||||||
|
|
||||||
|
This is the sharpest case of a general limit, so it is worth stating rather than inferring: the policy selects **this chart's** workload by its `app` label. The `oauth2-proxy` subchart renders its own Deployment with its own labels and ships no policy of its own.
|
||||||
|
|
||||||
|
So with the proxy enabled, the pod that **actually faces the Ingress** is unrestricted in both directions. Nothing in this chart can change that — an upstream chart is consumed as it comes — so do not read "this chart has a NetworkPolicy" as "this release is covered".
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Nothing to back up.** No PVC, no database, no persisted state; both volumes are caches refilled from the network. A rollback carries no data risk.
|
- **Nothing to back up.** No PVC, no database, no persisted state; both volumes are caches refilled from the network. A rollback carries no data risk.
|
||||||
|
|||||||
+34
@@ -143,6 +143,18 @@ The Gateway-API alternative to the Ingress, **off by default**. Added with group
|
|||||||
|
|
||||||
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-zipline-service` on port **80** — the port **number**, because `backendRefs` cannot name a port the way the Ingress backend does. One path covers the UI, the API and the short URL of every upload.
|
What the chart renders in this mode is **only** the `HTTPRoute`: one rule, `PathPrefix: /`, one `backendRefs` entry pointing at `<release>-zipline-service` on port **80** — the port **number**, because `backendRefs` cannot name a port the way the Ingress backend does. One path covers the UI, the API and the short URL of every upload.
|
||||||
|
|
||||||
|
### `networkPolicy`
|
||||||
|
|
||||||
|
Inbound **3000** only. Outbound **DNS, TCP 5432** (Postgres) and **443** (the optional OAuth providers, and an external S3 endpoint if one is configured).
|
||||||
|
|
||||||
|
| Value | Type | Default | Meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `networkPolicy.enabled` | bool | `true` | Renders the policy. **Only a boundary where the CNI enforces one** — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured. |
|
||||||
|
| `networkPolicy.allowedNamespaces` | list | `[]` | Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the `kubernetes.io/metadata.name` label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to **every** pod in it. |
|
||||||
|
| `networkPolicy.probeSources` | list | `[]` | CIDRs allowed to reach the probed port. **Empty means open, not blocked** — the rule is then rendered with no `from` at all. Probes come from the IP of the **worker node running the pod**, which the chart cannot know; a list that misses it drops every probe and the pod never becomes `Ready`. |
|
||||||
|
| `networkPolicy.ingressController` | object | `kube-system` / `app.kubernetes.io/name: rke2-traefik` | The controller allowed to reach the routed port once `probeSources` restricts it. The two keys render as **one** `from` entry, i.e. an AND — which is why this is not a line in `allowedNamespaces`, where it would open the port to every pod in `kube-system`. Default measured on the cluster, not assumed. |
|
||||||
|
| `networkPolicy.egress.extraPorts` | list | `[]` | Additional outbound **TCP** ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off. |
|
||||||
|
|
||||||
## Operating notes
|
## Operating notes
|
||||||
|
|
||||||
### Routing: Ingress or Gateway API
|
### Routing: Ingress or Gateway API
|
||||||
@@ -217,6 +229,28 @@ kubectl -n <namespace> rollout restart statefulset/<release>-zipline-sfs
|
|||||||
|
|
||||||
A new `CORE_SECRET` logs every user out.
|
A new `CORE_SECRET` logs every user out.
|
||||||
|
|
||||||
|
### The egress ports are the standard ones, not your configured ones
|
||||||
|
|
||||||
|
A `NetworkPolicy` matches CIDRs and selectors and **never hostnames**, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a **port** and not a destination.
|
||||||
|
|
||||||
|
Worse, the chart does not even know the port: the connection details arrive inside 1Password items it never parses. So the rules name the **conventional** ports, and a dependency listening anywhere else is **blocked**. Put the real port in `networkPolicy.egress.extraPorts`.
|
||||||
|
|
||||||
|
And note where the failure surfaces here: the probes address this app's own port and not the database, so a blocked Postgres leaves the pod `Ready` and shows up as an application error instead.
|
||||||
|
|
||||||
|
### The probed port is open inside the cluster by default
|
||||||
|
|
||||||
|
`networkPolicy.probeSources` is empty out of the box, so the rule for the port the probes address is rendered with **no `from` selector at all** — any pod the CNI routes to can reach it. Not from the internet (the port carries no NodePort), but this is **not** pod-level isolation, and reading the object as if it were is the mistake to avoid.
|
||||||
|
|
||||||
|
It is that way on purpose. The kubelet's liveness, readiness and startup probes originate from the IP of the **worker node running the pod** — an address no chart can 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. Shipping *open* is the honest default; narrowing it means listing the worker node IPs yourself.
|
||||||
|
|
||||||
|
Note the companion trap: the moment `probeSources` is non-empty the rule **is** restricted, so everything else that needs the port has to be in the same rule. The chart adds the release's own namespace, `ingressController` and `allowedNamespaces` for exactly that reason.
|
||||||
|
|
||||||
|
### The policy is only a boundary where the CNI enforces one
|
||||||
|
|
||||||
|
The API server accepts a `NetworkPolicy` whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.
|
||||||
|
|
||||||
|
Measured for this cluster: the CNI is **Canal** (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it **exempts the kubelet's probe traffic**, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.
|
||||||
|
|
||||||
## Upgrades and backup
|
## Upgrades and backup
|
||||||
|
|
||||||
- **Back up the database before every app-version bump.** Migrations run in place and are one-way; `helm rollback` returns the old image to a schema the new one has already rewritten.
|
- **Back up the database before every app-version bump.** Migrations run in place and are one-way; `helm rollback` returns the old image to a schema the new one has already rewritten.
|
||||||
|
|||||||
Reference in new issue
Block a user