|
| 1 | +# Cookies and security headers |
| 2 | + |
| 3 | +What the board puts in a visitor's browser, and what it tells the |
| 4 | +browser to refuse. This is the page to read when somebody asks what you |
| 5 | +store about them, when a cookie banner is being drafted, or when |
| 6 | +something on the board is being blocked and you need to know by what. |
| 7 | + |
| 8 | +None of it is configurable from the admin panel. It is part of the |
| 9 | +software, and the one thing that changes it is an environment variable, |
| 10 | +named below. |
| 11 | + |
| 12 | +## The cookies |
| 13 | + |
| 14 | +**The board sets no third-party cookies, runs no analytics, and stores |
| 15 | +nothing for advertising.** Every cookie below is first-party, set by the |
| 16 | +board itself, and there to make a specific thing work. |
| 17 | + |
| 18 | +| Cookie | What it is for | Lifetime | |
| 19 | +| --- | --- | --- | |
| 20 | +| `fs_session` | The signed-in session | Until it expires or you sign out | |
| 21 | +| `fs_remember` | *Remember me* on the sign-in form | The remember period | |
| 22 | +| `fs_guest` | Counts one reader once, for "who's online" | 1 day | |
| 23 | +| `fs_admin` | Admin-panel re-authentication | The admin session | |
| 24 | +| `fs_2fa` | A sign-in that has given a password and owes a second factor | Short | |
| 25 | +| `fs_sso` | The single sign-on handshake | 10 minutes | |
| 26 | +| `fs_passkey` | The passkey exchange | Short | |
| 27 | + |
| 28 | +Every one is **`HttpOnly`** — script cannot read any of them — and every |
| 29 | +one is **`Secure`** wherever the board is served over HTTPS. Over HTTPS |
| 30 | +they also carry the **`__Host-` prefix** (`__Host-fs_session` and so on), |
| 31 | +which binds a cookie to the exact origin that set it and forbids a |
| 32 | +subdomain from writing it. |
| 33 | + |
| 34 | +`SameSite` differs by purpose, and the differences are deliberate: |
| 35 | + |
| 36 | +- **`Lax`** for the session, remember, guest and SSO cookies. The SSO |
| 37 | + one has to be `Lax`: an identity provider returns the member with a |
| 38 | + top-level navigation from another site, and a `Strict` cookie is not |
| 39 | + sent on that request, so every federated sign-in would fail. |
| 40 | +- **`Strict`** for the admin, second-factor and passkey cookies. Nothing |
| 41 | + in those exchanges ever starts on another site, so nothing is lost by |
| 42 | + refusing to send them cross-site. |
| 43 | + |
| 44 | +The admin cookie is also **scoped to `/admin`**, so it is not sent with |
| 45 | +ordinary board requests at all. |
| 46 | + |
| 47 | +### The guest cookie, and what it is not |
| 48 | + |
| 49 | +`fs_guest` is the only cookie a visitor gets without signing in, and it |
| 50 | +exists for one figure: "37 guests reading". That number is not derivable |
| 51 | +from a stateless request — without something that comes back, every page |
| 52 | +view is a stranger. |
| 53 | + |
| 54 | +It is **an opaque random value and nothing else**. No code path turns it |
| 55 | +into an identity, and the session lookup refuses a row with no user |
| 56 | +behind it. The most it can say about the person holding it is that they |
| 57 | +were here. It lasts a day. |
| 58 | + |
| 59 | +Whether that needs consent where you operate is a question for you, not |
| 60 | +for this page — but "strictly necessary" is an argument you can actually |
| 61 | +make about it, which is not true of an analytics cookie. |
| 62 | + |
| 63 | +## The Content Security Policy |
| 64 | + |
| 65 | +Every page is served under a **nonce-based policy**, generated fresh per |
| 66 | +request: |
| 67 | + |
| 68 | +``` |
| 69 | +default-src 'self'; |
| 70 | +img-src 'self' data:; |
| 71 | +style-src 'self' 'unsafe-inline'; |
| 72 | +script-src 'self' 'nonce-<per-request>' 'strict-dynamic'; |
| 73 | +connect-src 'self'; worker-src 'self'; manifest-src 'self'; |
| 74 | +frame-ancestors 'self'; object-src 'none'; |
| 75 | +base-uri 'self'; form-action 'self' |
| 76 | +``` |
| 77 | + |
| 78 | +What that means in practice: |
| 79 | + |
| 80 | +- **An injected `<script>` does not run.** It has no nonce, and |
| 81 | + `'strict-dynamic'` means the browser trusts scripts the board's own |
| 82 | + nonced scripts load, and nothing else. |
| 83 | +- **Nothing loads from another origin** — no CDN, no font host, no |
| 84 | + embedded widget. A theme or plugin that reaches for one will be |
| 85 | + blocked, and the browser console will say so. |
| 86 | +- **`form-action 'self'`** means a form on your board cannot be made to |
| 87 | + post somewhere else. |
| 88 | +- **`frame-ancestors 'self'`** means the board cannot be framed by |
| 89 | + another site. |
| 90 | + |
| 91 | +**One environment variable changes it.** `REMOTE_IMAGES=1` adds `https:` |
| 92 | +to `img-src`, which is what lets members hotlink images from elsewhere. |
| 93 | +It is off by default: allowing remote images means every post can make a |
| 94 | +reader's browser fetch from a third party, which leaks the reader's IP |
| 95 | +address to whoever hosts it. |
| 96 | + |
| 97 | +The e2e suite asserts that every page carries the policy **and that |
| 98 | +nothing on the page is refused under it**, so a change that would have |
| 99 | +needed `unsafe-inline` fails before it ships. |
| 100 | + |
| 101 | +## The other headers |
| 102 | + |
| 103 | +Sent on every response: |
| 104 | + |
| 105 | +| Header | Value | What it stops | |
| 106 | +| --- | --- | --- | |
| 107 | +| `X-Content-Type-Options` | `nosniff` | A browser guessing a type and running an upload as script | |
| 108 | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | A full path leaking to another site | |
| 109 | +| `Strict-Transport-Security` | `max-age=63072000` | A downgrade to HTTP for two years | |
| 110 | +| `X-Frame-Options` | `SAMEORIGIN` | Framing, for browsers older than `frame-ancestors` | |
| 111 | +| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | Anything asking for hardware the board never uses | |
| 112 | + |
| 113 | +If you terminate TLS at a reverse proxy, it has to pass these through |
| 114 | +rather than replace them — see |
| 115 | +[Docker Compose](../../getting-started/deployment/docker-compose.md) for |
| 116 | +the CSP note on proxying. |
0 commit comments