eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Retiring sops β†’ the shared operator vault on Eleven
2
3> πŸ€– **Historical record β€” not a procedure.** The sops vault is retired and
4> the standing store is Bitwarden; what scripts read from it is pinned by
5> the vault contract (`deploy/vault-items.sh`). Operate via `./deploy/ops`.
6> The passphrase-derived recovery wrap this plan drove (R1/R2, #995–#997) was
7> **removed from the product 2026-07-25** (Paul: recovery is a passkey, never
8> a passphrase) β€” the reducers now reject `kind: "recovery"`, and the R1/R2
9> text below describes machinery that no longer exists.
10
11The plan of record for moving the fleet's operator secrets off the sops+age
12S3-bundle vault (`docs/secrets-vault.md`, RUNBOOK.md "Secrets vault") and onto a
13**shared, locked Eleven vault** β€” the Vault mini-app with the biometric lock
14layer (`docs/locked-vaults.md`), running in a dedicated bot-less space. This
15doc is the security analysis and the migration runbook; the design lives in
16`docs/locked-vaults.md`, operator-specific bindings (which space, which
17hostnames) go in RUNBOOK.md when the migration executes.
18
19**This plan was written, then adversarially red-teamed against the shipped code
20(three independent reviewers: crypto/protocol, operational/DR, macOS-keychain).
21The verdict shaped it: the confidentiality story is sound, but the first draft
22overclaimed disaster-recovery parity and audit accountability, and the vault is
23a *DR regression vs sops* until the recovery escrow (Β§"Build prerequisites") is
24built. So the bar is explicit: no real secret moves until the prerequisites
25ship AND the restore drill passes on a cold machine.** sops stays the vault of
26record, in parallel, for a full cycle after that.
27
28The access flow was subsequently simplified from the reviewed request/grant
29ceremony to the **membership/handoff** model of `docs/locked-vaults.md`: sharing
30a vault is adding the Vault app to a thread; `vault.lock` hands a sealed
31`member` handoff to every other member; each self-enrols a durable biometric
32wrap; a latecomer gets a handoff via `vault.give`. This does **not** move the
33security posture β€” confidentiality still rests on the same ECIES sealing and
34biometric wraps β€” and dropping the request/grant round-trip actually *removes*
35the grant-approval forge surface (there is no approval step to MITM). The
36analysis below reflects the membership model.
37
38## Why move at all
39
40sops+age works and is secure, but its *sharing* is painful: each operator seals
41their whole repo into an age-sealed S3 bundle slot (`sync.sh push`) and pulls
42the others' (`sync.sh pull`), an asynchronous, manual, merge-prone dance. The
43Eleven vault replaces that with the product's own always-on E2EE transport: one
44shared thread, every operator folds the same state live, with an append-only
45change log. It is also load-bearing dogfooding β€” "our own ops secrets ride
46Eleven" is the strongest statement of trust in the encryption. What the move
47must **not** do is weaken secrecy or recoverability. The rest of this doc proves
48where it does and doesn't, and what has to be built to close the gaps.
49
50## Does the unlock work shared between two operators? (yes β€” this is the core)
51
52A locked vault has **one vault keypair**. Its private half is wrapped once per
53enrolled unlocker into the keyring β€” N operators = N wraps of the *same* private
54key, each openable only by that operator's own biometric-gated KEK:
55
56- **Paul** unlocks with his passkey (browser, WebAuthn PRF β†’ KEK) or his Mac's
57 Touch ID keychain key (CLI). Two devices β†’ two wraps, both his.
58- **James** unlocks with *his* passkey / *his* Mac's keychain key β€” different
59 wraps, opening the same vault private key.
60
61Unlock is therefore **per-operator, per-device, and entirely local**: the
62in-memory key and the "lock now" / auto-expiry re-mask are properties of *one
63device's session*, never shared. Each operator seals new secrets to the public
64key with no unlock at all (adding is free); each reads only after their own
65biometric. **Onboarding the operators is membership-based** β€” access follows
66thread membership, there is no "request access" step. Paul (or any operator)
67creates the shared ops thread with the other operators and adds the Vault app,
68then **locks** it. `vault.lock` carries, besides Paul's own durable wrap, a
69`member` handoff for every other current member: the vault private key
70ECIES-sealed to that operator's roster public key (`sealVaultKeyTo`; bots are
71excluded, though this space is bot-less anyway). Each operator then
72**self-enrols** β€” opens their handoff with their own session key (no biometric,
73it's sealed to their identity), verifies it opens the vault, and wraps the vault
74key under their own passkey (web) or Touch ID keychain key
75(`eleven vault enroll`), which drops the handoff so a biometric is required
76thereafter. An operator who joins later (or loses every device) gets a fresh
77handoff from any enrolled operator via `vault.give`. The vault private key never
78transits the server in the clear at any step β€” only wraps and identity-sealed
79handoffs, all opaque blobs. **Sound** by the crypto review's verdict on the
80same primitive it examined: a handoff is ECIES-sealed to a member's roster
81public key β€” the identity the roster binds and verifies via the existing
82MAC-bind mechanism, not a self-asserted name β€” so it can only open for that
83member, and only an already-unlocked operator can create one. There is no
84ephemeral-key grant to replay or misdirect, and no approval step to forge,
85because there is no request.
86
87## Security analysis
88
89Threat model is CLAUDE.md's: *if the server is compromised, the attacker gets
90nothing.* Each property is stated against that bar and against "is this at least
91as good as sops." Claims marked **[verified]** survived the code red-team;
92claims marked **[gap]** are honest weaknesses the plan must carry.
93
94### 1. Server / infrastructure compromise β€” **[verified] holds the rule, equal to sops**
95
96The server stores only the thread-key wraps (ECIES to each member's user key),
97the sealed values (ECIES to the vault public key), and the keyring wraps (the
98vault private key under each operator's KEK). None is a usable key; the vault
99private key is never present on the server in any form. A full server compromise
100yields ciphertext β€” the same nothing sops's S3 gives. A hostile server can drop
101or replay whole ciphertext events (rollback β€” acknowledged as advisory; #573 key
102transparency is the systemic answer) but cannot read, forge a value, or maul a
103wrap (no key, GCM tag). The space is bot-less (Β§6), so no relay/push/announcer
104path touches it.
105
106### 2. Stolen or **locked/powered-off** operator device (no code-exec) β€” **[verified] better than sops**
107
108sops's root of trust is a **plaintext file on disk** (`~/.config/sops/age/
109keys.txt`, mode 600) β€” a stolen powered-off laptop, a disk image, or a backup
110that captured the file reads the age key and then every secret. The Eleven
111vault has **no plaintext *vault* key at rest**: the vault private key exists
112only as wraps, and on macOS the CLI KEK sits in the login keychain, *encrypted
113at rest*. A **locked or powered-off** Mac, a raw disk copy, or a `~/.eleven`
114copy taken cold yields the operator's user key (hence names + the change log)
115but **no values** β€” the KEK to open a wrap is unrecoverable without the login
116password. **This is the class where the vault genuinely beats sops.**
117
118Two honest bounds on this win:
119- The protection for a *locked* device is the login keychain's **at-rest
120 encryption**, not the biometric. On an **unlocked, unattended** Mac the KEK is
121 readable directly (the read path carries no LocalAuthentication context β€”
122 `eleven/vaultkek_darwin.go`), so an unlocked walk-up device reduces to Β§3.
123- "No plaintext key at rest" is true of the **vault** key only. The operator's
124 **user** key is a plaintext 0600 file in `~/.eleven` (`store.go`: "every file
125 is a private key in JSON clothing") β€” same posture as sops's age key. It
126 exposes names/change-log/any-unsealed-item, never sealed values.
127
128### 3. Malware running AS the operator β€” **[gap, = sops] no better, stated plainly**
129
130On macOS the CLI's KEK is a plain login-keychain item gated by an **in-process
131LocalAuthentication check**, *not* a Secure-Enclave `kSecAccessControl` ACL
132(enclave binding needs a signed binary with an application-identifier
133entitlement, which breaks the anyone-can-`go build` rule β€” see
134`docs/locked-vaults.md`; the read path in `vaultkek_darwin.go` sets no access
135control). So code running as the operator can read the KEK from the keychain
136*without* the LA prompt, combine it with `~/.eleven`, and decrypt everything.
137**sops is exactly as exposed** β€” the same malware reads `keys.txt`. Not a
138regression; both mitigate the same way (protect operator machines: WireGuard +
139named accounts + sudoers allowlist on the box; release keys offline). *Upgrade
140path (not a blocker):* a **signed** `eleven` build can move the KEK to a
141genuinely enclave-bound item without changing the wrap format (#950).
142
143### 4. Ex-operator / revocation β€” **[gap, = sops] ratchet unchanged**
144
145Removal does **not** retroactively protect anything an ex-operator saw: no
146rekey-on-leave (`db.go`), and they *read* the values while enrolled.
147`vault.revoke` drops their keyring wrap (hygiene), but removal means **rotating
148the real secrets** β€” identical to sops's `.sops.yaml` recipient ratchet. Do not
149mistake revoke for containment. (See Β§9 for the *offensive* use of revoke.)
150
151### 5. Bootstrap circularity + DR β€” **[gap β†’ closed only by the recovery escrow]**
152
153This is the finding that reshaped the plan. `eleven vault get -db <replica>`
154reads a vault from a raw DB replica with **no server** (CI-proven,
155`vault_offline_test.go`; the offline reader imports only `database/sql` +
156`internal/e2ee` β€” genuinely network-free). The *crypto* loop is broken. But the
157red-team showed the *operational* loop is **not** closed by the first draft:
158offline restore requires three inputs that, as designed, have **no fresh-machine
159source**, making the vault a **strict DR regression vs sops** in the
160all-machines-lost corner:
161
1621. **The keychain KEK** β€” Mac-local, in no backup (never synced), on the
163 never-in-vault list. On a fresh Mac, `get -db` finds no KEK and `enroll`
164 needs a live server β†’ dead end.
1652. **`~/.eleven`** for the ops space β€” a fresh identity isn't a DB member, and
166 joining needs a live server. Required to unwrap the thread key at all.
1673. **The S3 read credentials** β€” today only in sops + on the box (the
168 `litestream` IAM key in `/data/litestream.env`); there is **no independent
169 1Password copy**, despite the first draft asserting one.
170
171sops, by contrast, recovers from `1Password age key + any bundle`, on any fresh
172**Linux** box, by one operator. The Eleven vault, in the same disaster, recovers
173**only if a warm enrolled operator device survived** β€” worse than sops. That is
174unacceptable for the fleet's root secrets, so the migration is **gated on a
175recovery escrow** that restores sops-parity (Β§"Build prerequisites" R1–R3):
176
177- A **recovery wrap** (`kind: "recovery"`) of the vault private key under a KEK
178 derived (PBKDF2, reusing `crypto.js` `deriveBrandingKey`) from a
179 **high-entropy recovery passphrase** escrowed in 1Password β€” openable on
180 **any** machine (incl. Linux / `CGO_ENABLED=0`), no keychain, no biometric.
181- A **dedicated recovery member identity** whose `~/.eleven` is escrowed in
182 1Password β€” so a fresh machine can unwrap the thread key and read the log.
183- The **S3 read credentials** escrowed as an independent 1Password resident (not
184 only sops + box).
185
186With those three in 1Password + the S3 replica, any operator recovers on any
187machine β€” genuine parity with sops. **Never-in-the-vault (hard list):** the
188recovery passphrase, the recovery identity's `~/.eleven`, the S3 read creds,
189WireGuard/SSH/human passkeys, release + Sparkle signing keys, each operator's
190personal `~/.eleven` and keychain KEKs, the box control token, canary envs.
191Litmus: *"could I need this with every server, including this one, dead?"* β†’ yes
192means offline/1Password, never the vault.
193
194### 6. Bot-key exposure β€” **[verified] eliminated by construction, but deployment-enforced only**
195
196A bot is an E2EE member whose key lives on the box; a bot in the ops vault would
197put a decrypting key on the box and collapse Β§1. So the space runs **no bot and
198no sidecar** (`add-instance` without `add-bot`; no `lchat-bot@` unit), member
199invites off, admins = operators only. **Caveat the red-team raised:** this is
200deployment discipline, not code β€” a later `add-bot` on this host would silently
201re-introduce the exposure. The runbook adds a standing check (Β§runbook) that the
202ops host has no `lchat-bot@` unit and no agent member in its roster.
203
204### 7. Sessions-are-public (Claude transcripts) β€” **[gap on the read side] intake safe, read is a new footgun**
205
206Intake is airtight and matches sops: values enter only via the masked form or
207`eleven vault set` (hidden prompt / `--from-file`) in the operator's **own**
208terminal; **Claude is never a member of the ops thread** (bot-less, operators-
209only). But the red-team flagged a *new* exposure sops doesn't have: `eleven
210vault get` prints the value to **stdout** for command substitution, and box work
211here is routinely Claude-driven. A `get X | ssh box …`, a `set --from-file
212<file-Claude-just-wrote>`, or **the DR drill itself** (`get -db … name`) run
213inside a Claude-driven shell puts the secret in a public transcript β€” weaker
214than sops, whose `sops -d` writes a root-only file, not stdout. **Mitigations
215(R4 + rule):** add `eleven vault get -o <file>` writing a root-only file (never
216stdout), make it the *only* form used in any Claude-adjacent or deploy context,
217and keep the bare stdout `get` for interactive human use only. The gate/DR drill
218uses `-o`.
219
220### 8. Handoff targeting β€” **[verified] no approval to forge; sealing binds to identity**
221
222The membership model **removes the grant-approval round-trip entirely**, and
223with it the forge surface the old model carried. There is no `vault.request` for
224anyone to approve, so nothing takes an attacker-chosen ephemeral key *and*
225display name and waits for a human to bless it. Instead, `vault.lock` and
226`vault.give` seal the vault key **directly to a member's roster public key**
227(`sealVaultKeyTo`) β€” the identity key the roster already binds and verifies via
228the existing MAC-bind mechanism, not a self-asserted display name. So a handoff
229can only ever open for the member it was sealed to, and only an already-unlocked
230operator can create one. The residual insider risk is unchanged and is
231*availability*, not confidentiality: a compromised session can seal a **bogus**
232handoff to an operator (griefing), caught at self-enrol because it verifies the
233handoff opens a real sealed value before wrapping it (Β§9). The out-of-band
234discipline sops requires before adding a `.sops.yaml` recipient still applies β€”
235but to the human decision of *whom to add to the ops thread*, since there is no
236per-grant approval left to MITM. (Audit attribution is still self-asserted and
237unauthenticated β€” see Β§9.)
238
239### 9. Insider / compromised-session abuse β€” **[gap] worse than "nuisance"; audit is not accountable**
240
241Three real limits the first draft rounded off, all confirmed against the code:
242
243- **Audit attribution is self-asserted and unverifiable.** The actor
244 (`uid`/`name`) is written by the sender *inside* the encrypted event; the
245 server's authoritative `messages.user_id` is never exposed to any reader
246 (`hub.go`, `GET /api/messages`). Any member can post a `vault.*` event
247 attributed to another operator, and no shipped surface (web Access panel,
248 `eleven vault list`, offline `-db`) can detect or repair it. So the change log
249 records **what changed, with an *unauthenticated* actor hint** β€” it is **not**
250 the tamper-proof accountability the first draft's "attributed / better than
251 sops" implied. (It does not weaken Β§8: out-of-band verify goes around the
252 name.)
253- **Revoke can permanently lock everyone out β€” including DR.** Nothing binds a
254 wrap to an operator or stops revoking *others'* wraps; the only guard is
255 "never the last wrap." A compromised session can enroll its own wrap and
256 revoke every other down to one β†’ legitimate operators lose live *and* offline
257 read (`get -db` opens only surviving wraps). This is **permanent lockout**,
258 not a nuisance; recovery = re-provision, or fold the raw append-only log to
259 before the malicious revoke, or the **recovery escrow** (R1 β€” the recovery
260 wrap must be **un-revokable**, so griefing can never remove the escrow path).
261- **Destructive overwrite / downgrade.** Any member can `vault.change` any item
262 without knowing its value, and on a locked vault a plaintext `value` downgrades
263 a sealed item (rendered flagged; CLI "!" marker). Recoverable from the raw log
264 / parallel sops, but it is *destroy/downgrade*, not just spam.
265
266Confidentiality is already lost the moment a session is compromised, so these
267are integrity/availability harms on top of that β€” but they must be named, and
268they are why R1's recovery wrap is un-revokable and why sops stays a live
269parallel backstop through the cutover.
270
271### 10. Offline read authenticates *value content*, not *metadata* β€” **[gap, bounded]**
272
273`get -db` has no signature over the DB file. AES-GCM authenticates each blob, but
274ECIES-to-a-public-key is free: an attacker who tampers a raw replica can seal a
275chosen thread key to the member's (public) `users.public_key` and re-author the
276log. **Sealed values are safe** (a forged value / substituted vault key won't
277open without the reader's KEK β€” fails loudly). **Names, the change log, and any
278unsealed item are not** β€” a tampered replica returns attacker-controlled
279metadata silently. Bounded by keeping the *fetch path* trustworthy (S3 creds
280stay offline, Β§5), not by crypto. State it: offline read authenticates value
281*content* when sealed, not metadata.
282
283### Summary (honest)
284
285| Threat | sops+age | Eleven locked vault |
286|---|---|---|
287| Server / S3 compromise | ciphertext only | ciphertext only (**equal**) |
288| Stolen **locked/off** device, disk/backup | plaintext age key β†’ all secrets | KEK encrypted at rest β†’ **no values** (**better**) |
289| Unlocked walk-up / malware as operator | reads age key β†’ all | reads KEK β†’ all (**equal**) |
290| Ex-operator | rotate real secrets | rotate real secrets (**equal**) |
291| DR: all machines + fleet dead | 1Password age key, any Linux box, solo | **needs the recovery escrow (R1–R3)** to match; without it, **worse** |
292| Insider audit / accountability | none (files) | change log with **unauthenticated** actor hint (**β‰ˆ equal**, not "attributed") |
293| Insider lockout (revoke) | n/a (files) | permanent lockout unless recovery wrap is un-revokable (**gap β†’ R1**) |
294| Secret at deploy/read time | `sops -d` β†’ root-only file | `get` β†’ **stdout footgun**; needs `get -o` (**R4**) |
295| Sharing ergonomics | manual sealed bundles | live shared thread (**better**) |
296
297Net: **better or equal on confidentiality and sharing; a DR regression and an
298audit-accountability overclaim until the build prerequisites ship.** With
299R1–R4, it reaches parity-or-better on every axis.
300
301## Build prerequisites (BLOCKING β€” no real secret moves until these ship)
302
303Filed as issues; each is small and reuses existing primitives.
304
305- **R1 β€” Recovery wrap + un-revokable (#995).** A `kind: "recovery"` keyring wrap: the
306 vault private key AES-GCM-wrapped under a PBKDF2-derived KEK from a
307 high-entropy passphrase (reuse `crypto.js` `deriveBrandingKey`/PBKDF2, 600k
308 iters). Reducer: treat a `recovery` wrap like any wrap for opening, but make it
309 **un-revokable** (extend the last-wrap guard) so Β§9's revoke-lockout can't
310 remove it. Web: a "Create recovery key" flow (passphrase β†’ wrap) in the Access
311 panel. CLI: `eleven vault recovery-wrap` to mint it. Cross-platform: the open
312 path must work under `CGO_ENABLED=0` (no keychain), so recovery never needs a
313 Mac.
314- **R2 β€” `eleven vault get -db --recovery` (#996).** Offline read that prompts for the
315 recovery passphrase (PBKDF2 KEK) instead of the keychain, so a **cold machine
316 / Linux box** can recover. Pairs with R1.
317- **R3 β€” Recovery-identity + S3-cred escrow (ops, not code).** A dedicated
318 recovery member enrolled in the ops space; its `~/.eleven` and the S3 read
319 creds sealed into 1Password as independent residents. Documented in RUNBOOK.
320- **R4 β€” `eleven vault get -o <file>` (#997).** Write the value to a root-only file
321 (mode 600), never stdout; make it the only form used in deploy/DR/Claude-
322 adjacent contexts. (Optional companion: hold the web unlock key as a
323 non-extractable `CryptoKey` β€” closes the Β§2 XSS caveat; low priority given the
324 bot-less space.)
325
326## The gate (nothing real moves before this passes)
327
328Revised per the DR review β€” it must exercise the *real* disaster, not a warm
329shortcut:
330
3311. Stand up the ops space bot-less; lock it; enrol both operators; **create the
332 R1 recovery wrap** and escrow R3.
3332. Seed a few **throwaway** secrets; confirm both operators read them.
3343. **From S3 only** (no `/data` copy shortcut), from the **correct bucket** β€” a
335 streamed space replicates to the activator/hibernation bucket under a lease,
336 which the fleet-backup recipe *excludes*; confirm the restore config points
337 at the streamed replica, not the fleet-backup bucket.
3384. **Assert freshness:** a secret written just before the fetch is present in the
339 restored replica (guards the "streamed DB never picked up" trap).
3405. `eleven vault get -db --recovery -o <file>` **on a cold machine with no
341 enrolled keychain KEK** (ideally a Linux box), using only the 1Password escrow
342 (recovery passphrase + recovery `~/.eleven` + S3 creds). The value comes back
343 with the ops server stopped.
3446. Confirm a **wrong** identity, **wrong** KEK, and **wrong** passphrase each
345 yield nothing.
346
347Only when 1–6 pass do real secrets move; sops stays canonical in parallel for a
348full cycle afterward.
349
350## Migration runbook
351
3521. **Create the space, bot-less.** `lchat add-instance <ops-host>` (no
353 `add-bot`, invites off). Admin token β†’ 1Password. Add a standing check that
354 the host has no `lchat-bot@` unit and no agent in its roster (Β§6). Space
355 specifics β†’ RUNBOOK.md.
3562. **Streamed replication;** verify the S3 replica appears in the *correct*
357 bucket (gate step 3).
3583. **Share + lock + enrol + escrow.** Paul (or any operator) creates the shared
359 ops thread with the other operators and adds the Vault app, then **locks** it
360 β€” `vault.lock` hands a `member` handoff to each other operator automatically
361 (Β§"shared"). Each operator **self-enrols** from their handoff (web passkey, or
362 `eleven vault enroll` β†’ CLI keychain key) on each recovery machine; a latecomer
363 gets a handoff via `vault.give`. **Mint the R1 recovery wrap** and place the R3
364 escrow in 1Password.
3654. **Pass the gate** (above) with throwaway secrets, cold-machine recovery
366 included.
3675. **Seed for real.** For each `/data/*.env` / service token in sops **not** on
368 the never-in-vault list: `eleven vault set --from-file <file> <name>` (hidden,
369 own terminal). **`router.env` is split first** β€” its S3/AWS creds are
370 never-in-vault (they fetch the replica), while its DNS-01 token *is* vault-
371 worthy; migrate the DNS-01 token as its own item and keep the S3 creds out
372 (the red-team's file-vs-secret granularity trap). Binary bot envs stay base64
373 for now.
3746. **Parallel run with reconciliation.** Both vaults hold the same secrets for a
375 full cycle; edits go to **both**. Because values are sealed (`list` shows
376 names only; each `get` costs a biometric), divergence is invisible to
377 inspection β€” so **before the flip, reconcile every item** `vault get -o` vs
378 `sops -d`, per secret, and record it.
3797. **Flip canonical** only after step 6's reconciliation passes. Update
380 `docs/secrets.md` / RUNBOOK.md to name the Eleven vault as vault of record;
381 keep sops intact as fallback. Announce to operators.
3828. **Decommission later, deliberately.** Only after a real operating cycle, never
383 by deleting the age key or bundles β€” the archived sops bundle remains the
384 independent home of R3's escrow *unless* R3 truly duplicated it into
385 1Password (verify it did before pruning anything).
386
387## Rollback
388
389sops runs untouched in parallel through the whole migration, so rollback is
390"keep using sops" β€” no restore, no data motion. The Eleven ops space tears down
391(`lchat remove <ops-host> --stop` + `/data` delete) with zero sops impact. The
392only point of no return is step 8's deliberate decommission, gated on a real
393operating cycle β€” never a deadline.
394
395## What stays true afterward
396
397- The never-in-the-vault list (Β§5) is permanent, in this doc and the RUNBOOK,
398 and now explicitly includes the recovery passphrase and recovery `~/.eleven`.
399- Adding/removing an operator is a ratchet (Β§4): removal = rotate secrets.
400- Every secret enters via a hidden prompt in an operator's own terminal (Β§7);
401 reads in any deploy/Claude-adjacent context use `get -o` (R4), never stdout;
402 Claude never joins the thread.
403- The recovery wrap is un-revokable (R1) and its escrow (R3) is the independent,
404 any-machine root of trust that keeps this from being a DR regression.
405