
# Security Hardening

Sovrium designs out the recurring failure modes of insecure apps — exposed surfaces, missing auth, broken object-level authorization, enumerable endpoints — at the platform layer, so every app inherits the defenses with no per-app configuration. Every HTTP response carries a hardened header set, CSRF is enforced in production, sensitive endpoints are rate-limited, and unauthorized access returns `404` rather than `403`.

These guarantees are applied **in code** (not only at the reverse-proxy ingress), so they hold even for a self-hosted deploy with no proxy attached.

## HTTP Security Headers

Every response — page, API, static asset, or `404` — carries a hardened header set, applied via Hono's `secureHeaders` middleware registered as the **first** `*` middleware so it runs before route matching and covers error responses.

| Header                                | Value / behavior                                                                                                |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `Strict-Transport-Security`           | `max-age=31536000; includeSubDomains` (1-year HSTS, covers subdomains).                                         |
| `X-Content-Type-Options`              | `nosniff` (disables MIME sniffing).                                                                             |
| `X-Frame-Options`                     | `SAMEORIGIN` / `DENY` (anti-clickjacking).                                                                      |
| `Referrer-Policy`                     | `strict-origin-when-cross-origin`.                                                                              |
| `Cross-Origin-Opener-Policy`          | `same-origin`.                                                                                                  |
| `Cross-Origin-Resource-Policy`        | `same-origin`.                                                                                                  |
| `Permissions-Policy`                  | Denies camera, microphone, geolocation, payment, usb, accelerometer, …                                          |
| `Content-Security-Policy-Report-Only` | `default-src 'self'`, `frame-ancestors 'none'`, `object-src 'none'`, `base-uri 'self'`, `form-action 'self'`, … |
| `Content-Security-Policy` (enforced)  | **Absent** — report-only first; the enforced, nonce-based CSP is a Phase 2 promotion.                           |

:::callout
**CSP ships report-only first.** It surfaces violations without blocking while the SSR layer still emits inline `<script>`/`<style>`. The enforced header is deliberately absent for now, and a spec locks that absence in so a premature flip fails CI. The in-code directives mirror the Caddy ingress backstop so both layers agree.
:::

HSTS is emitted unconditionally even over plain HTTP — browsers ignore it on an `http://` connection, and a TLS-terminating proxy forwards exactly the value the server produces.

## CSRF & Cross-Origin Enforcement

Sovrium relies on Better Auth's built-in origin-check middleware. A state-changing, cookie-bearing request whose `Origin` is forged or stripped is rejected — a cross-site forgery cannot ride a victim's session cookie.

| Environment            | `disableCSRFCheck` | Behavior                                                                |
| ---------------------- | ------------------ | ----------------------------------------------------------------------- |
| `NODE_ENV=development` | `true`             | Origin check bypassed — forged `Origin` accepted (local cross-port DX). |
| `NODE_ENV=production`  | `false`            | Origin check active — forged / missing `Origin` rejected `403`.         |

- `trustedOrigins` is scoped to the app's own `BASE_URL` origin — **never** `'*'`. A wildcard is an open-redirect / origin-validation bypass; scoping it also makes Better Auth reject an external-origin `redirectTo` on password reset.
- `useSecureCookies` is enabled in production. Both the CSRF gate and secure cookies silently degrade if `NODE_ENV` is unset, so a missing-`NODE_ENV` risk is surfaced **loud** in the production startup banner (and suppressed in dev to keep output clean).

:::callout
**Set `NODE_ENV=production` in production.** CSRF enforcement and secure cookies both key off it. When unset, the server warns loudly in the startup banner — heed it before going live.
:::

## Rate Limiting

Sovrium replaces Better Auth's native rate limiting (known upstream bugs) with custom Hono middleware on the sensitive endpoints.

| Endpoint                                | Limit                     | Window |
| --------------------------------------- | ------------------------- | ------ |
| `POST /api/auth/sign-in/email`          | 5 attempts                | 60 s   |
| `POST /api/auth/sign-up/email`          | 5 attempts                | 60 s   |
| `POST /api/auth/request-password-reset` | 3 attempts                | 60 s   |
| `POST /api/auth/admin/*`                | General per-IP rate limit | —      |

Exceeding a limit returns `429 Too Many Requests`. Admin routes additionally run an auth-check middleware that returns `401` **before** parameter validation, so an unauthenticated caller never learns the shape of a protected endpoint.

## Anti-Enumeration: 404, Not 403

Unauthorized access to a protected resource returns **`404 Not Found`**, never `403 Forbidden`. A `403` confirms the resource exists; a `404` makes the resource's existence — and the id space — unobservable. This applies uniformly across record reads, the [admin dashboard](/en/docs/admin-dashboard), and the per-table force-delete endpoint (which returns `404` when force-delete is not allowed for that table).

Object-level authorization is enforced before any sensitive logic runs: a request is authenticated, then checked for access to the specific object, and on failure returns `404`. Combined with unguessable ids, this defeats enumeration even though the `404` itself is observable.

## Defense-in-Depth Summary

| Concern                | Mechanism                                                                          |
| ---------------------- | ---------------------------------------------------------------------------------- |
| Transport security     | HSTS (`max-age=31536000; includeSubDomains`).                                      |
| MIME / clickjacking    | `nosniff`, `X-Frame-Options`.                                                      |
| Cross-origin isolation | COOP + CORP `same-origin`, report-only CSP.                                        |
| CSRF                   | Better Auth origin check (production), scoped `trustedOrigins`.                    |
| Brute-force            | Per-endpoint rate limiting (`429`).                                                |
| Enumeration            | `404`-not-`403`, unguessable ids, auth-check before validation.                    |
| Authorization          | RBAC + [field-level permissions](/en/docs/table-permissions), object-level checks. |

## Related Pages

- [Authentication Overview](/en/docs/auth-overview) — auth strategies and the `auth` block.
- [Sessions](/en/docs/auth-sessions) — session lifetime, revocation, secure cookies.
- [Roles & RBAC](/en/docs/auth-roles-rbac) — role model and field-level permissions.
- [Table Permissions](/en/docs/table-permissions) — per-table and per-field access control.
- [Admin Dashboard](/en/docs/admin-dashboard) — the 404-on-unauthorized read surface.
- [GDPR & Privacy](/en/docs/gdpr-privacy) — data export and erasure guarantees.
- [Environment Variables](/en/docs/env-vars) — `NODE_ENV`, `BASE_URL`, and related settings.
