diff --git a/backend/UniVerse.Api/Middleware/LocalNetworksOnlyMiddleware.cs b/backend/UniVerse.Api/Middleware/LocalNetworksOnlyMiddleware.cs new file mode 100644 index 0000000..db9d6a4 --- /dev/null +++ b/backend/UniVerse.Api/Middleware/LocalNetworksOnlyMiddleware.cs @@ -0,0 +1,71 @@ +using System.Net; +using System.Net.Sockets; + +namespace UniVerse.Api.Middleware; + +public sealed class LocalNetworksOnlyMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public LocalNetworksOnlyMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + var remoteIpAddress = context.Connection.RemoteIpAddress; + + if (remoteIpAddress is null || !IsLocalNetwork(remoteIpAddress)) + { + _logger.LogWarning("Blocked metrics request from non-local address {RemoteIpAddress}", remoteIpAddress); + context.Response.StatusCode = StatusCodes.Status403Forbidden; + await context.Response.WriteAsync("Metrics endpoint is available only from local networks."); + return; + } + + await _next(context); + } + + private static bool IsLocalNetwork(IPAddress ipAddress) + { + if (IPAddress.IsLoopback(ipAddress)) + { + return true; + } + + if (ipAddress.IsIPv4MappedToIPv6) + { + ipAddress = ipAddress.MapToIPv4(); + } + + return ipAddress.AddressFamily switch + { + AddressFamily.InterNetwork => IsPrivateOrLinkLocalIPv4(ipAddress), + AddressFamily.InterNetworkV6 => IsPrivateOrLinkLocalIPv6(ipAddress), + _ => false + }; + } + + private static bool IsPrivateOrLinkLocalIPv4(IPAddress ipAddress) + { + var bytes = ipAddress.GetAddressBytes(); + + return bytes[0] == 10 + || bytes[0] == 127 + || (bytes[0] == 192 && bytes[1] == 168) + || (bytes[0] == 172 && bytes[1] is >= 16 and <= 31) + || (bytes[0] == 169 && bytes[1] == 254); + } + + private static bool IsPrivateOrLinkLocalIPv6(IPAddress ipAddress) + { + var bytes = ipAddress.GetAddressBytes(); + + return ipAddress.IsIPv6LinkLocal + || ipAddress.IsIPv6SiteLocal + || (bytes[0] & 0xfe) == 0xfc; + } +} diff --git a/backend/UniVerse.Api/Program.cs b/backend/UniVerse.Api/Program.cs index a9d76c9..e6a1089 100644 --- a/backend/UniVerse.Api/Program.cs +++ b/backend/UniVerse.Api/Program.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi; +using Prometheus; using Quartz; using Serilog; using UniVerse.Api.BackgroundServices; @@ -221,6 +222,7 @@ if (app.Environment.IsDevelopment()) app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseHttpMetrics(); if (app.Environment.IsDevelopment()) { app.UseAntiforgery(); @@ -228,4 +230,10 @@ if (app.Environment.IsDevelopment()) } app.MapControllers(); +// Restrict Prometheus scrape endpoint to local and private networks. +app.UseWhen( + context => context.Request.Path.StartsWithSegments("/metrics", StringComparison.OrdinalIgnoreCase), + branch => branch.UseMiddleware()); +app.MapMetrics(); + app.Run(); diff --git a/backend/UniVerse.Api/UniVerse.Api.csproj b/backend/UniVerse.Api/UniVerse.Api.csproj index 1f9a164..70e2746 100644 --- a/backend/UniVerse.Api/UniVerse.Api.csproj +++ b/backend/UniVerse.Api/UniVerse.Api.csproj @@ -30,6 +30,7 @@ +