carlos / platform Public

Security: root operates on the service user's /data registry files (local escalation, on main today) open

Found by the Opus review of claim Plan 9 (P-1); the gap predates the claim work. Root (adopt, system apply, bootstrap, and with claims on also instance-control) works on /data/registry.db*. /data belongs to the service user (carlos), so anything running as carlos (the agent, and any tenant that runs as carlos) can plant links there:

  • chownRegistryFiles: os.Stat and os.Chown follow symlinks. A planted registry.db-wal symlink hands any root file to carlos.
  • registry.Open runs as root before any check. SQLite resolves a main-database symlink (no SQLITEOPENNOFOLLOW), so a symlinked registry.db gives root a write-capable open of the target. SQLite also fchowns hard-linked -wal and -shm sidecars itself.
  • Bootstrap creates the registry with stat-then-open, without OEXCL or ONOFOLLOW, and runs chown -R carlos:carlos /data, which follows hard links.

Fix plan (branch fix/registry-chown-nofollow; a partial no-follow chown is already there):

  1. Root never opens the registry with root privileges. Registry open, migration and creation run in a privilege-dropped child under the service user's credentials.
  2. Any remaining root ownership repair uses an O_NOFOLLOW open, a plain single-link regular file check, and fchown through the descriptor.
  3. Bootstrap creates files OEXCL|ONOFOLLOW, and replaces chown -R with a walker that refuses symlinks and multi-link files. Hard-link exploitation of root-owned victims also needs fs.protected_hardlinks to be off; symlink exploitation does not.

First fix merged as 29dab2d8. It covers the registry routes: the service-user child for registry work, a no-follow checked chown, and the bootstrap O_EXCL create plus link-refusing walker. Opus: safe to merge, nothing breaks with the flag off. Still open, now on fix/267-adopt-writes: root's adopt pass writes other files by name under /data (Critical: release/programhistory.go RecordProgram lets carlos make root overwrite any file; also chmods in bake.go and restart.go, and lock files in adoptlock.go), plus four Important items. Pre-roll check for every box: /data, /opt/carlos and /run/carlos must be real directories, and the registry files regular with one link; otherwise adopt and bootstrap now refuse.

The second fix merged as 2d3ff963. Root's adopt, apply and bootstrap file effects are now anchored and refuse links; the registry child is pinned to its own executable, keeps a minimal fd table and takes per-call cancellation; instancectl goes through the child. Opus: safe to merge. Still open (round 4, fix/267-credentials): on off-cloud boxes /etc/carlos/credentials is carlos-owned, and root's AWS SDK runs credential_process from it. The fix makes it root:carlos 0640 and the registry child non-dumpable. Also: stale .carlos-write-* staging cleanup. Opus showed the FIFO concern on floor.min-release does not hold: the shell's -s test is false for a FIFO.

Round 3 merged to main as 75618480 (fix/267-credentials, kept). Earlier fixes: 29dab2d8, 2d3ff963.

What it closes:

  • Root AWS loads never read service-writable config. Every first-party SDK load goes through internal/rootaws. At start, main and the console scrub, for euid 0: the AWS file and profile selectors, the endpoint overrides (including AWSENDPOINTURL*), the IMDS endpoint, the CA bundle, the web-identity and container-credential variables, and SSLCERTFILE/SSLCERTDIR. Shared files always point at an impossible path, even under root's own HOME. This covers third-party loads too (Litestream's s3 replica client). An inherited proxy stays, but NOPROXY always covers IMDS.
  • Off-cloud credentials. /etc/carlos/credentials is parsed strictly: [default] static keys only, credential_process refused, SDK quote and comment semantics. Bootstrap and adopt repair it into a fresh root:carlos 0640 inode, a file that is already correct is left alone, and repair is non-fatal. Complete environment keys win over the file. An unsafe file refuses on use, not at config load. Root Litestream gets the off-cloud keys explicitly; a file session token is refused.
  • Registry child. It execs as root, then setresgid/setresuid, then PRSETDUMPABLE 0, all before any input.

Seven Astra rounds. Two items remain open:

  • P3, moves' floor gate. The update and recover units check the floor file, then reopen it with cmp; the service user can swap in a symlink or FIFO between the two. Platform-d8 is fixing it on fix-moves-floor-gate.
  • P3, staging sweeper. It can remove a substituted empty directory. Accepted.

Still owed:

  • Copy review of the infra/remote-host.md update (root:carlos 0640, fresh-inode repair, profile variables ignored) and the one session-token error string. That string currently returns errors.ErrUnsupported.
  • Not released or rolled.

The floor-gate P3 is closed: platform-d8 merged 00fdf541. The update and recover units now call carlos system floor-allows-prev. It opens each file once with ONOFOLLOW|ONONBLOCK, walks the path without following links, and requires a regular file with Nlink==1 that is at most 256 bytes. The floor's owner must be root or carlos; the verdict's must be root. It fails closed. Still open on #267: the copy review of the remote-host.md text and the session-token error string.

The remote-host.md runbook update and the off-cloud session-token error are on main as 843b7180; Paul approved the copy unchanged. Nothing remains open on #267 beyond release and roll.