diff --git a/ci/run-networkpolicy-test.sh b/ci/run-networkpolicy-test.sh index 53f58b1..3c9ed42 100755 --- a/ci/run-networkpolicy-test.sh +++ b/ci/run-networkpolicy-test.sh @@ -392,6 +392,11 @@ offers_probe_sources = bool(re.search(r"^\s*probeSources\s*:", values_yaml, re.M notes_warns = bool(re.search(r"never becomes? ready", notes, re.I)) and "probeSources" in notes RESTRICTED_RULES = 0 +# Counted separately from RESTRICTED_RULES, and the overlay guard uses THIS one: +# a chart like keycloak restricts other rules by source in its own defaults, so +# "some rule has a from" would satisfy the guard without the probe branch ever +# being rendered - which is the only branch an overlay exists to reach. +PROBE_RESTRICTED = 0 # --- Assertion 0: the chart ships a policy at all --------------------------- if not policies: @@ -668,6 +673,7 @@ for policy in policies: % (tag, port_number, ", ".join(who))) continue if offers_probe_sources and notes_warns: + PROBE_RESTRICTED += 1 ok("%s restricts probe port %d (%s) by source in ingress[%s], and the " "chart carries what the convention requires with it: " "networkPolicy.probeSources in values.yaml and the consequence " @@ -722,6 +728,7 @@ if policies: "reasoning stayed in template comments") print("RESTRICTED|%d" % RESTRICTED_RULES) +print("PROBERESTRICTED|%d" % PROBE_RESTRICTED) for line in lines: print(line) PY @@ -739,6 +746,7 @@ PY CHART_OK=0 CHART_SKIP=0 CHART_RESTRICTED=0 + CHART_PROBE_RESTRICTED=0 while IFS='|' read -r VERDICT DETAIL; do case "$VERDICT" in OK) @@ -752,6 +760,9 @@ PY RESTRICTED) CHART_RESTRICTED="$DETAIL" ;; + PROBERESTRICTED) + CHART_PROBE_RESTRICTED="$DETAIL" + ;; FAIL) CHART_FAIL=1 echo " FAIL $DETAIL" @@ -770,20 +781,20 @@ PY FAILED="$FAILED $CHART" SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | **failed** | $CHART_OK | $CHART_SKIP |"$'\n' echo "$CHART: FAILED" - elif [ -n "$HAS_OVERLAY" ] && [ "$CHART_RESTRICTED" -eq 0 ]; then + elif [ -n "$HAS_OVERLAY" ] && [ "$CHART_PROBE_RESTRICTED" -eq 0 ]; then # The overlay's ONE job is to render a code path the chart's defaults do not # reach - above all the RESTRICTED probe rule, which only appears once # probeSources is non-empty. If no restricted rule came out, this test is # vacuum-green for the very chart the overlay was written for. FAILED="$FAILED $CHART" SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | **failed** | $CHART_OK | $CHART_SKIP |"$'\n' - echo "::error title=networkpolicy $CHART::$CHART has an overlay at ci/networkpolicy/$CHART.yaml but rendered no source-restricted ingress rule, so the restricted branch - the one the chart's own defaults never reach - was not checked. That overlay exists for exactly this; either it no longer sets networkPolicy.probeSources where the chart reads it, or the template stopped rendering the restricted form. A green tick here would be a tick for a check that ran over nothing." - echo "$CHART: FAILED (overlay present, no restricted rule rendered)" + echo "::error title=networkpolicy $CHART::$CHART has an overlay at ci/networkpolicy/$CHART.yaml but rendered no source-restricted PROBE rule, so the restricted branch - the one the chart's own defaults never reach - was not checked. That overlay exists for exactly this; either it no longer sets networkPolicy.probeSources where the chart reads it, or the template stopped rendering the restricted form. A green tick here would be a tick for a check that ran over nothing." + echo "$CHART: FAILED (overlay present, no restricted probe rule rendered)" else CHECKED="$CHECKED $CHART" [ "$CHART_SKIP" -gt 0 ] && WITH_GAPS="$WITH_GAPS $CHART" SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | ok | $CHART_OK | $CHART_SKIP |"$'\n' - echo "$CHART: ok ($CHART_OK assertion(s) held, $CHART_SKIP not checked, $CHART_RESTRICTED source-restricted rule(s))" + echo "$CHART: ok ($CHART_OK assertion(s) held, $CHART_SKIP not checked, $CHART_RESTRICTED source-restricted rule(s), $CHART_PROBE_RESTRICTED of them on a probe port)" fi done diff --git a/keycloak/Chart.yaml b/keycloak/Chart.yaml index 8f6a805..5cafd20 100644 --- a/keycloak/Chart.yaml +++ b/keycloak/Chart.yaml @@ -43,7 +43,7 @@ type: application # procedure lives in NOTES.txt and on the wiki page instead of in a # packageRules entry. Read the "MINOR UPGRADES" section of NOTES.txt before # merging an app-version PR that changes the minor. -version: 1.3.0+up26.8.0 +version: 2.0.0+up26.8.0 appVersion: "26.8.0" maintainers: diff --git a/keycloak/templates/NOTES.txt b/keycloak/templates/NOTES.txt index f1a2735..5fddbcb 100644 --- a/keycloak/templates/NOTES.txt +++ b/keycloak/templates/NOTES.txt @@ -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 }} diff --git a/keycloak/templates/keycloak.networkpolicy.yaml b/keycloak/templates/keycloak.networkpolicy.yaml new file mode 100644 index 0000000..e58887a --- /dev/null +++ b/keycloak/templates/keycloak.networkpolicy.yaml @@ -0,0 +1,281 @@ +{{- /* +NetworkPolicy for keycloak (Gitea #5). + +THE RICHEST POLICY OF THIS ROLLOUT, AND THE ONE WITH THE LARGEST BLAST RADIUS. +Read both halves of that before changing anything here. + +Keycloak is the only chart in the repository whose three ports fall into three +DIFFERENT classes, so it is the one place 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. This is the only real inbound + restriction in the sweep, and it is possible only because + the probes use a different port. + management (9000) the port all three probes address, so it stays open by + default. That is unfortunate here specifically: 9000 + carries the health and metrics surface UNAUTHENTICATED, + and "no Ingress" was its only protection before this + object existed. 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 - + at the price of needing the worker node IPs. + jgroups (7800) the cluster bus between this release's own replicas. + Restricted to the pods this policy itself selects, which + is the narrowest rule this object can express. + +All three port numbers and the database port are DERIVED from the chart's own +values through keycloak.settings, not written down twice: an operator who moves +keycloak.ports.http moves the policy with it. The database port is +keycloak.db.port, so unlike most charts in this sweep the egress rule here is +the CONFIGURED port and not a guess. + +THE BLAST RADIUS, stated plainly because it is the largest in this issue: this +release is the cluster's identity provider. If the ingress-controller source +below is wrong, every login everywhere breaks, and NOTHING IN CI CAN CATCH IT - +no ingress controller runs in the install-test, and the probes use 9000, so the +pod becomes Ready either way. The source was measured on the live cluster +(DaemonSet rke2-traefik in kube-system, which also serves the Gateway API +here), but measured labels are not proven traffic. After the first upgrade, +verify with an actual login rather than with kubectl. + +AND THE SECOND TRAP, which is the same shape: a consumer that reaches Keycloak +on its IN-CLUSTER Service address rather than through its public URL is a pod +in another namespace, and the http rule blocks it. Those namespaces belong in +allowedNamespaces. Consumers that use the public issuer URL arrive through +Traefik and are already covered. + +WHAT THIS OBJECT IS: a ceiling on reachability, not an authentication layer. +Only the ports declared below are reachable at all - whatever else the process +binds, now or after the next app update, is blocked without anyone having to +notice it exists. + +WHAT IT IS NOT: + - EGRESS IS COARSE. A NetworkPolicy matches CIDRs and selectors and NEVER + hostnames, so "only to the database host" cannot be expressed at all. This + cluster's CNI is Canal (Flannel plus Calico for policy) and has no FQDN + rule; Calico's GlobalNetworkSet could express one but would bind this chart + to Calico, the same argument that kept implementation-specific objects out + of the Gateway-API sweep (#114). Each egress rule therefore permits its + PORT to any destination. + - NOT A BOUNDARY WHERE THE CNI DOES NOT ENFORCE ONE. An unenforced policy is + accepted by the API server and filters nothing, so everything reads as + configured while nothing is filtered. + - THE REASONING IS IN TEMPLATE COMMENTS, not in "#" comments: those are part + of the rendered object, travel into the cluster and appear in every release + diff. ci/run-networkpolicy-test.sh fails on a rendered "#". + +THE PORT NUMBERS BELOW ARE POD PORTS, NEVER NODEPORTS. A NetworkPolicy is +evaluated at the pod's network interface, after kube-proxy has already +translated a NodePort to the targetPort, so a NodePort number here would match +nothing while looking strict - and, being the only rule for that port, would +block every inbound connection instead. ci/run-networkpolicy-test.sh asserts +every port here against the containerPorts of the pod spec this policy +selects. +*/ -}} +{{- if .Values.networkPolicy.enabled }} +{{- $settings := fromYaml (include "keycloak.settings" .) }} +{{- $np := .Values.networkPolicy }} +{{- $ns := .Release.Namespace }} +{{- $probeSources := $np.probeSources | default list }} +{{- $allowedNs := $np.allowedNamespaces | default list }} +{{- $ic := $np.ingressController | default dict }} +{{- $routed := or .Values.ingress.enabled .Values.httpRoute.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy + labels: + app: {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy +spec: + {{- /* + Selects the Deployment's pods, and the same label the PodDisruptionBudget and + the two Services use. ci/run-networkpolicy-test.sh fails if it matches no pod + spec of the same render. + */}} + podSelector: + matchLabels: + app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment + policyTypes: + - Ingress + {{- /* + Egress is listed because there is a great deal to restrict and almost all of it + is known: the database on its configured port, the cluster bus, and HTTPS. What + is NOT permitted and is the likely first surprise: SMTP. Keycloak sends mail + (verification, password reset) through a server configured per realm, which + this chart cannot see - so it needs networkPolicy.egress.extraPorts. + */}} + - Egress + ingress: + {{- /* + THE HTTP PORT, AND THE ONE GENUINELY RESTRICTED INBOUND RULE IN THIS SWEEP. + + It can be restricted because no probe addresses it - the probes use the + management port - so there is no kubelet traffic to accommodate and no need for + probeSources here. Allowed: this release's own namespace, whatever + allowedNamespaces adds, and the ingress controller. + + Both selectors of the controller entry sit in ONE list item, which makes them + an AND: those pods in that namespace. Split into two items they would be an OR + and would open Keycloak's login endpoint to every pod in kube-system. + + If this rule is wrong, every login in the cluster breaks and no part of CI will + say so. See the header. + */}} + - ports: + - protocol: TCP + port: {{ $settings.ports.http | int64 }} + from: + - namespaceSelector: + matchLabels: + {{- /* + Set by Kubernetes itself on every namespace (>= 1.21), so this needs no + labelling work on a namespace this chart does not own. + */}} + kubernetes.io/metadata.name: {{ $ns }} + {{- if and $routed $ic.namespace }} + {{- /* + ONE from-entry with TWO selectors, which is an AND: pods carrying these labels + IN that namespace. Written as two list entries it would be an OR, and would + open the port to every pod in kube-system. + */}} + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ $ic.namespace | quote }} + {{- with $ic.podLabels }} + podSelector: + matchLabels: + {{- toYaml . | nindent 14 }} + {{- end }} + {{- end }} + {{- range $allowedNs }} + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ . | quote }} + {{- end }} + {{- /* + THE MANAGEMENT PORT - health, readiness and metrics, and all three probes + address it. So it is the probe path and stays open by default. + + That is worth naming rather than glossing: this surface is UNAUTHENTICATED, and + before this object existed its only protection was that no Ingress pointed at + it. What this policy adds is a smaller ceiling, not a closed door - the port is + reachable from inside the cluster. Closing it means listing the worker node IPs + in networkPolicy.probeSources, with the usual cost of getting them wrong. + */}} + - ports: + - protocol: TCP + port: {{ $settings.ports.management | int64 }} + {{- if $probeSources }} + {{- /* + RESTRICTED, because the operator supplied probeSources. The kubelet's probes + come from the IP of the WORKER NODE running the pod, so that address has to be + in the list or the pod never becomes Ready - and a pod that is never Ready + reads as a broken application rather than as a policy. + + No ingress-controller entry on this rule: nothing routes to the management port + from outside, and it should stay that way. + */}} + from: + - namespaceSelector: + matchLabels: + {{- /* + Set by Kubernetes itself on every namespace (>= 1.21), so this needs no + labelling work on a namespace this chart does not own. + */}} + kubernetes.io/metadata.name: {{ $ns }} + {{- range $allowedNs }} + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ . | quote }} + {{- end }} + {{- range $probeSources }} + - ipBlock: + cidr: {{ . | quote }} + {{- end }} + {{- else }} + {{- /* + NO "from" SELECTOR, WHICH MEANS OPEN - deliberately, and only inside the + cluster, because this port carries no NodePort. The alternative was to guess + the worker node CIDRs, and a wrong guess blocks every kubelet probe: the pod + stays un-Ready, the rollout times out, and the cause looks like the + application. An empty "from: []" would be worse than either - it allows + everything too, but while reading as a restriction. Narrow it with + networkPolicy.probeSources. + */}} + {{- end }} + {{- /* + THE CLUSTER BUS between this release's own replicas, and the narrowest rule + this object can express: a podSelector with no namespaceSelector, which means + "pods with these labels IN THIS NAMESPACE" - i.e. exactly the pods this policy + already selects. + + Infinispan discovery here is jdbc-ping (see values.yaml), so no multicast and + no extra port is involved; the peers find each other through the database and + then connect on this port. The matching egress rule below is what lets them + dial OUT - without it the cluster would form in one direction only. + */}} + - ports: + - protocol: TCP + port: {{ $settings.ports.jgroups | int64 }} + from: + - podSelector: + matchLabels: + app: {{ .Release.Name }}-{{ .Chart.Name }}-deployment + egress: + {{- /* + DNS, unconditionally. There is no value to switch this off, and that is + deliberate: a knob whose only other setting breaks every name lookup in + the pod is a trap and not a feature. + */}} + - ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + {{- /* + The external PostgreSQL, on THE CONFIGURED PORT - keycloak.db.port, derived + through keycloak.settings. This chart is the exception in the sweep: its + database port is a value rather than a field inside an opaque secret, so this + rule follows the configuration instead of guessing at 5432. + + It also carries Infinispan's jdbc-ping discovery, which is why a blocked + database here costs the cluster as well as the data. + */}} + - ports: + - protocol: TCP + port: {{ $settings.db.port | int64 }} + {{- /* + Outbound cluster bus. Needed as well as the ingress rule above: policyTypes + lists Egress, so a replica dialling its peer is subject to this policy too, and + without this rule the bus would be permitted inbound and blocked outbound - a + cluster that never forms, with two nodes that each look healthy. + */}} + - ports: + - protocol: TCP + port: {{ $settings.ports.jgroups | int64 }} + {{- /* + Outbound HTTPS. Keycloak needs it for identity brokering and for external + identity-provider metadata, both configured inside REALMS this chart never + sees - so the port has to be permitted here or a federated login fails with no + hint pointing at this object. Also covers an outbound webhook or a remote JWKS. + Ports only: these are hostnames. + */}} + - ports: + - protocol: TCP + port: 443 + {{- with $np.egress }} + {{- with .extraPorts }} + {{- /* + Ports only, to any destination - see the header. This is the operator's + escape hatch from the egress cap, and the only alternative to switching + the whole policy off. + */}} + - ports: + {{- range . }} + - protocol: TCP + port: {{ int . }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/keycloak/values.schema.json b/keycloak/values.schema.json index 42cc533..2c56f73 100644 --- a/keycloak/values.schema.json +++ b/keycloak/values.schema.json @@ -9,6 +9,57 @@ "description": "Helm's conventional cross-chart values. Unused by this chart, kept open so an umbrella install cannot break on it.", "type": "object" }, + "networkPolicy": { + "description": "The chart's NetworkPolicy (Gitea #5), on by default. A ceiling on reachability and not an authentication layer: only the declared container ports are reachable at all, egress is capped by PORT (a NetworkPolicy matches CIDRs and selectors, never hostnames, so a destination host cannot be expressed), and ingress is capped by source for every port no network probe touches. Only a boundary where the cluster's CNI enforces policies - an unenforced policy is accepted by the API server and filters nothing. The template's header carries the full list of what it does not do.", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "allowedNamespaces": { + "description": "Namespaces whose pods may reach this workload, in addition to the release's own namespace, which is always allowed. Matched on the kubernetes.io/metadata.name label Kubernetes sets on every namespace itself, so no labelling work is needed - and no typo protection either: a name that does not exist matches nothing and is silently ineffective. An entry opens the port to EVERY pod in that namespace; for the ingress controller use ingressController instead.", + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "probeSources": { + "description": "Source CIDRs allowed to reach the port the network probes address. EMPTY MEANS OPEN, not blocked: with no entry that rule is rendered without a \"from\" selector. The kubelet's probes come from the IP of the worker node running the pod, an address the chart cannot know and must not guess - a guessed CIDR that excludes the real node blocks every probe and the pod never becomes Ready, which reads as a broken application rather than as a policy. Setting this restricts the rule, which is why the chart adds the own namespace, ingressController and allowedNamespaces to the same rule.", + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "ingressController": { + "description": "The ingress controller allowed to reach the routed port once probeSources restricts it; rendered only while ingress.enabled or httpRoute.enabled is true. The two keys become ONE \"from\" entry and are therefore an AND - pods carrying podLabels IN namespace - which is the whole point: the controller's namespace in allowedNamespaces would open the port to every pod in kube-system. The default is measured on this cluster (DaemonSet rke2-traefik in kube-system, which also serves the Gateway API here), not assumed; a policy written against the wrong controller is accepted, looks strict and silently blocks all web traffic.", + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "namespace": { + "type": ["string", "null"] + }, + "podLabels": { + "description": "Label set the controller's pods carry, rendered verbatim as a podSelector's matchLabels. Kept open because the chart does not interpret the keys.", + "type": ["object", "null"] + } + } + }, + "egress": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "extraPorts": { + "description": "Additional outbound TCP ports, to ANY destination - ports and not destinations, because a NetworkPolicy cannot express a hostname. DNS is always allowed and has no switch. Without an entry here an outbound port the chart does not already derive is blocked, which is the cap's whole purpose and also its upgrade risk.", + "type": ["array", "null"], + "items": { + "type": "integer" + } + } + } + } + } + }, "scaleDown": { "description": "true renders the Deployment with replicas: 0 and takes precedence over keycloak.replicas. Also the first half of the minor-upgrade procedure, because Keycloak supports a rolling update only inside a minor line.", "type": "boolean" diff --git a/keycloak/values.yaml b/keycloak/values.yaml index aabbfc1..c3fe0b3 100644 --- a/keycloak/values.yaml +++ b/keycloak/values.yaml @@ -884,3 +884,101 @@ httpRoute: # Empty means [ingress.admin.domain]. Required (via that value) while # ingress.admin.enabled is true and httpRoute.enabled is true. hostnames: [] + +# --------------------------------------------------------------------------- +# NetworkPolicy (Gitea #5). On by default. The reference implementation with +# the full reasoning is template-chart; the convention is in README, "Chart +# style guidelines", and the operator's view on this chart's wiki page. +# --------------------------------------------------------------------------- +networkPolicy: + # THE RICHEST POLICY IN THIS REPOSITORY, AND THE ONE WITH THE LARGEST BLAST + # RADIUS. Both halves matter. + # + # WHAT IT ALLOWS, inbound, and the three ports are three different cases: + # - http (8080): RESTRICTED BY SOURCE to this namespace, allowedNamespaces + # and the ingress controller. The only real inbound restriction in the + # whole sweep, and possible only because no probe uses this port. + # - management (9000): open inside the cluster, because all three probes + # address it. This is the port whose health and metrics surface is + # UNAUTHENTICATED; before this object its only protection was that no + # Ingress pointed at it. probeSources closes the rest. + # - jgroups (7800): this release's own pods only. + # Outbound: DNS, the CONFIGURED database port (keycloak.db.port - this chart + # does not have to guess), the cluster bus, and 443 for identity brokering. + # + # THE BLAST RADIUS: this release is the cluster's identity provider. If + # ingressController below is wrong, EVERY LOGIN EVERYWHERE BREAKS, and no + # part of CI can catch it - no ingress controller runs in the install-test, + # and the probes use 9000 so the pod goes Ready either way. The source was + # measured on the live cluster, but measured labels are not proven traffic: + # after the first upgrade, verify with a real login. + # + # AND THE SECOND TRAP: a consumer that reaches Keycloak on its IN-CLUSTER + # Service address instead of its public URL is a pod in another namespace, + # and the http rule blocks it. Put those namespaces in allowedNamespaces. + # Consumers using the public issuer URL come through Traefik and are covered. + # + # NOT allowed, and the likely first surprise: SMTP. Mail is configured per + # REALM, which this chart never sees - see egress.extraPorts. + enabled: true + # Namespaces whose pods may reach this workload, in addition to the release's + # own namespace, which is always allowed. Empty means: own namespace only. + # + # Matched on the "kubernetes.io/metadata.name" label Kubernetes sets on every + # namespace itself, so no labelling work is needed on a namespace this chart + # does not own - and no typo protection either: a name that does not exist + # matches nothing and is silently ineffective. Note the granularity: an entry + # opens the port to EVERY pod in that namespace. + allowedNamespaces: [] + # Source CIDRs allowed to reach the management port (keycloak.ports.management, 9000 by default), which is the port all three probes address. + # + # EMPTY MEANS OPEN, NOT BLOCKED. With no entry that rule is rendered WITHOUT a + # "from" selector, so the port is reachable from anywhere the CNI routes to - + # inside the cluster, since it carries no NodePort. + # + # That is the deliberate choice between two bad defaults. The kubelet's probes + # originate from the IP of the WORKER NODE running the pod, an address this + # chart cannot know and must not guess: a CIDR that excludes the real node + # blocks every probe, the pod never becomes Ready, the rollout times out, and + # the symptom reads as a broken application rather than as a policy. + # + # Narrow it by listing the worker node IPs (e.g. ["10.0.1.11/32"]) - and mind + # the companion trap: once this list is non-empty the rule IS restricted, so + # everything else that needs the port has to be in it. The chart adds the own + # namespace, the ingressController below and allowedNamespaces to the same rule for exactly that reason. + probeSources: [] + # The ingress controller allowed to reach the routed port once probeSources + # restricts it. Rendered only while ingress.enabled or httpRoute.enabled is true, and only in + # the restricted branch - with the default (open) rule the controller reaches + # the port anyway. The two keys become ONE "from" entry and are therefore an + # AND: pods carrying podLabels IN namespace. That is the point of having them + # at all - the controller's namespace in allowedNamespaces would open the port + # to every pod in kube-system. + # + # MEASURED ON THIS CLUSTER, NOT ASSUMED (2026-10-14): ingress-nginx is gone, + # the only IngressClass is "traefik" and it is the default, and the controller + # is the DaemonSet rke2-traefik in kube-system. The same Traefik serves the + # Gateway API here, so this one source covers the Ingress and the HTTPRoute + # path alike. A policy written against the wrong controller is accepted, looks + # strict, and silently blocks all web traffic - and ingress.className says + # nothing about it, because a class names an implementation, not the pods the + # traffic comes from. + ingressController: + namespace: kube-system + podLabels: + app.kubernetes.io/name: rke2-traefik + egress: + # Additional outbound TCP ports. DNS (UDP and TCP 53) is always allowed and + # has no switch - a knob whose only other setting breaks every name lookup + # in the pod is a trap, not a feature. + # + # PORTS, NOT DESTINATIONS, and that is a limit of the object rather than + # laziness: a NetworkPolicy matches CIDRs and selectors and never hostnames, + # so "only to the database host" cannot be written at all. Each entry + # permits its port to ANY destination. What the cap still buys is real: a + # compromised pod cannot open arbitrary outbound connections. + # + # SMTP is the entry this chart needs most: 587 or 465, for the mail server + # configured inside a realm. Also an LDAP user federation (389 or 636), or + # a database that is somehow not on keycloak.db.port. + extraPorts: []