Configure ASP.NET Core to work with proxy servers and load balancers

In the recommended configuration for ASP.NET Core, the application is hosted using the ASP.NET Core Module (ANCM) for IIS, Nginx, or Apache. Proxy servers, load balancers, and other network devices often hide information about the request before it reaches the application:

  • When HTTPS requests are transmitted over HTTP, the original scheme (HTTPS) is lost and must be resent in a header.
  • Because an application receives a proxy request and not its true source on the Internet or corporate network, the IP address of the origin client must also be forwarded in a header.

This information can be important in request processing, for example, in redirects, authentication, link generation, policy evaluation, and geolocation of clients.

Forwarded Headers

By convention, proxy servers forward information in HTTP headers.

Header Description X-Forwarded-For (XFF) Contains information about the client who initiated the request and post proxies earlier in a chain of proxy servers. This parameter can contain IP addresses and, optionally, port numbers. In a proxy server chain, the first parameter indicates the client where the request was first made. Subsequent proxy identifiers follow. The last proxy in the chain is not in the parameter list. The IP address of the last proxy, and optionally a port number, are available as the remote IP address at the transport layer. X-Forwarded-Proto (XFP) The value of the origin scheme, HTTP or HTTPS. The value can also be a list of schemes if the request has traversed multiple proxies. X-Forwarded-Host (XFH) The original value of the Host header field. Proxies generally do not modify the Host header. See Microsoft Security Advisory CVE-2018-0787 for information about an elevation of privilege vulnerability that affects systems where the proxy does not validate or constrain host headers to known good values.

The forwarded headers middleware, ForwardedHeadersMiddleware, reads these headers and populates the associated fields in the HttpContext.

The middleware updates:

  • HttpContext.Connection.RemoteIpAddress: Set using the X-Forwarded-For header value. Additional configuration influences how the middleware sets RemoteIpAddress. For more information, see Forwarded Headers Middleware Options. Consumed values ​​are removed from X-Forwarded-For and old values ​​are kept in X-Original-For. The same pattern applies to the other headers, Host and Proto.
  • HttpContext.Request.Scheme – Set using the value of the X-Forwarded-Proto header.
  • HttpContext.Request .Host : Set using the value of the X-Forwarded-Host header.

For more information on the above, see this GitHub issue.

The default setting of the forwarded headers middleware can be configured. For the default configuration:

  • There is only one proxy between the application and the source of the requests.
  • Only loopback addresses are configured for known proxies and known networks.
  • The forwarded headers are named X-Forwarded-For and X-Forwarded-Proto.
  • The value of ForwardedHeaders is ForwardedHeaders.None, desired forwarders must be set here to enable the middleware.

Not all network devices add the X-Forwarded-For and X-Forwarded-Proto headers without additional configuration. Consult your device manufacturer’s guidance if proxy requests do not contain these headers when they reach the application. If the device uses header names other than X-Forwarded-For and X-Forwarded-Proto, set the ForwardedForHeaderName and ForwardedProtoHeaderName options to match the header names used by the device. For more information, see Forwarded Headers Middleware Settings and Options for a Proxy Using Different Header Names.

IIS/IIS Express Module and ASP.NET Core

The middleware Forwarded headers is enabled by default by the IIS Integration Middleware when the application is hosted out-of-process behind IIS and the ASP.NET Core Module (ANCM) for IIS. The forwarded headers middleware is enabled to run first in the middleware pipeline with a restricted configuration specific to the ASP.NET Core module. The restricted configuration is due to trust issues with forwarded headers, for example IP spoofing. The middleware is configured to forward the X-Forwarded-For and X-Forwarded-Proto headers and is restricted to a single localhost proxy. If additional configuration is required, see the Forwarded Headers Middleware options.

Other Proxy Server and Load Balancer Scenarios

Other than using IIS integration when hosting offsite Forwarded Headers Middleware is not enabled by default. The forwarded headers middleware must be enabled for an application to process headers forwarded with UseForwardedHeaders. After enabling the middleware if no ForwardedHeadersOptions are specified for the middleware, the default ForwardedHeadersOptions.ForwardedHeaders is ForwardedHeaders.None.

See Also:  How to create a pinnable image for shopify blogs

Configure the middleware with ForwardedHeadersOptions to forward the X-Forwarded-For and X-Forwarded- headers Proto.

Order of Forwarded Headers Middleware

The forwarded headers middleware must be executed before other middleware. This order ensures that middleware that depends on information in forwarded headers can consume the header values ​​for processing. The forwarded headers middleware can be run after diagnostics and error handling, but must be run before calling UseHsts:

using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); var app = constructor.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseForwardedHeaders(); app.UseHsts(); } else { app.UseDeveloperExceptionPage(); app.UseForwardedHeaders(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); application.Run();

Alternatively, call UseForwardedHeaders before diagnostics:

using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); var app = constructor.Build(); app.UseForwardedHeaders(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); application.Run();

Nginx Configuration

To forward the X-Forwarded-For and X-Forwarded-Proto headers, see Hosting ASP.NET Core on Linux with Nginx. For more information, see NGINX: Using the Forwarded Header.

Apache Configuration

X-Forwarded-For is added automatically. For more information, see Apache mod_proxy module: Reverse proxy request headers. For information about forwarding the X-Forwarded-Proto header, see Hosting ASP.NET Core on Linux with Apache.

Forwarded Headers Middleware Options

ForwardedHeadersOptions controls the behavior of the forwarded headers Middleware Headers. The following example changes the default values:

  • Limit the number of entries in the forwarded headers to 2.
  • Add a known proxy address of 127.0.10.1.
  • Changes the name of the forwarded header from the default X-Forwarded-For to X-Forwarded-For-My-Custom-Header-Name.

using System.Net; var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.Configure(options => { options.ForwardLimit = 2; options.KnownProxies.Add(IPAddress.Parse(“127.0.10.1”)); options.ForwardedForHeaderName = “X-Forwarded-For-My- Custom header name”; }); var app = constructor.Build(); app.UseForwardedHeaders(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); application.Run(); Option Description AllowedHosts Restricts hosts by the X-Forwarded-Host header to the values ​​provided.

  • Values ​​are compared using ordinal-ignore-case.
  • Port numbers must be excluded.
  • If the list is empty, all hosts are allowed.
  • A top-level wildcard * allows all non-empty hosts.
  • Wildcards subdomains are allowed, but do not match the root domain. For example, *.contoso.com matches the subdomain foo.contoso.com but not the root domain contoso.com.
  • Unicode hostnames are allowed, but are converted to Punycode for matching .
  • IPv6 addresses must include delimiter brackets and be in conventional format (for example, [ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]). IPv6 addresses are not case sensitive to verify logical equality between different formats, and canonicalization is not performed.
  • If the allowed hosts are not restricted, an attacker can spoof the links generated by the service.
  • li>

Default is an empty IList. ForwardedForHeaderName Use the header specified by this property instead of the one specified by ForwardedHeadersDefaults.XForwardedForHeaderName. This option is used when the proxy/forwarder does not use the X-Forwarded-For header but uses some other header to forward the information. The default is X-Forwarded-For. ForwardedHeaders Identifies which forwarders should be processed. See the Forwarded Headers enumeration for the list of fields that apply. Typical values ​​assigned to this property are ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto. Default is ForwardedHeaders.None. ForwardedHostHeaderName Use the header specified by this property instead of the one specified by ForwardedHeadersDefaults.XForwardedHostHeaderName. This option is used when the proxy/forwarder does not use the X-Forwarded-Host header but uses some other header to forward the information. The default is X-Forwarded-Host.ForwardedProtoHeaderName Use the header specified by this property instead of the one specified by ForwardedHeadersDefaults.XForwardedProtoHeaderName. This option is used when the proxy/forwarder does not use the X-Forwarded-Proto header but uses some other header to forward the information. The default is X-Forwarded-Proto. ForwardLimit Limits the number of entries in the headers that are processed. Set to null to disable the limit, but this should only be done if KnownProxies or KnownNetworks are configured. Setting a non-null value is a precaution (but not a guarantee) to protect against misconfigured proxies and malicious requests arriving from side channels in the network. The forwarded headers middleware processes headers in reverse order from right to left. If the default value (1) is used, only the rightmost value of the headers is processed unless the ForwardLimit value is increased. Default is 1. Known Networks Address ranges of known networks from which to accept forwarded headers. Provide IP ranges using classless inter-domain routing (CIDR) notation. If the server uses dual-mode sockets, IPv4 addresses are provided in IPv6 format (for example, 10.0.0.1 in IPv4 represented in IPv6 as ::ffff:10.0.0.1 ). See IPAddress.MapToIPv6. Determine if this format is required by looking at HttpContext.Connection.RemoteIpAddress. The default is IList which contains a single entry for IPAddress.Loopback. KnownProxies Addresses of known proxies from which to accept forwarded headers. Use KnownProxies to specify exact IP address matches. If the server uses dual-mode sockets, IPv4 addresses are provided in IPv6 format (for example, 10.0.0.1 in IPv4 represented in IPv6 as ::ffff:10.0.0.1). See IPAddress.MapToIPv6. Determine if this format is required by looking at HttpContext.Connection.RemoteIpAddress. The default is IList which contains a single entry for IPAddress.IPv6Loopback. OriginalForHeaderName Use the header specified by this property instead of the one specified by ForwardedHeadersDefaults.XOriginalForHeaderName. The default is X-Original-For. OriginalHostHeaderName Use the header specified by this property instead of the one specified by ForwardedHeadersDefaults.XOriginalHostHeaderName. The default is X-Original-Host. OriginalProtoHeaderName Use the header specified by this property instead of the one specified by ForwardedHeadersDefaults.XOriginalProtoHeaderName. The default is X-Original-Proto. RequireHeaderSymmetry Requires that the number of header values ​​be synchronized between the ForwardedHeadersOptions.ForwardedHeaders being processed. The default value in ASP.NET Core 1.x is true. The default value in ASP.NET Core 2.0 or later is false.

Scenarios and Use Cases

When it is not possible to add forwarded headers and all requests are safe

In some cases, it may not be possible to add forwarded headers to requests. requests sent to the application. If the proxy enforces all public external requests to be HTTPS, the scheme can be manually configured before using any kind of middleware:

using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); var app = constructor.Build(); app.Use((context, next) => { context.Request.Scheme = “https”; return next(context); }); app.UseForwardedHeaders(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); application.Run();

This code can be disabled with an environment variable or other configuration option in a development or staging environment:

using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); var app = constructor.Build(); if (!app.Environment.IsProduction()) { app.Use((context, next) => { context.Request.Scheme = “https”; return next(context); }); } app.UseForwardedHeaders(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); application.Run();

Work with the base route and proxies that change the request route

Some proxies pass the route intact but with an application base route that needs to be removed for routing to work properly. The UsePathBaseExtensions.UsePathBase middleware splits the path into HttpRequest.Path and the application base path into HttpRequest.PathBase.

If /foo is the application base path for a proxy path passed as /foo /api/1, the middleware sets Request.PathBase to /foo and Request.Path to /api/1 with the following command:

app.UsePathBase(“/foo”); // … application.UseRouting();

The original path and the base path are reapplied when the middleware is called again in reverse. For more information about middleware request processing, see ASP.NET Core Middleware.

If the proxy trims the route (for example, it forwards /foo/api/1 to /api/1), fix redirects and bindings by setting the request’s PathBase property:

app.Use((context, next) => { context.Request.PathBase = new PathString(“/foo”); return next(context); });

If the proxy is adding route data, discard part of the route to fix redirects and links by using StartsWithSegments and setting the Path property:

app.Use((context, next) => { if (context .Request.Path.StartsWithSegments(“/foo”, out var rest)) { context.Request.Path = rest; } return next(context); });

Configuration for a proxy that uses different header names

If the proxy does not use headers called X-Forwarded-For and X-Forwarded-Proto to forward the proxy address/port and server information source schema , set the ForwardedForHeaderName and ForwardedProtoHeaderName options to match the header names used by the proxy:

var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.Configure(options => { options.ForwardedForHeaderName = “HeaderNamUsedByProxy_X-Forwarded-For_Header”; options.ForwardedProtoHeaderName = “HeaderNamUsedByProxy_X-Forwarded-Proto_Header”; }); var app = constructor.Build(); app.UseForwardedHeaders(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); application.Run();

Forward scheme for Linux and non-IIS reverse proxies

Apps that call UseHttpsRedirection and UseHsts put a site in an infinite loop if deployed to an Azure Linux App Service, machine Azure Linux virtual (VM), or behind any other reverse proxy besides IIS. The reverse proxy terminates TLS and Kestrel does not know the correct request scheme. OAuth and OIDC also fail in this configuration because they cause bad redirects. UseIISIntegration adds and configures forwarded headers middleware when running behind IIS, but there is no matching autoconfiguration for Linux (Apache or Nginx integration).

To forward schema from proxy in scenarios that are not from IIS, enable Forwarded Headers Middleware by setting ASPNETCORE_FORWARDEDHEADERS_ENABLED to true. Warning: This flag uses configurations designed for cloud environments and does not enable features such as the KnownProxies option to restrict which IPs forwarders are accepted from.

Certificate Forwarding

Azure

To configure Azure App Service for certificate forwarding, see Configure TLS mutual authentication for Azure App Service. The following guide is for configuring the ASP.NET Core application.

  • Configure the certificate forwarding middleware to specify the header name used by Azure. Add the following code to configure the header from which the middleware generates a certificate.
  • Call UseCertificateForwarding before calling UseAuthentication.

var builder = WebApplication.CreateBuilder(args ); constructor.Services.AddRazorPages(); builder.Services.AddCertificateForwarding(options => options.CertificateHeader = “X-ARR-ClientCert”); var app = constructor.Build(); app.UseCertificateForwarding(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.UseAuthentication(); app.MapRazorPages(); application.Run();

Other web proxies

If using a proxy other than IIS or Azure App Service Application Request Routing (ARR), configure the proxy to forward the certificate it received in an HTTP header.

  • Configure the certificate forwarding middleware to specify the header name. Add the following code to configure the header from which the middleware generates a certificate.
  • Call UseCertificateForwarding before calling UseAuthentication.

var builder = WebApplication.CreateBuilder(args ); constructor.Services.AddRazorPages(); builder.Services.AddCertificateForwarding(options => options.CertificateHeader = “YOUR_CERTIFICATE_HEADER_NAME”); var app = constructor.Build(); app.UseCertificateForwarding(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.UseAuthentication(); app.MapRazorPages(); application.Run();

If the proxy does not encode the certificate in base64, as is the case with Nginx, set the HeaderConverter option. Consider the following example:

var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.AddCertificateForwarding(options => { options.CertificateHeader = “YOUR_CUSTOM_HEADER_NAME”; options.HeaderConverter = (headerValue) => { // Conversion logic to create an X509Certificate2.var clientCertificate = ConversionLogic.CreateAnX509Certificate2(); return client certificate; }; }); var app = constructor.Build(); app.UseCertificateForwarding(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.UseAuthentication(); app.MapRazorPages(); application.Run();

Troubleshooting

When headers are not forwarded as expected, enable debug-level logging and HTTP request logging. UseHttpLogging must be called after UseForwardedHeaders:

using Microsoft.AspNetCore.HttpLogging; using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.AddHttpLogging(options => { options.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders; }); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); var app = constructor.Build(); app.UseForwardedHeaders(); app.UseHttpLogging(); app.Use(async (context, next) => { // Connection: RemoteIp app.Logger.LogInformation(“Request RemoteIp: {RemoteIpAddress}”, context.Connection.RemoteIpAddress); wait for next(context); }); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); application.Run();

When processed, the X-Forwarded-{For|Proto|Host} values ​​are moved to X-Original-{For|Proto|Host}. If there are multiple values ​​in a given header, the Forwarded Headers Middleware processes the headers in reverse order, from right to left. The default ForwardLimit is 1 (one), so only the rightmost value of the headers is processed unless the ForwardLimit value is increased.

The original remote IP of the request must match with an entry in the KnownProxies or KnownNetworks lists before forwarded headers are processed. This limits header spoofing by not accepting forwarders from untrusted proxies. When an unknown proxy is detected, the log indicates the proxy address:

Sep 20, 2018 15:49:44.168 Unknown proxy: 10.0.0.100:54321

In the example above, 10.0.0.100 is a server proxy. If the server is a trusted proxy, add the server’s IP address to KnownProxies or add a trusted network to KnownNetworks. For more information, see the Forwarded Headers Middleware Options section.

using Microsoft.AspNetCore.HttpOverrides; using System.Net; var builder = WebApplication.CreateBuilder(args); constructor.Services.AddRazorPages(); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.KnownProxies.Add(IPAddress.Parse(“10.0.0.100”)); }); var app = constructor.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(“/Error”); app.UseForwardedHeaders(); app.UseHsts(); } else { app.UseDeveloperExceptionPage(); app.UseForwardedHeaders(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseAuthorization(); app.MapRazorPages(); application.Run();

To display the logs, add “Microsoft.AspNetCore.HttpLogging”: “Information” to the appsettings.Development.json file:

{ “DetailedErrors”: true, “Logging”: { “LogLevel”: { ” Default”: “Info”, “Microsoft.AspNetCore”: “Warning”, “Microsoft.AspNetCore.HttpLogging”: “Info” } } }

Additional Resources

  • ASP Host. NET Core in a Web Farm
  • Microsoft Security Advisory CVE-2018-0787: ASP.NET Core Elevation of Privilege Vulnerability
  • YARP: Another Reverse Proxy

.

See Also:  How to Make a Forum Website Like Reddit in 8 Simple Steps

Leave a Reply

Your email address will not be published. Required fields are marked *