carlos / platform Public

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.db

Same 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=invisible

A 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:

  1. 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
  2. A distinct uid per instance, if the activator can own transient identities the way DynamicUser does 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_KEY is deployment-wide
  • #219 — confirms shape 1's DynamicUser isolation holds in production