diff --git a/freesailarr/Chart.yaml b/freesailarr/Chart.yaml index e6c05b8..cb8e8e8 100644 --- a/freesailarr/Chart.yaml +++ b/freesailarr/Chart.yaml @@ -8,7 +8,7 @@ description: A Helm chart to deploy a Wireguard-tunneled media automation stack type: application -version: 3.3.0+up1.0.0 +version: 4.0.0+up1.0.0 # The chart's own version marker, freely chosen and tied to no upstream. The # stack bundles ten independently versioned projects (nine of them on by # default), so there is no single "app version" to track here. diff --git a/freesailarr/templates/NOTES.txt b/freesailarr/templates/NOTES.txt index bbf4908..f66c27f 100644 --- a/freesailarr/templates/NOTES.txt +++ b/freesailarr/templates/NOTES.txt @@ -52,6 +52,45 @@ SET gluetun.vpn.firewallOutboundSubnets — IT IS EMPTY RIGHT NOW fail, in a way that looks like an application bug. Set it to the cluster pod and service CIDRs, e.g. "10.42.0.0/16,10.43.0.0/16" on k3s. {{- end }} +{{- /* The servarr apps that are published through a routing object AND have no + trusted proxy network set - the one combination in which the empty + default actually costs something. Gated on freesailarr.published plus + membership in freesailarr.ingressApps, the same pair the ACCESS section + below uses, so it holds in Gateway mode too: reading ingress.enabled + directly would silence this warning in exactly the mode whose routes + still publish the apps. */}} +{{- $noTrust := list }} +{{- if $published }} +{{- range $a := $ingressApps }} +{{- if and (has $a.name (list "prowlarr" "radarr" "sonarr")) (not (index $.Values $a.name).trustedNetworks) }} +{{- $noTrust = append $noTrust $a.name }} +{{- end }} +{{- end }} +{{- end }} +{{- if $noTrust }} + +FORWARDED HEADERS ARE NOT TRUSTED — {{ join ", " $noTrust }} + These apps are published through a routing object and their trustedNetworks + value is empty, which is the chart's default. They therefore trust no proxy: + the client address each one logs and displays is your ingress controller's + pod IP rather than the real client, and each sees the scheme as http even + where TLS is terminated in front of it. If an app's authentication is set to + "Disabled for Local Addresses", a proxied request no longer counts as local + and is asked to authenticate. + + This is not a regression introduced by your values. Upstream removed the + hard-coded private networks (prowlarr 2.6.3, radarr 6.4.3; sonarr before + 4.0.19) because trusting X-Forwarded-For from any private network is a + spoofing vector, and this chart follows that default rather than reinstating + it behind your back. + + To honour the headers, set the network your ingress controller's pods sit in + — the chart cannot know it, as it is a property of the cluster: + + prowlarr.trustedNetworks: "10.42.0.0/16" # e.g. the k3s pod CIDR + + Leaving it empty is a valid choice; it is only not a silent one. +{{- end }} ACCESS {{- if and $published $ingressApps }} diff --git a/freesailarr/templates/_helpers.tpl b/freesailarr/templates/_helpers.tpl index 8f64e8e..e99b063 100644 --- a/freesailarr/templates/_helpers.tpl +++ b/freesailarr/templates/_helpers.tpl @@ -406,6 +406,46 @@ Input: dict "root" $ "app" <"radarr"|"sonarr"|"prowlarr">. {{- $path -}} {{- end -}} +{{/* +The value for a servarr app's __SERVER__TRUSTEDNETWORKS variable, or the +empty string when the chart must not set it. Same env schema as the UrlBase +variable above (__CONFIGNAMESPACE__CONFIGITEM): verified for all three +apps at the tags this chart pins, where Bootstrap.cs binds +services.Configure(config.GetSection(":Server")) next to a +plain .AddEnvironmentVariables(). + +The variable is emitted ONLY when the value is non-empty. An empty +TRUSTEDNETWORKS would be indistinguishable from an unset one for the app - +upstream reads string.Empty either way - but not for a reader of the manifest, +to whom it would look like a setting that had been taken care of. + +Empty - i.e. the chart sets nothing - in two cases: + + - the app's trustedNetworks value is empty, which is the DEFAULT. The apps + then trust no proxy at all; see the value's comment in values.yaml for what + that costs and why it is not a preserved behaviour; + - the app's own env block already carries the variable, for the same reason + the UrlBase helper above stays out of that case: the env blocks are + additive, and two identically named variables in one container are not a + defined state to build on. + +Input: dict "root" $ "app" <"radarr"|"sonarr"|"prowlarr">. +*/}} +{{- define "freesailarr.trustedNetworks" -}} +{{- $root := .root -}} +{{- $app := .app -}} +{{- $var := printf "%s__SERVER__TRUSTEDNETWORKS" (upper $app) -}} +{{- $value := (index $root.Values $app).trustedNetworks | default "" -}} +{{- if $value -}} +{{- range $entry := (index $root.Values $app).env -}} +{{- if eq (toString $entry.name) $var -}} +{{- $value = "" -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- $value -}} +{{- end -}} + {{/* Comma-separated FIREWALL_INPUT_PORTS for gluetun: the ports of the enabled app containers (ingress traffic and the kubelet probes both arrive on the pod IP) diff --git a/freesailarr/templates/freesailarr.sfs.yaml b/freesailarr/templates/freesailarr.sfs.yaml index fb9a5a5..e32ebf5 100644 --- a/freesailarr/templates/freesailarr.sfs.yaml +++ b/freesailarr/templates/freesailarr.sfs.yaml @@ -279,6 +279,14 @@ spec: - name: PROWLARR__SERVER__URLBASE value: {{ . | quote }} {{- end }} + {{- /* Proxy networks whose forwarded headers prowlarr honours. + Emitted only when set - empty is the default and means no + proxy is trusted. See the freesailarr.trustedNetworks + helper and the value's comment in values.yaml. */}} + {{- with (include "freesailarr.trustedNetworks" (dict "root" $ "app" "prowlarr")) }} + - name: PROWLARR__SERVER__TRUSTEDNETWORKS + value: {{ . | quote }} + {{- end }} {{- include "freesailarr.env" (dict "root" $ "env" .Values.prowlarr.env) | nindent 12 }} ports: - containerPort: 9696 @@ -348,6 +356,11 @@ spec: - name: RADARR__SERVER__URLBASE value: {{ . | quote }} {{- end }} + {{- /* See the prowlarr TRUSTEDNETWORKS comment above. */}} + {{- with (include "freesailarr.trustedNetworks" (dict "root" $ "app" "radarr")) }} + - name: RADARR__SERVER__TRUSTEDNETWORKS + value: {{ . | quote }} + {{- end }} {{- include "freesailarr.env" (dict "root" $ "env" .Values.radarr.env) | nindent 12 }} ports: - containerPort: 7878 @@ -424,6 +437,13 @@ spec: - name: SONARR__SERVER__URLBASE value: {{ . | quote }} {{- end }} + {{- /* See the prowlarr TRUSTEDNETWORKS comment above. Sonarr did + not lose the trust with this chart version - 4.0.19 was + already in the empty state. */}} + {{- with (include "freesailarr.trustedNetworks" (dict "root" $ "app" "sonarr")) }} + - name: SONARR__SERVER__TRUSTEDNETWORKS + value: {{ . | quote }} + {{- end }} {{- include "freesailarr.env" (dict "root" $ "env" .Values.sonarr.env) | nindent 12 }} ports: - containerPort: 8989 diff --git a/freesailarr/values.schema.json b/freesailarr/values.schema.json index 374a021..57f454d 100644 --- a/freesailarr/values.schema.json +++ b/freesailarr/values.schema.json @@ -329,6 +329,9 @@ "env": { "$ref": "#/definitions/env" }, + "trustedNetworks": { + "$ref": "#/definitions/trustedNetworks" + }, "livenessProbe": { "$ref": "#/definitions/probe" }, @@ -395,6 +398,9 @@ "env": { "$ref": "#/definitions/env" }, + "trustedNetworks": { + "$ref": "#/definitions/trustedNetworks" + }, "livenessProbe": { "$ref": "#/definitions/probe" }, @@ -464,6 +470,9 @@ "env": { "$ref": "#/definitions/env" }, + "trustedNetworks": { + "$ref": "#/definitions/trustedNetworks" + }, "livenessProbe": { "$ref": "#/definitions/probe" }, @@ -690,6 +699,13 @@ } } }, + "trustedNetworks": { + "description": "Comma-separated CIDRs whose X-Forwarded-For/-Proto/-Host headers this servarr app honours, rendered as __SERVER__TRUSTEDNETWORKS and only when non-empty. Empty is the default and means no proxy is trusted - the app then reports the ingress controller's pod IP as the client and sees the scheme as http behind TLS termination. Kept null-safe deliberately: an erased value is the same permissive-to-nobody state as the default, so it must not fail the render.", + "type": [ + "string", + "null" + ] + }, "appStorage": { "description": "Size of this app's config volumeClaimTemplate. Not null-safe on purpose: an erased size yields an invalid claim template and fails loudly.", "type": "object", diff --git a/freesailarr/values.yaml b/freesailarr/values.yaml index a604b2c..7d2a26b 100644 --- a/freesailarr/values.yaml +++ b/freesailarr/values.yaml @@ -423,7 +423,7 @@ qbittorrent: enabled: true image: repository: docker.io/linuxserver/qbittorrent - tag: 5.2.3 + tag: 5.2.4 # Every app gets its own Ingress object (named ---ingress) # with its own TLS secret tls----ingress, so the apps can # be exposed independently and under unrelated hosts. @@ -628,7 +628,43 @@ prowlarr: enabled: true image: repository: docker.io/linuxserver/prowlarr - tag: 2.5.2 + tag: 2.6.5 + # Networks whose X-Forwarded-For / -Proto / -Host headers prowlarr honours, + # as comma-separated CIDRs (e.g. "10.42.0.0/16"). Rendered as + # PROWLARR__SERVER__TRUSTEDNETWORKS, and only when this value is non-empty. + # + # EMPTY IS THE DEFAULT, AND WITH IT PROWLARR TRUSTS NO PROXY AT ALL. That is + # a change in behaviour and not a preserved one. Up to 2.5.2 the app + # hard-coded 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7 and + # fe80::/10 as trusted proxy networks in Startup.cs; 2.6.3 removed all five + # and replaced them with this setting, whose own upstream default is + # string.Empty - ForwardedHeadersConfigurator returns without registering a + # single KnownNetwork while it is empty. Upstream's reason for the change is + # that trusting X-Forwarded-For from every private network is a spoofing + # vector, and this chart follows that intent rather than papering over it. + # + # What it costs while this stays empty and the app is published through an + # Ingress or an HTTPRoute: the client address prowlarr logs and shows is the + # ingress controller's pod IP rather than the real client, and the app sees + # the scheme as http even behind TLS termination. If its authentication is + # set to "Disabled for Local Addresses", a proxied request no longer counts + # as local and is asked to authenticate. + # + # The chart cannot fill this in for you. The network to trust is the one your + # ingress controller's pods sit in - the cluster pod CIDR, or that single + # controller pod's IP - which is a property of the cluster and not of the + # release. Set it yourself if you want the forwarded headers honoured. + # + # AllowedHosts, the other key 2.6.3 added, is deliberately NOT exposed as a + # value here. Its upstream default is string.Empty as well, and while it is + # empty the app accepts requests for any host ("Allowed Hosts is not + # configured, accepting requests for any host"), so there is nothing to + # repair. One trap to know before anyone sets it through the env block + # anyway: the app only ever appends localhost, 127.0.0.1, [::1] and its own + # hostname to that list, so neither the pod IP nor the in-cluster Service DNS + # name is covered - and the kubelet addresses the probes to the pod IP, so an + # incomplete list answers them with HTTP 400. + trustedNetworks: "" # See the qbittorrent ingress comment. ingress: enabled: true @@ -687,7 +723,7 @@ flaresolverr: enabled: true image: repository: ghcr.io/flaresolverr/flaresolverr - tag: v3.5.0 + tag: v3.5.2 # No ingress and no config volume: flaresolverr is stateless and only # consumed by prowlarr over localhost. env: [] @@ -742,7 +778,13 @@ radarr: enabled: true image: repository: docker.io/linuxserver/radarr - tag: 6.3.0 + tag: 6.4.4 + # The same setting, the same empty default and the same consequence as + # prowlarr's - read the trustedNetworks comment there for the whole picture. + # Radarr dropped the five hard-coded private networks in 6.4.3 (6.3.0, the + # version this chart shipped before, still had them) and reads this as + # RADARR__SERVER__TRUSTEDNETWORKS. + trustedNetworks: "" # See the qbittorrent ingress comment. ingress: enabled: true @@ -800,7 +842,7 @@ recyclarr: enabled: true image: repository: ghcr.io/recyclarr/recyclarr - tag: 8.7.1 + tag: 8.7.3 # No ingress: recyclarr has no web UI, it syncs TRaSH guides to radarr and # sonarr on a schedule (daemon mode). storage: @@ -853,7 +895,17 @@ sonarr: enabled: true image: repository: docker.io/linuxserver/sonarr - tag: 4.0.19 + tag: 4.0.20 + # The same setting as prowlarr's - read the trustedNetworks comment there. + # Reads as SONARR__SERVER__TRUSTEDNETWORKS. + # + # One difference worth stating plainly: sonarr did NOT lose the forwarded + # header trust with this chart version. 4.0.19, the version this chart + # shipped before, already had no hard-coded networks and the same + # empty-default TrustedNetworks, so the value is being supplied here for a + # state that already existed rather than for a new one. It had arrived + # unnoticed in what the convention classes as a plain app-version bump. + trustedNetworks: "" # See the qbittorrent ingress comment. ingress: enabled: true @@ -911,7 +963,7 @@ seerr: enabled: true image: repository: ghcr.io/seerr-team/seerr - tag: v3.4.1 + tag: v3.5.0 # See the qbittorrent ingress comment. ingress: enabled: true @@ -968,7 +1020,7 @@ bazarr: enabled: true image: repository: docker.io/linuxserver/bazarr - tag: 1.6.0 + tag: 1.6.2 # See the qbittorrent ingress comment. ingress: enabled: true