← The specification

ADR-0037: An integration key is a credential the web API may mint, where the operator proves themself again

Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page

Status: Accepted Date: 2026-10-05 Decided: 2026-10-05, by the maintainer, Wessel Verheij: scoped keys, mintable at the command line and over the web API from an operator session that gives the password again, with the scopes read, act and member:<account>. Amends: ADR-0031, whose rule that a credential is not a web write keeps every credential but this one.

Context

Two things admit a request to the web API (the contract). One is the per-run token printed on the terminal that started the process. The other is a session bought with the operator’s password, which expires on an absolute clock and ends with the process. Both suit a person in front of a screen. Neither suits a program that runs for months beside the stack and reads it on its own schedule, such as Home Assistant (F12).

A program like that, given only these two, has to keep the operator’s password and sign in again after every expiry and every restart. The master credential would then live in a second system’s storage, with that system’s backups, logs and add-ons around it, so that reading a disk figure costs the power to do everything.

Decision

An integration key is a third way in. The operator mints it under a name and with exactly one scope (C10):

Scope Admits
read Every served read and the event stream
act read, and the actions the contract publishes as callable by a key
member:<account> Exactly what that household member’s own session admits, and nothing once the account leaves the household

A key travels in X-Lemonfiber-Token, as the per-run token and a session do, so the surface still reads one credential header. It survives restarts. It is stored only as a digest, shown once in the reply that minted it, listed by name, scope, minting time and last use, and revoked by name with effect at the next request.

Minting and revoking are web writes, and the only credential writes that are. ADR-0031 keeps a credential off the web API for two reasons: a printed value passes through caches, proxies and their logs, and a replacement is a write a request could be forged into. Both are answered here rather than set aside.

  1. The operator proves themself again. Only an operator session may mint or revoke, and minting asks for the password in the same request. A session left open on a shared screen cannot mint a key without the password, and a forged request, which cannot know it, mints nothing. No key may mint, list or revoke keys, whatever its scope.
  2. The secret is born in the reply and nowhere else. It is generated by the core, returned once with Cache-Control: no-store, and never readable again. No read, listing, log, support bundle, alert or journal entry carries it. A minted key replaces nothing, so there is no working credential a forged request could swap out.
  3. The operator hears about every one. Each mint and revoke is journaled and raises an operator alert naming the key and its scope, so a key nobody meant to mint is seen.

The mint reply carries what a client off this machine needs beside the secret: the stack’s address and its certificate pin (ADR-0025). A key is accepted off loopback only over the TLS that pin verifies, so a key never crosses a network in the clear.

What act may call is published, and short. Each action declares whether a key may call it, the contract lists the ones that may, and a key asking for any other is refused naming its scope. The list starts as: restarting a service, running the doctor, applying a stack update, and pausing or resuming downloads. Uninstalling, anything touching a credential, a key or the password, installing, updating or removing a plugin, a reset and a restore are never callable by a key. Each of those either cannot be undone or widens what the stack trusts, and a key is the credential most likely to be held somewhere the operator is not.

Consequences

  • C10 gains its requirements, and the web API contract gains keys under Getting in and the published list of key-callable actions.
  • Failed key presentations count against the same surface-wide limit as wrong passwords. Guessing a key is the same attack as guessing the password, so it meets the same wall.
  • A member key admits nothing a member’s session does not, so Home Assistant’s member entities answer to the same rules as the companion’s member screens (G2-R16).

Alternatives considered

Minting at the command line only. Keeps ADR-0031 whole, but an operator who runs the stack from the companion would have to reach a terminal to connect Home Assistant. With the password asked again and the secret born in the reply, the web write carries the same assurance the terminal gives.

Keeping the operator password in the integration. No change to the core, and the master credential in a second system’s storage. Rejected for that reason.

One unscoped key. Simpler, and a leaked Home Assistant key could uninstall the stack. Rejected: a scope is what makes a key safe to hand to another program.

Revisit when

  • An action a key may not call turns out to be one integrations routinely need. That is an argument for adding it to the list, one action at a time, never for an operator scope.
  • Keys are minted often enough that an expiry, rather than revocation alone, would close real exposure.