Skip to content

Seal the audit log to the team, and show the vault on Account - #116

Merged
Alexgodoroja merged 1 commit into
mainfrom
vault-account-audit
Sep 11, 2026
Merged

Alexgodoroja merged 1 commit into
mainfrom
vault-account-audit

Conversation

@Alexgodoroja

Copy link
Copy Markdown
Collaborator

Why

Typed input (commands, agent prompts) was recorded for the team in plain text: the accounts service could read the whole audit log, and so could anyone with a copy of the database. Recording it is a deliberate choice and stays; who can read it changes.

The team audit key

  • One key pair per organization. The service keeps the public half and ciphertext; every member holds the private half.
  • A member's copy is sealed to their vault with the sender's own vault key (static ECDH), so a browser can tell a copy came from a teammate. Anyone can seal to a public key, so without this the service could invent a team key, seal it to everyone, and read the log.
  • Each browser re-seals its copy to itself, so the copy no longer depends on the teammate who sent it staying in the team, and pins the team's key: a later "your team has no key", or a different key, is refused with an explanation instead of silently re-keying the team.
  • Entries are bound to org, session, author, kind, time and key version.

What changed around it

  • POST /api/audit refuses unencrypted input; GET /api/audit no longer searches text it cannot read.
  • Search, CSV export and the charts run in the browser.
  • History recorded before this is sealed in place by an owner's or admin's browser. Because that browser is handed the author and time by the service, the entry records who sealed it, and the log shows "sealed later by X" and marks entries still in the clear.
  • Removing a member deletes their copy of the key.
  • Migrations 009_team_keys.sql and 010_audit_sealed_by.sql, both additive.

Vault on the Account page

What the vault is, that only its owner can see it (the service returns only that person's own sealed copies and cannot open them), the key fingerprint, when it was made, whether this browser keeps it unlocked, what it holds — session passwords, revealed on request and hidden after 30 seconds, and the team audit key — and Lock in this browser.

Copy that was wrong

The sign-up page said terminal input was not copied to the service. It was. That page, the terms, llms.txt and SECURITY.md now say what is recorded, who can read it, and what the service still sees (who typed in which session, and when). The shell login consent copy is not touched here: #113 owns it, and I will add the encryption wording on top once it merges.

Security review

Two findings from review of this branch, both fixed here:

  • High: team key creation sealed to roster keys without checking them against the keys this browser had pinned, and was entered whenever the service said there was no key. A service could have had a browser generate a key and seal it straight to the service. Now: pinned keys only, each pinned as it is used, and the refusals above.
  • Medium: sealing old history could mint authentic-looking entries attributed to others. Now recorded and shown as "sealed later by".

Verification

  • Typecheck clean; 777 app tests; 400 server tests; 139 store tests against real Postgres 16; protocol check clean.
  • Driven in a browser: vault setup, the Account panel showing the team key held with its fingerprint, and the audit page showing the encrypted-for-your-team note and marking pre-encryption entries.

Merge order

Based on main before #113 and #115. #115 adds 008_deleted_accounts.sql, so on deploy apply 008 → 009 → 010, then the Worker. I will rebase once those merge.

What people typed into a session reached the service in plain text, and the
whole audit log sat there readable. It is now sealed in the browser to one
audit key per organization: every member holds its private half, the service
holds the public half and ciphertext, and it can read neither what was typed
nor the key.

- The key's private half reaches each member sealed to their vault by a
  teammate, using that teammate's own vault key rather than a throwaway one,
  so a browser can tell a copy came from a person and not from the service.
  Each browser re-seals its copy to itself, remembers the team's key, and
  refuses a different one, or a claim that the team has none.
- Entries are bound to their organization, session, author, kind and time, so
  the service cannot move one person's command onto another session or person.
- Search, CSV export and the charts run in the browser, over entries opened
  there. The service no longer searches what it cannot read.
- History from before this is sealed in place by an owner's or admin's
  browser. That browser is handed the author and time by the service, so the
  entry records who sealed it and the log says "sealed later by", and marks
  entries still in the clear.
- Session lifecycle entries the service writes itself stay as they were: they
  hold session names and addresses it already stores.

Account gains a vault section: what the vault is, that only its owner can see
it, the key's fingerprint, what it holds, each session password shown on
request and hidden again, and a way to lock the vault in this browser.

The sign-up page said typed input was not recorded, which was untrue. It, the
screen that links a terminal, the terms, llms.txt and the security policy now
describe what is recorded, who can read it, and what the service still sees.
@Alexgodoroja
Alexgodoroja merged commit c9f5f64 into main Sep 11, 2026
15 checks passed
@Alexgodoroja
Alexgodoroja deleted the vault-account-audit branch September 11, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant