aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-18 13:51:16 +0300
committergrm <grm@eyesin.space>2026-09-18 13:51:16 +0300
commit1b99463c1908037bb7dff9a766917791c25f484c (patch)
treeedc96c13bf3a06352e6bf0e90989a6cc525071d7 /README.md
parent2e31733093077c00be495d5725c7930f6ac9083c (diff)
downloadblogspace-1b99463c1908037bb7dff9a766917791c25f484c.tar.gz
blogspace-1b99463c1908037bb7dff9a766917791c25f484c.tar.bz2
blogspace-1b99463c1908037bb7dff9a766917791c25f484c.zip
Security: Document the hardening and the proxy's part in it
README: limit_req in the nginx sample, what HTTPS and TRUST_PROXY are for, and the auth notes cover the throttles, Sec-Fetch-Site, headers and logging. AGENTS.md records what the security pass checked and left alone, so the next one need not repeat it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'README.md')
-rw-r--r--README.md30
1 files changed, 25 insertions, 5 deletions
diff --git a/README.md b/README.md
index ba79190..84980bd 100644
--- a/README.md
+++ b/README.md
@@ -112,19 +112,29 @@ with the original `Host` header. Keep `HTTPS=true` and `TRUST_PROXY=true` in
nginx example:
```nginx
+limit_req_zone $binary_remote_addr zone=blogspace:10m rate=10r/s; # per client; the app throttles login and search itself
+
server {
listen 443 ssl;
server_name example.com *.example.com; # wildcard certificate
client_max_body_size 101m; # the Files page sends up to 10 files per request: >= 10 x the largest blog limit + 1 MB
+ limit_req zone=blogspace burst=20 nodelay;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $remote_addr;
+ proxy_set_header X-Forwarded-For $remote_addr; # exactly one address: TRUST_PROXY reads the last one
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
+The app never sees TLS itself, so tell it: `HTTPS=true` (https links, `Secure`
+cookie, HSTS for the whole domain) and `TRUST_PROXY=true` (client addresses from
+`X-Forwarded-For`). Flood control for the site as a whole belongs to the proxy
+(`limit_req` above); the app throttles the two anonymous endpoints worth
+abusing on its own: login attempts (10, then 10 a minute, per address and per
+account) and blog search (30, then 30 a minute, per address).
+
Caddy: `example.com, *.example.com { reverse_proxy 127.0.0.1:8080 }` (wildcard
certificates need the DNS challenge).
@@ -195,7 +205,17 @@ shadowed by management routes (`webadmin`, `admin`, `b`, …) are rejected for e
### Auth notes
-Sessions are HS256 JWTs in an `HttpOnly` cookie. The token carries the user's
-`token_version`; changing a password or disabling a user bumps it, which logs
-out every existing session. Every POST carries a `_csrf` field derived from the
-same secret, so old browsers without `SameSite` support are protected too.
+Sessions are HS256 JWTs in an `HttpOnly`, `SameSite=Lax` cookie (`Secure` with
+`HTTPS=true`). The token carries the user's `token_version`; changing a
+password or disabling a user bumps it, which logs out every existing session.
+Every POST carries a `_csrf` field derived from the same secret, so old browsers
+without `SameSite` support are protected too, and a POST a modern browser marks
+as coming from another origin (`Sec-Fetch-Site`) is refused outright — a blog
+on a subdomain counts as another origin. Failed logins and throttled requests
+are logged with the client address (fail2ban-friendly). Management pages send
+`X-Frame-Options: DENY` and a CSP that keeps their forms on this origin; every
+response says `X-Content-Type-Options: nosniff`.
+
+Content bloggers write in HTML mode and the custom HTML module are published
+unsanitised on the blog's own origin, by design; the dashboard origin never
+renders them (HTML previews run in a sandboxed frame).