diff --git a/DeepDrftWeb/Middleware/CmsStealthRoutingHandler.cs b/DeepDrftWeb/Middleware/CmsStealthRoutingHandler.cs
new file mode 100644
index 0000000..8e88204
--- /dev/null
+++ b/DeepDrftWeb/Middleware/CmsStealthRoutingHandler.cs
@@ -0,0 +1,34 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization.Policy;
+
+namespace DeepDrftWeb.Middleware;
+
+///
+/// Returns 404 for any /cms/* request that fails authorization.
+/// This prevents the CMS from acknowledging its own existence to unauthorized callers
+/// (a redirect to /account/login would reveal that the route exists).
+/// CMS-PLAN §3.4 stealth-routing constraint.
+///
+public class CmsStealthRoutingHandler : IAuthorizationMiddlewareResultHandler
+{
+ private readonly AuthorizationMiddlewareResultHandler _default = new();
+
+ public async Task HandleAsync(
+ RequestDelegate next,
+ HttpContext context,
+ AuthorizationPolicy policy,
+ PolicyAuthorizationResult authorizeResult)
+ {
+ // For /cms/* routes (including an exact /cms hit), map any authorization
+ // failure to 404 regardless of cause (unauthenticated, wrong role, or any
+ // future policy failure). This prevents the CMS from acknowledging its
+ // own existence to callers outside the Admin hierarchy.
+ if (context.Request.Path.StartsWithSegments("/cms") && !authorizeResult.Succeeded)
+ {
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ return;
+ }
+
+ await _default.HandleAsync(next, context, policy, authorizeResult);
+ }
+}
diff --git a/DeepDrftWeb/Program.cs b/DeepDrftWeb/Program.cs
index 01d0ff1..a4f5ad0 100644
--- a/DeepDrftWeb/Program.cs
+++ b/DeepDrftWeb/Program.cs
@@ -2,6 +2,8 @@ using AuthBlocksLib;
using AuthBlocksLib.Options;
using DeepDrftCms;
using DeepDrftWeb;
+using DeepDrftWeb.Middleware;
+using Microsoft.AspNetCore.Authorization;
using MudBlazor.Services;
using DeepDrftWeb.Components;
using Microsoft.AspNetCore.HttpOverrides;
@@ -64,6 +66,11 @@ builder.Services.AddAuthBlocks(options =>
};
});
+// CMS stealth routing: unauthorized /cms/* requests return 404, not a redirect.
+// This prevents the CMS from revealing its own existence to unauthenticated callers.
+// See CMS-PLAN §3.4.
+builder.Services.AddSingleton();
+
// AuthBlocksWeb: Blazor JWT client services (auth API is mounted on this same host via MapAuthBlocks).
// AuthBlocksWeb.Startup.ConfigureAuthServices registers AddCascadingAuthenticationState server-side.
AuthBlocksWeb.Startup.ConfigureAuthServices(builder.Services, baseUrl);