Skip to content

Security model

The browser cookie contains a random Hanami session identifier, not the user’s A+ token. Ktor stores a SHA-256 digest of the identifier and an AES-256-GCM encrypted A+ token. Additional authenticated data binds the ciphertext to the session digest, A+ user ID, creation time, and absolute expiry.

Opaque sessions limit what a stolen cookie can do. The cookie remains usable through Hanami until it expires or is revoked, but it is not a reusable credential for direct A+ API access. This does not protect against a complete Ktor compromise because Ktor must be able to decrypt active tokens.

Session policy is enforced in Ktor:

  • Students default to a seven-day idle timeout and a 90-day absolute timeout.
  • Teachers and assistants default to a one-day idle timeout and a 30-day absolute timeout.
  • Last use is written at most once per hour.
  • Each user has a bounded number of active sessions.
  • A background task deletes expired and revoked rows in bounded batches.
  • Logout revokes the current row. An A+ 401 also revokes the current session.

The cookie is host-only, HttpOnly, SameSite=Lax, and uses Path=/. It is Secure on HTTPS. Plain HTTP is accepted automatically only on loopback hosts; other development origins require the explicit ALLOW_INSECURE_HTTP=true opt-in.

SvelteKit calls /internal/frontend with both the opaque user session and X-Hanami-Frontend-Secret. Ktor compares the service secret in constant time. The service secret proves which server called Ktor; it does not identify the user.

Keep Ktor off the public network where possible. Store the frontend service secret and session encryption keys in deployment secrets, separate from Postgres and its backups. Do not place them in source control, container image layers, Compose defaults, or database snapshots.

Each session row stores an encryption key version. New sessions use FRONTEND_SESSION_KEY_VERSION and FRONTEND_SESSION_ENCRYPTION_KEY. During rotation, FRONTEND_SESSION_PREVIOUS_ENCRYPTION_KEYS supplies the older version=base64 keys needed to read existing sessions.

Use this sequence for routine rotation:

  1. Deploy the new active key and retain the previous key in the key ring.
  2. Wait until sessions encrypted with the previous key have expired.
  3. Remove the previous key and deploy again.

Changing the active key version does not rewrite session rows. If a key may have leaked, do not use a gradual rotation. Disable session creation, replace the encryption and service secrets, revoke every frontend session with ./scripts/revoke-frontend-sessions.sh, and ask affected users to regenerate their A+ personal tokens.

Ktor resolves the session’s A+ identity and checks the role for the requested course:

  • Submission, student, document, rubric, and grading reads require at least assistant access.
  • Rubric changes and grade publication require teacher access.
  • Students use dedicated my routes that expose only their shared feedback, sent assessments, and owned submissions.

A request must pass the role check before any course or submission cache is read. Token-scoped metadata caches and submitter ownership checks prevent a cache hit from widening access.

Postgres stores A+ user IDs rather than copied names and email addresses. Hanami resolves current profile data through A+ when it needs to display or send it.

Application-level AES-256-GCM encryption protects the selected token ciphertext in database dumps, backups, snapshots, and read-only SQL access when the encryption keys remain separate. It does not encrypt every database field and cannot protect tokens if an attacker controls Ktor or obtains both the database and its keys.

Operators must define a retention period for grading data and backups, publish a privacy contact, and restrict access to Postgres, backups, logs, and deployment secrets. SMTP is part of the same data-handling path.

Session request bodies, authorization headers, cookies, ciphertext, service credentials, and environment values must not enter logs, traces, exception reports, or diagnostic dumps. Ktor access logging records request metadata, not bodies or authentication headers.

SvelteKit validates or creates X-Request-ID and forwards it to Ktor. Ktor returns the same identifier and includes it in logs and safe error bodies. Client errors contain stable codes without A+ bodies, upstream URLs, SQL details, SMTP diagnostics, tokens, or secrets.

  • Write bodies are capped at 2 MB.
  • Normal JSON responses are capped at 2 MiB. Submission content has a 25 MiB allowance, and archive downloads stream without buffering.
  • A+ absolute URLs, pagination links, and redirects must match the configured A+ origin before a token is attached. Ktor follows no more than three redirects.
  • A+ feedback HTML is sanitized in Ktor before it reaches SvelteKit.
  • Submission previews, decoded archives, and ZIP entries have independent size and path limits.
  • Manual-grading callbacks use an allowlisted origin, reject redirects, and bind encrypted capabilities to their registration and submission context.

If session encryption or service credentials may have leaked:

  1. Set FRONTEND_SESSION_CREATION_ENABLED=false and deploy.
  2. Rotate the frontend service secret and session encryption key.
  3. Run ./scripts/revoke-frontend-sessions.sh and confirm it exits successfully.
  4. Review access logs, backups, and secret distribution for exposure.
  5. Tell affected users to regenerate their A+ personal API tokens when token disclosure is possible.
  6. Restore session creation only after the new secrets are active on both SvelteKit and Ktor.

This procedure signs everyone out. Do not retain sessions after a suspected key compromise.