vault, sftp: a key reads sealed files only when allowed to

Since e4e3e2b every registered SSH key could read sealed files over the
mount while the vault was open in the browser. A key is a standing
credential on another machine, so it now needs vault access, given and
taken per key with :sshkey vault <name> on|off (PATCH /api/ssh-keys/{id},
migration 0016). Existing keys start without it. The access is asked on
every read of a sealed file, so taking it away cuts off a read under way,
as locking does. That includes a write handle opened over a sealed file:
its .part holds the decrypted bytes and could be read back through it.

A mount no longer holds the vault open: it takes the key through the new
Vault::peek_key, which does not count as use. Opening sealed files over
and over from an indexer kept the idle lock from ever firing.

The rest of the audit's findings on this side:
- An unfinished upload's .part is removed when it is dropped, so a sealed
  file's plaintext does not sit in uploads/ after a client goes away.
- A deleted key can no longer write, setstat or close-and-save through a
  session that was already open.
- A session holds at most 256 handles and 64 MiB in memory, the server at
  most 32 connections; keepalives drop a peer that stopped answering.
- A sealed download in the browser ends when the vault locks, the way a
  read over the mount already did.
- Certificate inspection refuses a key or keystore that asks for more
  than ten million rounds, or scrypt beyond 2^20 and r = 8, before running
  the derivation: the file chooses those numbers, and the work could not
  be stopped once begun.
This commit is contained in:
nvrl committed 2026-10-20 11:43:39 +02:00
1 parent f450883ac0
commit a1bc0a9995
11 files changed
+578 -67

No files matched your search

+5
View File
@@ -64,6 +64,8 @@ export interface SshKey {
fingerprint: string
created_at: number
last_used_at: number | null
/** May read sealed files over the mount while the vault is open. */
vault: boolean
}
export interface SharedDocument {
@@ -464,4 +466,7 @@ export const api = {
request<SshKey>('/ssh-keys', { method: 'POST', body: body({ name, public_key }) }),
removeSshKey: (id: number) => request<{ ok: true }>(`/ssh-keys/${id}`, { method: 'DELETE' }),
setSshKeyVault: (id: number, vault: boolean) =>
request<{ ok: true }>(`/ssh-keys/${id}`, { method: 'PATCH', body: body({ vault }) }),
}
+18 -4
View File
@@ -784,16 +784,16 @@ export const commands: CommandSpec[] = [
},
{
name: 'sshkey',
args: '[add <name> <key> | rm <name>]',
args: '[add <name> <key> | rm <name> | vault <name> on|off]',
group: 'view',
help: 'list, add or remove keys allowed to mount the workspace over sftp',
help: 'list, add or remove keys allowed to mount the workspace over sftp, and say which may read sealed files',
run: (arg) =>
workspace.guard(async () => {
const trimmed = arg.trim()
if (trimmed === '' || trimmed === 'ls' || trimmed === 'list') {
const keys = await api.listSshKeys()
if (keys.length === 0) return workspace.say('no keys registered — :sshkey add <name> <key>')
return workspace.say(keys.map((k) => `${k.name} ${k.fingerprint}`).join(' · '))
return workspace.say(keys.map((k) => `${k.name}${k.vault ? ' [vault]' : ''} ${k.fingerprint}`).join(' · '))
}
const addMatch = /^add\s+(\S+)\s+(.+)$/.exec(trimmed)
if (addMatch) {
@@ -809,7 +809,21 @@ export const commands: CommandSpec[] = [
await api.removeSshKey(target.id)
return workspace.say(`removed ${target.name}`)
}
workspace.say('usage: :sshkey [add <name> <key> | rm <name>]', 'error')
// A key reads sealed files over the mount only once given vault access.
const vaultMatch = /^vault\s+(\S+)\s+(on|off)$/.exec(trimmed)
if (vaultMatch) {
const keys = await api.listSshKeys()
const target = keys.find((k) => k.name === vaultMatch[1])
if (!target) return workspace.say(`no key named ${vaultMatch[1]}`, 'error')
const on = vaultMatch[2] === 'on'
await api.setSshKeyVault(target.id, on)
return workspace.say(
on
? `${target.name} reads sealed files over the mount while the vault is open`
: `${target.name} no longer reads sealed files`,
)
}
workspace.say('usage: :sshkey [add <name> <key> | rm <name> | vault <name> on|off]', 'error')
}),
},
{