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:

Key Meaning
ForwardedHeaders:KnownProxies Exact proxy IPs to trust (e.g. 10.0.0.7)
ForwardedHeaders:KnownNetworks Proxy-only CIDR ranges to trust (e.g. 10.0.0.0/28)

Both arrays default to empty. When both are empty, ASP.NET Core’s loopback defaults (127.0.0.0/8 and ::1) stay in place. Forwarded headers from other immediate peers are ignored, including X-Forwarded-Proto. Configure your proxy’s IP as seen by the API even for a single proxy.

Populating either array replaces both framework trust lists. Only the configured addresses and networks are trusted, so include loopback explicitly if local callers need forwarded headers. Prefer exact IPs or a narrow subnet containing only trusted proxies; do not trust an entire shared Docker network. Override the arrays with environment variables, using the __ separator and a __0, __1 index for entries (see Configuration):

ForwardedHeaders__KnownProxies__0=10.0.0.7
ForwardedHeaders__KnownProxies__1=127.0.0.1
ForwardedHeaders__KnownProxies__2=::1

Replace 10.0.0.7 with the actual proxy peer IP. For a proxy-only subnet, use ForwardedHeaders__KnownNetworks__0 with its CIDR instead. In production Compose, add the entries to api.environment in docker-compose.prod.yml; setting .env values alone does not pass them to the API. Keep both loopback entries for the localhost readiness probe, which sends X-Forwarded-Proto: https. With both arrays empty, the probe uses the framework defaults.

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. Configure the API to trust nginx’s peer IP so it accepts the client IP and 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.
  • A non-loopback proxy: configure its peer IP or proxy-only CIDR in KnownProxies or KnownNetworks.
  • Explicit entries replace both defaults: include 127.0.0.1 and ::1 for the localhost readiness probe.
  • Send forged forwarded headers from an untrusted peer and confirm they are ignored.
  • 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.

Live demo

Explore the running app

No sign-up. Pick an account to open the demo with its credentials prefilled, or open it and sign in yourself.

Open without signing in

Credentials are prefilled. Shared password Slicekit-Demo-2026!.