diff --git a/Dockerfile b/Dockerfile
index aa30ca5..0328a37 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -24,7 +24,8 @@ RUN mkdir -p /app/downloads
COPY --from=build /app/publish .
+# Only expose the main proxy port (8080)
+# Admin UI runs on 5275 but is NOT exposed - access via docker exec or SSH tunnel
EXPOSE 8080
-ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "allstarr.dll"]
diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs
index ea38a0c..f13416b 100644
--- a/allstarr/Controllers/AdminController.cs
+++ b/allstarr/Controllers/AdminController.cs
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Options;
using allstarr.Models.Settings;
using allstarr.Services.Spotify;
using allstarr.Services.Common;
+using allstarr.Filters;
using System.Text.Json;
using System.Text.RegularExpressions;
@@ -11,9 +12,11 @@ namespace allstarr.Controllers;
///
/// Admin API controller for the web dashboard.
/// Provides endpoints for viewing status, playlists, and modifying configuration.
+/// Only accessible on internal admin port (5275) - not exposed through reverse proxy.
///
[ApiController]
[Route("api/admin")]
+[ServiceFilter(typeof(AdminPortFilter))]
public class AdminController : ControllerBase
{
private readonly ILogger _logger;
diff --git a/allstarr/Filters/AdminPortFilter.cs b/allstarr/Filters/AdminPortFilter.cs
new file mode 100644
index 0000000..b69cb8c
--- /dev/null
+++ b/allstarr/Filters/AdminPortFilter.cs
@@ -0,0 +1,28 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace allstarr.Filters;
+
+///
+/// Filter that restricts access to admin endpoints to only the admin port (5275).
+/// This prevents the admin API from being accessed through the main proxy port.
+///
+public class AdminPortFilter : IActionFilter
+{
+ private const int AdminPort = 5275;
+
+ public void OnActionExecuting(ActionExecutingContext context)
+ {
+ var requestPort = context.HttpContext.Connection.LocalPort;
+
+ if (requestPort != AdminPort)
+ {
+ context.Result = new NotFoundResult();
+ }
+ }
+
+ public void OnActionExecuted(ActionExecutedContext context)
+ {
+ // No action needed after execution
+ }
+}
diff --git a/allstarr/Middleware/AdminStaticFilesMiddleware.cs b/allstarr/Middleware/AdminStaticFilesMiddleware.cs
new file mode 100644
index 0000000..14e7965
--- /dev/null
+++ b/allstarr/Middleware/AdminStaticFilesMiddleware.cs
@@ -0,0 +1,51 @@
+namespace allstarr.Middleware;
+
+///
+/// Middleware that only serves static files on the admin port (5275).
+/// This keeps the admin UI isolated from the main proxy port.
+///
+public class AdminStaticFilesMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly StaticFileMiddleware _staticFileMiddleware;
+ private readonly DefaultFilesMiddleware _defaultFilesMiddleware;
+ private const int AdminPort = 5275;
+
+ public AdminStaticFilesMiddleware(
+ RequestDelegate next,
+ IWebHostEnvironment env,
+ ILoggerFactory loggerFactory)
+ {
+ _next = next;
+
+ var staticFileOptions = new StaticFileOptions();
+ var defaultFilesOptions = new DefaultFilesOptions();
+
+ _staticFileMiddleware = new StaticFileMiddleware(
+ _next,
+ env,
+ Microsoft.Extensions.Options.Options.Create(staticFileOptions),
+ loggerFactory);
+
+ _defaultFilesMiddleware = new DefaultFilesMiddleware(
+ (ctx) => _staticFileMiddleware.Invoke(ctx),
+ env,
+ Microsoft.Extensions.Options.Options.Create(defaultFilesOptions));
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ var port = context.Connection.LocalPort;
+
+ if (port == AdminPort)
+ {
+ // Serve static files on admin port
+ await _defaultFilesMiddleware.Invoke(context);
+ }
+ else
+ {
+ // Skip static files on main port
+ await _next(context);
+ }
+ }
+}
diff --git a/allstarr/Program.cs b/allstarr/Program.cs
index 4febdde..ac3af6c 100644
--- a/allstarr/Program.cs
+++ b/allstarr/Program.cs
@@ -43,11 +43,18 @@ static List DecodeSquidWtfUrls()
var backendType = builder.Configuration.GetValue("Backend:Type");
// Configure Kestrel for large responses over VPN/Tailscale
+// Also configure admin port on 5275 (internal only, not exposed)
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.MaxResponseBufferSize = null; // Disable response buffering limit
serverOptions.Limits.MaxRequestBodySize = null; // Allow large request bodies
serverOptions.Limits.MinResponseDataRate = null; // Disable minimum data rate for slow connections
+
+ // Main proxy port (exposed)
+ serverOptions.ListenAnyIP(8080);
+
+ // Admin UI port (internal only - do NOT expose through reverse proxy)
+ serverOptions.ListenAnyIP(5275);
});
// Add response compression for large JSON responses (helps with Tailscale/VPN MTU issues)
@@ -99,6 +106,9 @@ builder.Services.AddHttpContextAccessor();
builder.Services.AddExceptionHandler();
builder.Services.AddProblemDetails();
+// Admin port filter (restricts admin API to port 5275)
+builder.Services.AddScoped();
+
// Configuration - register both settings, active one determined by backend type
builder.Services.Configure(
builder.Configuration.GetSection("Subsonic"));
@@ -565,9 +575,8 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection();
-// Serve static files from wwwroot
-app.UseDefaultFiles();
-app.UseStaticFiles();
+// Serve static files only on admin port (5275)
+app.UseMiddleware();
app.UseAuthorization();
@@ -578,9 +587,6 @@ app.MapControllers();
// Health check endpoint for monitoring
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));
-// Admin dashboard redirect
-app.MapGet("/admin", () => Results.Redirect("/index.html"));
-
app.Run();
///
diff --git a/docker-compose.yml b/docker-compose.yml
index ffc39c4..2016eed 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -34,6 +34,9 @@ services:
restart: unless-stopped
ports:
- "5274:8080"
+ # Admin UI on port 5275 - ONLY expose if you need local access
+ # DO NOT expose through reverse proxy - contains sensitive config
+ # - "5275:5275"
depends_on:
redis:
condition: service_healthy