The machine pairing vault borrows the GitHub vault's active key and cannot survive its rotation #192

Closed AtlantisPleb opened this 3d ago 1 comment

Evidence

Shipped in the release at 8c53d09, promoted 2026-08-26.

1 pushes receipt

What happened

#180's rehearsal 4 asks that rotating each key-like secret leave already-issued records readable. For the machine pairing vault it does not, and the coupling that causes it is not recorded in any document.

OpenAgents.Machines.TokenVault derives its key like this:

defp key do
  with encoded when is_binary(encoded) <-
         Application.get_env(:openagents, :github_token_encryption_key),
       {:ok, key} when byte_size(key) == 32 <- Base.decode64(encoded) do
    {:ok, key}
  else
    _missing -> {:error, :token_vault_not_configured}
  end
end

It reads the GitHub vault's active key. Its envelope carries a version byte and no key id, and it consults no github_token_decryption_keys keyring. OpenAgents.Accounts.TokenVault, by contrast, puts the key id inside the envelope, binds it into the AAD, and keeps up to sixteen prior keys so old envelopes still open.

So the documented GitHub key rotation in docs/github-auth-plan.md — add the old key to the keyring, activate the new one, rewrap with rotate_github_tokens!/0 — rewraps every GitHub token and, in the same step, makes every outstanding machine_pairings.token_ciphertext permanently unopenable. open/1 answers {:error, :token_unsealable} and the keyring cannot help, because key/0 never reads it.

OpenAgents.Voice.RecordingVault has the same shape with its own key: one key, no key id, no keyring, no rewrap path. test/openagents/voice/recordings_test.exs:319 already pins that a wrong key fails closed; what is missing is that nothing says rotating it is unrecoverable.

Blast radius, stated honestly

Bounded rather than absent. @pairing_lifetime_seconds is 600, and both terminal transitions null the ciphertext column — a claimed pairing at lib/openagents/machines.ex:395 and an expired one at :413. So a GitHub key rotation loses at most ten minutes of unclaimed pairings, and a person retries. The long-lived machine token is a bare sha256 digest with no key under it and is unaffected.

That bound is why this is a documentation and keyring defect rather than an incident. It is still a silent loss during a procedure whose written steps say nothing about it.

Acceptance criteria

  • OpenAgents.Machines.TokenVault reads its own configured key rather than the GitHub vault's, or the sharing is deliberate and both docs/github-auth-plan.md and docs/security/secrets-and-log-handling.md state that rotating GITHUB_TOKEN_ENCRYPTION_KEY invalidates outstanding pairings.
  • docs/forge-exit-rehearsals.md rehearsal 4 records which of the three vaults can be rotated without loss. Today the answer is one of three.
  • A proof covers the coupling, so a future change to either vault's key source turns it red.

Found by: #180's rehearsal 4, docs/forge-exit-rehearsals.md.

  1. AtlantisPleb opened this issue 3d ago
  2. AtlantisPleb closed this as completed in 7023ed4 2d ago
  3. A AtlantisPleb Author 2d ago

    Closing as completed. Merged to main and pushed as 7023ed4 (WAL receipt seq 306). The pairing vault now seals with its own key (MACHINE_TOKEN_ENCRYPTION_KEY) and never borrows the GitHub key to write. Migration posture, chosen from the code rather than by habit: no rewrap, because pairing ciphertext has exactly one reader (claim_locked_pairing/1) which nulls the column in the same transaction, and expire_elapsed_pairings/0 enforces a 600-second lifetime — a rewrap-on-read branch is unreachable and an eager sweep would chase a population that turns over in ten minutes. Decrypt falls back to the whole GitHub keyring (active plus github_token_decryption_keys), so a rotation performed mid-transition still reads. Missing dedicated key fails typed from both seal and open. New invariant VAULT-001: rotating one vault's key never unreads another's records; rehearsal 4 now states each vault's rotation cost (GitHub lossless, pairing bounded at 10 minutes, voice permanent). Dev and test configs use keys distinct from the GitHub key so the suite catches re-coupling. 52 targeted tests pass. Owner action recorded in the workspace NEEDS_OWNER.md: provision openagents-staging-machine-vault-active and mount it; until then the vault runs on a recorded bridge to the GitHub key.

Sign in with GitHub to comment on this issue.