Suggested label: bug
Summary
When authentication is disabled (the default), the API-key management endpoints are gated to "local or private-network" callers. That private-network check (SecurityRequestUtils.IsPrivateOrLoopback) only accepts RFC1918 IPv4 (10/8, 172.16/12, 192.168/16) + loopback/link-local. It does not include the CGNAT range 100.64.0.0/10 that Tailscale assigns to nodes, nor does it accept global IPv6. As a result, a client reaching Listenarr through a (trusted) reverse proxy from a Tailscale IP is treated as a public/untrusted caller and receives 403 on GET/POST /api/v{ver}/configuration/apikey*.
In the UI this surfaces as an empty API Key field (Settings → General): copy is greyed out and the regenerate button does nothing, because the frontend silently swallows the 403.
Environment
- Listenarr: 1.0.7 (official Docker image)
- Host: unraid, Docker; .NET 10.0.8 (Linux x64)
- Reverse proxy: Traefik edge (TLS terminates at the proxy, forwards plain HTTP +
X-Forwarded-For)
- Access path: browser → Traefik (
192.168.2.96:443) → Listenarr (192.168.2.179:4546)
- Client network: Tailscale — client source IP
100.83.13.2 (in 100.64.0.0/10)
- Auth: disabled (default)
- Browser-independent (the gate is server-side / IP-based).
Steps to reproduce
- Run Listenarr with authentication disabled (default).
- Put it behind a reverse proxy on a trusted RFC1918 network (so
app.UseForwardedHeaders() honors the proxy's X-Forwarded-For).
- Access the UI from a client whose forwarded source IP is in
100.64.0.0/10 (e.g. any Tailscale node), and open Settings → General.
Minimal unit-level reproduction (no proxy needed):
SecurityRequestUtils.IsPrivateOrLoopback(IPAddress.Parse("100.83.13.2")); // returns false
Expected behavior
The API Key field should never silently render blank with a greyed copy button and a no-op regenerate button. When the security gate blocks the request, the UI should clearly explain why the key is hidden and how to view it — rather than appearing broken.
Actual behavior
GET /api/v1/configuration/apikey → 403
{"message":"API key management is only available from local or private-network clients when authentication is disabled"}
- API Key field renders empty; copy button greyed (
:disabled="!apiKey"); regenerate is a no-op.
- Every other part of the app works for the same client — only the API-key endpoints are IP-gated, which is why just this field breaks.
Root cause
listenarr.application/Security/SecurityRequestUtils.cs:124-156 — IsPrivateOrLoopback IPv4 branch lacks 100.64.0.0/10; IPv6 branch accepts only loopback/link-local/site-local/fc00::/7 (all global IPv6 is "non-private").
listenarr.api/Attributes/RequireApiKeyManagementAccessAttribute.cs:51-84 — with auth disabled, the filter returns 403 unless IsLocalOrPrivateRequest is true.
listenarr.api/Program.cs:271-283,700 — UseForwardedHeaders trusts RFC1918/fc00::/7/fe80::/10 proxies with the default ForwardLimit (1); the single peeled X-Forwarded-For entry (the Tailscale client IP) is what the gate evaluates.
Resolution
The security gate is intentional and kept as-is — it must not be weakened. The trust-broadening options first floated here (hardcode 100.64.0.0/10; a configurable trusted-networks list; or trusting the forwarded client via an already-trusted proxy) were rejected, because IsLocalOrPrivateRequest also drives ShouldRedactSecretsForCaller: widening "trusted" would hand un-redacted config secrets to those callers across ~11 endpoints, for every deployment — far beyond the API key.
The real defect is the silently-swallowed 403 in the frontend (SettingsView.vue → catch { apiKey.value = '' }), which turned the gate's response into a blank field with no explanation. The accepted fix keeps the gate unchanged and makes the UI explain it: the API Key field's help text now states the key is only revealed to signed-in administrators or local/private-network clients, and tells the user how to view it (enable authentication and sign in, or access from a local/trusted network). (PR to follow.)
Related / follow-ups
- Contextual error surfacing (optional enhancement): this fix communicates the gate via help text; a richer version would surface a note only when the 403 actually occurs — bind the error in
fe/src/views/SettingsView.vue:1393-1398 (it carries .status) and render an inline callout near the field.
- Regenerate unusable behind any reverse proxy: when the field is blank,
onRegenerate calls the loopback-only generateInitialApiKey (listenarr.api/Controllers/Configurations/ApiKeyController.cs:96-104, IPAddress.IsLoopback), which can never succeed for a proxied client (and 409s if a key already exists). Separate follow-up: distinguish "load failed" from "no key" and use the management-access path.
Workaround
Enable authentication and sign in as admin (the API-key endpoints then authorize via the admin session instead of the IP check), or read the key directly from config.json (ApiKey).
Suggested label:
bugSummary
When authentication is disabled (the default), the API-key management endpoints are gated to "local or private-network" callers. That private-network check (
SecurityRequestUtils.IsPrivateOrLoopback) only accepts RFC1918 IPv4 (10/8,172.16/12,192.168/16) + loopback/link-local. It does not include the CGNAT range100.64.0.0/10that Tailscale assigns to nodes, nor does it accept global IPv6. As a result, a client reaching Listenarr through a (trusted) reverse proxy from a Tailscale IP is treated as a public/untrusted caller and receives 403 onGET/POST /api/v{ver}/configuration/apikey*.In the UI this surfaces as an empty API Key field (Settings → General): copy is greyed out and the regenerate button does nothing, because the frontend silently swallows the 403.
Environment
X-Forwarded-For)192.168.2.96:443) → Listenarr (192.168.2.179:4546)100.83.13.2(in100.64.0.0/10)Steps to reproduce
app.UseForwardedHeaders()honors the proxy'sX-Forwarded-For).100.64.0.0/10(e.g. any Tailscale node), and open Settings → General.Minimal unit-level reproduction (no proxy needed):
Expected behavior
The API Key field should never silently render blank with a greyed copy button and a no-op regenerate button. When the security gate blocks the request, the UI should clearly explain why the key is hidden and how to view it — rather than appearing broken.
Actual behavior
GET /api/v1/configuration/apikey→ 403{"message":"API key management is only available from local or private-network clients when authentication is disabled"}:disabled="!apiKey"); regenerate is a no-op.Root cause
listenarr.application/Security/SecurityRequestUtils.cs:124-156—IsPrivateOrLoopbackIPv4 branch lacks100.64.0.0/10; IPv6 branch accepts only loopback/link-local/site-local/fc00::/7(all global IPv6 is "non-private").listenarr.api/Attributes/RequireApiKeyManagementAccessAttribute.cs:51-84— with auth disabled, the filter returns 403 unlessIsLocalOrPrivateRequestis true.listenarr.api/Program.cs:271-283,700—UseForwardedHeaderstrusts RFC1918/fc00::/7/fe80::/10proxies with the defaultForwardLimit(1); the single peeledX-Forwarded-Forentry (the Tailscale client IP) is what the gate evaluates.Resolution
The security gate is intentional and kept as-is — it must not be weakened. The trust-broadening options first floated here (hardcode
100.64.0.0/10; a configurable trusted-networks list; or trusting the forwarded client via an already-trusted proxy) were rejected, becauseIsLocalOrPrivateRequestalso drivesShouldRedactSecretsForCaller: widening "trusted" would hand un-redacted config secrets to those callers across ~11 endpoints, for every deployment — far beyond the API key.The real defect is the silently-swallowed 403 in the frontend (
SettingsView.vue→catch { apiKey.value = '' }), which turned the gate's response into a blank field with no explanation. The accepted fix keeps the gate unchanged and makes the UI explain it: the API Key field's help text now states the key is only revealed to signed-in administrators or local/private-network clients, and tells the user how to view it (enable authentication and sign in, or access from a local/trusted network). (PR to follow.)Related / follow-ups
fe/src/views/SettingsView.vue:1393-1398(it carries.status) and render an inline callout near the field.onRegeneratecalls the loopback-onlygenerateInitialApiKey(listenarr.api/Controllers/Configurations/ApiKeyController.cs:96-104,IPAddress.IsLoopback), which can never succeed for a proxied client (and 409s if a key already exists). Separate follow-up: distinguish "load failed" from "no key" and use the management-access path.Workaround
Enable authentication and sign in as admin (the API-key endpoints then authorize via the admin session instead of the IP check), or read the key directly from
config.json(ApiKey).