Skip to content

Commit 80a4e65

Browse files
committed
Improve CF reverse proxy support
- Move reverse proxy configuration to Common - When present, add networks for CF_INSTANCE IP vars to KnownNetworks - Parameter-less UseCertificateAuthorization now tries to retrieve ForwardedHeadersOptions from DI container before falling back on creating a new instance
1 parent 86ae318 commit 80a4e65

8 files changed

Lines changed: 248 additions & 7 deletions

File tree

src/Common/src/Hosting/Properties/AssemblyInfo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
[assembly: InternalsVisibleTo("Steeltoe.Management.Endpoint")]
1414
[assembly: InternalsVisibleTo("Steeltoe.Management.Endpoint.Test")]
1515
[assembly: InternalsVisibleTo("Steeltoe.Management.Prometheus.Test")]
16+
[assembly: InternalsVisibleTo("Steeltoe.Security.Authorization.Certificate.Test")]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
static Steeltoe.Common.Hosting.ServiceCollectionExtensions.ConfigureForwardedHeadersOptionsForCloudFoundry(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
3+
Steeltoe.Common.Hosting.ServiceCollectionExtensions
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Net;
6+
using System.Net.Sockets;
7+
using Microsoft.AspNetCore.Builder;
8+
using Microsoft.AspNetCore.HttpOverrides;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Logging;
11+
using Microsoft.Extensions.Logging.Abstractions;
12+
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
13+
14+
namespace Steeltoe.Common.Hosting;
15+
16+
public static class ServiceCollectionExtensions
17+
{
18+
/// <summary>
19+
/// Configures <see cref="ForwardedHeadersOptions" /> to use forwarded headers as they are provided in Cloud Foundry. Includes
20+
/// <see cref="ForwardedHeaders.XForwardedHost" /> and <see cref="ForwardedHeaders.XForwardedProto" />, and adds known networks from environment
21+
/// variables.
22+
/// </summary>
23+
/// <param name="services">
24+
/// The <see cref="IServiceCollection" /> to configure.
25+
/// </param>
26+
/// <returns>
27+
/// The same <see cref="IServiceCollection" /> instance, for chaining.
28+
/// </returns>
29+
/// <remarks>
30+
/// Evaluates the environment variables CF_INSTANCE_IP and CF_INSTANCE_INTERNAL_IP, and if they contain valid IP addresses, adds their network to
31+
/// <see cref="ForwardedHeadersOptions.KnownNetworks" />.
32+
/// <para />
33+
/// IMPORTANT: <see cref="ForwardedHeadersExtensions.UseForwardedHeaders(IApplicationBuilder)" /> must be called separately to activate these options.
34+
/// </remarks>
35+
/// <exception cref="ArgumentNullException">
36+
/// Thrown if <paramref name="services" /> is <c>null</c>.
37+
/// </exception>
38+
public static IServiceCollection ConfigureForwardedHeadersOptionsForCloudFoundry(this IServiceCollection services)
39+
{
40+
ArgumentNullException.ThrowIfNull(services);
41+
42+
services.AddOptions();
43+
44+
services.AddOptions<ForwardedHeadersOptions>().Configure<IServiceProvider>((options, serviceProvider) =>
45+
{
46+
ILogger logger = serviceProvider.GetService<ILoggerFactory>()?.CreateLogger("Steeltoe.Common.Hosting.ServiceCollectionExtensions") ??
47+
NullLogger.Instance;
48+
49+
options.ForwardedHeaders |= ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto;
50+
51+
AddKnownNetworkFromEnv("CF_INSTANCE_IP", options, logger);
52+
AddKnownNetworkFromEnv("CF_INSTANCE_INTERNAL_IP", options, logger);
53+
});
54+
55+
return services;
56+
}
57+
58+
private static void AddKnownNetworkFromEnv(string envVar, ForwardedHeadersOptions options, ILogger logger)
59+
{
60+
string? ipString = Environment.GetEnvironmentVariable(envVar);
61+
62+
if (IPAddress.TryParse(ipString, out IPAddress? address))
63+
{
64+
// Assume a /24 subnet mask (255.255.255.0), adjust if needed
65+
int prefixLength = address.AddressFamily == AddressFamily.InterNetwork ? 24 : 64;
66+
67+
IPNetwork network = GetNetworkFromAddress(address, prefixLength);
68+
69+
if (!options.KnownNetworks.Any(n => n.Prefix.Equals(network.Prefix) && n.PrefixLength == network.PrefixLength))
70+
{
71+
logger.LogDebug("Adding known network {Network}/{PrefixLength} from {EnvVar} to ForwardedHeadersOptions.", network.Prefix, network.PrefixLength,
72+
envVar);
73+
74+
options.KnownNetworks.Add(network);
75+
}
76+
}
77+
}
78+
79+
internal static IPNetwork GetNetworkFromAddress(IPAddress address, int prefixLength)
80+
{
81+
byte[] bytes = address.GetAddressBytes();
82+
int fullBytes = prefixLength / 8;
83+
int remainingBits = prefixLength % 8;
84+
85+
for (int i = fullBytes + (remainingBits > 0 ? 1 : 0); i < bytes.Length; i++)
86+
{
87+
bytes[i] = 0;
88+
}
89+
90+
if (remainingBits > 0)
91+
{
92+
int mask = 0xFF << (8 - remainingBits);
93+
bytes[fullBytes] &= (byte)mask;
94+
}
95+
96+
return new IPNetwork(new IPAddress(bytes), prefixLength);
97+
}
98+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Net;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.HttpOverrides;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Options;
10+
using Steeltoe.Common.TestResources;
11+
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
12+
13+
namespace Steeltoe.Common.Hosting.Test;
14+
15+
public sealed class ServiceCollectionExtensionsTest
16+
{
17+
private static readonly IPAddress LocalhostV4 = IPAddress.Parse("127.0.0.1");
18+
private static readonly IPAddress LocalhostV6 = IPAddress.Parse("::1");
19+
20+
[Fact]
21+
public void Adds_XForwardedHost_and_XForwardedProto_Headers()
22+
{
23+
var services = new ServiceCollection();
24+
25+
services.ConfigureForwardedHeadersOptionsForCloudFoundry();
26+
ServiceProvider provider = services.BuildServiceProvider();
27+
ForwardedHeadersOptions options = provider.GetRequiredService<IOptions<ForwardedHeadersOptions>>().Value;
28+
29+
options.ForwardedHeaders.Should().HaveFlag(ForwardedHeaders.XForwardedHost);
30+
options.ForwardedHeaders.Should().HaveFlag(ForwardedHeaders.XForwardedProto);
31+
AssertDefaultKnownNetworks(options);
32+
AssertDefaultKnownProxies(options);
33+
}
34+
35+
[Fact]
36+
public void Adds_KnownNetworks_FromEnvironmentVariables()
37+
{
38+
const string instanceIPAddress = "192.168.7.100";
39+
const string instanceInternalIPAddress = "10.11.12.255";
40+
using var instanceScope = new EnvironmentVariableScope("CF_INSTANCE_IP", instanceIPAddress);
41+
using var internalScope = new EnvironmentVariableScope("CF_INSTANCE_INTERNAL_IP", instanceInternalIPAddress);
42+
43+
var services = new ServiceCollection();
44+
services.ConfigureForwardedHeadersOptionsForCloudFoundry();
45+
ServiceProvider provider = services.BuildServiceProvider();
46+
47+
ForwardedHeadersOptions options = provider.GetRequiredService<IOptions<ForwardedHeadersOptions>>().Value;
48+
49+
AssertKnownNetworksContains(options, IPAddress.Parse(instanceIPAddress), 24);
50+
AssertKnownNetworksContains(options, IPAddress.Parse(instanceInternalIPAddress), 24);
51+
AssertDefaultKnownProxies(options);
52+
}
53+
54+
[Fact]
55+
public void Does_not_add_duplicate_KnownNetworks()
56+
{
57+
const string sameIP = "172.16.0.1";
58+
Environment.SetEnvironmentVariable("CF_INSTANCE_IP", sameIP);
59+
Environment.SetEnvironmentVariable("CF_INSTANCE_INTERNAL_IP", sameIP);
60+
61+
var services = new ServiceCollection();
62+
63+
services.ConfigureForwardedHeadersOptionsForCloudFoundry();
64+
ServiceProvider provider = services.BuildServiceProvider();
65+
ForwardedHeadersOptions options = provider.GetRequiredService<IOptions<ForwardedHeadersOptions>>().Value;
66+
67+
AssertKnownNetworksContains(options, IPAddress.Parse(sameIP), 24);
68+
AssertDefaultKnownProxies(options);
69+
}
70+
71+
[Fact]
72+
public void Does_not_add_invalid_EnvironmentVariables()
73+
{
74+
Environment.SetEnvironmentVariable("CF_INSTANCE_IP", "invalid-ip");
75+
Environment.SetEnvironmentVariable("CF_INSTANCE_INTERNAL_IP", "also-bad");
76+
77+
var services = new ServiceCollection();
78+
79+
services.ConfigureForwardedHeadersOptionsForCloudFoundry();
80+
ServiceProvider provider = services.BuildServiceProvider();
81+
ForwardedHeadersOptions options = provider.GetRequiredService<IOptions<ForwardedHeadersOptions>>().Value;
82+
83+
AssertDefaultKnownNetworks(options);
84+
AssertDefaultKnownProxies(options);
85+
}
86+
87+
[Fact]
88+
public void Throws_if_services_null()
89+
{
90+
Action act = () => ServiceCollectionExtensions.ConfigureForwardedHeadersOptionsForCloudFoundry(null!);
91+
92+
act.Should().Throw<ArgumentNullException>();
93+
}
94+
95+
// KnownNetworks and KnownProxies are mutually exclusive. Proxies don't cover all Cloud Foundry cases, so don't configure them.
96+
private static void AssertDefaultKnownProxies(ForwardedHeadersOptions options)
97+
{
98+
options.KnownProxies.Should().ContainSingle().Which.Should().BeOneOf(LocalhostV4, LocalhostV6);
99+
}
100+
private static void AssertDefaultKnownNetworks(ForwardedHeadersOptions options)
101+
{
102+
options.KnownNetworks.Should().ContainSingle(network => network.Prefix.Equals(LocalhostV4) && network.PrefixLength == 8);
103+
}
104+
105+
private static void AssertKnownNetworksContains(ForwardedHeadersOptions options, IPAddress ipAddress, int prefixLength)
106+
{
107+
IPNetwork network = ServiceCollectionExtensions.GetNetworkFromAddress(ipAddress, prefixLength);
108+
options.KnownNetworks.Should().ContainEquivalentOf(network);
109+
}
110+
}

src/Security/src/Authorization.Certificate/CertificateApplicationBuilderExtensions.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
// See the LICENSE file in the project root for more information.
44

55
using Microsoft.AspNetCore.Builder;
6-
using Microsoft.AspNetCore.HttpOverrides;
6+
using Microsoft.Extensions.Options;
77

88
namespace Steeltoe.Security.Authorization.Certificate;
99

1010
public static class CertificateApplicationBuilderExtensions
1111
{
1212
/// <summary>
13-
/// Enables certificate and header forwarding, along with ASP.NET Core authentication and authorization middlewares. Sets ForwardedHeaders to
14-
/// <see cref="ForwardedHeaders.XForwardedProto" />.
13+
/// Enables certificate and header forwarding, along with ASP.NET Core authentication and authorization middlewares.
1514
/// </summary>
1615
/// <param name="builder">
1716
/// The <see cref="IApplicationBuilder" /> to configure.
@@ -21,7 +20,10 @@ public static class CertificateApplicationBuilderExtensions
2120
/// </returns>
2221
public static IApplicationBuilder UseCertificateAuthorization(this IApplicationBuilder builder)
2322
{
24-
return UseCertificateAuthorization(builder, new ForwardedHeadersOptions());
23+
ArgumentNullException.ThrowIfNull(builder);
24+
25+
var headerOptions = (IOptions<ForwardedHeadersOptions>?)builder.ApplicationServices.GetService(typeof(IOptions<ForwardedHeadersOptions>));
26+
return UseCertificateAuthorization(builder, headerOptions?.Value ?? new ForwardedHeadersOptions());
2527
}
2628

2729
/// <summary>
@@ -31,7 +33,7 @@ public static IApplicationBuilder UseCertificateAuthorization(this IApplicationB
3133
/// The <see cref="IApplicationBuilder" /> to configure.
3234
/// </param>
3335
/// <param name="options">
34-
/// Custom header forwarding policy. <see cref="ForwardedHeaders.XForwardedProto" /> is added to your <see cref="ForwardedHeadersOptions" />.
36+
/// Custom header forwarding policy.
3537
/// </param>
3638
/// <returns>
3739
/// The incoming <paramref name="builder" /> so that additional calls can be chained.
@@ -41,8 +43,6 @@ public static IApplicationBuilder UseCertificateAuthorization(this IApplicationB
4143
ArgumentNullException.ThrowIfNull(builder);
4244
ArgumentNullException.ThrowIfNull(options);
4345

44-
options.ForwardedHeaders |= ForwardedHeaders.XForwardedProto;
45-
4646
builder.UseForwardedHeaders(options);
4747
builder.UseCertificateForwarding();
4848
builder.UseAuthentication();

src/Security/src/Authorization.Certificate/CertificateAuthorizationBuilderExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Extensions.DependencyInjection;
88
using Microsoft.Extensions.Options;
99
using Steeltoe.Common.Certificates;
10+
using Steeltoe.Common.Hosting;
1011

1112
namespace Steeltoe.Security.Authorization.Certificate;
1213

@@ -66,6 +67,7 @@ public static AuthorizationBuilder AddOrgAndSpacePolicies(this AuthorizationBuil
6667
}
6768
});
6869

70+
builder.Services.ConfigureForwardedHeadersOptionsForCloudFoundry();
6971
builder.Services.AddSingleton<IPostConfigureOptions<CertificateAuthenticationOptions>, PostConfigureCertificateAuthenticationOptions>();
7072
builder.Services.AddSingleton<IAuthorizationHandler, CertificateAuthorizationHandler>();
7173

src/Security/src/Authorization.Certificate/Steeltoe.Security.Authorization.Certificate.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414

1515
<ItemGroup>
1616
<ProjectReference Include="..\..\..\Common\src\Certificates\Steeltoe.Common.Certificates.csproj" />
17+
<ProjectReference Include="..\..\..\Common\src\Hosting\Steeltoe.Common.Hosting.csproj" />
1718
</ItemGroup>
1819
</Project>

src/Security/test/Authorization.Certificate.Test/CertificateAuthorizationTest.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using Microsoft.Extensions.Options;
1313
using Steeltoe.Common.Certificates;
1414
using Steeltoe.Common.TestResources;
15+
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
16+
using ServiceCollectionExtensions = Steeltoe.Common.Hosting.ServiceCollectionExtensions;
1517

1618
namespace Steeltoe.Security.Authorization.Certificate.Test;
1719

@@ -185,6 +187,31 @@ public async Task CertificateAuth_AllowsCustomHeader()
185187
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
186188
}
187189

190+
[Fact]
191+
public async Task CertificateAuth_AddsInstanceIPsAsKnownNetworks()
192+
{
193+
const string instanceIPAddress = "192.168.0.110";
194+
const string instanceInternalIPAddress = "10.255.31.112";
195+
using var instanceScope = new EnvironmentVariableScope("CF_INSTANCE_IP", instanceIPAddress);
196+
using var internalScope = new EnvironmentVariableScope("CF_INSTANCE_INTERNAL_IP", instanceInternalIPAddress);
197+
198+
WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault();
199+
builder.Services.AddAuthentication().AddCertificate();
200+
builder.Services.AddAuthorizationBuilder().AddOrgAndSpacePolicies();
201+
await using WebApplication application = builder.Build();
202+
203+
ForwardedHeadersOptions options = application.Services.GetRequiredService<IOptions<ForwardedHeadersOptions>>().Value;
204+
205+
AssertKnownNetworksContains(options, instanceIPAddress);
206+
AssertKnownNetworksContains(options, instanceInternalIPAddress);
207+
}
208+
209+
private static void AssertKnownNetworksContains(ForwardedHeadersOptions options, string ipAddress)
210+
{
211+
IPNetwork network = ServiceCollectionExtensions.GetNetworkFromAddress(IPAddress.Parse(ipAddress), 24);
212+
options.KnownNetworks.Should().ContainEquivalentOf(network);
213+
}
214+
188215
private HostBuilder GetHostBuilder()
189216
{
190217
HostBuilder hostBuilder = TestHostBuilderFactory.CreateWeb();

0 commit comments

Comments
 (0)