Skip to content

Latest commit

 

History

History
176 lines (144 loc) · 8.9 KB

platform-guide.md

File metadata and controls

176 lines (144 loc) · 8.9 KB

Platform guide

Details of the Go platform that the README only summarises: the security and database model, the judge boundary, what works end to end, the first run, and key rotation. Decisions are in the ADR index; current status in the roadmap.

Security and database model

The User service is the canonical Casbin-backed authorization decision service. Its private mTLS authz/v1.Authorize API issues a fresh, five-second, database-audience-bound HMAC capability for each allowed request. A target database validates that capability in authz.set_context, binds it to the current PostgreSQL backend and transaction, and checks the local authorization-revision projection under FORCE ROW LEVEL SECURITY. A failed decision, expired capability, or projection lag denies access. The complete contract is in docs/database/authorization-context.md.

Authorization recovery is fail closed as well. Each RLS-protected service starts with its local authorization projection unavailable and, after an outbox or authorization-consumer failure, writes a target-specific resync request through its local outbox. The User service returns a manifest-verified grant snapshot; the target reopens only after every item has been applied. This prevents a stream-retention gap from silently retaining access after a revocation.

Every stateful platform service owns one logical database in the three-node platform PostgreSQL topology. Database owners are non-login roles; migrations, applications, and authorization-projection workers use separate least-privilege identities. Run migrations only as the service migrator after the role/database provisioner has run. Authorization HMAC material is supplied by the approved KMS/secret controller after bootstrap with backend/scripts/provision-authz-context-key; the script neither generates nor stores a secret.

The platform HA chart is deliberately render-gated on client certificates and India-resident encrypted backup inputs. A successful render or install is not HA acceptance: the node-failure and PITR exercises in the platform PostgreSQL runbook are required before promotion.

Judge boundary and release gate

Judge control-plane state is isolated in aether_judge_wrapper with its own PostgreSQL HA deployment and RabbitMQ quorum cluster. It does not own or share Redis with the platform; Redis is an internal dependency of the separately operated Judge0 engine after approval. The wrapper accepts durable, encrypted references and leases completions through private mTLS gRPC.

Implemented on the judge side:

  • a real Judge0 HTTP client (backend/services/judge/internal/adapters/judge0), selected with JUDGE_ENGINE and only enabled when JUDGE_ENGINE_COMPATIBILITY_APPROVED is set;
  • fan-out of an evaluation bundle into one execution unit per test case (ADR-0014);
  • per-unit results surfaced to submission, with a candidate-versus-faculty visibility boundary (ADR-0015).

Submission dispatches each queued evaluation request and code run to Judge over mTLS; Judge decrypts the bundle and source, runs one execution unit per test case on the engine, and Submission scores the attempt by test weight (ADR-0014, ADR-0015, ADR-0021). The single-server stack runs code on the AetherCode Execution Engine (ADR-0028).

The Judge0 engine chart is disabled by default. It must remain blocked until the gVisor, no-network, non-privileged compatibility gate has approved an immutable image and recorded queue-replay, node-failure, and 10,000-candidate / five-minute load evidence, including the 60-second final-verdict P95 target. See the compatibility-gate runbook.

Platform: current backend scope

Working end to end through the gateway (verified by deploy/single-server/smoke.py):

  • identity: login by username or roll number, MFA and recovery; administrator-provisioned accounts and CSV student import (ADR-0019);
  • colleges, departments, batches, roles and placement affiliations;
  • staff authoring into a global question bank with server-built, encrypted, weighted test bundles (ADR-0020); immutable question and exam versions; batch assignment;
  • attempts with per-candidate deadlines, autosaved answers, Run against sample tests with full output (ADR-0021), submit, judging, weighted scoring and time-up auto-submission;
  • Safe Exam Browser lockdown with per-URL key checks and .seb launch files (ADR-0022);
  • results and CSV export, result release, a live monitor with extra time and announcements, regrading, a similarity report and staff operations views (ADR-0027, 0031, 0039, 0043);
  • question pools, multiple choice, model-solution checks that gate publishing, question review, practice problems and exam previews (ADR-0033 to 0035, 0041, 0044 to 0047);
  • community content for students: interview experiences, blogs, contest, hackathon and open-source listings, linked coding accounts and tasks (ADR-0048);
  • single sign-on through the college's ZITADEL (ADR-0042) and college branding from a file (ADR-0045);
  • in-app notifications, event-fed analytics projections, cursor-paginated lists and soft delete (ADR-0013).

The local MinIO and KMS adapters (backend/libs/pkg/storage/minio, backend/libs/pkg/kms/local) are what the single-server stack uses. A multi-college production deployment should use managed object storage and KMS; see the production gates in the roadmap.

Platform: first run

A fresh deployment has no principals and no role assignments, so there is no way to call any authenticated endpoint. Run the one-time bootstrap command to create the first platform administrator after migrations are applied:

export IDENTITY_DATABASE_URL="postgres://..."
export USER_DATABASE_URL="postgres://..."
make bootstrap EMAIL=admin@college.edu NAME="Platform Admin"

The command creates the principal in the identity database and a self-granted super_admin role assignment in the user database. The created account has no password. Activate it by triggering the password-reset flow for the supplied email address. No password is ever written, printed, or accepted by this command.

The command is safe to re-run. A crash between the two database writes is repaired by running it again — each half independently no-ops if its row already exists. Once the platform has any principal or any super_admin, both functions permanently refuse further calls.

Platform: HMAC capability key rotation

authz.context_keys (present in the analytics, assessment, identity, notification, question-bank, seb, submission, tenant, and user databases — not gateway or judge, which have no such table) supports zero-downtime rotation through overlapping not_before/not_after/retired_at validity windows, but publishing and retiring a key still requires an operator action. make rotate-authz-key targets exactly one database per invocation:

export DATABASE_URL="postgres://..."
make rotate-authz-key ACTION=publish AUDIENCE=aether_submission NOT_AFTER=2026-09-24T00:00:00Z

The command generates a new key ID (a UUIDv7, unless KEY_ID is supplied) and 32 bytes of random HMAC key material, inserts the row, and prints the key ID and base64-encoded secret to stderr exactly once, with a clear warning that it is the only time the secret is shown. The secret is never written to a file, a log, or stdout. Copy it out-of-band into the target's operational secret store before it is lost.

Full rotation procedure:

  1. Publish a new key against all nine target databases, one invocation per database, with a not-before a few minutes in the future (the default) so already-deployed services have time to pick up the new configuration before the key becomes valid, and a not-after far enough out to cover the rotation window.
  2. Wait until not-before has passed on every database, then confirm the new key works before relying on it.
  3. Update AUTHZ_CAPABILITY_KEYS in the User service's configuration (the canonical signing service, per backend/libs/pkg/authz) to the new key.
  4. Once confident no capability signed with the old key is still in flight — capabilities have a five-second TTL (capabilityTTL in backend/libs/pkg/authz/capability.go), so the safe window is generous — retire the old key on all nine databases:
export DATABASE_URL="postgres://..."
make rotate-authz-key ACTION=retire AUDIENCE=aether_submission KEY_ID=<old-key-id>

retire fails if the key is already retired or does not exist for that audience. This CLI is for operational key rotation; the KMS-provisioned first key for a freshly bootstrapped database still comes from backend/scripts/provision-authz-context-key, whose secret is supplied externally rather than generated locally.