Skip to content

TotlProvision — Architecture

Three deployable surfaces, one product.

engine/   PowerShell provisioning engine (runs on each PC / USB)
backend/  Cloudflare Worker + D1 (fleet reporting, zero-knowledge escrow, audit)
portal/   Cloudflare Pages (Access/Entra SSO; engineer dashboard + secret reveal)
docs/     MkDocs Material site, auto-deployed to Cloudflare Pages on push
build/    release/packaging (Phase 1)

Tooling

  • CI (.github/workflows/ci.yml) runs on every push/PR: Node tests (backend/, portal/) + JS syntax checks, Pester (engine/tests) on a Windows runner, and mkdocs build --strict.
  • Docs build from the repo root via MkDocs and deploy to a dedicated Cloudflare Pages project. See Deploying these docs.

Engine (engine/)

Unchanged from 1.0 in structure; paths are relative to engine/ as the repo root. - src/Invoke-Provision.ps1 — orchestrator: ordered phases, state.json, resume scheduled task. - src/modules/Totl.* — one module per phase + shared Totl.Core. - New: Totl.Crypto (encrypt-only escrow crypto) and Totl.Report (reporting + escrow client). These are not yet wired into the provisioning phases — Phase 0 builds the spine only. - gui/, oobe/, scripts/, config/, data/, assets/, tests/ as before.

Backend (backend/)

Cloudflare Worker (src/index.js) over D1 (migrations/0001_init.sql).

Two trust paths: - Machine ingest — per-tenant Bearer API token (stored as a salted hash). Endpoints: /v1/report, /v1/secret, /v1/tenant/pubkey. - Portal — Cloudflare Access JWT (Entra SSO), verified against the team JWKS in auth.js, mapped to a tenant + role via the users table. Endpoints: tenant key onboarding, dashboard reads, secret reveal, audit.

Multi-tenant from day one: every table carries tenant_id.

Portal (portal/)

Static site (Pages) behind Access. All crypto is client-side (crypto.js, WebCrypto). The tenant private key is generated in-browser, wrapped by an org passphrase, and never sent to Cloudflare in the clear. Reveal fetches ciphertext and decrypts locally.

Secret-escrow data flow

machine: New-TotlPassword / BitLocker key
      └─ Protect-TotlSecret (RSA-OAEP-SHA256, tenant public JWK)
           └─ POST /v1/secret  ──►  D1.secrets.ciphertext   (ciphertext only)

engineer browser (after Access SSO):
      GET /v1/tenant/wrapped-key  ──►  unwrap with passphrase (PBKDF2+AES-GCM)
      POST /v1/secret/:id/reveal  ──►  ciphertext + audit row
           └─ decryptSecret (RSA-OAEP, in-memory private key)  ──►  plaintext shown locally

Interop contract (engine ↔ portal): tenant public key is a JWK {kty,n,e,alg:"RSA-OAEP-256"}; ciphertext is base64(RSA-OAEP-SHA256(utf8(secret))). JWK (not SPKI) because PowerShell 5.1 (.NET Framework) builds the key from n/e.

Enrollment at finalize (end-to-end slice)

When reporting.enabled is true, Invoke-Provision calls Totl.Enroll\Invoke-TotlEnrollment after all phases complete:

  1. If identity.localAdmin.rotatePassword is true, generate a unique per-machine admin password, set it on the local account, and escrow it (RSA-OAEP to the tenant key) via POST /v1/secret — replacing the shared setup password right before autologon credentials are cleared.
  2. Roll up per-phase results (Get-TotlRunStatus) and POST /v1/report with machine facts + timings.

Reporting is off by default; with it off the engine behaves exactly as before.

What is still NOT built yet

BitLocker enable + key escrow, config serving (/v1/config), the installer/build artifacts, RBAC beyond the admin/engineer split, white-label, and licensing. Those are Phases 1–7 in BUILD-PLAN.md.