From 9d8e866dcd00aec3a6ec1c97010a1ce26bad9df0 Mon Sep 17 00:00:00 2001 From: Alan Deutscher Date: Sun, 13 Sep 2026 18:35:59 -0700 Subject: [PATCH 1/6] Methods/tests. --- .../Extensions/ForwardingHeadersExtensions.cs | 92 +++++++++ .../Extensions/HttpRequestExceptions.cs | 22 +++ .../Extensions/WebApplicationExtensions.cs | 21 -- src/RedShirt.Example.Api/Program.cs | 4 +- .../ForwardingHeadersExtensionsTests.cs | 181 ++++++++++++++++++ .../Extensions/HttpRequestExceptionsTests.cs | 54 ++++++ .../WebApplicationExtensionsTests.cs | 131 ------------- 7 files changed, 351 insertions(+), 154 deletions(-) create mode 100644 src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs create mode 100644 src/RedShirt.Example.Api/Extensions/HttpRequestExceptions.cs delete mode 100644 src/RedShirt.Example.Api/Extensions/WebApplicationExtensions.cs create mode 100644 test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs create mode 100644 test/RedShirt.Example.Api.UnitTests/Tests/Extensions/HttpRequestExceptionsTests.cs delete mode 100644 test/RedShirt.Example.Api.UnitTests/Tests/Extensions/WebApplicationExtensionsTests.cs diff --git a/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs b/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs new file mode 100644 index 0000000..a00170f --- /dev/null +++ b/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs @@ -0,0 +1,92 @@ +using Microsoft.AspNetCore.HttpOverrides; +using IPNetwork = System.Net.IPNetwork; + +namespace RedShirt.Example.Api.Extensions; + +public static class ForwardingHeadersExtensions +{ + /// + /// Parses a CIDR string (for example 172.16.0.0/12) into an . + /// + /// CIDR string + /// The parsed network when successful. + /// true if successfully parsed, otherwise false. + private static bool TryParseCidrAddressToNetwork(this string input, out IPNetwork network) + { + if (!string.IsNullOrWhiteSpace(input)) + { + return IPNetwork.TryParse(input.AsSpan().Trim(), out network); + } + + network = default; + return false; + } + + private static ConfigurationModel GetConfigurationModel(IConfiguration configuration) + { + return configuration + .GetSection(nameof(Configuration)) + .Get() ?? new ConfigurationModel + { + RespectHeaders = false, + TrustedNetworks = [] + }; + } + + public static IServiceCollection ConsiderConfiguringForwardingHeaders(this IServiceCollection serviceCollection, + IConfigurationRoot configuration) + { + var configurationModel = GetConfigurationModel(configuration); + + if (!configurationModel.RespectHeaders) + { + // Not enabled, immediately return + return serviceCollection; + } + + return serviceCollection.Configure(options => + { + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor + | ForwardedHeaders.XForwardedProto + | ForwardedHeaders.XForwardedHost + | ForwardedHeaders.XForwardedPrefix; + + // TCP peer must be a trusted proxy for forwarding headers, so add range to known network. + // A known network is a CIDR whose TCP peers ASP.NET treats as trusted proxies for X-Forwarded-* headers. + // Example for Cloudflare proxy: https://www.cloudflare.com/ips/ + + // Remove the default loopback-only network allowlist so the next lines fully define trust. + options.KnownIPNetworks.Clear(); + // Remove the default loopback proxy IPs so they do not mix with a custom proxy CIDR. + options.KnownProxies.Clear(); + + // Docker user-defined bridges usually sit in 172.16.0.0/12, `proxy`. + foreach (var candidateString in configurationModel.TrustedNetworks) + { + if (!candidateString.TryParseCidrAddressToNetwork(out var network)) + { + continue; + } + + options.KnownIPNetworks.Add(network); + } + }); + } + + public static IApplicationBuilder ConsiderUsingForwardedHeaders(this WebApplication app) + { + if (GetConfigurationModel(app.Configuration).RespectHeaders) + { + app.UseForwardedHeaders(); + } + + return app; + } + + private sealed class ConfigurationModel + { + public required bool RespectHeaders { get; init; } + public required List TrustedNetworks { get; init; } + } +} \ No newline at end of file diff --git a/src/RedShirt.Example.Api/Extensions/HttpRequestExceptions.cs b/src/RedShirt.Example.Api/Extensions/HttpRequestExceptions.cs new file mode 100644 index 0000000..e79149c --- /dev/null +++ b/src/RedShirt.Example.Api/Extensions/HttpRequestExceptions.cs @@ -0,0 +1,22 @@ +namespace RedShirt.Example.Api.Extensions; + +public static class HttpRequestExceptions +{ + public static Uri GetPublicBaseUri(this HttpRequest request) + { + ArgumentNullException.ThrowIfNull(request); + var builder = new UriBuilder + { + Scheme = request.Scheme, + Host = request.Host.Host, + Port = request.Host.Port ?? -1, + Path = request.PathBase.HasValue ? request.PathBase.Value! : "/" + }; + if (builder.Port is 80 or 443) + { + builder.Port = -1; + } + + return builder.Uri; + } +} \ No newline at end of file diff --git a/src/RedShirt.Example.Api/Extensions/WebApplicationExtensions.cs b/src/RedShirt.Example.Api/Extensions/WebApplicationExtensions.cs deleted file mode 100644 index 3f35787..0000000 --- a/src/RedShirt.Example.Api/Extensions/WebApplicationExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace RedShirt.Example.Api.Extensions; - -internal static class WebApplicationExtensions -{ - public static WebApplication ConsiderAddingPathBase(this WebApplication app) - { - var pathBase = app.Configuration.GetSection("API").Get()?.PathBase; - - if (!string.IsNullOrWhiteSpace(pathBase)) - { - app.UsePathBase(pathBase); - } - - return app; - } - - private sealed class ConfigurationModel - { - public required string? PathBase { get; init; } - } -} \ No newline at end of file diff --git a/src/RedShirt.Example.Api/Program.cs b/src/RedShirt.Example.Api/Program.cs index b61747d..6e86ba5 100644 --- a/src/RedShirt.Example.Api/Program.cs +++ b/src/RedShirt.Example.Api/Program.cs @@ -38,13 +38,13 @@ builder.Services .AddApiSwaggerDocument(builder.Configuration) .ConfigureApiServices(builder.Configuration) + .ConsiderConfiguringForwardingHeaders(builder.Configuration) .AddControllersWithViews(); var app = builder.Build(); app - // If we add a path base, then it must be the first middleware that we set. - .ConsiderAddingPathBase() + .ConsiderUsingForwardedHeaders() .UseExceptionHandler(); if (app.Environment.IsDevelopment()) diff --git a/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs new file mode 100644 index 0000000..68f982c --- /dev/null +++ b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs @@ -0,0 +1,181 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using RedShirt.Example.Api.Extensions; +using System.Net; +using IPNetwork = System.Net.IPNetwork; + +namespace RedShirt.Example.Api.UnitTests.Tests.Extensions; + +public class ForwardingHeadersExtensionsTests +{ + private const string ConfigurationSectionName = "Configuration"; + + private static IConfigurationRoot CreateConfiguration( + bool? respectHeaders = null, + params string[] trustedNetworks) + { + var values = new Dictionary(); + if (respectHeaders is not null) + { + values[$"{ConfigurationSectionName}:RespectHeaders"] = respectHeaders.Value.ToString(); + } + + for (var index = 0; index < trustedNetworks.Length; index++) + { + values[$"{ConfigurationSectionName}:TrustedNetworks:{index}"] = trustedNetworks[index]; + } + + return new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + } + + [Fact] + public void ConsiderConfiguringForwardingHeaders_ReturnsSameServiceCollection() + { + var services = new ServiceCollection(); + var configuration = CreateConfiguration(respectHeaders: true, "10.0.0.0/8"); + + var result = services.ConsiderConfiguringForwardingHeaders(configuration); + + Assert.Same(services, result); + } + + [Fact] + public void ConsiderConfiguringForwardingHeaders_DoesNotRegisterOptionsWhenDisabled() + { + var services = new ServiceCollection(); + var configuration = CreateConfiguration(respectHeaders: false); + + services.ConsiderConfiguringForwardingHeaders(configuration); + + using var provider = services.BuildServiceProvider(); + + Assert.Null(provider.GetService>()); + } + + [Fact] + public void ConsiderConfiguringForwardingHeaders_ConfiguresOptionsWhenEnabled() + { + var services = new ServiceCollection(); + var configuration = CreateConfiguration( + respectHeaders: true, + "10.0.0.0/8", + "not-a-cidr", + " 172.16.0.0/12 ", + "2001:db8::/32"); + + services.ConsiderConfiguringForwardingHeaders(configuration); + + using var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + + Assert.Equal( + ForwardedHeaders.XForwardedFor + | ForwardedHeaders.XForwardedProto + | ForwardedHeaders.XForwardedHost + | ForwardedHeaders.XForwardedPrefix, + options.ForwardedHeaders); + Assert.Empty(options.KnownProxies); + Assert.Equal(3, options.KnownIPNetworks.Count); + Assert.Contains(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8), options.KnownIPNetworks); + Assert.Contains(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12), options.KnownIPNetworks); + Assert.Contains(new IPNetwork(IPAddress.Parse("2001:db8::"), 32), options.KnownIPNetworks); + } + + [Fact] + public void ConsiderConfiguringForwardingHeaders_UsesDefaultsWhenSectionMissing() + { + var services = new ServiceCollection(); + var configuration = new ConfigurationBuilder().Build(); + + services.ConsiderConfiguringForwardingHeaders(configuration); + + using var provider = services.BuildServiceProvider(); + + Assert.Null(provider.GetService>()); + } + + [Fact] + public void ConsiderUsingForwardedHeaders_ReturnsSameApplicationInstance() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + var app = builder.Build(); + + var result = app.ConsiderUsingForwardedHeaders(); + + Assert.Same(app, result); + } + + [Fact] + public async Task ConsiderUsingForwardedHeaders_DoesNotApplyHeadersWhenDisabled() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + builder.Configuration.AddInMemoryCollection(CreateConfiguration(respectHeaders: false) + .AsEnumerable() + .ToDictionary(entry => entry.Key, entry => entry.Value)); + builder.Services.AddRouting(); + + string? capturedScheme = null; + + await using var app = builder.Build(); + app.ConsiderUsingForwardedHeaders(); + app.UseRouting(); + app.MapGet("/", (HttpRequest request) => + { + capturedScheme = request.Scheme; + return Results.NoContent(); + }); + + await app.StartAsync(TestContext.Current.CancellationToken); + var client = app.GetTestClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.TryAddWithoutValidation("X-Forwarded-Proto", "https"); + + var response = await client.SendAsync(request, TestContext.Current.CancellationToken); + + Assert.Equal(StatusCodes.Status204NoContent, (int)response.StatusCode); + Assert.Equal("http", capturedScheme); + } + + [Fact] + public async Task ConsiderUsingForwardedHeaders_AppliesHeadersWhenEnabled() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + builder.Configuration.AddInMemoryCollection(CreateConfiguration(respectHeaders: true, "0.0.0.0/0") + .AsEnumerable() + .ToDictionary(entry => entry.Key, entry => entry.Value)); + builder.Services + .ConsiderConfiguringForwardingHeaders((IConfigurationRoot)builder.Configuration) + .AddRouting(); + + string? capturedScheme = null; + + await using var app = builder.Build(); + app.ConsiderUsingForwardedHeaders(); + app.UseRouting(); + app.MapGet("/", (HttpRequest request) => + { + capturedScheme = request.Scheme; + return Results.NoContent(); + }); + + await app.StartAsync(TestContext.Current.CancellationToken); + var client = app.GetTestClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.TryAddWithoutValidation("X-Forwarded-Proto", "https"); + + var response = await client.SendAsync(request, TestContext.Current.CancellationToken); + + Assert.Equal(StatusCodes.Status204NoContent, (int)response.StatusCode); + Assert.Equal("https", capturedScheme); + } +} diff --git a/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/HttpRequestExceptionsTests.cs b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/HttpRequestExceptionsTests.cs new file mode 100644 index 0000000..77c1fcc --- /dev/null +++ b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/HttpRequestExceptionsTests.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Http; +using RedShirt.Example.Api.Extensions; + +namespace RedShirt.Example.Api.UnitTests.Tests.Extensions; + +public class HttpRequestExceptionsTests +{ + private static HttpRequest CreateRequest( + string scheme, + string host, + int? port = null, + string pathBase = "") + { + var context = new DefaultHttpContext + { + Request = + { + Scheme = scheme, + Host = port is null ? new HostString(host) : new HostString(host, port.Value), + PathBase = new PathString(pathBase), + }, + }; + + return context.Request; + } + + [Fact] + public void GetPublicBaseUri_ThrowsWhenRequestIsNull() + { + HttpRequest? request = null; + + Assert.Throws(() => request!.GetPublicBaseUri()); + } + + [Theory] + [InlineData("https", "example.com", null, "", "https://example.com/")] + [InlineData("http", "example.com", null, "", "http://example.com/")] + [InlineData("https", "example.com", 443, "", "https://example.com/")] + [InlineData("http", "example.com", 80, "", "http://example.com/")] + [InlineData("https", "example.com", 8443, "", "https://example.com:8443/")] + [InlineData("http", "example.com", 8080, "", "http://example.com:8080/")] + [InlineData("https", "example.com", null, "/api", "https://example.com/api")] + public void GetPublicBaseUri_BuildsUriFromSchemeHostPortAndPathBase( + string scheme, + string host, + int? port, + string pathBase, + string expected) + { + var uri = CreateRequest(scheme, host, port, pathBase).GetPublicBaseUri(); + + Assert.Equal(expected, uri.ToString()); + } +} diff --git a/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/WebApplicationExtensionsTests.cs b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/WebApplicationExtensionsTests.cs deleted file mode 100644 index 0809dab..0000000 --- a/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/WebApplicationExtensionsTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using RedShirt.Example.Api.Extensions; - -namespace RedShirt.Example.Api.UnitTests.Tests.Extensions; - -public class WebApplicationExtensionsTests -{ - public class ConsiderAddingPathBase - { - private static Dictionary ConfigurationWithPathBase(string? pathBase) - { - var values = new Dictionary(); - if (pathBase is not null) - { - values["API:PathBase"] = pathBase; - } - - return values; - } - - [Fact] - public async Task DoesNotSetPathBase_WhenApiSectionMissing() - { - var builder = WebApplication.CreateBuilder(); - builder.WebHost.UseTestServer(); - builder.Services.AddRouting(); - - string? capturedPathBase = null; - - await using var app = builder.Build(); - app.ConsiderAddingPathBase(); - app.UseRouting(); - app.MapGet("/ping", (HttpContext context) => - { - capturedPathBase = context.Request.PathBase.Value; - return Results.NoContent(); - }); - - await app.StartAsync(TestContext.Current.CancellationToken); - var client = app.GetTestClient(); - var response = await client.GetAsync(new Uri("/ping", UriKind.Relative), - TestContext.Current.CancellationToken); - - Assert.Equal(StatusCodes.Status204NoContent, (int) response.StatusCode); - Assert.True(string.IsNullOrEmpty(capturedPathBase)); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task DoesNotSetPathBase_WhenPathBaseMissingOrWhitespace(string? pathBase) - { - var builder = WebApplication.CreateBuilder(); - builder.WebHost.UseTestServer(); - builder.Configuration.AddInMemoryCollection(ConfigurationWithPathBase(pathBase)); - builder.Services.AddRouting(); - - string? capturedPathBase = null; - string? capturedPath = null; - - await using var app = builder.Build(); - app.ConsiderAddingPathBase(); - app.UseRouting(); - app.MapGet("/ping", (HttpContext context) => - { - capturedPathBase = context.Request.PathBase.Value; - capturedPath = context.Request.Path.Value; - return Results.NoContent(); - }); - - await app.StartAsync(TestContext.Current.CancellationToken); - var client = app.GetTestClient(); - var response = await client.GetAsync(new Uri("/ping", UriKind.Relative), - TestContext.Current.CancellationToken); - - Assert.Equal(StatusCodes.Status204NoContent, (int) response.StatusCode); - Assert.True(string.IsNullOrEmpty(capturedPathBase)); - Assert.Equal("/ping", capturedPath); - } - - [Fact] - public void ReturnsSameApplicationInstance() - { - var builder = WebApplication.CreateBuilder(); - builder.WebHost.UseTestServer(); - var app = builder.Build(); - - var result = app.ConsiderAddingPathBase(); - - Assert.Same(app, result); - } - - [Fact] - public async Task SetsPathBase_WhenConfigured() - { - const string pathBase = "/example"; - - var builder = WebApplication.CreateBuilder(); - builder.WebHost.UseTestServer(); - builder.Configuration.AddInMemoryCollection(ConfigurationWithPathBase(pathBase)); - builder.Services.AddRouting(); - - string? capturedPathBase = null; - string? capturedPath = null; - - await using var app = builder.Build(); - app.ConsiderAddingPathBase(); - app.UseRouting(); - app.MapGet("/ping", (HttpContext context) => - { - capturedPathBase = context.Request.PathBase.Value; - capturedPath = context.Request.Path.Value; - return Results.NoContent(); - }); - - await app.StartAsync(TestContext.Current.CancellationToken); - var client = app.GetTestClient(); - var response = await client.GetAsync(new Uri($"{pathBase}/ping", UriKind.Relative), - TestContext.Current.CancellationToken); - - Assert.Equal(StatusCodes.Status204NoContent, (int) response.StatusCode); - Assert.Equal(pathBase, capturedPathBase); - Assert.Equal("/ping", capturedPath); - } - } -} \ No newline at end of file From 5e84049a0643686ddea8dda79748d2ecb061a08c Mon Sep 17 00:00:00 2001 From: Alan Deutscher Date: Sun, 13 Sep 2026 18:43:07 -0700 Subject: [PATCH 2/6] config --- .../Extensions/ForwardingHeadersExtensions.cs | 2 +- .../Tests/Extensions/ForwardingHeadersExtensionsTests.cs | 2 +- test/local/docker-compose.yaml | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs b/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs index a00170f..122a96b 100644 --- a/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs +++ b/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs @@ -25,7 +25,7 @@ private static bool TryParseCidrAddressToNetwork(this string input, out IPNetwor private static ConfigurationModel GetConfigurationModel(IConfiguration configuration) { return configuration - .GetSection(nameof(Configuration)) + .GetSection("HeaderForwarding") .Get() ?? new ConfigurationModel { RespectHeaders = false, diff --git a/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs index 68f982c..f3cf22f 100644 --- a/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs +++ b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs @@ -13,7 +13,7 @@ namespace RedShirt.Example.Api.UnitTests.Tests.Extensions; public class ForwardingHeadersExtensionsTests { - private const string ConfigurationSectionName = "Configuration"; + private const string ConfigurationSectionName = "HeaderForwarding"; private static IConfigurationRoot CreateConfiguration( bool? respectHeaders = null, diff --git a/test/local/docker-compose.yaml b/test/local/docker-compose.yaml index 8d57d92..1f02887 100644 --- a/test/local/docker-compose.yaml +++ b/test/local/docker-compose.yaml @@ -98,6 +98,11 @@ services: ASPNETCORE_ENVIRONMENT: Development # Enable swagger ASPNETCORE_URLS: http://*:8080 API__PATH_BASE: null + # Header forwarding (X-Forwarded-* from trusted reverse proxies in front of Kestrel) + ## + HEADER_FORWARDING__RESPECT_HEADERS: "${HEADER_FORWARDING__RESPECT_HEADERS-false}" + # Docker user-defined bridge networks (typical compose / `proxy` network peers). Add more CIDRs as __1, __2, ... + HEADER_FORWARDING__TRUSTED_NETWORKS__0: "${HEADER_FORWARDING__TRUSTED_NETWORKS__0-172.16.0.0/12}" # AWS Settings ## AWS_SERVICE_URL: http://ministack:4566 From 2e6ac98d6a5102b9ea21a88b7652620b9a474eb2 Mon Sep 17 00:00:00 2001 From: Alan Deutscher Date: Sun, 13 Sep 2026 18:44:35 -0700 Subject: [PATCH 3/6] rename --- .../Extensions/ForwardingHeadersExtensions.cs | 2 +- .../Tests/Extensions/ForwardingHeadersExtensionsTests.cs | 2 +- test/local/docker-compose.yaml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs b/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs index 122a96b..d84e449 100644 --- a/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs +++ b/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs @@ -25,7 +25,7 @@ private static bool TryParseCidrAddressToNetwork(this string input, out IPNetwor private static ConfigurationModel GetConfigurationModel(IConfiguration configuration) { return configuration - .GetSection("HeaderForwarding") + .GetSection("ForwardingHeaders") .Get() ?? new ConfigurationModel { RespectHeaders = false, diff --git a/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs index f3cf22f..a17a50a 100644 --- a/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs +++ b/test/RedShirt.Example.Api.UnitTests/Tests/Extensions/ForwardingHeadersExtensionsTests.cs @@ -13,7 +13,7 @@ namespace RedShirt.Example.Api.UnitTests.Tests.Extensions; public class ForwardingHeadersExtensionsTests { - private const string ConfigurationSectionName = "HeaderForwarding"; + private const string ConfigurationSectionName = "ForwardingHeaders"; private static IConfigurationRoot CreateConfiguration( bool? respectHeaders = null, diff --git a/test/local/docker-compose.yaml b/test/local/docker-compose.yaml index 1f02887..6e5249d 100644 --- a/test/local/docker-compose.yaml +++ b/test/local/docker-compose.yaml @@ -98,11 +98,11 @@ services: ASPNETCORE_ENVIRONMENT: Development # Enable swagger ASPNETCORE_URLS: http://*:8080 API__PATH_BASE: null - # Header forwarding (X-Forwarded-* from trusted reverse proxies in front of Kestrel) + # Forwarding headers (X-Forwarded-* from trusted reverse proxies in front of Kestrel) ## - HEADER_FORWARDING__RESPECT_HEADERS: "${HEADER_FORWARDING__RESPECT_HEADERS-false}" + FORWARDING_HEADERS__RESPECT_HEADERS: "${FORWARDING_HEADERS__RESPECT_HEADERS-false}" # Docker user-defined bridge networks (typical compose / `proxy` network peers). Add more CIDRs as __1, __2, ... - HEADER_FORWARDING__TRUSTED_NETWORKS__0: "${HEADER_FORWARDING__TRUSTED_NETWORKS__0-172.16.0.0/12}" + FORWARDING_HEADERS__TRUSTED_NETWORKS__0: "${FORWARDING_HEADERS__TRUSTED_NETWORKS__0-172.16.0.0/12}" # AWS Settings ## AWS_SERVICE_URL: http://ministack:4566 From 24c003c2e200ad90c2fe72720dcc2bfbcc22aa33 Mon Sep 17 00:00:00 2001 From: Alan Deutscher Date: Sun, 13 Sep 2026 18:45:05 -0700 Subject: [PATCH 4/6] clean --- .../Extensions/ForwardingHeadersExtensions.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs b/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs index d84e449..46e1ab7 100644 --- a/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs +++ b/src/RedShirt.Example.Api/Extensions/ForwardingHeadersExtensions.cs @@ -64,12 +64,10 @@ public static IServiceCollection ConsiderConfiguringForwardingHeaders(this IServ // Docker user-defined bridges usually sit in 172.16.0.0/12, `proxy`. foreach (var candidateString in configurationModel.TrustedNetworks) { - if (!candidateString.TryParseCidrAddressToNetwork(out var network)) + if (candidateString.TryParseCidrAddressToNetwork(out var network)) { - continue; + options.KnownIPNetworks.Add(network); } - - options.KnownIPNetworks.Add(network); } }); } From 5a7698c654aa4ee03e28193f2906bb34879f9dd4 Mon Sep 17 00:00:00 2001 From: Alan Deutscher Date: Sun, 13 Sep 2026 20:02:34 -0700 Subject: [PATCH 5/6] comment around authorization --- src/RedShirt.Example.Api/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RedShirt.Example.Api/Program.cs b/src/RedShirt.Example.Api/Program.cs index 6e86ba5..0d4e80a 100644 --- a/src/RedShirt.Example.Api/Program.cs +++ b/src/RedShirt.Example.Api/Program.cs @@ -70,6 +70,7 @@ app.UseRouting(); app.ConsiderUsingAuthentication(); +// Always set UseAuthorization because requirements aren't guaranteed to be based on authenticated identity. app.UseAuthorization(); if (Environment.GetEnvironmentVariable("NSWAG_RUN") != "1") From 3dec9c505abca4c7e7dcfea58e635ac725832a86 Mon Sep 17 00:00:00 2001 From: Alan Deutscher Date: Sun, 13 Sep 2026 20:09:58 -0700 Subject: [PATCH 6/6] Remove PathBase from docker compose --- test/local/docker-compose.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/test/local/docker-compose.yaml b/test/local/docker-compose.yaml index 6e5249d..c7549dc 100644 --- a/test/local/docker-compose.yaml +++ b/test/local/docker-compose.yaml @@ -97,7 +97,6 @@ services: ## ASPNETCORE_ENVIRONMENT: Development # Enable swagger ASPNETCORE_URLS: http://*:8080 - API__PATH_BASE: null # Forwarding headers (X-Forwarded-* from trusted reverse proxies in front of Kestrel) ## FORWARDING_HEADERS__RESPECT_HEADERS: "${FORWARDING_HEADERS__RESPECT_HEADERS-false}"