docs(adr): add ADR-011 for credential provisioning (open)

The PAT-in-flash shortcut (ADR-005 v0.1) is safe for the dev's bench unit
but not for a shipped one: it's plaintext in flash, the same token on every
unit, and rotation needs a reflash. ADR-005 decided the auth model but left
provisioning mechanics open.

Capture that as ADR-011 (Proposed/Open): the options (build-time bake →
on-device paste → eFuse-encrypted NVS, per-device fine-grained PAT, GitHub
App) and the likely shape, deferred beyond v0.1. It gates the first non-dev
distribution. Point the Spike 7 postmortem's shortcut entry at it.
This commit is contained in:
Julien Calixte
2026-07-07 00:03:55 +02:00
parent 2f2f1227ec
commit cce62bf9f7
2 changed files with 67 additions and 2 deletions

View File

@@ -475,6 +475,68 @@ retry). Failure surfaces as a single retry-able outcome in the status line.
---
## ADR-011: Credential provisioning — how the PAT reaches the device and is protected at rest
**Status:** Proposed / **Open** — 2026-07-07 (deferred beyond v0.1)
**Scope:** Gates the first distribution to any non-dev user.
### Context
[ADR-005](#adr-005-auth--https--github-personal-access-token) decided the auth
model (HTTPS + PAT) and sketched an endgame ("from v0.9 the PAT moves to
encrypted storage with an eFuse-derived key"), but left the *mechanics* open:
**how does a token get onto a device, and how is it protected once there?**
Spike 7 made this concrete. The PAT is currently **baked into the firmware image
at build time** (`build.rs``env!()` → a `const` in the `.bin`) — ADR-005's
deliberate v0.1 shortcut. On a real device that means:
- the token sits in **plaintext in flash** — anyone with physical access can
`esptool read_flash` and extract a working GitHub PAT (push/pull as the user);
- it is the **same token on every unit** built from that image — no per-device
revocation;
- **rotation requires a reflash**.
This is fine for the dev's own bench unit (it's their token, their device) and is
why the [Spike 7 postmortem](postmortems/2026-07-05-spike7-gix-https-push.md)
lists it as the *last standing shortcut*. It is not fine for a unit in anyone
else's hands. Resolving it needs a **provisioning path**, which the current design
(["build-time only, no provisioning module"](v0.1-mvp-technical.md#provisioning--build-time-only-no-module-on-device))
deliberately omits.
### Options considered
| Option | Pros | Cons |
| --- | --- | --- |
| **Build-time bake** (current, ADR-005 v0.1) | Zero UX; nothing to build. | Plaintext in flash; same token per unit; reflash to rotate. **Dev-bench only.** |
| **On-device paste → NVS (plaintext)** | No reflash; per-device token. | Still plaintext at rest; needs a first-run entry UI (captive portal / keyboard). |
| **On-device paste → NVS encrypted (eFuse key)** | ADR-005's stated target; a flash dump alone doesn't yield the token; per-device + rotatable. | Needs NVS encryption + eFuse key derivation + first-run UI — the whole provisioning module. |
| **USB provisioning tool** (host writes NVS over serial) | No on-device text entry; scriptable at assembly. | Needs a host-side tool; token still needs at-rest protection (combine with encryption). |
| **Per-device fine-grained PAT** (orthogonal) | Least blast radius; per-device revoke; repo-scoped. | User mints one PAT per device; pairs with any storage option above. |
| **GitHub App installation token** | Strongest, rotating creds. | Heavy for a single-user appliance — rejected in ADR-005 for overhead. |
### Decision
**Open — deferred.** v0.1 stays on the build-time bake (dev-bench only). Before
shipping to any non-dev user, pick a provisioning mechanism — the likely shape is
**on-device paste → eFuse-encrypted NVS** (ADR-005's target) **+ a per-device
fine-grained PAT** to bound the blast radius. That requires the provisioning
module currently deferred, NVS encryption, and eFuse key derivation, and ties
into [ADR-007](#adr-007-storage-split--fat-on-sd-for-working-copy-littlefs-on-flash-for-config)
(where config lives).
### Consequences
- **Blocks nothing in v0.1** — the bench unit runs on the baked PAT.
- **Gates first non-dev distribution** — this ADR must flip to Accepted (with a
chosen mechanism) before a unit leaves the dev's hands.
- Whatever is chosen, the token must never be logged or written into the working
copy — already enforced in the git module (PAT lives only in libgit2's
credential callback).
- Rotation UX and the first-run flow are the real work here, not the crypto.
---
## How to add a new ADR
1. Append a new `## ADR-NNN: <title>` section to this file.

View File

@@ -418,8 +418,11 @@ microcommits through `a15789a`).
would prefer esp-idf's bundle via a custom subtransport (it can't reach
libgit2's private mbedtls config without touching libgit2 sources).
- **PAT baked into flash — STILL STANDING** (ADR-005 spike shortcut). `build.rs`
embeds `TW_PAT` in the git_push image via `env!()`. A product must not ship the
token in flash.
embeds `TW_PAT` in the git_push/git_sync image via `env!()` — plaintext in
flash, extractable with `esptool read_flash`. Fine for the dev's bench unit,
not for a shipped one. The provisioning + at-rest-hardening decision is now
tracked as an **open** ADR: [ADR-011](../adr.md#adr-011-credential-provisioning--how-the-pat-reaches-the-device-and-is-protected-at-rest)
(likely on-device paste → eFuse-encrypted NVS + a per-device fine-grained PAT).
- **Product sync transport — DECIDED 2026-07-06: HTTPS + PAT.** On-device libgit2
is HTTPS-only (mbedTLS build; no ssh client, libssh2 unported), and the proven
path is HTTPS+PAT, so the product keeps ADR-005 rather than porting SSH. The