GET /docs/security · accept: text/markdown200 text/markdown; charset=utf-8 · vary: Accept
# Security and privacy notes ## What signo stores Everything lives in one SQLite file, `signo.db`, in `DATA_DIR`. | Table | What it holds | How long | |---|---|---| | `users` | a random ID (the `sub` apps receive), email, optional name, when the account was created | until the account is deleted | | `passkeys` | for each passkey: its credential ID, public key, signature counter, transport hints, when it was added and last used | until removed or the account is deleted | | `magic_links` | for each pending email link: a SHA-256 of its token (never the token), the email, the sign-in it belongs to, its expiry | until used, or 15 minutes | | `oidc_store` | oidc-provider's records: sessions, consents, sign-ins in progress, authorization codes, access tokens and single-page apps' refresh tokens, plus the registered apps; apps added on the sites page also carry their owner's account ID | sessions 14 days, consents 1 year, sign-ins 1 hour, codes 1 minute, access tokens 10 minutes, refresh tokens 14 days at most; apps until removed, or until their owner's account is deleted | Expired rows are deleted every 10 minutes. `keys.json` beside the database holds the ID-token signing key and the cookie-signing key (see Keys below). signo stores no passwords. It writes no request logs, no IP addresses and no user agents, and pages load no third-party scripts, fonts or analytics. Rate-limit counters are the only place IP addresses appear; they live in memory and are dropped when their window ends. When `SMTP_URL` is not set, sign-in links are printed to the console instead of emailed, together with the address they are for; set it in production. ## Cookies | Cookie | Purpose | Lifetime | |---|---|---| | `signo_session` | who is signed in | 14 days | | `_interaction`, `_interaction_resume` | tie the browser to a sign-in in progress | only during a sign-in, at most 1 hour | Each has a `.sig` companion that signs it, as oidc-provider recommends. All are HttpOnly and SameSite=Lax, and Secure over HTTPS. There are no other cookies. ## Signing in and recovery - Accounts are created only after the person opens a sign-in link sent to their email. Apps therefore always receive `email_verified: true`, and nobody can create an account in someone else's name. - Passkeys are the main way to sign in. They are WebAuthn credentials bound to signo's domain, so another site cannot use them. - Email links work once, expire after 15 minutes, and finish the sign-in only in the browser that asked for them. Opening a link shows a "Continue" button and only that button uses the token, so email scanners that open links can't use it up. - Recovery: someone who has lost their passkeys asks for an email link, signs in, and adds a passkey on their new device. ## Protocol settings - Authorization code flow only, with PKCE (S256) required for every app. - ID tokens are signed with RS256 and carry email and name, as Google's do. Access tokens last 10 minutes. - Redirect URIs must use https; http is allowed only for localhost. - Single-page apps get refresh tokens bound to a key their browser keeps (DPoP, RFC 9449), so a copied token is useless. They last at most 14 days from sign-in, end when the person signs out of signo in a browser where they used the app, and aren't replaced when used. A DPoP proof whose time is more than five minutes off gets a nonce from signo to sign instead, so a device with a wrong clock can still sign in. - Revoking tokens never removes consent: when someone signs out, or an authorization code is used twice, the tokens go and the consent stays until the person withdraws it. - Browsers may call the token endpoint only for single-page apps, which have no secret, and only from the origin of one of the app's callback URLs. Any site's page may call userinfo, from the same origins. - Consent is remembered per account and app for up to a year. Signing out ends the session and revokes the tokens of every site used in it, in all of the person's browsers, but keeps the consent; deleting the account removes it. ## Sites added by account holders - Any account can add up to 10 sites at `/account/sites`. Only the account that added a site can see it, give it a new secret or remove it. - A site added as a single-page app gets no secret, since anyone can read its code. It signs in with PKCE, and its token requests must carry a DPoP proof. - The client secret is shown once, when the site is added or given a new secret. signo stores it as given, because oidc-provider compares it on every token request. A new secret replaces the old one at once. - Anyone can give a site any name, so the consent screen names the domain the person returns to before listing what the site receives. - Callback URLs follow the same rules as apps added with the CLI: https only, except for localhost. ## Protections on the sign-in, consent and account pages - Cross-site request forgery: a POST to these pages is refused unless the browser reports it as same-origin (`Sec-Fetch-Site`, or `Origin` on browsers without it). - A Content-Security-Policy allows scripts only from signo itself and forbids other sites from framing the pages, which blocks clickjacking of the sign-in and consent screens. - They are sent with `Cache-Control: no-store`. - The sign-out confirmation is oidc-provider's own form, protected by its own CSRF token. ## Rate limits | What | Limit | |---|---| | Email links to one address | 5 per 15 minutes | | Email links from one IP address | 30 per 15 minutes | | Sign-in attempts from one IP address | 60 per 15 minutes | | Token requests from one IP address | 600 per minute | Counters are kept per signo process and reset on restart. Behind a proxy, the IP address comes from `X-Forwarded-For` when `TRUST_PROXY` is set, or from the header named in `CLIENT_IP_HEADER` for proxies that use their own. ## Deleting an account Deleting an account removes its row in `users`, its passkeys, its pending email links, all of its sessions, consents, codes and tokens, and the sites it added. SQLite's `secure_delete` setting overwrites the freed space in the database file. A recently deleted row can survive briefly in SQLite's write-ahead log (`signo.db-wal`) until the next checkpoint, and in any backups you keep. Apps the person signed in to keep whatever they saved at the time. ## Keys The signing key and cookie key are created on first start and kept in `DATA_DIR/keys.json`, readable only by signo's user. Back the file up together with the database. If it is lost, everyone is signed out and apps fetch the new public key from `/jwks` on their next sign-in. v0.1 does not rotate keys automatically. ## Moving to a new domain Passkeys belong to the domain signo runs on. If you move signo to a new domain, people sign in once with an email link and add a passkey again, and each app must be given the new issuer URL.