CrowdControl.Backend.Docker.HostConfig (crowd_control v0.1.0)

Copy Markdown View Source

The one place Docker container hardening defaults are defined.

Both CrowdControl.Backend.Docker (which runs the CLI directly, over a FIFO and a tee file) and CrowdControl.Provider.Docker (which runs sandboxd and talks HTTP to it) create containers, and both must create them equally hardened. Two copies of these defaults would drift, and the failure mode is silent: a sandbox that quietly lost CapDrop: ALL looks and behaves exactly like one that did not.

It lives in the Backend.Docker.* namespace rather than under Provider because it is Docker Engine API vocabulary, and because that is already where CrowdControl.Backend.Docker.API lives — which every Docker-shaped provider also calls. One namespace for "things that speak to the Engine API".

What is a default and what is opt-in

On by default, because the code running in a sandbox is model-driven and untrusted and none of these breaks an ordinary CLI:

  • CapDrop: ["ALL"] — a CLI needs no Linux capabilities.
  • SecurityOpt: ["no-new-privileges:true"] — it never needs to gain any.
  • PidsLimit: 512Memory and NanoCpus do not bound PIDs, so the fork-bomb ceiling has to be set separately.
  • RestartPolicy: "no" — non-negotiable. A restarted container truncates the capture/tee file and invalidates every persisted byte_offset. Making restart impossible is cheaper and safer than detecting it.

Opt-in, because both genuinely break images that expect otherwise:

  • :readonly_rootfs — breaks any CLI that writes outside the tmpfs mounts (npm caches, ~/.claude, and so on).
  • :user — breaks images that expect root. Applied by the caller, since it is a container-level rather than a host-config field.

Summary

Functions

Build a Docker HostConfig object from caller options.

The tmpfs mounts used when :readonly_rootfs is enabled.

The hardening defaults, as a map, with no network or resource limits.

Functions

build(config, opts)

@spec build(keyword(), keyword()) :: map()

Build a Docker HostConfig object from caller options.

:network_mode is required and has no default here on purpose: the two call sites want genuinely different networks ("none" for the Docker backend, a per-sandbox bridge for the Docker provider, which needs a publishable port), and a default would let one of them inherit the other's posture silently.

Recognised keys in config: :cap_drop, :security_opt, :pids_limit, :cpus, :memory, :readonly_rootfs, :tmpfs.

default_tmpfs()

@spec default_tmpfs() :: map()

The tmpfs mounts used when :readonly_rootfs is enabled.

hardening_defaults()

@spec hardening_defaults() :: map()

The hardening defaults, as a map, with no network or resource limits.

Exists so a test can assert that every call site produces the same hardening without having to know either call site's network posture.