From 568477ae8a4cfed25dc2bce276f12841d6dee70a Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 9 Sep 2026 08:26:52 -0500 Subject: [PATCH 1/3] Use new mutual TLS APIs --- Security/src/AuthApi/Program.cs | 2 +- Security/src/AuthConsole/Program.cs | 2 +- Security/src/AuthWeb/Program.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Security/src/AuthApi/Program.cs b/Security/src/AuthApi/Program.cs index bef41923e..9f46c82ac 100644 --- a/Security/src/AuthApi/Program.cs +++ b/Security/src/AuthApi/Program.cs @@ -35,7 +35,7 @@ policy.RequireClaim("scope", Globals.RequiredJwtScope); }) // Steeltoe: Register policies requiring space or org to match between client and server certificates. - .AddOrgAndSpacePolicies(); + .AddOrgAndSpacePoliciesForMutualTls(); // Steeltoe: Add actuator endpoints. builder.Services.AddAllActuators(); diff --git a/Security/src/AuthConsole/Program.cs b/Security/src/AuthConsole/Program.cs index 209fbc88f..e7d875e8c 100644 --- a/Security/src/AuthConsole/Program.cs +++ b/Security/src/AuthConsole/Program.cs @@ -20,7 +20,7 @@ builder.Configuration.AddAppInstanceIdentityCertificate(new Guid(orgId), new Guid(spaceId)); // Steeltoe: register a typed HttpClient that includes the application instance identity certificate. -builder.Services.AddHttpClient(SetBaseAddress).AddAppInstanceIdentityCertificate().ConfigureLogging(); +builder.Services.AddHttpClient(SetBaseAddress).AddAppInstanceIdentityCertificateForMutualTls().ConfigureLogging(); IHost host = builder.Build(); host.Run(); diff --git a/Security/src/AuthWeb/Program.cs b/Security/src/AuthWeb/Program.cs index 0c0a9b14e..49bf577ed 100644 --- a/Security/src/AuthWeb/Program.cs +++ b/Security/src/AuthWeb/Program.cs @@ -42,7 +42,7 @@ // Steeltoe: Register HttpClients for communicating with a backend service, including an application instance certificate for authorization. builder.Services.AddHttpClient(SetBaseAddress).ConfigureLogging(); -builder.Services.AddHttpClient(SetBaseAddress).AddAppInstanceIdentityCertificate().ConfigureLogging(); +builder.Services.AddHttpClient(SetBaseAddress).AddAppInstanceIdentityCertificateForMutualTls().ConfigureLogging(); // Steeltoe: Add actuator endpoints. builder.Services.AddAllActuators(); From 7e9b0d7a9c85acb7821e69620335f5c4dfcc08cc Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Wed, 9 Sep 2026 17:20:41 -0500 Subject: [PATCH 2/3] simulate gorouter mtls termination locally - trust Steeltoe-generated client certs --- Security/src/AuthApi/LocalMutualTlsSupport.cs | 95 +++++++++++++++++++ Security/src/AuthApi/Program.cs | 11 +++ 2 files changed, 106 insertions(+) create mode 100644 Security/src/AuthApi/LocalMutualTlsSupport.cs diff --git a/Security/src/AuthApi/LocalMutualTlsSupport.cs b/Security/src/AuthApi/LocalMutualTlsSupport.cs new file mode 100644 index 000000000..33f054eab --- /dev/null +++ b/Security/src/AuthApi/LocalMutualTlsSupport.cs @@ -0,0 +1,95 @@ +using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; + +namespace Steeltoe.Samples.AuthApi; + +internal static class LocalMutualTlsSupport +{ + private const string ForwardedClientCertHeaderName = "X-Forwarded-Client-Cert"; + + // Mirrors LocalCertificateWriter's layout in Steeltoe.Common.Certificates: CA materials live one level + // up from the app directory, under GeneratedCertificates/trust, shared across samples in the solution. + private static readonly string TrustStorePath = ResolveTrustStorePath(); + private static readonly X509Certificate2 RootCaCertificate = LoadTrustedCertificate("SteeltoeCA.crt"); + private static readonly X509Certificate2 IntermediateCertificate = LoadTrustedCertificate("SteeltoeIntermediate.crt"); + + public static IServiceCollection AddLocalMutualTlsSupport(this IServiceCollection services) + { + services.PostConfigure(options => options.ConfigureHttpsDefaults(httpsOptions => + { + httpsOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + + // Kestrel's default validation rejects Steeltoe certs because the Steeltoe-generated CA isn't OS-trusted. + // Validate against the Steeltoe CA/intermediate chain directly instead of trusting the OS store. + httpsOptions.ClientCertificateValidation = ValidateAgainstSteeltoeTrustChain; + })); + + return services; + } + + public static IApplicationBuilder UseLocalMutualTlsSupport(this IApplicationBuilder builder) + { + builder.Use(async (context, next) => + { + context.Request.Headers.Remove(ForwardedClientCertHeaderName); + + if (IsLoopbackAddress(context.Connection.RemoteIpAddress) && context.Connection.ClientCertificate is { } clientCertificate) + { + context.Request.Headers[ForwardedClientCertHeaderName] = Convert.ToBase64String(clientCertificate.RawData); + } + + await next(context); + }); + + return builder; + } + + private static bool IsLoopbackAddress(IPAddress? address) + { + if (address == null) + { + return false; + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return IPAddress.IsLoopback(address); + } + + private static bool ValidateAgainstSteeltoeTrustChain(X509Certificate2? certificate, X509Chain? remoteChain, SslPolicyErrors sslPolicyErrors) + { + if (certificate == null) + { + return false; + } + + using var customChain = new X509Chain(); + customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + customChain.ChainPolicy.CustomTrustStore.Add(RootCaCertificate); + customChain.ChainPolicy.CustomTrustStore.Add(IntermediateCertificate); + customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + return customChain.Build(certificate); + } + + private static string ResolveTrustStorePath() + { + string appBasePath = AppContext.BaseDirectory.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal) + ? AppContext.BaseDirectory[..AppContext.BaseDirectory.LastIndexOf($"{Path.DirectorySeparatorChar}bin", StringComparison.Ordinal)] + : AppContext.BaseDirectory[..^1]; + + string parentPath = Directory.GetParent(appBasePath)?.ToString() ?? string.Empty; + return Path.Combine(parentPath, "GeneratedCertificates", "trust"); + } + + private static X509Certificate2 LoadTrustedCertificate(string fileName) + { + return X509Certificate2.CreateFromPem(File.ReadAllText(Path.Combine(TrustStorePath, fileName))); + } +} diff --git a/Security/src/AuthApi/Program.cs b/Security/src/AuthApi/Program.cs index 9f46c82ac..8b6c9fb4c 100644 --- a/Security/src/AuthApi/Program.cs +++ b/Security/src/AuthApi/Program.cs @@ -24,6 +24,12 @@ // Steeltoe: Add instance identity certificate to configuration. builder.Configuration.AddAppInstanceIdentityCertificate(new Guid(orgId), new Guid(spaceId)); +if (builder.Environment.IsDevelopment()) +{ + // Steeltoe: Simulate Gorouter's mTLS termination locally. + builder.Services.AddLocalMutualTlsSupport(); +} + // Steeltoe: Register Microsoft's JWT Bearer and Certificate libraries for authentication, configure JWT to work with UAA/Cloud Foundry. builder.Services.AddAuthentication().AddJwtBearer().ConfigureJwtBearerForCloudFoundry().AddCertificate(); @@ -42,6 +48,11 @@ WebApplication app = builder.Build(); +if (app.Environment.IsDevelopment()) +{ + app.UseLocalMutualTlsSupport(); +} + // Steeltoe: Use certificate and header forwarding along with ASP.NET Core Authentication and Authorization middleware. app.UseCertificateAuthorization(); From 296c00d5e939ea490cd73ddf743873ce1ff21c04 Mon Sep 17 00:00:00 2001 From: Tim Hess Date: Fri, 11 Sep 2026 09:52:19 -0500 Subject: [PATCH 3/3] pr feedback --- Security/src/AuthApi/LocalMutualTlsSupport.cs | 5 ++--- Security/src/AuthApi/Program.cs | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Security/src/AuthApi/LocalMutualTlsSupport.cs b/Security/src/AuthApi/LocalMutualTlsSupport.cs index 33f054eab..7280b687a 100644 --- a/Security/src/AuthApi/LocalMutualTlsSupport.cs +++ b/Security/src/AuthApi/LocalMutualTlsSupport.cs @@ -36,9 +36,9 @@ public static IApplicationBuilder UseLocalMutualTlsSupport(this IApplicationBuil { context.Request.Headers.Remove(ForwardedClientCertHeaderName); - if (IsLoopbackAddress(context.Connection.RemoteIpAddress) && context.Connection.ClientCertificate is { } clientCertificate) + if (IsLoopbackAddress(context.Connection.RemoteIpAddress) && context.Connection.ClientCertificate != null) { - context.Request.Headers[ForwardedClientCertHeaderName] = Convert.ToBase64String(clientCertificate.RawData); + context.Request.Headers[ForwardedClientCertHeaderName] = Convert.ToBase64String(context.Connection.ClientCertificate.RawData); } await next(context); @@ -72,7 +72,6 @@ private static bool ValidateAgainstSteeltoeTrustChain(X509Certificate2? certific using var customChain = new X509Chain(); customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; customChain.ChainPolicy.CustomTrustStore.Add(RootCaCertificate); - customChain.ChainPolicy.CustomTrustStore.Add(IntermediateCertificate); customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; return customChain.Build(certificate); diff --git a/Security/src/AuthApi/Program.cs b/Security/src/AuthApi/Program.cs index 8b6c9fb4c..2930fbb77 100644 --- a/Security/src/AuthApi/Program.cs +++ b/Security/src/AuthApi/Program.cs @@ -50,6 +50,7 @@ if (app.Environment.IsDevelopment()) { + // Steeltoe: Simulate Gorouter's mTLS termination locally. app.UseLocalMutualTlsSupport(); }