Files
rootnode-academy/docs/ENGINEERING_SOP.md
T
ced4568 bf1e1b5c76 docs(phase4): production infrastructure documentation + runbooks
Master overview (PHASE4_PRODUCTION_INFRASTRUCTURE.md) plus five detail docs
(PRODUCTION_DEPLOYMENT, PRODUCTION_NETWORK_SECURITY, BACKUP_DR,
OBSERVABILITY, CAPACITY_PLANNING), a master PRODUCTION_VALIDATION_CHECKLIST,
and 12 runbooks (first deployment, normal deploy, rollback, Postgres
restore, VM rebuild for both hosts, Docker TLS rotation, TLS renewal, owner
recovery, stuck Lab cleanup, failed migration, backup failure, disk
failure, Lab Host resource pressure).

PRODUCTION_NETWORK_SECURITY.md prominently documents the one genuine
architectural gap this phase surfaced: the Gateway's existing
env.internal_ip mechanism for reaching a Lab container assumes Gateway and
Lab share a Docker daemon, which breaks once Labs run on a genuinely
separate rn-lab-host-01 VM. The network-policy half is built (a firewall
allowance for Gateway->Lab-Host on a published port range); the
application half (Provisioner/Gateway address handling) is an explicit,
reviewed, NOT-YET-BUILT follow-up flagged there rather than silently
decided.

Updated LAB_ARCHITECTURE.md, ENGINEERING_SOP.md, README.md, and
platform/README.md with Phase 4 pointers, consistent with how every prior
phase updated these.

Status: PHASE 4 BUILD COMPLETE / PRODUCTION DEPLOYMENT PENDING HARDWARE.
2026-08-18 17:13:51 -05:00

20 KiB
Raw Blame History

RootNode Academy — Engineering SOP

Living doc. Covers the "how" of building this platform — the "why" and business/curriculum process live in the Business Pack (~/Documents/CDAA Technologies/07 RootNode academy/RootNode_Academy_Business_Pack/), this doc doesn't duplicate that. Update this file in the same commit as the change it describes.

1. Where this actually lives

  • Repo: ~/Dev/Gitea/ced4568/rootnode-academy — remote is Gitea (git.synthossystems.com/ced4568/rootnode-academy), not GitHub.
  • Gotcha: ~/Dev/GitHub/ced4568/kubekosh is a different clone of the same original upstream fork, still pointed at GitHub, and it's behind (missing the RootNode logo commit and the Real Ops enforce-limits scenario as of this writing). It's not deployed anywhere. Don't edit it by mistake — always confirm git remote -v shows gitea: before making changes.
  • Known-good baseline as of this doc: commit 6ddfb1c ("add probe-port-mismatch and enforce-limits to rootnode-real-ops bundle"). Everything below builds on top of that, clean working tree.

2. Architecture at a glance

backend/server.js      Express API + SQLite (better-sqlite3) + WebSocket PTY terminal — the
                        "Lab image," single-tenant, unmodified by the Platform work below
frontend/               Vite + React SPA, built to frontend/dist and served by nginx
scenarios/data/         One JSON file per scenario (task or MCQ)
scenarios/bundles/      Flat scenario_id lists grouped by cert track (basics/appdev/admin/security/real-ops)
scenarios/rootnode-path.json   The RootNode guided learning path (staged, see §4)
scripts/entrypoint.sh   Container boot: starts k3s, nginx, node backend, sets the shell banner/PS1
Dockerfile              ubuntu:22.04 + k3s + node20 + nginx, single image, one port (80)

platform/provisioner/   Phase 1 Platform Provisioner — disposable per-student Lab lifecycle,
                        PostgreSQL registry, egress firewall. See §8 and
                        platform/provisioner/README.md.
platform/egress-proxy/  Shared allowlisting egress proxy every Lab environment's traffic must
                        pass through.

Single Express process, single SQLite file at /data/progress.db (or $PROGRESS_DB), single embedded k3s cluster per Lab environment. The Lab image itself is still single-tenant by design — see §5 for identity, and §8 for how Phase 1 multiplies it into a real multi-student platform without modifying it.

3. Local dev / safe smoke-testing

Never test schema changes against the real /data/progress.db first. Point at a scratch file:

cd backend && npm install   # first time only
node --check server.js      # syntax check, catches typos fast
PROGRESS_DB=/tmp/scratch/progress.db node server.js &
curl -s http://localhost:4000/api/health
# ...exercise the endpoints you changed...
fuser -k 4000/tcp            # stop it

To test a migration against real prior data without touching prod: copy the actual /data/progress.db out of the running container/volume, point PROGRESS_DB at the copy, boot, and verify the data you expect is still there before ever pointing at the real file.

kubectl/k3s-backed endpoints (/setup, /validate, /context, the WS terminal) won't work outside the container — that's expected. API-and-DB-only endpoints (roster, login, progress, curriculum, bundles, scenarios list) all smoke-test fine on a bare host.

4. Curriculum content

Two separate structures, don't confuse them:

  • Bundles (scenarios/bundles/*.json) — flat scenario_id lists, one per cert track. Loaded by loadBundles()/GET /api/bundles. Adding a bundle file here = adding a new cert-track library. Every scenario referenced must have a matching file in scenarios/data/.
  • RootNode path (scenarios/rootnode-path.json) — the guided onboarding sequence, separate from bundles on purpose (it's staged, bundles are flat, and the bundle loader would choke on a staged file if it were dropped in scenarios/bundles/). Loaded by loadCurriculumPath()/GET /api/curriculum/path (raw content) and, as of Phase 3B, computeCurriculumState()/GET /api/curriculum/state (rule-driven lock/complete/next-scenario state — see docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §4). Stages are Foundation → Workloads → Networking → Configuration → Troubleshooting → Real Ops → Capstone, built by reusing existing 1-k8s-basics and rootnode-real-ops scenarios grouped by their existing category field — no new scenario content was written for this. Each stage carries a completion_rule (all_required/threshold/prerequisites). Capstone is no longer a hardcoded placeholder — it's "type": "capstone" with an "unlock_rule" and a "capstone_assessment_id" pointing at the rootnode-capstone assessment definition (Postgres, assessment_definitions table), which is where its actual scenario draw lives — see the Phase 3B doc §7. Don't put real scenarios directly into the JSON stage's scenario_ids for capstone; that field stays empty by design, the draw is defined in the assessment definition instead.

To add/reorder a stage: edit rootnode-path.json directly, referencing existing scenarios/data/*.json ids. No backend code change needed — it's read fresh via POST /api/cache/reload or on server restart.

5. Student identity — what's built, what isn't

Built (backend only, as of this doc):

  • students table: student_id (student-NNN, sequential), display_name, cohort, created_at, last_active_at.
  • Login model: instructor roster, pick-your-name, no password. POST /api/roster adds a student (instructor-only action, currently unauthenticated at the API level — see "not built" below). GET /api/roster?cohort=X lists names for a dropdown. POST /api/login just validates the id exists and returns it; the frontend is expected to hold onto student_id and send it back as an X-Student-Id header on every request.
  • progress table now keyed on (student_id, scenario_id) instead of scenario_id alone — each student's completion/attempts/hints are isolated. Verified by smoke test: two students answering the same scenario don't affect each other's state, and pre-migration data was preserved under student_id = 'unassigned'.
  • Exam sessions now carry student_id (set at session start), so exam history and the timer-reset-on-submit logic are correctly scoped per student instead of leaking across whoever's browser hits the API next.

Not built yet — do not assume these exist:

  • No frontend UI. No login screen, no name picker, no roster-add form. The API works; nothing calls it yet. This is the next slice.
  • No auth on /api/roster POST. Anyone who can reach the API can add a "student." Fine for a dev instance, not fine once this is reachable by actual students — needs an instructor-only gate before that happens (ties into the instructor dashboard, roadmap item 3).
  • No concurrency / isolation. Exam sessions are still global-single-active (WHERE status='active' LIMIT 1 — see server.js), and there is one shared k3s cluster for the whole instance. Per-student identity ≠ per-student environment. Multiple students cannot yet use this platform at the same time without stepping on each other. That's roadmap item 4 (isolation) and it gates real classroom/minors use, same as the plan called out — this SOP is not claiming that gate is cleared.

6. Schema migration pattern

SQLite can't ALTER a primary key. The codebase's existing convention (see progress table history) is:

  1. PRAGMA table_info(<table>) → check if the new column/shape is already there (idempotent, safe to run on every boot).
  2. Simple new nullable column → ALTER TABLE ... ADD COLUMN.
  3. Primary-key or structural change → build <table>_new with the new shape, INSERT ... SELECT the old data across with sane defaults for new required columns, DROP TABLE, RENAME TO. Never drop data — give it a legacy/default value ('unassigned', etc.) instead of discarding rows.
  4. Test the migration against a copy of real data (§3) before it ever runs against /data/progress.db in the container.

7. Roadmap status

Order agreed 2026-08-17/18. Status as of this doc:

  1. RootNode identity/branding pass — partial. Shell banner fixed (⎈ RootNode Academy Lab — node: k8s-lab). Upstream KubeKosh attribution left untouched in footer/README/LICENSE. Broader "make Real Ops first-class" UI work not started.
  2. RootNode curriculum mode — done. Rule-driven stage completion/unlock as of Phase 3B (§12), student-facing tabs (My Learning/Practice/Assessments/Capstone/Achievements) at frontend/src/platform/.
  3. Student accounts / persistent progress — done, Postgres-authoritative as of Phase 2B (§9), sessions persistent as of Phase 3A (§10). Roster name-picking is still classroom/pilot identification, not strong student auth — documented, not silently glossed over (docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §7).
  4. Instructor dashboard — done (§9/§10). Full admin portal at /admin: dashboard, cohort and student management, classroom Prepare/Destroy Labs, per-instructor accounts with owner/admin/instructor roles, audit log.
  5. Per-student isolation — Phase 1 + Phase 2A + Phase 2B + Phase 3A done, see §8/§9/§10. Ephemeral per-student Lab environments, Gateway-mediated routing with persistent server-controlled sessions, a full instructor/admin control plane with real per-person accounts and RBAC, and a working entry→dashboard→launch→lab→progress flow are implemented and end-to-end tested on a single dev machine, including a 3-student cohort classroom workflow and a real Platform API restart mid-session. Still not cleared for minors — the production two-VM Proxmox split this depends on doesn't exist yet, and OIDC/SSO is a documented adapter, not a working integration (docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §16).
  6. Scenarios → teaching modules (incident context/investigation/prevention) — not started.
  7. Capstone/exam engine — done as of Phase 3B (§12). RootNode Capstone (Real Ops-weighted, unlockable by curriculum-completion rule, not a permanent placeholder) plus a general exam engine (definitions, cohort assignment, random scenario draw, server-authoritative timer).
  8. Progressive hint levels — done as of Phase 3B (§12). Server-enforced sequential reveal, tracked per attempt, policy-aware under assessment mode (free/disabled/flagged).
  9. Production observability — not started.
  10. CI scenario testing — not started.
  11. Container/build optimization — explicitly deprioritized by design (platform works, not urgent).

Next slice: production OIDC/SSO, the production two-VM Proxmox deployment, and teaching-module scenario content are the biggest remaining items. See docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §21 for the full Phase 4 boundary.

8. Platform Provisioner (Phase 1)

Full design: docs/LAB_ARCHITECTURE.md + docs/PHASE1_PLATFORM_FOUNDATION.md. Operational reference (config, local dev, known limitations): platform/provisioner/README.md. This section is just the map.

  • What it is: an internal-only service that provisions a disposable, fully isolated Lab environment per student — its own Docker network, its own container running the existing, unmodified Lab image, its own firewall-enforced egress. Postgres-backed registry (environments, environment_events, lab_hosts), idle reaper, startup reconciliation.
  • What it is not: directly reachable by a student or the Gateway anymore — as of Phase 2B, the Platform API (§9) is the only caller (student launch/reset AND admin prepare/destroy/ reset both go through it). Still exercised directly via its internal API (shared-secret auth) or platform/provisioner/test/run.js for isolated Provisioner testing.
  • Where identity lives: as of Phase 2B, Postgres is authoritative — students/cohorts/progress/scenario_attempts live in the same rootnode_platform database as environments, owned by the Platform API's own migrations. See §9. As of Phase 3A, instructors/sessions/cohort_instructors/audit_events live there too. See §10. environments.student_id is still a plain opaque reference, not a real FK — that's a deliberate, documented choice (docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §2), not an oversight.
  • Local dev: platform/provisioner/docker-compose.dev.yml runs Postgres + the egress proxy + the Provisioner on one machine — the documented Phase 1 dev exception. Production is a two-VM Proxmox split (Platform VM / Lab Host VM); the dev compose file is explicitly not that.
  • Test procedure: platform/provisioner/test/run.js — 12 automated integration tests against a live dev stack (isolation, firewall ordering, reset/destroy cleanup, audit events). Some properties (idle reaper timing, remote TLS lab-host, reconciliation-on-crash, real-image k3s behavior) were validated manually during implementation rather than automated — see the Phase 1 final report for what and why.

9. Platform API (Phase 2B)

Full design: docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md. Operational reference: platform/api/README.md.

  • What it is: platform/api/ — a lightweight Node/Express service (no k3s, no --privileged, no Lab terminal runtime) owning students, cohorts, enrollments, curriculum assignments, progress, scenario attempts, and — as of Phase 3A — instructor accounts, persistent sessions, and the audit log (§10). The sole caller of the Provisioner for both the student launch/reset path and the instructor/admin control plane.
  • What it is not: reachable directly by a browser — always through the Gateway (platform/gateway), same trust boundary the Provisioner has relative to this service.
  • Local dev: part of the same platform/docker-compose.dev.yml stack as the Provisioner — no separate compose file.
  • Test procedure: platform/test/e2e-phase2a.js (student flow) and platform/test/e2e-phase2b.js (classroom acceptance workflow) — both need a bootstrapped instructor account as of Phase 3A, see §10.

10. Identity and instructor access (Phase 3A)

Full design: docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md. Operational reference: platform/api/README.md (bootstrap, config).

  • What it is: persistent Postgres-backed sessions (student + instructor, survive a Platform API restart), real per-instructor accounts (owner/admin/instructor roles, scrypt password hashing, per-instructor salt), server-enforced RBAC on every /api/admin/* route, cohort-scoped access for the plain instructor role, CSRF double-submit protection, login rate limiting, and a platform-level audit_events log.
  • What it replaced: Phase 2B's in-memory session Maps (gone on every restart) and the single shared ADMIN_PASSWORD env var (no accountability, no roles, no revocation).
  • Bootstrap: node platform/api/bootstrap-owner.js <email> <name> <password> — the only supported way to create the first instructor account; no manual database editing.
  • Test procedure: platform/test/e2e-phase3a.js — 21 automated tests covering session persistence (including a real docker compose restart api), instructor auth, RBAC, cohort scoping, CSRF, rate limiting, password change, and audit logging.
  • Known limitations, future OIDC path, production HTTPS expectations: see docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §15–17.

11. Dependency lock convention

Every first-party Node service commits its package-lock.json: backend/, frontend/, platform/api/, platform/gateway/, platform/provisioner/. This wasn't consistently true before Phase 3B — platform/api/'s lockfile existed locally but was never committed, and platform/gateway//platform/provisioner/ never had one generated at all. Neither was a deliberate policy; both were oversights, fixed in Phase 3B (chore(deps) commits).

Going forward: after any npm install that changes a service's dependencies, the updated package-lock.json is part of the same commit as the package.json change — never left untracked, never regenerated-and-ignored. Validate a lockfile actually matches its package.json with npm ci (fails loudly on drift) before committing either.

12. Learning & Assessment Engine (Phase 3B)

Full design: docs/PHASE3B_LEARNING_AND_ASSESSMENT.md.

  • What it is: the teaching/assessment layer on top of everything Phases 1–3A built — detailed per-attempt telemetry (scenario_attempts, evolved from Phase 2B's one-row-per-click shape into a real attempt lifecycle), server-enforced progressive hints, rule-driven curriculum stage/capstone unlock logic, a full exam + capstone assessment engine (assessment_definitions/assessment_sessions/assessment_session_scenarios), and persistent achievements records.
  • Where the boundary moved: the Gateway now rewrites one Lab response (GET /lab/:token/api/scenarios/:scenarioId, stripping hints) so progressive hints can be genuinely server-gated rather than just UI-collapsed — the Lab image's own code/bytes are still untouched; this is a response rewrite at the Gateway boundary, same pattern as every other Lab-mediation decision made in earlier phases. The Lab's own frontend bundle did change this phase (ScenarioPanel.jsx, new ProgressiveHints/AssessmentBanner components) — Phase 3B explicitly authorized enhancing Lab UX, unlike prior phases.
  • Test procedure: platform/test/e2e-phase3b.js — covers attempt telemetry, hint sequencing/persistence-through-Lab-reset, curriculum/capstone lock-unlock, the exam engine (random draw, timer, expiry, restart persistence), two-student isolation, and instructor analytics/RBAC.
  • Known limitations: see the Phase 3B doc §20 — no automatic abandoned-attempt detection, no per-cohort needs-attention threshold configuration, raw-JSON pool_spec authoring in the admin UI (correct, not yet a guided form).

13. Production Infrastructure (Phase 4)

Full design: docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md, plus detail docs PRODUCTION_DEPLOYMENT.md, PRODUCTION_NETWORK_SECURITY.md, BACKUP_DR.md, OBSERVABILITY.md, CAPACITY_PLANNING.md, PRODUCTION_VALIDATION_CHECKLIST.md, and 12 RUNBOOK_*.md files, all under docs/. Everything lives in infra/ (Ansible, Terraform, Compose, firewall, monitoring, backup, CI, load-test tooling).

  • What it is: the infrastructure-as-code needed to run rn-platform-01/rn-lab-host-01 on a real Hetzner/Proxmox host — buildable and locally testable now, even though the dedicated server hasn't been purchased. Status is explicitly PHASE 4 BUILD COMPLETE / PRODUCTION DEPLOYMENT PENDING HARDWARE, not "deployed."
  • Where the boundary held: the Platform VM / Lab Host VM security split from docs/LAB_ARCHITECTURE.md is codified in firewall rulesets and mutual-TLS Docker config, not weakened anywhere.
  • Known gap surfaced, not silently fixed: the Gateway's Lab-reachability mechanism doesn't survive the move to two real separate VMs — see docs/PRODUCTION_NETWORK_SECURITY.md's "Known gap" section for what's built (firewall policy) vs. what's an explicit, reviewed, not-yet-built application follow-up (Provisioner/Gateway address handling).
  • Validated without the hardware: real tofu validate, ansible-playbook --syntax-check, docker compose config, caddy validate, promtool/amtool, and a genuine end-to-end backup→encrypt→restore→verify cycle against disposable local Postgres with the real production migrations applied. None of this proves the real Hetzner network boundary — that needs docs/PRODUCTION_VALIDATION_CHECKLIST.md walked against the real server.