Move admin UI to separate internal port (5275) for security

- Admin API and static files only accessible on port 5275
- Main proxy port (8080) no longer serves admin endpoints
- AdminPortFilter rejects admin requests on wrong port
- AdminStaticFilesMiddleware only serves static files on admin port
- Port 5275 NOT exposed in Dockerfile or docker-compose by default
- Access admin UI via SSH tunnel or by uncommenting port mapping
This commit is contained in:
2026-02-03 14:39:07 -05:00
parent 6abf0e0717
commit a8d04b225b
6 changed files with 99 additions and 7 deletions

View File

@@ -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"]

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
[ApiController]
[Route("api/admin")]
[ServiceFilter(typeof(AdminPortFilter))]
public class AdminController : ControllerBase
{
private readonly ILogger<AdminController> _logger;

View File

@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace allstarr.Filters;
/// <summary>
/// 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.
/// </summary>
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
}
}

View File

@@ -0,0 +1,51 @@
namespace allstarr.Middleware;
/// <summary>
/// Middleware that only serves static files on the admin port (5275).
/// This keeps the admin UI isolated from the main proxy port.
/// </summary>
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);
}
}
}

View File

@@ -43,11 +43,18 @@ static List<string> DecodeSquidWtfUrls()
var backendType = builder.Configuration.GetValue<BackendType>("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<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
// Admin port filter (restricts admin API to port 5275)
builder.Services.AddScoped<allstarr.Filters.AdminPortFilter>();
// Configuration - register both settings, active one determined by backend type
builder.Services.Configure<SubsonicSettings>(
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<allstarr.Middleware.AdminStaticFilesMiddleware>();
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();
/// <summary>

View File

@@ -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