Skip to content

Security notes

  • No secrets in JSON. Passwords are read from environment variables loaded from .env. .env and real client configs are gitignored.
  • autounattend.xml contains a plain-text password. This is unavoidable for unattended OOBE. Mitigations: use a per-batch temporary local-admin password, store the answer file only on removable media, and wipe/rotate after deployment. Never commit autounattend.xml.
  • Auto-logon during setup stores the resume password as an LSA private secret (Totl.Security\Set-TotlAutoLogon), not as a plaintext DefaultPassword registry value, and bounds the window via Winlogon AutoLogonCount. A SYSTEM credential-cleanup watchdog guarantees the credentials are cleared even if a run crashes before finalize. To force cleanup manually: Invoke-Provision.ps1 -ClearCredentials (or -Finalize).
  • URL installers support an optional sha256 field — set it to verify downloads before executing.
  • Least privilege. The toolkit must run elevated; it does not create additional privileged accounts beyond the configured local admin.
  • Third-party scripts. The optional winutil path executes a live remote script. It is off by default; prefer the native baked-in tweaks for production.

Zero-knowledge secret escrow (backend)

Admin passwords and BitLocker recovery keys are escrowed to the Cloudflare backend, but the backend never holds a decryption key and can never read them.

  • Encrypt-only engine. Each machine fetches the tenant public key (GET /v1/tenant/pubkey, a JWK) and encrypts the secret with RSA-OAEP-SHA256 (Totl.Crypto\Protect-TotlSecret). Only base64 ciphertext is uploaded (POST /v1/secret).
  • Browser-only decryption. The tenant private key is generated in the engineer's browser (portal/crypto.js), wrapped with an org passphrase (PBKDF2-SHA256 → AES-GCM), and stored only as that wrapped blob. Reveal returns ciphertext; the browser unwraps the key and decrypts locally.
  • D1 stores ciphertext only for secrets.ciphertext and tenants.wrapped_private_key. Platform at-rest encryption is assumed but treated as insufficient on its own.
  • API tokens are hashed (sha-256(salt + token)), never stored in plaintext — a DB leak cannot replay them.
  • Every ingest and reveal is audit-logged (append-only audit table: actor email from the verified Access JWT, action, machine, time, IP).

Data classification (D1)

  • App-layer encrypted, retrievable: BitLocker keys, local-admin passwords, BIOS passwords, domain/Entra-join creds, RMM keys, Wi-Fi PSKs.
  • Hashed, never retrievable: machine API tokens.
  • Field-level encrypt (PII): usernames / emails.
  • Tenant-isolated + access-controlled only: run status, timings, asset inventory.
  • Append-only / tamper-evident: audit log.

Key recovery / break-glass (REQUIRED)

Because the backend cannot decrypt, a lost tenant passphrase/private key means all that tenant's escrowed secrets are unrecoverable. Operate a recovery scheme before relying on escrow in production: Shamir secret-sharing (M-of-N) of the tenant passphrase (or the private key), with shares held by separate trusted custodians and/or sealed offline. The portal warns at onboarding that the passphrase cannot be recovered from the server.

Per-machine credentials & hardening (Phase 2, implemented)

  • Unique per-machine local-admin password. At finalize the engine rotates the local admin to a random per-box password (Totl.Crypto\New-TotlPassword) and escrows it per machine — never a shared fleet password (Totl.Enroll\Invoke-TotlEnrollment, gated by identity.localAdmin.rotatePassword).
  • LSA-backed autologon. The resume password is written to the LSA private store, never as plaintext in the Winlogon registry key (Totl.Security\Set-TotlAutoLogon). security.autoLogon.maxLogonCount bounds how many automatic logons can occur before Windows disables autologon on its own.
  • Crash-proof credential cleanup. A SYSTEM startup watchdog (TotlProvision-CredCleanup) runs the orchestrator in -CredentialWatchdog mode and clears autologon credentials if the run has finalized or stalled past security.credentialCleanup.staleMinutes (a stale state-heartbeat = crash). This closes the gap where a crash before finalize could leave credentials behind. Disable via security.credentialCleanup.enabled: false.
  • Secret source. With security.secretSource: "prompt", the engine prompts for the local-admin password at runtime (Read-Host -AsSecureString) instead of reading it from a .env file — so secrets need not ship on USB media. Default remains env for compatibility.
  • Pinned downloads. URL installers verify an optional sha256 before running; the Totl.Security\Save-TotlVerifiedDownload helper refuses to fetch without a pinned hash unless -AllowUnverified is passed. Residual risk: the Chocolatey bootstrap script is fetched live — prefer pre-installing choco on the image, or pin via your own mirror.
  • Code signing. engine/scripts/Sign-TotlRelease.ps1 Authenticode-signs all scripts and packaged binaries (SHA256 + RFC3161 timestamp) to support moving execution policy toward AllSigned.