feat(keycloak)!: NetworkPolicy, on by default - the full convention in one chart
Install-test / Install-test apache-tika (pull_request) Successful in 1m14s
Install-test / Install-test rss-bridge (pull_request) Successful in 54s
Install-test / Install-test patchmon (pull_request) Successful in 1m35s
Install-test / Install-test bentopdf (pull_request) Successful in 1m2s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 3s
Validate Helm Charts / Resource quantities (pull_request) Successful in 13s
Validate Helm Charts / Selector match (pull_request) Successful in 10s
Validate Helm Charts / Bump level (pull_request) Successful in 4s
Validate Helm Charts / YAML scalars (pull_request) Successful in 10s
Validate Helm Charts / NetworkPolicy (pull_request) Failing after 15s
Install-test / Install-test bulwark (pull_request) Successful in 1m25s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 9s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 6s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 5s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 14s
Validate Helm Charts / Build bulwark (pull_request) Successful in 5s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 6s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 7s
Validate Helm Charts / Build rallly (pull_request) Successful in 13s
Validate Helm Charts / Build searxng (pull_request) Successful in 17s
Validate Helm Charts / Build sure (pull_request) Successful in 18s
Validate Helm Charts / Build template-chart (pull_request) Successful in 6s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 5s
Validate Helm Charts / Build homeassistant (pull_request) Successful in 7s
Validate Helm Charts / Build keycloak (pull_request) Successful in 11s
Validate Helm Charts / Build outline (pull_request) Successful in 8s
Validate Helm Charts / Build patchmon (pull_request) Successful in 6s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 5s
Install-test / Install-test rallly (pull_request) Successful in 1m39s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 5s
Validate Helm Charts / Build zipline (pull_request) Successful in 5s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 6s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 6s
Validate Helm Charts / Trivy config homeassistant (pull_request) Successful in 11s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 15s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 13s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 16s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 26s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 19s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 13s
Validate Helm Charts / Trivy config keycloak (pull_request) Successful in 19s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 12s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 16s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 13s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 14s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 21s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 17s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 11s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 15s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 13s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 13s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 2m8s
Install-test / Install-test cyberchef (pull_request) Successful in 1m9s
Install-test / Install-test gotenberg (pull_request) Successful in 1m40s
Install-test / Install-test homeassistant (pull_request) Successful in 2m30s
Install-test / Install-test keycloak (pull_request) Successful in 1m58s
Install-test / Install-test searxng (pull_request) Successful in 1m5s
Install-test / Install-test outline (pull_request) Successful in 1m57s
Install-test / Install-test sure (pull_request) Successful in 2m30s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m38s
Install-test / Install-test voucher-vault (pull_request) Successful in 2m6s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 1m5s
Install-test / Install-test zipline (pull_request) Successful in 1m50s
Validate Helm Charts / Install-test (pull_request) Successful in 29m30s

Major. This is the richest policy of the sweep and the one with the largest
blast radius, and it is the only chart whose three ports fall into three
different classes - so it is where the convention is visible in full:

  http (8080)       NOT addressed by any probe, so it CAN be restricted by
                    source, and it is: own namespace, allowedNamespaces, and
                    the ingress controller. The only real inbound restriction
                    in this whole rollout, possible only because the probes use
                    a different port.
  management (9000) the port all three probes address, so it stays open inside
                    the cluster. That matters here specifically: this surface
                    carries health and metrics UNAUTHENTICATED, and "no
                    Ingress" was its only protection before this object. The
                    policy turns "anything that can route to the pod" into
                    "anything inside the cluster"; probeSources closes the rest.
  jgroups (7800)    the cluster bus, restricted to the pods this policy itself
                    selects - a podSelector with no namespaceSelector, the
                    narrowest rule the object can express.

All four port numbers come from keycloak.settings rather than being written
down twice, so moving keycloak.ports.http moves the policy with it. And the
egress rule for the database is the CONFIGURED port, keycloak.db.port - this is
the one chart in the sweep that does not have to guess, because its database
port is a value instead of a field inside an opaque secret.

The outbound cluster-bus rule is not a duplicate of the inbound one: policyTypes
lists Egress, so a replica dialling its peer is subject to this policy too, and
without it the bus would be open inbound and blocked outbound - a cluster that
never forms, with two nodes that each look healthy.

THE BLAST RADIUS IS WRITTEN INTO THE CHART, because nothing in CI can catch it:
this release is the cluster's identity provider, no ingress controller runs in
the install-test, and the probes use 9000 - so the pod becomes Ready whether or
not the http rule is right. NOTES.txt therefore asks for two checks after the
first upgrade: log in, and look at whether any consumer uses the in-cluster
Service address instead of the public issuer URL (those namespaces need
allowedNamespaces). Not allowed at all, and named as the likely first surprise:
SMTP, because the mail server is configured per realm where the chart cannot
see it.

The test's overlay guard is tightened in the same commit, and keycloak is why.
It counted any source-restricted rule, and this chart restricts two of them in
its own defaults - so the guard would have been satisfied while the probe
branch, the only branch an overlay exists to reach, was never rendered. It now
counts restricted PROBE rules. All twelve failure paths were re-run against the
change.

Part of #5
This commit is contained in:
jklein committed 2026-10-14 14:30:30 +00:00
1 parent e1adcef71f
commit 96c23b7912
6 files changed
+523 -5

No files matched your search

+77
View File
@@ -363,3 +363,80 @@ WHAT THIS CHART DOES NOT RENDER
platform objects and live in cluster-config; this chart renders an
HTTPRoute at most, which is the part that looks the same on every Gateway
implementation.
{{- if .Values.networkPolicy.enabled }}
NETWORKPOLICY IS ACTIVE — WHAT IT CAPS, AND WHAT IT DOES NOT
{{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects this workload's pods.
Inbound, and all three ports differ:
- http ({{ $settings.ports.http | int64 }}): RESTRICTED BY SOURCE — this namespace, anything in
networkPolicy.allowedNamespaces, and the ingress controller. No probe uses
this port, which is what makes the restriction possible.
- management ({{ $settings.ports.management | int64 }}): open inside the cluster, because all three probes
address it. Its health and metrics surface is UNAUTHENTICATED, and before
this object the only thing protecting it was that no Ingress pointed at
it. Narrow it with networkPolicy.probeSources.
- jgroups ({{ $settings.ports.jgroups | int64 }}): this release's own pods only.
Outbound: DNS, TCP {{ $settings.db.port | int64 }} (the CONFIGURED database port — this chart derives it
rather than guessing), {{ $settings.ports.jgroups | int64 }} (the cluster bus, outbound as well as in) and 443{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }},
plus TCP {{ join ", " . }}{{ end }}{{ end }}.
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
ingress-controller source is wrong for this cluster, every login
everywhere stops — and no part of CI can see it, because no ingress
controller runs there 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.
AND 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.
Three limits belong in the same breath, because none of them is visible in
the object itself:
- EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and
selectors and cannot express a hostname, so every egress rule permits its
port to ANY address. That is a limit of the object, not a shortcut — this
cluster's CNI (Canal: Flannel plus Calico) has no FQDN rule either. An
outbound port the chart does not know about is BLOCKED: add it to
networkPolicy.egress.extraPorts, and expect to find out through an
application-level connection error rather than through this object.
{{- if .Values.networkPolicy.probeSources }}
- THE PROBE PATH IS RESTRICTED in this release, because
networkPolicy.probeSources is set to
{{ join ", " .Values.networkPolicy.probeSources }}
Probes come from the IP of the WORKER NODE running the pod. If none of those
CIDRs covers the node this pod lands on, the kubelet's probes are dropped
and THE POD NEVER BECOMES READY — the rollout then times out and the symptom
looks like a broken application. Compare
kubectl -n {{ .Release.Namespace }} describe pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-deployment
against `kubectl get node -o wide`.
{{- else }}
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
empty, so that rule carries no source selector at all and any pod the CNI
routes to can reach it. Not from the internet — but this is NOT pod-level
isolation. Narrowing it means listing the worker node IPs in
networkPolicy.probeSources, because the kubelet probes from the node's own
address and the chart cannot know it; a wrong guess there leaves the pod
permanently un-Ready.
{{- end }}
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
this object is accepted and filters nothing, so everything reads as
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
shows the object exists; only a connection attempt from an unrelated
namespace shows that it bites.
Switch it off with networkPolicy.enabled: false.
{{- end }}