All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m52s
CI / Build container images (pull_request) Successful in 3m55s
CI / Auth e2e pack (pull_request) Successful in 7m52s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 16s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m55s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m34s
CI / Import/export fidelity gate (push) Successful in 58s
Chosen path: an instance master switch following the api.enabled/ mcp.enabled pattern — while off, both feed routes AND the feed-token management answer 404 (existence hidden). Default ON: feeds predate the switch, existing instances and their subscribed readers keep working; the VS-NfD reference configuration (#227) turns it off. Admin UI gets the toggle next to the API/MCP switches (i18n de+en). Moving the token out of the query string is documented as rejected: a path segment lands in the same proxy and request logs, and feed readers cannot send headers — that is why the credential is in the URL at all. What DID leak was our own request log (pino logs req.url): the req serializer now masks ?token= values (common/mask-token-param.ts), so no code path logs the credential. Refs #191 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
6.7 KiB
6.7 KiB
Security concept
Threat-driven summary; detailed mechanics live in the referenced ADRs.
Assets & main threats
Wiki content (possibly confidential per pond/label), user credentials and e-mail addresses, instance availability. Threat actors: anonymous internet (public instance with self-signup), malicious registered users, malicious or sloppy plugin authors, compromised dependencies.
Authentication & session security (ADR 0007)
- Argon2id password hashing; opaque server-side sessions in HttpOnly, Secure, SameSite=Lax cookies; CSRF protected by SameSite + origin checks on mutating requests (double-submit token for the file-download edge cases).
- The origin check fails closed (issue #189): a cookie-carrying
mutation without
OriginandReferer(or with an unparsable one) is rejected with403 csrf_origin_mismatch. Non-browser clients authenticate with a PAT/bearer token and no cookie, which never reaches the check — the exception is structural, not a header loophole; a request that does carry the session cookie is always checked. Scripted cookie clients must sendOrigin: <APP_BASE_URL>. - Session bounds are configurable (issue #190): an absolute lifetime
(
SESSION_ABSOLUTE_HOURS, default 7 days, never extended by activity — also the cookiemaxAge) and an idle timeout (SESSION_IDLE_HOURS, default 3 days), both enforced server-side, the idle bound againstlastSeenAt. - E-mail verification (double opt-in) before an account can create content; password reset via single-use hashed tokens; both rate-limited.
- Rate limiting (DB-backed) on login, signup, reset, and API; lockout backoff on repeated failed logins per account+IP.
- Feed tokens (issue #149) authenticate feed URLs via
?token=— feed readers cannot send headers, which is why the credential lives in the URL at all. Moving it into a path segment was rejected (issue #191): a path lands in the same proxy and request logs as a query string. Instead: the instance switchfeeds.enabledhides the whole feed surface with 404 semantics (the VS-NfD reference configuration turns feeds off), tokens are stored hashed, and the api's request log masks?token=values (common/mask-token-param.ts), so no code path logs the credential. - Self-registration can be disabled instance-wide; personal-pond quotas (editors/readers/ponds/storage) bound the blast radius of spam accounts.
Authorization
- Single resolution algorithm (
permissions.md) inpackages/shared, enforced in API guards and at collab token issuance — never in the client. - Default-closed: no grant → no access. Public access is always an explicit grant.
- Admin actions are audit-logged (
operations.md).
Content & upload security
- Editor content is structured (ProseMirror schema) — no raw HTML from
users. The HTML render endpoint escapes everything outside the schema;
link protocols allowlisted (
https,http,mailto). - Uploads (ADR 0011): MIME/extension allowlist, size limits, magic-byte
checks, SVG sanitization or rejection,
Content-Disposition: attachmentfor non-image types, no user content served same-origin as executable (X-Content-Type-Options: nosniff; uploads path never servestext/html). - App CSP (strict):
default-src 'self';font-src 'self'(ADR 0016); no third-party origins at all — the GDPR posture is "zero external requests".
Plugin sandboxing (ADR 0008, operational)
- Code plugins: opaque-origin iframes, no network (
connect-src 'none'), capability-scoped postMessage API executed with the viewer's permissions server-side; declared capabilities surfaced to the Site Admin at install time. - Style plugins: CSS sanitized (no
@import/externalurl()), scoped class names. - Install surface restricted to Site Admins; packages size-limited and
schema-validated; the
plugins/directory watcher only trusts the volume (host-level access implies game over anyway).
Collaboration layer
- WebSocket connect requires a short-lived (≤ 60 s) single-purpose JWT
bound to user + page + mode; write revocation closes sessions via
LISTEN/NOTIFY (
realtime-collaboration.md). - Update size and document size ceilings prevent resource-exhaustion via crafted CRDT updates.
Secrets & configuration
- Secrets (DB password, token root key, SMTP credentials) live only in
the stage
.env(mode 600, never in git) and container env — not in the database (instance_settingsstores non-secret config; the SMTP password entered in the setup wizard is written to the env-backed secret store, not to a DB row). - Token key hierarchy (ADR 0020, issue #188):
COLLAB_TOKEN_SECRETis a ROOT key. Each token purpose uses its own HKDF-SHA-256 subkey (deriveTokenKeyinpackages/shared/src/token-crypto.ts):collabfor the collaboration JWTs (signed and verified byjose, HS256 as an explicit allowlist),unsubscribefor the digest unsubscribe links. No code path signs with the root key directly, so a compromise of one purpose's tokens is not transferable to the other. - Dual-verify window: unsubscribe links minted before the key separation
live in already-sent mail (90-day TTL). Verification accepts the legacy
derivation (root key + purpose prefix) until 2026-11-01
(
LEGACY_VERIFY_UNTILinapps/api/src/notifications/unsubscribe-token.ts), after which the legacy path goes dead automatically. New tokens are only ever signed with the subkey. - Key rotation: rotating the root key rotates every derived subkey at once
(desired: one secret to rotate) via env change plus rolling restart;
procedure documented in
operations.mdrunbooks. - Dependencies: lockfile-pinned; monthly update batch; images pinned to digests in Prod.
Privacy (GDPR)
- No external requests from the browser (fonts self-hosted, no CDNs, no analytics by default).
- Instance-configurable legal pages (imprint, privacy policy) are a core feature; dorfteich.online uses the operator's standard texts.
- Data minimization: username, e-mail, password hash, locale — nothing else required. Account deletion: personal pond and authored ponds follow the trash/purge path; authorship on shared content is pseudonymized ("deleted user"). A data-export endpoint (own profile + own ponds as Markdown/ZIP) supports access/portability requests.
- IP addresses appear only in rate-limit counters (short TTL) and reverse proxy logs (host-level rotation) — documented in the privacy-policy template.
Out of scope (v1, explicit)
- No end-to-end encryption of page content (server sees plaintext — needed for search, export, rendering).
- No plugin marketplace/signing — installation is a deliberate Site Admin act of trust in the reviewed package.
- No SSO in MVP (OIDC-ready per ADR 0007).