Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/content/docs/changelog/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Notable changes to the kit, newest first.

## 2026-07-13

- **The real client IP now reaches the pipeline behind a reverse proxy (security fix).** `UseHeroPlatform` never called `UseForwardedHeaders`, so behind an ingress (cloudflared → Caddy → app) `Connection.RemoteIpAddress` was always the proxy's container IP. Two consequences: the IP-partitioned rate limiters (the anonymous `auth` policy and the global IP limiter) collapsed into a single shared bucket — one anonymous spike throttled every tenant's login, and per-origin brute-force protection was gone — and audit / `UserSession` IPs recorded the proxy for every request. The kit now honours `X-Forwarded-For` / `X-Forwarded-Proto`, applied first in the pipeline so rate limiting, auth, HTTPS redirect, and audit all see the real client. Trust is bound to a new **`TrustedProxyOptions`** section (`KnownProxies`, `KnownNetworks` CIDRs, `ForwardLimit`): forwarded headers are honoured **only** from the configured ingress, so a client reaching the app directly can't forge its IP or scheme. **Action required behind a proxy:** set `TrustedProxyOptions` to your ingress CIDR(s) and hop count — with nothing configured the framework default (loopback only) stands and forwarded headers are ignored. See [CORS & security headers](/docs/security/cors-and-headers/) and the [production checklist](/docs/security/production-checklist/).

- **Dashboard: tenants can now edit their own branding from Settings.** A new **Settings → Branding** tab lets a tenant admin holding `Tenants.UpdateTheme` customise their **light and dark palettes** and **brand asset URLs** (logo, dark-mode logo, favicon) with a live preview — mirroring the operator's existing tenant-branding card, but self-service and with no `tenant:` header, since the theme endpoints are already scoped to the current tenant. The tab renders only for holders of that permission; a direct-URL visit without it hits the API's `403`, surfaced as an error band. Editing is draft-based — a **Reset to defaults** action and per-palette reset are available, and unsaved edits are preserved while you work (a co-admin's concurrent change appears on a manual refresh rather than overwriting your form).

- **The `fsh` CLI and `dotnet new` template are now on NuGet as stable `10.0.0`.** The two distribution packages that 10.0.0 had been waiting on have shipped: `FullStackHero.CLI` (install with `dotnet tool install -g FullStackHero.CLI` — no more `--prerelease`) and `FullStackHero.NET.StarterKit` (`dotnet new install FullStackHero.NET.StarterKit`). Because `fsh new` scaffolds *from* that template, the one-command flow is now end-to-end: `dotnet tool install -g FullStackHero.CLI && fsh new MyApp` produces a fully renamed project — unique JWT signing key, generated Docker secrets, `npm install` run, initial commit on `main`. The [Install](/docs/getting-started/install/) and [CLI](/docs/cli/) pages now lead with the CLI as the recommended path; `git clone` and the GitHub template remain available for reading the source or zero-install runs. See the [10.0.0 release](https://github.com/fullstackhero/dotnet-starter-kit/releases/tag/10.0.0).
Expand Down
35 changes: 28 additions & 7 deletions src/content/docs/security/cors-and-headers.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: CORS & security headers
lastUpdated: 2026-06-11
description: CORS-before-HTTPS-redirect ordering, the SignalR-credentialed-CORS gotcha, and the production security headers the kit emits by default.
lastUpdated: 2026-07-13
description: CORS-before-HTTPS-redirect ordering, the SignalR-credentialed-CORS gotcha, forwarded-headers trusted-proxy config, and the production security headers the kit emits by default.
sidebar:
label: CORS & headers
order: 7
Expand Down Expand Up @@ -47,13 +47,34 @@ Pipeline order (relevant slice):

```
1. UseExceptionHandler
2. UseResponseCompression
3. UseCors ← before HTTPS redirect
4. UseHttpsRedirection
5. Security headers
6. ...
2. UseForwardedHeaders ← before anything reads the client IP or scheme
3. UseResponseCompression
4. UseCors ← before HTTPS redirect
5. UseHttpsRedirection
6. Security headers
7. ...
```

## Reverse proxy & forwarded headers

The kit runs behind a reverse proxy in production (Cloudflare / cloudflared → Caddy / Nginx → app). Without `UseForwardedHeaders`, `Connection.RemoteIpAddress` is the proxy's IP and `Request.Scheme` is the internal `http`, which breaks two things: the **IP-partitioned rate limiters** (`auth` policy + global IP limiter) collapse into one shared bucket — losing per-origin brute-force protection — and audit / `UserSession` records log the proxy IP for every request. `UseHeroPlatform` mounts `UseForwardedHeaders` **first** (right after the exception handler), so `X-Forwarded-For` / `X-Forwarded-Proto` are applied before rate limiting, auth, HTTPS redirect, and audit read the client.

Blindly trusting `X-Forwarded-For` is itself a hole — any client that can reach the app could forge its own IP, poisoning audit trails and evading the rate limiter. So trust is bound to the ingress you actually run, via `TrustedProxyOptions`:

```jsonc
{
"TrustedProxyOptions": {
"KnownProxies": [ "10.0.0.5" ], // individual upstream proxy IPs
"KnownNetworks": [ "10.0.0.0/8" ], // or trusted upstream CIDRs
"ForwardLimit": 2 // ingress hop count (cloudflared → Caddy → app = 2)
}
}
```

- Forwarded headers are honoured **only** when the immediate upstream is one of the configured proxies/networks; from any other source they're ignored and the connection IP/scheme stand.
- `ForwardLimit` must match the real number of proxy hops. The framework default of `1` reads only the rightmost hop, which in a multi-hop ingress yields the nearest proxy's IP (or an attacker-injected value) instead of the real client.
- **Secure by default:** with `KnownProxies` and `KnownNetworks` both empty (as `appsettings.json` / `appsettings.Production.json` ship them), the framework default — trust loopback only — stands, so forwarded headers from a real proxy are ignored until you configure the ingress. Set them as part of your deploy.

## Why not AllowAnyOrigin for SignalR

CORS spec says: when a response has `Access-Control-Allow-Credentials: true`, the `Access-Control-Allow-Origin` must be an explicit origin, not `*`. SignalR's negotiate request is credentialed (it carries `Cookie` or the JWT via `accessTokenFactory`'s query-param fallback). With `AllowAnyOrigin()`, the server emits `Allow-Origin: *`, which violates the spec — the browser silently refuses to use the response, and SignalR's `HubConnection` fails to start with a confusing CORS error.
Expand Down
17 changes: 15 additions & 2 deletions src/content/docs/security/production-checklist.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Production security checklist
lastUpdated: 2026-06-11
lastUpdated: 2026-07-13
description: Ten configuration items you must check before shipping fullstackhero to production. Skip none.
sidebar:
label: Production checklist
Expand Down Expand Up @@ -114,9 +114,22 @@ Adjust `Auth` to your traffic profile: relax for high-volume consumer apps on sh
`UseHttpsRedirection` is on by default. Verify:

- Reverse proxy / load balancer terminates TLS with a valid certificate.
- `X-Forwarded-Proto: https` forwards to the kit so the redirect middleware doesn't double-redirect — and so the HSTS header (emitted only on HTTPS requests) actually fires.
- `X-Forwarded-Proto: https` forwards to the kit so the redirect middleware doesn't double-redirect — and so the HSTS header (emitted only on HTTPS requests) actually fires. **This requires `TrustedProxyOptions` (below):** the kit only honours `X-Forwarded-Proto` / `X-Forwarded-For` from a configured trusted proxy, so with it unset the scheme stays `http` and the real client IP never reaches rate limiting or audit.
- HTTP/2 or HTTP/3 enabled at the LB for performance.

Set `TrustedProxyOptions` to your ingress so forwarded headers are honoured — and only from your proxy, never a direct client (which could otherwise forge its IP/scheme):

```jsonc
{
"TrustedProxyOptions": {
"KnownNetworks": [ "10.0.0.0/8" ], // your ingress CIDR(s), or KnownProxies for individual IPs
"ForwardLimit": 2 // real hop count (cloudflared → Caddy → app = 2)
}
}
```

Both lists ship empty (trust loopback only), so this is opt-in — see [CORS & security headers](/docs/security/cors-and-headers/#reverse-proxy--forwarded-headers).

If you're behind Cloudflare / a CDN, also enable "Always Use HTTPS" + "HSTS" at the CDN.

## 7. Lock down or remove debug endpoints
Expand Down