How to securely reverse-proxy ASP.NET Core web apps
Tags: dotnet, csharp, webdev, networking
Published: 2024-04-04 21:12:18

[Kestrel](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel) is the solid, fast, and reliable web server that powers ASP.NET Core applications. It is entirely capable of serving as a front-facing web server, as proven by the Azure teams when they chose Kestrel and YARP to handle [reverse-proxying all services hosted on Azure App Services](https://devblogs.microsoft.com/dotnet/bringing-kestrel-and-yarp-to-azure-app-services).

However, it's very unlikely that .NET developers will directly expose their Kestrel-based web apps to the internet. Typically, we use other popular web servers like [Nginx](https://nginx.org/), [Traefik](https://traefik.io/), and [Caddy](https://caddyserver.com/) to act as a reverse-proxy in front of Kestrel for various reasons:

- To add a [TLS termination layer](https://en.wikipedia.org/wiki/TLS_termination_proxy): the reverse-proxy manages the TLS encryption and decryption (HTTPS),
- To handle requests on a specific domain and port, often using a virtual host configuration,
- To implement security features such as rate limiting, request filtering, [HSTS](https://developer.mozilla.org/en-US/docs/Glossary/HSTS), etc.,
- To provide request compression and caching, depending on the end-user's browser capabilities,
- For load balancing, failover support, and response transformation.

While these features can be configured with Kestrel, managing them across many web apps is more straightforward with a unified configuration point.

## Understanding the need for header forwarding

When placing a reverse-proxy in front of our ASP.NET Core applications, the apps may lose access to certain original HTTP request information, like the client's IP address, specific HTTP headers, the original domain, and port. We need this information for the proper functioning of our applications.

This is where the concept of header forwarding comes into play. The reverse-proxy forwards the original HTTP request's information to the backend application server via specific headers. The most common forwarding headers are:

- `X-Forwarded-For`: contains the original client's IP address,
- `X-Forwarded-Host`: contains the original host requested by the client,
- `X-Forwarded-Proto`: contains the original protocol (HTTP or HTTPS),
- `X-Forwarded-Prefix`: contains the original path base used by the client.

Most ASP.NET Core applications, especially those that are containerized, listen on the loopback address and port `8080`. Thus, `X-Forwarded-Host` and `X-Forwarded-Proto` are sometimes essential in scenarios where ASP.NET Core needs to generate URLs, such as in Razor templates or when generating redirect links in an OAuth2 authorization flow.

From a security perspective, `X-Forwarded-For` is also crucial. Without this header, your application would not be able to obtain the client's IP address, potentially affecting logging, your authentication and authorization stack, rate limiting logic, etc.

## How to securely interpret X-Forwarded-* headers in ASP.NET Core

Ensuring ASP.NET Core securely interprets `X-Forwarded-*` headers is vital. Attackers could manipulate these headers to bypass security measures. How can we trust these header values?

Your reverse-proxy is responsible for stripping these headers from client requests. If not done, an end-user could, for instance, impersonate an IP address. By default, well-configured reverse-proxies don't accept them and populate these headers with reliable values like the TCP connection and original HTTP headers such as `Host`.

Avoid writing your logic to read and interpret `X-Forwarded-*` headers. ASP.NET Core already includes middleware that handles this securely:

```csharp
// [...]
app.UseForwardedHeaders(); // <-- Add this line

app.UseAuthentication();
app.UseAuthorization();
// [...]
```

`UseForwardedHeaders` should be placed before any other middleware, especially before `UseHsts`, and can follow diagnostics and error handling middlewares.

Remember to configure the middleware options to specify which headers you wish to forward; otherwise, the middleware will not take any action. For example, to forward `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto`:

```csharp
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
});
```

The [ForwardedHeadersOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersoptions?view=aspnetcore-8.0) are very flexible, allowing you also to change the forwarding headers' names if you're not using the standard `X-Forwarded-*`.

The [ForwardedHeadersMiddleware](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.forwardedheadersmiddleware), inserted via `UseForwardedHeaders`, automatically updates HTTP request properties like `RemoteIpAddress`, `Host`, `Scheme`, and `PathBase`.

## Restricting IP addresses of reverse-proxies authorized to forward headers

If you know the IP address or range of your reverse-proxy, you can specify them in the `ForwardedHeadersOptions` to ensure that the `X-Forwarded-*` headers come from a trusted source:

```csharp
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    // 10.0.0.100 is the IP address of the reverse proxy
    // Use KnownProxies to add individual IP addresses you trust
    options.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));

    // In case you have a range of IP addresses you trust, you can use KnownNetworks
    IPNetwork.Parse("10.0.0.0/24");
});
```

## References

- [Configure ASP.NET Core to work with proxy servers and load balancers](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer)
- [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
- [X-Forwarded-Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host)
- [X-Forwarded-Proto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto)