Skip to content
Slicekit

Operations

Reverse proxy

Run the API behind a reverse proxy (TLS, forwarded headers, cookies) in production.

View .md
On this page

In production the API runs plain HTTP. docker-compose.prod.yml binds it with ASPNETCORE_URLS=http://+:8080 and expects a TLS-terminating reverse proxy (nginx, Caddy, Traefik, an ALB, an ingress controller) in front of it. The SPA is served as static assets and does not care about any of this; see Deployment for the build.

Without help, every request behind the proxy looks like it came from the proxy itself over HTTP: the wrong client IP and the wrong scheme. Three things break as a result:

  • HTTPS redirection loops, because Request.Scheme is always http.
  • Rate limiting buckets every caller under the proxy’s single IP.
  • Auditing records the proxy’s address instead of the client’s.
  • Secure cookies are dropped, because the request looks insecure.

Forwarded headers

The fix is forwarded-headers middleware. app.UseForwardedHeaders() runs first in api/src/Slicekit.Api/Program.cs, before app.UseHttpsRedirection() and everything else, and rewrites HttpContext.Connection.RemoteIpAddress and Request.Scheme from the X-Forwarded-For and X-Forwarded-Proto headers the proxy sets:

app.UseForwardedHeaders();
// ...
app.UseHttpsRedirection();

The options are configured in api/src/Slicekit.Api/Configuration/Api.cs. Only X-Forwarded-For and X-Forwarded-Proto are processed:

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

    var knownProxies = builder.Configuration.GetSection("ForwardedHeaders:KnownProxies").Get<string[]>() ?? [];
    var knownNetworks = builder.Configuration.GetSection("ForwardedHeaders:KnownNetworks").Get<string[]>() ?? [];
    // ...
});

Trusting the right hops

Trust is controlled by two optional config arrays:

KeyMeaning
ForwardedHeaders:KnownProxiesExact proxy IPs to trust (e.g. 10.0.0.7)
ForwardedHeaders:KnownNetworksCIDR ranges to trust (e.g. 10.0.0.0/8)

Both default to empty. That clears ASP.NET Core’s loopback defaults and makes the middleware trust the immediate forwarder regardless of address. This is the correct, simplest setup when exactly one proxy sits in front of the API.

Populate either array to restrict trust to known hops. Do this when the proxy is reachable from untrusted networks, so a client cannot spoof X-Forwarded-For. Override them with environment variables, using the __ separator and a __0, __1 index for array entries (see Configuration):

ForwardedHeaders__KnownNetworks__0=10.0.0.0/8
ForwardedHeaders__KnownProxies__0=10.0.0.7

Chained proxies

The default ForwardLimit is 1: one forwarded hop is consumed. If two proxies sit in front of the API (a CDN in front of an ingress, say), raise it so the real client IP at the far end is read:

options.ForwardLimit = 2;

Set it to exactly the number of trusted hops, never higher. Each extra hop is one more X-Forwarded-For entry a client could forge.

TLS termination

Terminate TLS at the proxy and forward plain HTTP to the API on port 8080. The proxy is responsible for the certificate, HTTP-to-HTTPS redirects at the edge, and setting X-Forwarded-Proto: https so the API knows the original request was secure.

Cookies and CSRF

This matters most for authentication. The auth cookies in api/src/Slicekit.Api/Auth/CookieHelper.cs are issued with Secure = true outside development and SameSite=Strict, and the session cookie in Configuration/Auth.cs uses CookieSecurePolicy.Always in production. A browser will only return a Secure cookie over HTTPS, and the SPA and API share an origin, so the SameSite policy is the CSRF defence rather than a header check.

Forwarded headers are what make this work behind the proxy. With X-Forwarded-Proto: https propagated and trusted, the request is treated as secure, cookies are accepted, and HTTPS redirection does not loop. Get the forwarded headers wrong and login silently fails: the cookie is set but the browser refuses to store it. See Authentication for the cookie and CSRF model.

Sample nginx config

server {
    listen 443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/ssl/certs/app.example.com.pem;
    ssl_certificate_key /etc/ssl/private/app.example.com.key;

    location / {
        proxy_pass http://api:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

$proxy_add_x_forwarded_for appends the client to any existing chain, and $scheme is https here, so the API sees the real client IP and the original scheme.

Checklist

  • Proxy terminates TLS and forwards plain HTTP to the API on 8080.
  • Proxy sets X-Forwarded-For and X-Forwarded-Proto on every request.
  • One proxy in front: leave KnownProxies and KnownNetworks empty.
  • Proxy reachable from untrusted networks: populate KnownProxies or KnownNetworks.
  • More than one hop: set ForwardLimit to the exact number of trusted hops.
  • Log in over HTTPS and confirm the auth cookies are stored, not dropped.
  • Hit an audited endpoint and confirm the recorded IP is the client’s, not the proxy’s.
  • Exhaust an IP-partitioned rate limit from one client and confirm a second client is not throttled.