# Reverse proxy

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

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](/docs/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:

```csharp
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:

```csharp
builder.Services.Configure(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` | CIDR 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](/docs/configuration)):

```bash
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:

```csharp
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](/docs/authentication) for the cookie and CSRF
model.

## Sample nginx config

```nginx
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.
