Keep state and keys out of the shared repo

The dev VM is treated as untrusted, and the repo is a read-write virtiofs
mount shared with the backend host -- so a CA private key sitting in it is
a key that machine can sign with, and a leaf signed by this CA is one the
phone's pinned app accepts without question. Pinning against a CA the
attacker holds is no pinning at all.

So certificates are now generated on the machine that serves them, into
$XDG_CONFIG_HOME/ai-app/certs at 0700 with 0600 keys (AI_APP_CERTS
overrides), and config.json and session transcripts move to the XDG config
and data directories. Transcripts move for a plainer reason than the keys:
they are whole conversations, and they were world-readable at 0644.

Two smaller things fall out. The host and VM stop sharing one config,
which had already put a test token on the production backend. And state
stops living where `git clean -xdf` would take the enrollment and every
transcript with it.

State that predates the move is still read from the repo, with a warning
naming where to move it, so an existing install keeps working rather than
silently coming up on an empty config -- the precedence is covered by a
test, since picking the wrong file would otherwise be silent.

Verified: 31 tests, clippy clean; the certificate script writing 0700/0600
into an overridden directory; and the server logging the fallback and
serving from it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
irisandClaude Fable 5 committed 2026-08-25 03:49:34 -04:00
1 parent fff1fb49e8
commit d2d2832ec8
8 files changed
+248 -37

No files matched your search

+34 -3
View File
@@ -252,15 +252,46 @@ GET /usage cached usage windows
GET/PUT /hosts, /models config editing from the phone
```
Sessions live in `config.json` + a per-session directory (transcript.jsonl,
attachments, produced images). Deleting a session is the complete path out of
everything spawning one created.
Sessions live in `config.json` (`$XDG_CONFIG_HOME/ai-app/`) + a per-session
directory under `$XDG_DATA_HOME/ai-app/sessions/` (transcript.jsonl,
attachments, produced images), owner-only. Deleting a session is the
complete path out of everything spawning one created. State that predates
the move is still read from the repo, loudly, so an existing install keeps
working until it is moved by hand.
### Security
- TLS with a self-signed CA, pinned in the app — `gen-dev-cert.sh` and
`PinnedCert.kt` copied from local-updater, same idempotent-CA/reissued-leaf
scheme, same one-way-door caveat about regenerating the CA.
- **The dev VM is untrusted** (decided 2026-08-25): a machine that isn't
malicious but could become so. It matters because the repo is a
read-write virtiofs mount shared between the VM and the backend host, so
under this model everything in it — source, `server/target/` binaries,
and the shell scripts the host runs, some with sudo — is
attacker-writable. Two consequences:
- **Nothing secret lives in the repo.** Certificates are generated on
the machine that serves them and written to
`$XDG_CONFIG_HOME/ai-app/certs` (0700, keys 0600); `config.json` and
session transcripts go to the XDG config and data directories, per
machine. A CA private key the VM could read would let it mint a leaf
the pinned app accepts, which is precisely the attack pinning exists
to stop — pinning against a CA the attacker holds is no pinning at
all. Transcripts move for a plainer reason: they are whole
conversations. As a bonus this ends the host and VM sharing one
config, which had already produced a test token live on the backend,
and takes state out of reach of `git clean -xdf`.
- **The host should not execute what the VM can write** — build and run
the backend from a host-only checkout rather than the shared mount.
Moving the keys closes the smaller door; this is the larger one.
- Development in the VM generates its own throwaway CA. Whatever is
installed on the real phone must pin only the host's.
- The CA key is not needed by the server at all (only `leaf.pem` and
`leaf-key.pem` are read), so it can move offline once the setup is
stable; reissuing a leaf is the only time it is wanted.
- Not addressed, and accepted: a compromised VM can return anything it
likes from the sessions it runs, since running an agent there is the
point. The blast radius is that session's content, not the backend.
- This server is strictly more dangerous than the updater: its API *is*
remote code execution (spawn a bypass-permissions Claude on any SSH host).
Pinning authenticates the server to the phone but not the phone to the