doghat.ioFIELD
NOTES
INDEPENDENT NOTES FROM THE MACHINE ROOM
PROJECT / GOACTIVE

Blackdog

A deny-first control plane for privileged Docker tasks: an authenticated API, a socket proxy with an explicit allowlist, and an isolated runner.

Blackdog exposes an authenticated HTTP API for explicitly registered host operations. The API creates and tracks jobs and publishes lifecycle events to MQTT. Callers submit an operation name only: they cannot supply commands, executable paths, scripts, arguments, environment variables, or Compose files.

Boundaries

blackdog
External HTTP authentication, job lifecycle, single-flight coordination, and MQTT. Runs unprivileged: no Docker CLI, no project-tree mount, no Docker socket, and no access to the Docker-proxy network.
blackdog-runner
The isolated privileged execution boundary. Runs as container root with all capabilities dropped except DAC_READ_SEARCH, receives a single read-only project-root bind mount, and executes only fixed scripts registered in its image. Script stdout and stderr are discarded, never returned over IPC.
blackdog-docker-proxy
The only component with the Docker socket. It independently applies Docker API route and request-body policy.

The API and the runner share no network. They communicate over a dedicated Unix-domain socket in a private volume, with a separate runner bearer token as a second check. The runner and the proxy share only one internal network, and the API has its own network for MQTT. The runner has no Docker socket, so it cannot bypass the proxy.

DAC_READ_SEARCH bypasses ordinary read and search permission checks only for paths visible in the runner's mount namespace. It does not expose unmounted host files, and the API never receives the mount or the capability.

Operations

operations.json is the administrator-controlled allowlist, copied into the API and runner images at build time. Callers can select only a name present in the API registry, and the runner independently checks its own registry before executing anything.

The initial docker.update entry invokes a fixed script baked into the runner image. It works through a curated list of Compose projects sequentially with up -d --pull always, and fails fast. No automatic project discovery is used.

To add an operation, change the registry, add its fixed script, then rebuild and redeploy. Neither artifact should ever be writable by untrusted users.

Proxy

The proxy denies unlisted methods and routes, privileged containers, host PID, IPC, and user namespaces, host networking (except for explicitly labeled services), added capabilities, device mappings and requests, unsafe security options, and bind sources outside a configured root.

Bind sources must resolve inside that root. Symlink escapes and mounts to sensitive kernel or device paths, or to the Docker socket, are rejected. Container-create bodies are validated field by field.

Image pulls are not restricted by an image inventory, so the network between runner and proxy has to stay limited to trusted Blackdog components: anything that can reach the proxy can request a pull.

API

RequestPurpose
GET /healthzLiveness check
POST /v1/jobsStart a job with {"operation":"docker.update"}. Bearer token required.
GET /v1/jobs/{id}Read job state. Bearer token required.

Jobs move through accepted, running, and then succeeded or failed. Lifecycle events are published to MQTT at QoS 1, non-retained, on infra/operations by default. Runner output is deliberately left out of both API responses and MQTT events, so anything sensitive in it never crosses the IPC boundary.

Status

This is not yet a complete semantic audit of every Docker API field, and it has not been validated against every real Compose project. Command, entrypoint, and environment semantics, and each project's actual bind, image, network, and device requirements, still need auditing before the proxy can be treated as a complete independent security boundary. The policy is never weakened globally just to make an update pass.

Verification: go test ./... in docker-proxy; build the API and runner from their Dockerfiles; docker compose config; then test the workflow against the real projects before deploying. Docker image builds and runtime or Compose validation were not run in the environment where this was reconstructed, so they are not claimed.