Running a site
Security
Sessions and the session cookie
Signing in creates a session: a random token sent to the browser in a cookie, of which only the SHA-256 hash is stored. The cookie is HttpOnly and SameSite=Lax, lasts [auth].session_days (14 days by default), and is marked Secure when public_url starts with https or [auth].secure_cookies is on. Expired sessions are swept from the database every hour.
The cookie's name is gsy_session_ followed by eight characters worked out from the site and its public URL, so several sites on one host never overwrite each other's sign-in. It also means that changing public_url signs everyone out.
Passwords are hashed with Argon2id and must be at least 10 characters. People can add a second factor from any authenticator app, with recovery codes. gansey user reset-password sets a new password and signs out every session of that account.
The request header
Every request to the API apart from /api/health and /api/auth/* needs a signed-in session, and every request that is not GET or HEAD must also send the x-gansey-request header. The admin sends it; a form on another site cannot, so together with SameSite=Lax it protects against cross-site requests. A script that calls the API sends the session cookie and the header:
curl -X POST https://example.test/api/backups \
-H "x-gansey-request: 1" \
-H "content-type: application/json" \
-b "gsy_session_1a2b3c4d=<token>" \
-d '{"note":"from a script"}'
Module routes on the public site get the same care from core: sign-in, roles, capabilities, rate limits and cross-site posts are declared on the route and checked before the module runs.
Roles and capabilities
Every account has one or more roles, and a role is a set of capabilities: manage_site, manage_users, manage_roles, edit_pages, publish_pages, delete_pages, edit_theme, publish_theme, upload_media, delete_media and manage_modules. Four presets (administrator, editor, author, viewer) are set up when a site starts for the first time, and custom roles can be made in Users & roles. Updates and backups need manage_site; installing and switching modules needs manage_modules. Getting started covers roles in detail.
The robot check
Settings → Robot check is Gansey's own captcha, served by the site itself; nothing is loaded from, or sent to, anyone else. A challenge is a small piece of work the browser does on its own (a hash puzzle, a second or so on a phone at the normal effort), with one of three fronts: Quiet (nothing to do; the work runs while the form is filled in), a Tick box ("I'm not a robot", then "Verified"), or a Question every time: a short plain-text question such as "Which of these is a fruit: table, apple, river?", which screen readers read like any other text and which has no pictures to make out. The box is a card with the site icon (Settings → Site) at its edge, and two buttons beside it: one has the browser read a question aloud, the other opens the accessibility options (answer a question instead of ticking, or hear it). A question is checked as soon as it is answered and can be swapped for another. Without JavaScript the question is asked instead, so nobody is locked out. The tick is a real button with a checkbox role, progress is announced to assistive technology, and the design follows the visitor's reduced-motion and forced-colours settings.
Where it asks is up to you: the admin's sign-in (by default only after two wrong passwords from the same address in 15 minutes, so staff rarely see it), the public sign-in pages, and the forms modules build (sign-up, contact forms, checkout), which take it through ctx.captcha. An address that keeps failing gets harder work. Challenges live for ten minutes and work once; the answer to a question never leaves the server. The widget takes its colours and corners from the theme (the theme builder's Buttons tab has a Robot check section, and the captcha-* custom tokens), or a look of its own set here; the admin's own sign-in always keeps the admin theme. The card shows how many checks passed and failed since the site started.
Error reporting
In local and dev, API errors include a detail field and error pages show what went wrong. In staging and production they say only that something went wrong, and the detail goes to the log. [server].error_reporting (or GANSEY_ERROR_REPORTING) turns detail on or off whatever the environment; leave it unset or false on a public site.
Indexing outside production
Only production may be indexed by search engines. Every other environment answers /robots.txt with Disallow: / and marks pages noindex with a meta tag and an X-Robots-Tag header, so a staging copy cannot turn up in search results. In production the "Ask search engines not to index" switch in Settings → Site does the same. Search results, previews and the not-found page are never indexed.
Email and sign-in links
Without an SMTP server configured, sign-in links, invitations and password resets are written to the log instead of being sent. On local and dev the admin also shows the sign-in link so work can carry on; on staging and production it does not, because that would let anyone sign in as anyone. Set up [email] before a site goes live, and keep the logs private.
Modules
Modules reach the site only through ctx, and core checks the permissions in each module's manifest on every call. Modules installed from the registry are checked against the hub's pinned signing key before anything unpacks them. Your own modules (a folder, a .tar.gz file, a git repository or a package URL, from gansey mod add or Modules → Add your own) go in without a signature check, so only add modules from sources you trust. The admin shows what such a package asks for before it installs it, and marks it unsigned afterwards. To keep a site's admin to signed packages, set [modules].allow_unsigned = false. See Modules.
The signing key
A site that signs files needs a signing key of its own. In practice that is the hub: it signs every core binary and module package it publishes with it, and every site that updates from the hub pins its public half. Ordinary sites do not need one.
gansey signing-key new --out /srv/hub/data/signing.key
gansey signing-key show
signing-key new writes a new key to a file only its owner can read and refuses to replace an existing one. Point [signing].key_file at it, or put the key itself in GANSEY_SIGNING_KEY. Gansey refuses to load a key file that other users can read. The key is never stored in the database and is not part of a backup.
Keep a copy somewhere safe and offline. If the key is lost, every site that pinned it refuses new releases and packages until each one is pointed at a new key by hand. If it leaks, someone else could sign releases those sites would install, so treat it like the most sensitive password you hold.