Note·

The Docker socket is root

Writing Cargo's threat model: admitting the control plane is host-level, and defending the boundary below it instead.


Cargo is a self-hosted PaaS. Its control plane mounts the Docker socket read-write and runs as root. Writing the threat model started with saying what that means out loud, because everything else follows from it.

Write access to the socket is root

A PaaS deploys containers, so it needs the Docker daemon. And there's no subset of the Docker API that lets you deploy containers but not escape them. Mount the host filesystem, run privileged, share the host PID namespace: it's all the same API. Whoever can write to the socket owns the machine.

So remote code execution in the control plane isn't container compromise. It's host compromise. Hardening the control plane's own container doesn't change that, and pretending it does gives you a threat model that's wrong in the most important place.

ROOT ON THE HOSTvalidatedrwtenant inputcompose · labels · urlscontrolplaneruns as rootdocker.sockdaemonevery containerand the hostthe line Cargo defends
Everything to the right of the dotted line is effectively root. The boundary worth defending is where tenant-controlled input crosses into it.

Defend the boundary below it

If the control plane is root, the question becomes: what can a tenant make it do? Tenants control compose files, build paths, container labels, repository URLs, and resource caps. Each of those is validated, not passed through:

  • Compose files and labels are validated before they reach the daemon. A tenant's own config is exactly where a privileged flag or a host mount would try to get in.
  • Repository URLs reach git clone on the control plane's network. Only https://, ssh:// and git@host:path are accepted, and hosts resolving to loopback, private, or link-local addresses are refused. Otherwise “clone my repo” becomes a way to reach the cloud metadata endpoint.
  • Resource caps have instance-wide ceilings, so a tenant can raise their own limits but not past what the operator allows.

Deployed apps run with no-new-privileges on a proxy network. The platform database is on a separate internal network they can't reach.

What it means for the operator

A threat model that only lives in the code doesn't help the person running it, so these consequences are written into the README:

  • An account is a host-level grant. That's why sign-up is invite-only by default. Give admin to people you'd give SSH to.
  • Only Traefik publishes ports. Don't expose the control plane directly.
  • Keep the master key apart from the database. Someone with both has every stored secret. Someone with neither has none.
  • Rotate after any suspected exposure. cargod rotate-key re-seals every secret under a new key in one transaction.
  • A worker host is the same grant again. Cargo drives it through Docker over SSH, so its stored key is root on that machine too.

Writing down a risk instead of fixing it

The control-plane process is root inside its own container. The usual advice is to drop to a non-root user. Here that buys nothing: with a read-write socket mounted, an attacker undoes it with one docker run. It would also need a data-directory ownership migration on every existing install.

So it's listed as a known, accepted risk, with the reasoning next to it. A fix that looks like security but changes nothing is worse than an honest note, because it tells the next reader the problem is handled when it isn't.