Featured image of post How to securely reverse-proxy ASP.NET Core web apps

How to securely reverse-proxy ASP.NET Core web apps

Learn to configure your ASP.NET Core web apps to securely work with reverse-proxies and load balancers, such as Nginx, Traefik, and Caddy.

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.

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, Traefik, and Caddy to act as a reverse-proxy in front of Kestrel for various reasons:

  • To add a TLS termination layer: 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, 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:

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

// [...]

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:

builder.Services.Configure<ForwardedHeadersOptions>(options =>
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;

The ForwardedHeadersOptions are very flexible, allowing you also to change the forwarding headers’ names if you’re not using the standard X-Forwarded-*.

The 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:

builder.Services.Configure<ForwardedHeadersOptions>(options =>
    // is the IP address of the reverse proxy
    // Use KnownProxies to add individual IP addresses you trust

    // In case you have a range of IP addresses you trust, you can use KnownNetworks

# References

Licensed under CC BY 4.0
Ko-fi donations Buy me a coffee