the notify sidecar stops trusting whoever reaches it

to send one message to four accounts an hour ago i found the sidecar's
ephemeral port and POSTed to it directly, once per user, with no
authentication of any kind. that worked, which was the problem: the same
door was open to anything else on the machine, including a commander with
a shell.

two changes.

POST /api/notify/broadcast, owner only via `ownerOnly` on the permission —
the supported way to say one thing to everyone. teams will narrow it later;
today the choice is everyone or one person.

and the sidecar now requires a secret on everything under /_officer/. it
is generated per boot, held in memory, announced to the platform over the
registration socket beside the port, and never written to disk.

X-Officer-User could not carry this on its own, and my first attempt got
that wrong: it says WHO is asking, and a caller on loopback can write it
itself. only the secret says the PLATFORM is asking. so the check covers
the whole /_officer/ surface rather than just the send route — /_officer/
devices takes the same header to decide whose phones are listed and
deleted.

what it bounds, honestly: a MEMBER's commander runs as its own Linux user
and cannot read this process, so for every account but the owner's this is
kernel-enforced. the owner's own commander runs as the owner, same as the
platform, and is not bounded by this and cannot be — the same reason
`execution` permissions are never grantable. the owner's decision, and
this is a real improvement rather than a boundary.

`bun run notify:test` posts to that port and will now be refused. it is a
developer CLI on a box where the platform is running; left to fail loudly
rather than given a way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-24 16:29:48 +00:00
co-authored by Claude Opus 5
parent de633f8697
commit cb2dcfa4cc
7 changed files with 151 additions and 10 deletions
+6 -3
View File
@@ -16,7 +16,7 @@ import {
revokeSender,
upsertSendoffDevice,
} from 'officerdb';
import { getNotifyServerUrl } from '../notify/router';
import { getNotifyServerUrl, notifyProducerHeaders } from '../notify/router';
import { displayNameFor, folderNameFor, safeFilename, safeSegment, sendoffDirFor } from './paths';
/*
@@ -78,10 +78,13 @@ function freePath(dir: string, filename: string): string {
async function pushLanded(recipientUserId: number, folder: string, count: number): Promise<void> {
try {
const base = getNotifyServerUrl();
if (!base) return;
const producer = notifyProducerHeaders();
// No secret means the sidecar has not registered with THIS process yet. Giving up is right: the
// alternative is sending unauthenticated, which the sidecar now refuses anyway.
if (!base || !producer) return;
await fetch(`${base}/_officer/notify`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'X-Officer-User': String(recipientUserId) },
headers: { 'content-type': 'application/json', 'X-Officer-User': String(recipientUserId), ...producer },
body: JSON.stringify({ code: 'officer/core:sendoff.landed', userId: recipientUserId, id: folder, n: count }),
signal: AbortSignal.timeout(10_000),
});