Skip to content

Trust & safety

Security

IcebergTTX is built API-first with a hardened default posture. This page summarises the security model; the canonical policy is SECURITY.md in the repository.

Reporting a vulnerability

Do not open a public GitHub issue for security vulnerabilities. Report privately via GitHub's private vulnerability reporting ("Report a vulnerability" under the repository's Security tab). Include a description and impact, reproduction steps (PoC if possible), and the affected component(s) and configuration. We aim to acknowledge within 5 business days.

Access model

Facilitator access to exercises is scoped per-exercise. A facilitator can read and mutate only:

  • exercises they created;
  • exercises they are enrolled on as a co-facilitator; or
  • any exercise, for a global-admin account (User.is_admin).

Any other facilitator gets 403 plus an authz.denied audit event. Bypasses of require_exercise_access / require_exercise_owner (privilege escalation, IDOR) are in scope for reports.

Intentionally shared-by-design (not vulnerabilities on their own):

  • the scenario library — any facilitator may list, read, edit, and export scenarios (they are reusable templates);
  • GET /users — it is the member-enrolment picker;
  • the facilitator role and is_admin flag are assigned out-of-band (seeded / admin-managed), never via self-registration, which creates participants only.

Reports that these shared surfaces leak data beyond their intended audience (e.g. a participant reading another team's data, or an unauthenticated caller reaching them) remain in scope.

Hardening highlights

  • Secrets & tokens


    Startup aborts if SECRET_KEY is unset, default, or under 32 chars. JWTs carry an iat claim with a per-user token_valid_after revocation cutoff; changing your password revokes all other sessions.

  • Cookies & CSRF


    Auth cookie is httpOnly + Secure; an Origin/Referer check guards cookie-authenticated state-changing /api/ requests. WebSockets authenticate from the cookie with a CSWSH origin check.

  • Rate limiting


    Sliding-window login brute-force protection (429 + Retry-After), plus a separate per-IP registration flood limiter. Registration can be disabled entirely with REGISTRATION_ENABLED=false.

  • Strict CSP & headers


    script-src 'self' with no unsafe-*, plus X-Frame-Options: DENY, nosniff, Referrer-Policy, Permissions-Policy, and HSTS in production — emitted by the app itself, not the proxy. (Caddy serves /static/* directly and adds only nosniff + caching headers to those asset responses.)

  • Audit logging


    Structured JSON audit events (login, register, inject release, exports, authz denials, CSRF blocks…) to a logger and a table nothing in the request path rewrites, sanitised against log injection. An optional retention window prunes old history; expired sign-in link tokens are always purged.

  • SIEM forwarding


    The app forwards each event off the response path to file / syslog / HTTP sinks (Splunk HEC, Elastic, webhook). Tokens are env-only and never logged.

Authentication

Local auth (bcrypt password hashing, NIST-aligned length-only policy) runs alongside or instead of OpenID Connect SSO (Authorization-Code + PKCE via Authlib). Adapters ship for Entra, Authentik, Auth0, and Okta. JIT-provisioned SSO users default to participant, and a user cannot elevate themselves: a higher role is only granted when one of their IdP groups appears in the administrator-configured OIDC_<provider>_ROLE_MAP allowlist (unknown groups and unknown role names are ignored). Client secrets are env-only.

Operator responsibilities

Deployment hardening — secret management, TLS termination, network policy, and the storage prerequisites for scaling out — is the operator's responsibility. Rate-limit counters and config caches are shared through PostgreSQL, so the login, registration, and password-reset limits hold however many replicas are running, and a policy change reaches all of them. Two operational notes carry security weight: do not place a transaction-mode connection pooler in front of the app (it silently breaks the LISTEN/NOTIFY subscription that carries those invalidations), and a migration must be forward-compatible across one release, since a rolling update serves the previous version against the new schema. See Deployment for the full list and the hardened Compose and Kubernetes baselines.