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

By Luke Latham and Chris Ross

In the recommended configuration for ASP.NET Core, the app is hosted using IIS/ASP.NET Core Module, Nginx, or Apache. Proxy servers, load balancers, and other network appliances often obscure information about the request before it reaches the app:

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

This information may be important in request processing, for example in redirects, authentication, link generation, policy evaluation, and client geolocation.

Forwarded headers

By convention, proxies forward information in HTTP headers.

Header Description
X-Forwarded-For Holds information about the client that initiated the request and subsequent proxies in a chain of proxies. This parameter may contain IP addresses (and, optionally, port numbers). In a chain of proxy servers, the first parameter indicates the client where the request was first made. Subsequent proxy identifiers follow. The last proxy in the chain isn't in the list of parameters. The last proxy's IP address, and optionally a port number, are available as the remote IP address at the transport layer.
X-Forwarded-Proto The value of the originating scheme (HTTP/HTTPS). The value may also be a list of schemes if the request has traversed multiple proxies.
X-Forwarded-Host The original value of the Host header field. Usually, proxies don't modify the Host header. See Microsoft Security Advisory CVE-2018-0787 for information on an elevation-of-privileges vulnerability that affects systems where the proxy doesn't validate or restict Host headers to known good values.

The Forwarded Headers Middleware, from the Microsoft.AspNetCore.HttpOverrides package, reads these headers and fills in the associated fields on HttpContext.

The middleware updates:

Forwarded Headers Middleware default settings can be configured. The default settings are:

  • There is only one proxy between the app 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.

Not all network appliances add the X-Forwarded-For and X-Forwarded-Proto headers without additional configuration. Consult your appliance manufacturer's guidance if proxied requests don't contain these headers when they reach the app. If the appliance uses different header names than X-Forwarded-For and X-Forwarded-Proto, set the ForwardedForHeaderName and ForwardedProtoHeaderName options to match the header names used by the appliance. For more information, see Forwarded Headers Middleware options and Configuration for a proxy that uses different header names.

IIS/IIS Express and ASP.NET Core Module

Forwarded Headers Middleware is enabled by default by IIS Integration Middleware when the app is run behind IIS and the ASP.NET Core Module. Forwarded Headers Middleware is activated to run first in the middleware pipeline with a restricted configuration specific to the ASP.NET Core Module due to trust concerns 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

Outside of using IIS Integration Middleware, Forwarded Headers Middleware isn't enabled by default. Forwarded Headers Middleware must be enabled for an app to process forwarded headers with UseForwardedHeaders. After enabling the middleware if no ForwardedHeadersOptions are specified to the middleware, the default ForwardedHeadersOptions.ForwardedHeaders are ForwardedHeaders.None.

Configure the middleware with ForwardedHeadersOptions to forward the X-Forwarded-For and X-Forwarded-Proto headers in Startup.ConfigureServices. Invoke the UseForwardedHeaders method in Startup.Configure before calling other middleware:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders = 
            ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseForwardedHeaders();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();
    // In ASP.NET Core 1.x, replace the following line with: app.UseIdentity();
    app.UseAuthentication();
    app.UseMvc();
}

Note

If no ForwardedHeadersOptions are specified in Startup.ConfigureServices or directly to the extension method with UseForwardedHeaders(IApplicationBuilder, ForwardedHeadersOptions), the default headers to forward are ForwardedHeaders.None. The ForwardedHeadersOptions.ForwardedHeaders property must be configured with the headers to forward.

Nginx configuration

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

Apache configuration

X-Forwarded-For is added automatically (see Apache Module mod_proxy: Reverse Proxy Request Headers). For information on how to forward the X-Forwarded-Proto header, see Host ASP.NET Core on Linux with Apache.

Forwarded Headers Middleware options

ForwardedHeadersOptions control the behavior of the Forwarded Headers Middleware. 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.
  • Change the forwarded header name from the default X-Forwarded-For to X-Forwarded-For-My-Custom-Header-Name.
services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardLimit = 2;
    options.KnownProxies.Add(IPAddress.Parse("127.0.10.1"));
    options.ForwardedForHeaderName = "X-Forwarded-For-My-Custom-Header-Name";
});
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.
  • Subdomain wildcards are permitted but don't match the root domain. For example, *.contoso.com matches the subdomain foo.contoso.com but not the root domain contoso.com.
  • Unicode host names are allowed but are converted to Punycode for matching.
  • IPv6 addresses must include bounding brackets and be in conventional form (for example, [ABCD:EF01:2345:6789:ABCD:EF01:2345:6789]). IPv6 addresses aren't special-cased to check for logical equality between different formats, and no canonicalization is performed.
  • Failure to restrict the allowed hosts may allow an attacker to spoof links generated by the service.
The default value is an empty IList<string>.
ForwardedForHeaderName Use the header specified by this property instead of the one specified by ForwardedHeadersDefaults.XForwardedForHeaderName. This option is used when the proxy/forwarder doesn't 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 ForwardedHeaders Enum for the list of fields that apply. Typical values assigned to this property are ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto.

The default value 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 doesn't 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 doesn't 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.

The default is 1.
KnownNetworks Address ranges of known networks to accept forwarded headers from. Provide IP ranges using Classless Interdomain Routing (CIDR) notation.

If the server is using dual-mode sockets, IPv4 addresses are supplied in an 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 the HttpContext.Connection.RemoteIpAddress. For more information, see the Configuration for an IPv4 address represented as an IPv6 address section.

The default is an IList<IPNetwork> containing a single entry for IPAddress.Loopback.
KnownProxies Addresses of known proxies to accept forwarded headers from. Use KnownProxies to specify exact IP address matches.

If the server is using dual-mode sockets, IPv4 addresses are supplied in an 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 the HttpContext.Connection.RemoteIpAddress. For more information, see the Configuration for an IPv4 address represented as an IPv6 address section.

The default is an IList<IPAddress> containing 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 Require the number of header values to be in sync between the ForwardedHeadersOptions.ForwardedHeaders being processed.

The default in ASP.NET Core 1.x is true. The default in ASP.NET Core 2.0 or later is false.
Option Description
ForwardedForHeaderName Use the header specified by this property instead of the one specified by ForwardedHeadersDefaults.XForwardedForHeaderName. This option is used when the proxy/forwarder doesn't 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 ForwardedHeaders Enum for the list of fields that apply. Typical values assigned to this property are ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto.

The default value 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 doesn't 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 doesn't 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.

The default is 1.
KnownNetworks Address ranges of known networks to accept forwarded headers from. Provide IP ranges using Classless Interdomain Routing (CIDR) notation.

If the server is using dual-mode sockets, IPv4 addresses are supplied in an 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 the HttpContext.Connection.RemoteIpAddress. For more information, see the Configuration for an IPv4 address represented as an IPv6 address section.

The default is an IList<IPNetwork> containing a single entry for IPAddress.Loopback.
KnownProxies Addresses of known proxies to accept forwarded headers from. Use KnownProxies to specify exact IP address matches.

If the server is using dual-mode sockets, IPv4 addresses are supplied in an 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 the HttpContext.Connection.RemoteIpAddress. For more information, see the Configuration for an IPv4 address represented as an IPv6 address section.

The default is an IList<IPAddress> containing 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 Require the number of header values to be in sync between the ForwardedHeadersOptions.ForwardedHeaders being processed.

The default in ASP.NET Core 1.x is true. The default in ASP.NET Core 2.0 or later is false.

Scenarios and use cases

When it isn't possible to add forwarded headers and all requests are secure

In some cases, it might not be possible to add forwarded headers to the requests proxied to the app. If the proxy is enforcing that all public external requests are HTTPS, the scheme can be manually set in Startup.Configure before using any type of middleware:

app.Use((context, next) =>
{
    context.Request.Scheme = "https";
    return next();
});

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

Deal with path base and proxies that change the request path

Some proxies pass the path intact but with an app base path that should be removed so that routing works properly. UsePathBaseExtensions.UsePathBase middleware splits the path into HttpRequest.Path and the app base path into HttpRequest.PathBase.

If /foo is the app 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");

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

If the proxy trims the path (for example, forwarding /foo/api/1 to /api/1), fix redirects and links by setting the request's PathBase property:

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

If the proxy is adding path data, discard part of the path to fix redirects and links by using StartsWithSegments(PathString, PathString) and assigning to the Path property:

app.Use((context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/foo", out var remainder))
    {
        context.Request.Path = remainder;
    }

    return next();
});

Configuration for a proxy that uses different header names

If the proxy doesn't use headers named X-Forwarded-For and X-Forwarded-Proto to forward the proxy address/port and originating scheme information, set the ForwardedForHeaderName and ForwardedProtoHeaderName options to match the header names used by the proxy:

services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedForHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-For_Header";
    options.ForwardedProtoHeaderName = "Header_Name_Used_By_Proxy_For_X-Forwarded-Proto_Header";
});

Configuration for an IPv4 address represented as an IPv6 address

If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format (for example, 10.0.0.1 in IPv4 represented in IPv6 as ::ffff:10.0.0.1 or ::ffff:a00:1). See IPAddress.MapToIPv6. Determine if this format is required by looking at the HttpContext.Connection.RemoteIpAddress.

In the following example, a network address that supplies forwarded headers is added to the KnownNetworks list in IPv6 format.

IPv4 address: 10.11.12.1/8

Converted IPv6 address: ::ffff:10.11.12.1
Converted prefix length: 104

You can also supply the address in hexadecimal format (10.11.12.1 represented in IPv6 as ::ffff:0a0b:0c01). When converting an IPv4 address to IPv6, add 96 to the CIDR Prefix Length (8 in the example) to account for the additional ::ffff: IPv6 prefix (8 + 96 = 104).

// To access IPNetwork and IPAddress, add the following namespaces:
// using using System.Net;
// using Microsoft.AspNetCore.HttpOverrides;
services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    options.KnownNetworks.Add(new IPNetwork(
        IPAddress.Parse("::ffff:10.11.12.1"), 104));
});

Troubleshoot

When headers aren't forwarded as expected, enable logging. If the logs don't provide sufficient information to troubleshoot the problem, enumerate the request headers received by the server. Use inline middleware to write request headers to an app response or log the headers. Place either of the following code examples immediately after the call to UseForwardedHeaders in Startup.Configure.

To write the headers to the app's response, use the following terminal inline middleware:

app.Run(async (context) =>
{
    context.Response.ContentType = "text/plain";

    // Request method, scheme, and path
    await context.Response.WriteAsync(
        $"Request Method: {context.Request.Method}{Environment.NewLine}");
    await context.Response.WriteAsync(
        $"Request Scheme: {context.Request.Scheme}{Environment.NewLine}");
    await context.Response.WriteAsync(
        $"Request Path: {context.Request.Path}{Environment.NewLine}");

    // Headers
    await context.Response.WriteAsync($"Request Headers:{Environment.NewLine}");

    foreach (var header in context.Request.Headers)
    {
        await context.Response.WriteAsync($"{header.Key}: " +
            $"{header.Value}{Environment.NewLine}");
    }

    await context.Response.WriteAsync(Environment.NewLine);

    // Connection: RemoteIp
    await context.Response.WriteAsync(
        $"Request RemoteIp: {context.Connection.RemoteIpAddress}");
});

You can also write to logs instead of the response body by using the following inline middleware. This allows the site to function normally while debugging.

var logger = _loggerFactory.CreateLogger<Startup>();

app.Use(async (context, next) =>
{
    // Request method, scheme, and path
    logger.LogDebug("Request Method: {METHOD}", context.Request.Method);
    logger.LogDebug("Request Scheme: {SCHEME}", context.Request.Scheme);
    logger.LogDebug("Request Path: {PATH}", context.Request.Path);

    // Headers
    foreach (var header in context.Request.Headers)
    {
        logger.LogDebug("Header: {KEY}: {VALUE}", header.Key, header.Value);
    }

    // Connection: RemoteIp
    logger.LogDebug("Request RemoteIp: {REMOTE_IP_ADDRESS}", 
        context.Connection.RemoteIpAddress);

    await next();
});

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

The request's original remote IP must match 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, logging indicates the address of the proxy:

September 20th 2018, 15:49:44.168 Unknown proxy: 10.0.0.100:54321

In the preceding example, 10.0.0.100 is a proxy server. If the server is a trusted proxy, add the server's IP address to KnownProxies (or add a trusted network to KnownNetworks) in Startup.ConfigureServices. For more information, see the Forwarded Headers Middleware options section.

services.Configure<ForwardedHeadersOptions>(options =>
{
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
});

Important

Only allow trusted proxies and networks to forward headers. Otherwise, IP spoofing attacks are possible.

Additional resources