activator-spawned instances share a uid and a flat data dir: no per-instance isolation (shape 1 has it, shape 2 does not) Issue open
Migrated from github.com/carlosframework/platform issue #232, opened 2026-08-19. GitHub issues are closed for this repo; see AGENTS.md, "Work is tracked on amadan, not GitHub".
Still outstanding: Activator-spawned instances share a uid and a flat data dir, with no per-instance filesystem isolation.
---
The gap
Activator-spawned instances (backing shape 2 in #18: exec, socket-upstream) get no per-instance isolation. They are forked by carlos-edge.service, which sets neither TemporaryFileSystem= nor BindPaths=, and they run as the static carlos user. So every such instance sees the whole flat data directory — each peer's database, and the deployment's own registry.
Measured on carlos-tito-ie today:
# carlos-edge.service — the process that forks them
User=carlos
TemporaryFileSystem= (empty)
BindPaths= (empty)
RestrictNamespaces=yes
ProtectProc=invisible
# what an instance forked from it can read
drwx--x--x carlos carlos /data
-rw-r--r-- carlos carlos /data/registry.db <- identity store
-rw-r--r-- carlos carlos /data/bill.go.tito.io.db
-rw-r--r-- carlos carlos /data/like-hotcakes.go.tito.io.db
-rw-r--r-- carlos carlos /data/tito.go.tito.io.dbSame uid, same namespace, one directory. A file-read bug in any one instance reads every other tenant's database and the registry — which on this deployment holds sessions, TOTP secrets, CLI tokens and recovery codes.
Why this is shape-specific, not a deployment mistake
The platform already has the primitive. Unit-backed tenants (carlos-app@, shape 1) get DynamicUser with state under /var/lib/private at 0700 — #219 documents that working well enough that even carlos-restore-verify (running as carlos) cannot read into it. On this same box, cribado-csp and cribado-dmarc have backing=unit and get exactly that.
Every titogo route has backing = NULL. None of them can reach it. #18 named the reason and parked the fix:
> Activator takeover: the long-standing "titogo family needs a static user / DynamicUser is per-unit" caveat resolved by carlos spawning the instances. Much bigger; probably right eventually; not for this.
The activator takeover half has since happened for three titogo routes — they are activator-spawned today — but the isolation half did not come with it. So those three tenants moved from a hardened per-account mount namespace onto flat /data.
Why it is blocking work
titogo isolates its own systemd-managed instances per account, after a real cross-tenant bug (a template include that reached peer databases and the identity registry). titogo@.service:
User=titogo
TemporaryFileSystem=/data:ro
BindPaths=/data/titogo/instances/%i
RestrictNamespaces=yes
ProtectProc=invisibleA peer's database is not permission-denied there; it does not exist in the namespace.
We want to move the remaining titogo instances onto the platform and retire that unit template. As things stand that migration is a net isolation regression for every account it touches, so it is blocked on this issue rather than on anything in titogo.
What would close it
Per-instance isolation for exec/activator-spawned instances, equivalent to what shape 1 gets. Either:
- A private state directory per instance, with the spawned process confined to it (
TemporaryFileSystem=+BindPaths=, applied per instance rather than to the edge as a whole) — matches what titogo proves works today; or - A distinct uid per instance, if the activator can own transient identities the way
DynamicUserdoes per unit. #139 (spawned instances get no supplementary groups) suggests the credential path here needs attention regardless.
Either way the property wanted is the one shape 1 already has: a peer's database is not reachable, not merely unreadable.
Related
- #18 — the three backing shapes; parked this as "Activator takeover"
- #139 — activator: spawned instances get no supplementary groups
- #179 —
CARLOS_SECRETS_KEYis deployment-wide - #219 — confirms shape 1's DynamicUser isolation holds in production