# Security > Status: threat model and security posture for the engine as implemented. volumen is a small, file-based blog engine: a single Ruby process exposing a read-only public JSON API and a session-authenticated admin, behind nginx. This document describes the trust model, the controls in place, and the known limitations. If you find a security issue, please email **opensource@petrbalvin.org** rather than opening a public issue. ## Trust model - **Content authors are trusted.** Posts are written by authenticated admin users. kramdown renders Markdown and **passes raw HTML through** on purpose, so an author can embed HTML. The stored/served `html` is therefore only as safe as the people who can log in. If you need to accept posts from untrusted authors, add an HTML sanitiser before serving. - **The public API is read-only.** It exposes published (non-draft) posts, tags, a feed and a sitemap. No public endpoint mutates state. ## Authentication - Passwords are hashed with **scrypt** via `OpenSSL::KDF` (`N=16384`, `r=8`, `p=1`, 32-byte key, 16-byte random salt). Verification uses `OpenSSL.fixed_length_secure_compare` (constant-time). - Login takes a **username + password**. The error message is intentionally generic ("Invalid username or password.") to avoid user enumeration. - Sessions use signed cookies (`Rack::Session::Cookie`) marked **`HttpOnly`** and **`SameSite=Strict`**. The **`Secure`** attribute is added automatically on HTTPS requests (detected via `request.ssl?` / `X-Forwarded-Proto`), so the cookie never travels over plain HTTP in production. The signing secret comes from `admin.session_key`; if it is shorter than 64 bytes a random secret is generated per start (sessions then reset on restart, so set a stable key in production). ## Authorization (roles) - Two roles: **admin** (full access, manages users) and **author** (posts and own account only). - User management (`add`/`delete`/role change) is gated by `require_admin!` **on the server**, not merely hidden in the UI. - Safeguards prevent lockout: the **last admin** cannot be deleted or demoted, and a user cannot delete their own account or change their own role. ## CSRF - Every state-changing form (`POST`) carries a per-session token (`SecureRandom.hex(32)`), validated with `Rack::Utils.secure_compare`. Requests without a valid token get `403`. - `SameSite=Strict` cookies provide a second, independent layer. ## CORS and Host handling - The public read API sends `Access-Control-Allow-Origin: *` (read-only, no credentials) so any front-end may consume it. The admin sends **no** CORS headers and is same-origin only. - `Rack::Protection::HostAuthorization` is disabled (`permitted_hosts: []`) because volumen is intended to run behind nginx, which controls the `Host` header. Do not expose the Puma port directly to the internet. ## File uploads and path traversal - Uploaded media is stored under `content_dir/media` with a **sanitised, randomised** filename (`-.`), so uploads cannot overwrite each other or escape the directory. - `GET /media/*` resolves names with `File.basename` and an explicit containment check, so `../` traversal cannot read files outside the media directory. - Upload content-type is **not** validated (the `accept="image/*"` attribute is only a UI hint); this is acceptable under the trusted-author model. ## Secrets and files - `config.toml` and `users.toml` may contain password hashes — keep them readable only by the service user (`chmod 600`). - `volumen hash-password` reads the password from the terminal without echo; never pass passwords as command-line arguments. ## Transport - Bind to `127.0.0.1` and terminate TLS at nginx. The browser ↔ nginx hop is HTTPS; the nginx ↔ Puma hop is local. ## Dependencies A small, audited set: `sinatra`, `kramdown` (+ `kramdown-parser-gfm`), `toml-rb`, `puma`, `rackup`. Cryptography uses the Ruby standard library (`openssl`, `securerandom`). ## Known limitations / non-goals - **No rate limiting or lockout** on `/admin/login`. Put nginx `limit_req` in front of it, or use fail2ban, to slow brute-force attempts. - **No 2FA** and **no audit log**. - **Raw HTML in posts is trusted** (see the trust model). There is no built-in HTML sanitiser.