Skip to content

fix(kernel-cli,kernel-node-runtime): restrict the daemon control socket's permissions and RPC surface - #1034

Open
sirtimid wants to merge 4 commits into
mainfrom
sirtimid/daemon-socket-security-988
Open

fix(kernel-cli,kernel-node-runtime): restrict the daemon control socket's permissions and RPC surface#1034
sirtimid wants to merge 4 commits into
mainfrom
sirtimid/daemon-socket-security-988

Conversation

@sirtimid

@sirtimid sirtimid commented Aug 26, 2026

Copy link
Copy Markdown
Member

Fixes #988.

The daemon control socket had no authentication and no explicit permissions, so authorization was whatever the ambient umask happened to leave behind. It also served executeDBQuery, which runs caller-supplied SQL against kernel state.

Permissions are explicit

$OCAP_HOME is created 0700 and daemon.sock 0600. The directory mode is reapplied on every start, so an $OCAP_HOME created before this change is brought forward rather than keeping its old permissions.

The socket bind runs under a 0o177 umask so it is not briefly reachable by other local users between bind and chmod. A chmod that fails closes the server rather than leaving it listening on a socket whose mode is unknown.

Dev-only methods

executeDBQuery, clearState, and terminateAllVats are served only when the daemon is started with OCAP_DEV_MODE=true. Their handlers are not registered at all in default mode, so the executeDBQuery hook is never constructed. The refusal message names the flag rather than reporting a bare "Method not found" for a method the operator can see in the source.

OCAP_DEV_MODE is matched exactly: 1, TRUE, and yes all leave dev mode off, with a warning.

Trust model

Documented in packages/kernel-cli/README.md. Anyone who can open the socket controls the kernel — launchSubcluster runs caller-supplied vat code in a worker thread of the daemon process, and queueMessage invokes any method on any object the kernel holds. So the gating narrows the surface but is not a security boundary, and is not a substitute for keeping the socket private.

Two points on scope:

  • reset is not a separate RPC method. kernel.reset() is reachable only through clearState, which is gated.
  • revoke, terminateVat, and terminateSubcluster stay reachable. The criterion is blast radius, not destructiveness — per-vat operations are part of normal operation. Gating terminateAllVats is close to cosmetic, since getStatus plus a loop over terminateSubcluster reproduces it; it is gated for symmetry with clearState.

Testing

rpc-socket-server.test.ts is new — the file had no unit coverage. It covers the socket mode, the fail-closed chmod path, gating in both directions driven by the exported DEV_ONLY_METHODS, and that a refused executeDBQuery never reaches the database. dev-mode.test.ts pins the OCAP_DEV_MODE value table.

E2E coverage in both tiers, including the upgrade path: the CLI helper loosens $OCAP_HOME to 0755 after mkdtemp so the daemon's chmod has something real to tighten.

@metamask/kernel-cli is added to the CI e2e matrix. It was in no workflow, so the $OCAP_HOME assertions and the only end-to-end exercise of OCAP_DEV_MODE never ran on a PR.

Follow-ups, not addressed here

  • isSocketLive treats any connect error as "stale". With the socket now 0600, a probe against another user's live daemon returns EACCES and reads as absent. Only reachable when $OCAP_SOCKET_PATH points at a shared non-sticky directory, but it should decide on the errno.
  • The process.umask narrowing in bind() is best-effort and unobservable when skipped (it throws on a worker thread). A logger would make the claim falsifiable.

🤖 Generated with Claude Code


Note

Medium Risk
Changes daemon authorization (permissions) and breaking RPC defaults for methods that touch kernel state; socket access still fully controls the kernel per the documented trust model.

Overview
Hardens the daemon control socket by setting explicit filesystem permissions and narrowing the default RPC surface, with docs and CI to match.

Permissions: On start, $OCAP_HOME is ensured 0700 (including upgrading older dirs) and daemon.sock is 0600. The RPC server binds under a restrictive umask and refuses to listen if post-bind chmod fails, instead of leaving a world- or group-writable socket.

Dev-only RPC (breaking): executeDBQuery, clearState, and terminateAllVats are not registered unless the daemon runs with OCAP_DEV_MODE=true (exact string only; other values warn and stay off). Default callers get a method-not-found message that names the flag. The CLI wires env resolution via new resolveDevMode and passes devMode into startDaemon / startRpcSocketServer.

Docs & CI: README adds a trust model for the unauthenticated socket and drops resolved “limitations.” @metamask/kernel-cli joins the e2e workflow matrix. Unit and e2e tests cover permissions, gating, and the chmod fail-closed path.

Reviewed by Cursor Bugbot for commit 3e0329a. Bugbot is set up for automated code reviews on this repo. Configure here.

sirtimid and others added 3 commits August 26, 2026 15:33
The control socket had no authentication and no explicit permissions, so
authorization was whatever the ambient umask happened to leave behind. It
also served `executeDBQuery`, which runs caller-supplied SQL against kernel
state.

- Create `$OCAP_HOME` 0700 and `daemon.sock` 0600 explicitly. The directory
  mode is reapplied on every start so an `$OCAP_HOME` from before this
  change is brought forward rather than keeping its old permissions.
- Withhold `executeDBQuery`, `clearState`, and `terminateAllVats` from the
  handler set unless the daemon is started with `OCAP_DEV_MODE=true`.
- Document the trust model: anyone who can open the socket controls the
  kernel, so this narrows the surface without being a security boundary.

Fixes #988

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…parsing

The $OCAP_HOME mode assertions and the only end-to-end exercise of
OCAP_DEV_MODE lived in a suite that no workflow ran, so deleting the
chmod in daemon-entry would have left CI green.

`resolveDevMode` moves to its own module taking `env` and `warn` as
parameters. daemon-entry calls `main()` at module load and so cannot be
imported by a test; the exact-match parsing is the part worth pinning,
since relaxing it to something like `!== 'false'` would serve arbitrary
SQL on a daemon whose operator believed the flag was unset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sirtimid
sirtimid marked this pull request as ready for review August 26, 2026 13:49
@sirtimid
sirtimid requested a review from a team as a code owner August 26, 2026 13:49
@sirtimid sirtimid changed the title fix(kernel-cli,kernel-node-runtime): harden the daemon control socket fix(kernel-cli,kernel-node-runtime): restrict the daemon control socket's permissions and RPC surface Aug 26, 2026
@sirtimid
sirtimid marked this pull request as draft August 26, 2026 13:51
@sirtimid
sirtimid marked this pull request as ready for review August 26, 2026 13:51

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 57d5620. Configure here.

Comment thread packages/kernel-cli/test/e2e/helpers.ts Outdated
@cursor
cursor Bot requested review from FUDCo and rekmarks August 26, 2026 13:57
An ambient OCAP_DEV_MODE=true leaked through the spawn env and unlocked
the dev-only methods, making the refusal assertions vacuous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 73.04%
⬆️ +0.50%
9789 / 13401
🔵 Statements 72.89%
⬆️ +0.49%
9953 / 13653
🔵 Functions 73.6%
⬆️ +0.61%
2301 / 3126
🔵 Branches 67.25%
⬆️ +0.36%
4022 / 5980
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/kernel-cli/src/app.ts 0%
🟰 ±0%
0%
🟰 ±0%
0%
🟰 ±0%
0%
🟰 ±0%
36-465
packages/kernel-cli/src/dev-mode.ts 100% 100% 100% 100%
packages/kernel-cli/src/commands/daemon-entry.ts 0%
🟰 ±0%
0%
🟰 ±0%
0%
🟰 ±0%
0%
🟰 ±0%
29-303
packages/kernel-node-runtime/src/daemon/index.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
packages/kernel-node-runtime/src/daemon/rpc-socket-server.ts 83.96%
⬆️ +83.96%
62.79%
⬆️ +62.79%
80%
⬆️ +80.00%
85.43%
⬆️ +85.43%
147, 158-161, 170-172, 189, 231-240, 283-287, 348, 355, 357, 374-377, 388, 449
packages/kernel-node-runtime/src/daemon/start-daemon.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
Generated in workflow #4685 for commit 3e0329a by the Vitest Coverage Report Action

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.

Daemon control socket is unauthenticated and exposes arbitrary SQL

1 participant