Skip to content

fix: add com.apple.secd mach-lookup for Keychain access on macOS 13+#215

Open
javabrett wants to merge 2 commits into
anthropic-experimental:mainfrom
javabrett:fix/keychain-secd-mach-lookup
Open

fix: add com.apple.secd mach-lookup for Keychain access on macOS 13+#215
javabrett wants to merge 2 commits into
anthropic-experimental:mainfrom
javabrett:fix/keychain-secd-mach-lookup

Conversation

@javabrett

@javabrett javabrett commented Apr 13, 2026

Copy link
Copy Markdown

Problem

The sandbox profile allows `(allow mach-lookup (global-name "com.apple.SecurityServer"))` with the intent of permitting Keychain operations. This works on macOS 12 (Monterey) and earlier. On macOS 13 (Ventura) and later it silently fails for write and delete operations, because Apple moved those operations to a different daemon.

Observed symptom: applications that store short-lived OAuth credentials in the Keychain -- such as Claude Code storing its access token under `"Claude Code-credentials"` -- can read the token but cannot write the refreshed token back after expiry. The Security framework returns a failure code without raising EPERM, surfacing as:

```
Failed to delete keychain entry
```

The token refresh network call succeeds. The credential cannot be persisted. The user is forced to re-authenticate repeatedly.

Root cause (two-part fix)

Part 1: com.apple.secd mach-lookup

In macOS 13 (Ventura), Apple refactored the Keychain subsystem. The Security Entry Daemon (`secd`, registered as `com.apple.secd`) became the primary handler for Keychain credential CRUD operations. `SecurityServer` (`securityd`) was retained for compatibility but no longer handles writes and deletes on modern macOS.

A sandboxed process making a Keychain write or delete call on macOS 13+ sends the Mach IPC message to `com.apple.secd`. If that service name is not in the sandbox allow-list, the IPC is silently dropped.

Part 2: security.mac.sandbox.sentinel sysctl

After applying the secd fix, the macOS `security` CLI (`/usr/bin/security`) still failed inside srt with `UNIX[Operation not permitted]`. Capturing all sandbox denials via:

```bash
log stream --predicate 'eventMessage CONTAINS "Sandbox:"' --style compact
```

showed only one denial during a failing `security add-generic-password` call:

```
deny(1) sysctl-read security.mac.sandbox.sentinel
```

No secd deny, no file-write deny. The `security` CLI reads `security.mac.sandbox.sentinel` as a pre-flight sandbox check. When the read fails with EPERM it aborts before attempting any Keychain operation -- so the secd fix alone was not sufficient.

The sentinel is enforced by seatbelt (not a deeper kernel mechanism): default sandbox profiles simply don't include an allow rule for it. An explicit `(allow sysctl-read (sysctl-name "security.mac.sandbox.sentinel"))` is sufficient to unblock it. srt's other enforcement rules (mach-lookup, file-write, network) remain intact.

Why these grants are safe

secd: same class of permission as `SecurityServer`, which the profile already includes. The Keychain enforces its own per-item ACL for every operation -- the grant changes which daemon routes the call, not what the call may do.

sentinel sysctl: the sentinel is a convention, not a security boundary. It signals to callers that they are in a sandbox, prompting them to self-restrict. srt's actual security enforcement is the seatbelt profile itself; allowing the sentinel read simply prevents tools from prematurely refusing to operate.

Changes

  • `src/sandbox/macos-sandbox-utils.ts`:
    • Add `(allow mach-lookup (global-name "com.apple.secd"))` after the existing `SecurityServer` rule
    • Add `(sysctl-name "security.mac.sandbox.sentinel")` to the sysctl-read allow block
  • `test/sandbox/macos-seatbelt.test.ts`: new `macOS Seatbelt built-in security mach-lookup rules` describe block with three tests -- `SecurityServer` baseline, `secd`, and `security.mac.sandbox.sentinel`

Relation to #179

This PR is a companion to #179 (fix `CLOUDSDK_PROXY_TYPE=https` -> `http`), also submitted from observed failures running tools inside the srt sandbox.

Background
----------
The sandbox profile already allows `(allow mach-lookup (global-name
"com.apple.SecurityServer"))` with the stated intent of permitting
Keychain operations. This works correctly on macOS 12 (Monterey) and
earlier. On macOS 13 (Ventura) and later, Apple refactored the Keychain
subsystem as part of the data-protection improvements introduced in that
release: the Security Entry Daemon (`secd`, Mach service name
`com.apple.secd`) became the primary handler for credential CRUD
operations, with `SecurityServer` (`securityd`) retained as a
compatibility shim for older code paths.

The result is that on macOS 13+, Keychain write and delete operations
made by sandboxed processes - including those using the `apple-tool:`
partition for password-less access - route through `com.apple.secd`, not
`com.apple.SecurityServer`. A sandbox profile that only allows
`SecurityServer` blocks these operations silently (no EPERM visible to
the caller, just a failed return code from the Security framework), which
manifests as errors like:

  "Failed to delete keychain entry"

observed in applications that refresh short-lived OAuth tokens stored in
the Keychain, such as Claude Code. The token refresh network call succeeds
but the refreshed credential cannot be persisted back to the Keychain,
forcing repeated re-authentication.

Why secd is safe to allow
-------------------------
`com.apple.secd` is the same class of grant as `com.apple.SecurityServer`,
which the profile already includes. Allowing Mach IPC to `secd` does not
grant the sandboxed process any capability that was not already intended:

- The Keychain still enforces its own per-item ACL for each operation.
  Processes without an explicit ACL entry rely on `partition_id: apple-tool:`
  which `secd` validates - this is unchanged.
- Applications cannot access Keychain items belonging to other applications
  without an explicit ACL entry for each item, regardless of whether
  `secd` IPC is allowed.
- No credential is readable, writable, or deletable through `secd` that
  was not already accessible through `SecurityServer`. The grant shifts
  which daemon routes the call, not what the call is permitted to do.

On macOS 13+ the existing `SecurityServer` allow is effectively a no-op
for Keychain writes. Omitting `secd` makes the built-in security grant
misleading: it appears to allow Keychain access but does not function on
the OS versions where most users are now running.

References
----------
- Apple Platform Security guide, "Keychain data protection" section
- macOS 13 Ventura release notes on Security framework changes
- WWDC 2022 session on data protection and Keychain
- Observed failure: Claude Code OAuth token refresh writing to
  "Claude Code-credentials" keychain entry fails on macOS 13+ inside
  the srt sandbox; succeeds after adding this rule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
javabrett added a commit to javabrett/sandbox-runtime that referenced this pull request Apr 13, 2026
Identifies the local build containing two fixes pending upstream:
- fix/cloudsdk-proxy-type-invalid (PR anthropic-experimental#179)
- fix/keychain-secd-mach-lookup (PR anthropic-experimental#215)
Companion fix to the com.apple.secd mach-lookup change.

The `security` CLI (and similar tools) read security.mac.sandbox.sentinel
as a pre-flight sandbox check. When the read fails with EPERM they abort
before attempting any Keychain operation - so the secd mach-lookup fix
alone is not sufficient for CLI-based Keychain access.

Diagnosis: with the secd fix applied, running security add-generic-password
inside srt still failed. Capturing all sandbox denials via:

  log stream --predicate 'eventMessage CONTAINS "Sandbox:"' --style compact

showed only one denial during the failing call:

  deny(1) sysctl-read security.mac.sandbox.sentinel

No secd deny, no file-write deny. The sentinel check was the sole remaining
blocker.

The sentinel is enforced by seatbelt (not a deeper kernel mechanism):
default sandbox profiles simply don't include an allow rule for it, making
it readable only from unsandboxed processes by convention. An explicit
(allow sysctl-read ...) in srt's profile is sufficient to unblock it.
srt's other enforcement rules (mach-lookup, file-write, network) remain
intact.

Adds a test asserting the sentinel sysctl is in the generated profile,
with a comment documenting the diagnostic process.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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