Skip to content

Commit 5363bcf

Browse files
craigjbassclaude
andcommitted
docs: correct ES client count to 3 and add ADR-A09 slot-cost decision
opfilter actually opens three ES clients (FAA, jail, tamper-resistance), not two — the README, marketing site, glossary, architecture overview, and opfilter component doc all said two and pre-dated the tamper- resistance client. ADR-A09 records the decision to accept the three-slot cost against the system-wide 48-client ceiling, extending ADR-A06 and ADR-S05 which justified each client in isolation but did not weigh cumulative slot consumption. README also gains a troubleshooting entry for ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 41dbc9f commit 5363bcf

6 files changed

Lines changed: 70 additions & 8 deletions

File tree

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ ClearanceKit occupies a specific part of the macOS endpoint security space. Two
7070
Two components work together:
7171

7272
- **clearancekit.app** — SwiftUI sidebar app. Manages policies, displays live events, renders a real-time pipeline throughput graph, and communicates with the system extension over XPC.
73-
- **uk.craigbass.clearancekit.opfilter** — System extension (Endpoint Security). Runs two Endpoint Security clients: one for path-based policy enforcement that intercepts file-system authorization events (`ES_EVENT_TYPE_AUTH_OPEN`, `AUTH_RENAME`, `AUTH_UNLINK`, `AUTH_LINK`, `AUTH_CREATE`, `AUTH_TRUNCATE`, `AUTH_COPYFILE`, `AUTH_READDIR`, `AUTH_EXCHANGEDATA`, `AUTH_CLONE`), and a second dedicated jail client that tracks jailed processes by audit token and denies file access outside their allowed path prefixes. Both clients evaluate policies and serve the GUI over XPC.
73+
- **uk.craigbass.clearancekit.opfilter** — System extension (Endpoint Security). Runs three Endpoint Security clients: one for path-based policy enforcement that intercepts file-system authorization events (`ES_EVENT_TYPE_AUTH_OPEN`, `AUTH_RENAME`, `AUTH_UNLINK`, `AUTH_LINK`, `AUTH_CREATE`, `AUTH_TRUNCATE`, `AUTH_COPYFILE`, `AUTH_READDIR`, `AUTH_EXCHANGEDATA`, `AUTH_CLONE`), a dedicated jail client that tracks jailed processes by audit token and denies file access outside their allowed path prefixes, and a tamper-resistance client that blocks signal- and suspend-based attacks on opfilter itself (`AUTH_SIGNAL`, `AUTH_PROC_SUSPEND_RESUME`). All three evaluate policies and serve the GUI over XPC.
7474

7575
### Event pipeline
7676

@@ -165,6 +165,16 @@ View extension logs:
165165
log stream --predicate 'subsystem == "uk.craigbass.clearancekit.opfilter"' --level debug
166166
```
167167

168+
### Too many Endpoint Security clients
169+
170+
If `opfilter` logs `ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS` at startup, the system-wide Endpoint Security client limit (48 slots, shared across every ES-based tool on the machine) has been exhausted. ClearanceKit itself takes three of those slots. Enumerate other installed system extensions with:
171+
172+
```
173+
systemextensionsctl list
174+
```
175+
176+
and disable or uninstall any ES-based security tools that are not in active use. The slot pool is reclaimed only when the offending extension is unloaded.
177+
168178
## Managing with MDM
169179

170180
ClearanceKit can receive policy from any MDM solution that delivers Apple Configuration Profile payloads. The managed preferences are read from `/Library/Managed Preferences/uk.craigbass.clearancekit.plist` via `CFPreferences`, so any MDM system that delivers a `com.apple.ManagedClient.preferences` payload for the `uk.craigbass.clearancekit` domain will work.

docs/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -769,8 +769,8 @@ <h1>Stop supply chain attacks<br/>before they read your secrets</h1>
769769
<div class="stat-label">ES event types intercepted</div>
770770
</div>
771771
<div class="stat">
772-
<div class="stat-number">2</div>
773-
<div class="stat-label">ES clients — policy + jail</div>
772+
<div class="stat-number">3</div>
773+
<div class="stat-label">ES clients — policy + jail + tamper</div>
774774
</div>
775775
<div class="stat">
776776
<div class="stat-number">0</div>
@@ -792,7 +792,7 @@ <h1>Stop supply chain attacks<br/>before they read your secrets</h1>
792792
<p class="section-eyebrow">How it works</p>
793793
<h2 class="section-title">Enforce policy at the kernel boundary</h2>
794794
<p class="section-subtitle">
795-
ClearanceKit uses Apple's Endpoint Security framework to place itself between every
795+
ClearanceKit uses Apple's Endpoint Security API to place itself between every
796796
file-system operation and the process that triggered it — before the access is granted.
797797
</p>
798798
<div class="grid-2">
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
id: ADR-A09
3+
domain: architecture
4+
date: 2026-05-23
5+
status: Accepted
6+
extends: ADR-A06, ADR-S05
7+
---
8+
# ADR-A09: Accept Three-Client Cost Against 48-Client System Limit
9+
10+
## Context
11+
12+
macOS enforces a hardcoded, system-wide limit of **48 concurrent Endpoint Security clients**. The limit is per-machine, not per-application: every ES client across every installed security tool, Apple's own internal subscribers, and any debugging tools all draw from the same pool. Once exhausted, `es_new_client` returns `ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS` and no new clients can be created until existing ones are released.
13+
14+
ClearanceKit's `opfilter` system extension currently opens **three** ES clients, instantiated in `opfilter/main.swift`:
15+
16+
| Adapter | ADR | Purpose |
17+
|---------|-----|---------|
18+
| `ESInboundAdapter` | [[ADR-A06-dual-es-client]] | FAA path-based policy enforcement |
19+
| `ESJailAdapter` | [[ADR-A06-dual-es-client]] | Process jail enforcement |
20+
| `ESTamperResistanceAdapter` | [[ADR-S05-tamper-resistance-adapter]] | Block signal/suspend attacks on opfilter |
21+
22+
Each of those ADRs justified the *existence* of its client in isolation. None weighed the cumulative slot-cost against the 48-client system ceiling, because that constraint was not part of the design budget at the time. This ADR records the decision to accept the three-slot cost rather than collapse clients to reclaim slots.
23+
24+
### Cumulative budget on a real machine
25+
26+
Other security and EDR tools each consume one or more ES client slots from the same 48-slot pool; per-tool counts vary by product, version, and configuration, and have not been verified here. Apple's own `endpointsecurityd` retains an opaque handful of internal subscribers. A typical end-user Mac with one or two security tools sits well below the ceiling; a heavily-stacked enterprise endpoint with multiple EDR products plus ClearanceKit can plausibly approach the low double digits but remains far short of 48 in any documented configuration.
27+
28+
The relevant fact for this decision is that three is a meaningful share of a per-tool budget — large enough that adding a fourth client should not be done casually, small enough that the 48-slot ceiling is not at risk under any realistic deployment.
29+
30+
## Options
31+
32+
1. **Refactor to a single multiplexed ES client.** Combine all subscription sets (FAA + Jail + tamper) into one client. Demultiplex events in the handler by event type and audit-token lookup. Reclaims two slots. Costs: re-entangles the three response paths that ADR-A06 and ADR-S05 deliberately separated; reintroduces the scheduling conflict between the two-stage pipeline (ADR-A07), the synchronous jail path, and the inline tamper response; makes runtime toggling of jail (currently a client start/stop) much more invasive; tamper-resistance becomes harder to reason about as a security boundary when it shares a client with general FAA logic.
33+
2. **Partial collapse — fold tamper-resistance into the FAA client.** FAA already handles AUTH events inline; tamper events could ride the same client. Reclaims one slot. Costs: tamper-resistance loses its security isolation (a bug in FAA event handling could now affect self-protection), and the tamper client's narrow subscription set (`AUTH_SIGNAL`, `AUTH_PROC_SUSPEND_RESUME`) ceases to be a separate, auditable surface.
34+
3. **Dynamic client lifecycle.** Open the jail client only while at least one jail rule is active, close it otherwise. Reclaims a slot opportunistically. Costs: adds a new lifecycle state machine, races between rule mutation and client creation, and incident-class bugs where the client fails to open under load.
35+
4. **Keep three clients.** Pay the three-slot cost. Preserves the architectural separation rationales of ADR-A06 and ADR-S05. Costs: three slots consumed on every ClearanceKit install; ClearanceKit will not coexist cleanly with another tool that *also* takes liberties with the slot budget if a future stack pushes the total near 48.
36+
37+
## Decision
38+
39+
**Option 4: keep three clients.** The 48-slot ceiling is not a realistic constraint for any documented user environment, and the architectural guarantees of the constituent ADRs — independent client toggling, deadline-safe synchronous jail responses, no pipeline interference, and an isolated tamper-resistance security boundary — are load-bearing. Spending architectural simplification or weakening a security boundary to save one or two slots in a 48-slot pool is a bad trade today.
40+
41+
Option 1 (full multiplexing) is reconsidered if a future feature requires a fourth ES client. At four, the cumulative complexity of one multiplexed handler becomes worth its cost.
42+
43+
Option 2 (folding tamper into FAA) is explicitly rejected on security-isolation grounds — the separation in ADR-S05 is the value, and merging negates it.
44+
45+
Option 3 is rejected as adding failure modes (open-on-demand under load) that are worse than the resource cost it avoids.
46+
47+
## Consequences
48+
49+
- ClearanceKit continues to consume three ES client slots per install. Documented as a known cost in this ADR.
50+
- A troubleshooting note belongs in the README covering `ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS`: direct users to enumerate competing tools via `systemextensionsctl list`, confirm the 48-slot ceiling has been reached system-wide rather than within ClearanceKit alone, and note that a fresh-installed Mac with only ClearanceKit will draw three of the 48.
51+
- Any future ES-client addition re-opens this ADR. A fourth client triggers re-evaluation of Option 1 (full multiplexing); new features should subscribe to an existing client where possible.
52+
- The architectural rationales in [[ADR-A06-dual-es-client]] and [[ADR-S05-tamper-resistance-adapter]] remain in force. This ADR adds the cumulative resource-cost analysis that neither weighed.

obsidian/clearancekit-obsidian/ClearanceKit/Architecture Overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ClearanceKit is a macOS file-access authorisation tool that places Apple's Endpo
66

77
Two code-signed binaries run in separate processes:
88

9-
- **`opfilter`** — a system extension (bundle ID `uk.craigbass.clearancekit.opfilter`, team ID `37KMK6XFTT`). Holds two Endpoint Security clients, owns the SQLite policy database, and makes allow/deny decisions. No user interface.
9+
- **`opfilter`** — a system extension (bundle ID `uk.craigbass.clearancekit.opfilter`, team ID `37KMK6XFTT`). Holds three Endpoint Security clients (FAA policy, jail, tamper-resistance), owns the SQLite policy database, and makes allow/deny decisions. No user interface.
1010
- **`clearancekit`** — a SwiftUI menu-bar application (`uk.craigbass.clearancekit`). Connects to `opfilter` over XPC to read events and mutate policy. Has no direct Endpoint Security or SQLite access.
1111

1212
The GUI may crash, restart, or be uninstalled without affecting enforcement: `opfilter` continues to apply policy even with no GUI attached.

obsidian/clearancekit-obsidian/ClearanceKit/Components/opfilter.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# opfilter
22

3-
The system extension (`uk.craigbass.clearancekit.opfilter`) that holds the Endpoint Security client and enforces policy. It is the only binary in ClearanceKit with ES and SQLite access; every policy decision the product makes is made here.
3+
The system extension (`uk.craigbass.clearancekit.opfilter`) that holds the Endpoint Security clients (FAA policy, jail, tamper-resistance — see [[ADRs/architecture/ADR-A09-accept-three-es-client-slot-cost]]) and enforces policy. It is the only binary in ClearanceKit with ES and SQLite access; every policy decision the product makes is made here.
44

55
## Role
66

obsidian/clearancekit-obsidian/ClearanceKit/Glossary.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ Domain and infrastructure terms used throughout the ClearanceKit codebase and it
55
| Term | Definition |
66
|------|------------|
77
| FAA | File Access Authorization — macOS Endpoint Security mechanism for intercepting and allowing/denying file operations. |
8-
| ES | Endpoint Security — Apple kernel framework for system security monitoring and enforcement. |
8+
| ES | Endpoint Security — Apple kernel API for system security monitoring and enforcement. |
99
| AUTH event | Endpoint Security event requiring a synchronous allow/deny response before the deadline. |
1010
| NOTIFY event | Endpoint Security event delivered post-facto for logging/monitoring; no response required. |
11-
| opfilter | The ClearanceKit System Extension process that holds the ES client and enforces policy. |
11+
| opfilter | The ClearanceKit System Extension process that holds the ES clients (FAA, jail, tamper-resistance) and enforces policy. |
1212
| clearancekit | The ClearanceKit GUI app (menu bar) that allows users to manage policy rules. |
1313
| XPC | Cross-Process Communication — macOS IPC mechanism used between clearancekit and opfilter. |
1414
| audit token | Kernel-provided unforgeable process identity token; safer than PID for authentication. |

0 commit comments

Comments
 (0)