diff --git a/Api/LancacheManager/Middleware/MetricsAuthenticationMiddleware.cs b/Api/LancacheManager/Middleware/MetricsAuthenticationMiddleware.cs
index e7be45050..17cafa25d 100644
--- a/Api/LancacheManager/Middleware/MetricsAuthenticationMiddleware.cs
+++ b/Api/LancacheManager/Middleware/MetricsAuthenticationMiddleware.cs
@@ -1,21 +1,26 @@
+using System.Security.Claims;
using LancacheManager.Core.Interfaces;
using LancacheManager.Security;
namespace LancacheManager.Middleware;
///
-/// Middleware to optionally require API key authentication for Prometheus metrics endpoint (/metrics)
+/// Middleware to optionally require API key authentication for Prometheus metrics endpoint (/metrics).
+///
+/// This middleware MUST be registered before UseAuthorization() in Program.cs so it can set
+/// context.User before the FallbackPolicy (RequireAuthenticatedUser) is evaluated.
///
/// Configuration Priority:
/// 1. UI Toggle (StateService) - if set via UI, takes precedence
/// 2. Environment Variable / appsettings.json (Security:RequireAuthForMetrics) - default fallback
///
/// Values:
-/// - false (default): Metrics are PUBLIC - no authentication required
-/// - true: Metrics require API Key in X-Api-Key header
+/// - false (default): Metrics are PUBLIC - a synthetic principal is set so the FallbackPolicy
+/// is satisfied without requiring any credentials from the scraper.
+/// - true: Metrics require an API key via X-Api-Key header or Authorization: Bearer <key>.
///
/// Use Cases:
-/// - false: Prometheus/Grafana can scrape metrics without authentication (common setup)
+/// - false: Prometheus/Grafana can scrape metrics without authentication (common LAN setup)
/// - true: Secure metrics endpoint with API key (for internet-exposed instances)
///
public class MetricsAuthenticationMiddleware
@@ -51,15 +56,18 @@ public async Task InvokeAsync(HttpContext context, AuthenticationHelper authHelp
if (!requireAuth)
{
- // Metrics are public - allow Prometheus/Grafana to scrape without authentication
+ // Metrics are public — set a synthetic authenticated principal so the
+ // FallbackPolicy (RequireAuthenticatedUser) does not reject the request.
+ context.User = CreateMetricsPrincipal();
await _next(context);
return;
}
- // Metrics require authentication - check for API key
+ // Metrics require authentication — validate API key (X-Api-Key or Authorization: Bearer).
var result = authHelper.ValidateApiKey(context);
if (result.IsAuthenticated)
{
+ context.User = CreateMetricsPrincipal();
await _next(context);
return;
}
@@ -70,4 +78,9 @@ public async Task InvokeAsync(HttpContext context, AuthenticationHelper authHelp
await AuthenticationHelper.WriteErrorResponseAsync(
context, result.StatusCode, result.ErrorMessage ?? "API key required for metrics");
}
+
+ private static ClaimsPrincipal CreateMetricsPrincipal() =>
+ new(new ClaimsIdentity(
+ [new Claim(ClaimTypes.Name, "prometheus-scraper")],
+ authenticationType: "Metrics"));
}
diff --git a/Api/LancacheManager/Program.cs b/Api/LancacheManager/Program.cs
index bfcc7a692..ce70d8226 100644
--- a/Api/LancacheManager/Program.cs
+++ b/Api/LancacheManager/Program.cs
@@ -780,11 +780,14 @@
// ASP.NET Core authentication & authorization pipeline
// SessionAuthenticationHandler populates HttpContext.Items["Session"] for backward compatibility.
app.UseAuthentication();
-app.UseAuthorization();
-// Add Metrics Authentication Middleware (optional API key for /metrics)
+// MetricsAuthenticationMiddleware MUST run before UseAuthorization so it can set context.User
+// before the FallbackPolicy (RequireAuthenticatedUser) is evaluated. When RequireAuthForMetrics
+// is false it sets a synthetic principal; when true it validates the API key first.
app.UseMiddleware();
+app.UseAuthorization();
+
// Swagger authentication middleware (requires API key when Security:ProtectSwagger=true)
app.UseMiddleware();
diff --git a/Api/LancacheManager/Security/AuthenticationHelper.cs b/Api/LancacheManager/Security/AuthenticationHelper.cs
index eb6fffb21..44c224595 100644
--- a/Api/LancacheManager/Security/AuthenticationHelper.cs
+++ b/Api/LancacheManager/Security/AuthenticationHelper.cs
@@ -71,10 +71,21 @@ public bool IsAuthenticated(HttpContext context)
///
/// Gets the API key from request headers.
+ /// Accepts X-Api-Key header (primary) or Authorization: Bearer <key> (Prometheus convention).
///
public static string? GetApiKeyFromHeader(HttpContext context)
{
- return context.Request.Headers["X-Api-Key"].FirstOrDefault();
+ var apiKey = context.Request.Headers["X-Api-Key"].FirstOrDefault();
+ if (apiKey != null)
+ return apiKey;
+
+ // Support Authorization: Bearer so Prometheus scrape_config can use the
+ // standard `authorization: { type: Bearer, credentials: }` block.
+ var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
+ if (authHeader?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true)
+ return authHeader["Bearer ".Length..].Trim();
+
+ return null;
}
///
diff --git a/README.MD b/README.MD
index dae012d04..3d72d7dfc 100644
--- a/README.MD
+++ b/README.MD
@@ -31,6 +31,7 @@ A web UI for [LANCache](https://lancache.net/). Watch downloads as they happen,
- [Reverse Proxy (Nginx)](#nginx-reverse-proxy)
- [Monitoring (Grafana & Prometheus)](#grafana--prometheus)
- [Troubleshooting](#troubleshooting)
+ - [/metrics returns 401 despite env var](#metrics-401)
- [Building from Source](#building-from-source)
- [Contributing Translations](#contributing-translations)
- [Support and License](#support-and-license)
@@ -300,7 +301,7 @@ services:
| `Security__EnableAuthentication` | `true` | Require an API key for admin actions. Only turn off for local dev. |
| `Security__MaxAdminDevices` | `3` | How many devices can share the same API key. |
| `Security__GuestSessionDurationHours` | `6` | Default guest session length (also configurable in the UI). |
-| `Security__RequireAuthForMetrics` | `false` | Require an API key on `/metrics`. |
+| `Security__RequireAuthForMetrics` | `false` | Require an API key on `/metrics`. **This env var is the default only** — if the toggle has ever been saved via the UI (Management → Security), the persisted value takes priority. See [Metrics returns 401 despite env var](#metrics-401) if the env var appears to have no effect. |
| `Security__ProtectSwagger` | `true` | Require auth on Swagger docs in production. |
| `Security__AllowedOrigins` | (empty) | Comma-separated CORS allow list. Empty allows all. |
| `Security__AllowedBrowsePaths` | (empty) | Comma-separated allow-list of file-browser roots. Empty disables the file browser entirely (every request returns `403`). Example: `/data,/mnt`. |
@@ -831,6 +832,45 @@ lancache_cache_size_bytes / 1024 / 1024 / 1024
2. The mapping service queries the Epic API to identify what's in your cache.
3. Game names and cover art come down automatically.
+
+### `/metrics` returns 401 despite `Security__RequireAuthForMetrics=false`
+
+**Symptom:** Prometheus (or a manual `curl`) gets a `401 Unauthorized` from `/metrics` even though `Security__RequireAuthForMetrics=false` is set in the environment and is visible in `docker inspect`.
+
+**Why it happens — config priority:**
+
+```
+UI toggle (persisted in state.json) > env var / appsettings.json default
+```
+
+`Security__RequireAuthForMetrics` is the *default fallback*. Once the toggle is saved via the UI (Management → Security → "Require authentication for metrics"), that value is written to `data/state/state.json` and permanently wins — the env var is no longer consulted.
+
+**Fix option 1 — via the UI:**
+
+Go to **Management → Security** and toggle "Require authentication for metrics" off, then save.
+
+**Fix option 2 — edit `state.json` directly** (when the UI is inaccessible or you prefer config-as-code):
+
+```bash
+# Stop the container first to avoid a concurrent write race
+docker stop lancache-manager
+
+# Null out the field so the env var becomes the effective value again
+python3 -c "
+import json
+with open('./data/state/state.json') as f:
+ s = json.load(f)
+s['requireAuthForMetrics'] = None
+with open('./data/state/state.json', 'w') as f:
+ json.dump(s, f, indent=2)
+print('done')
+"
+
+docker start lancache-manager
+```
+
+After the restart, `Security__RequireAuthForMetrics=false` takes effect.
+
### Lost API key
```bash