Skip to content

feat(compose): dual-mode network/socket containment — base neutral + overlays + gate (PR 1/3)#151

Merged
jraicr merged 7 commits into
devfrom
feat/dual-mode-compose-core
Jun 4, 2026
Merged

feat(compose): dual-mode network/socket containment — base neutral + overlays + gate (PR 1/3)#151
jraicr merged 7 commits into
devfrom
feat/dual-mode-compose-core

Conversation

@jraicr

@jraicr jraicr commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Splits drydock's network/socket posture into two distinct compose modes:

  • docker-compose.yml — now network-neutral: no host networking, no Docker socket, no hardcoded network config. This is the starting point; it carries no mode opinion.
  • docker-compose.dood.yml — DooD overlay: host networking + Docker socket bind-mount. Restores the current behaviour for users who need Docker-out-of-Docker. Opt-in.
  • docker-compose.contain.yml — contained overlay: fixed-name drydock_net bridge network (no internal: true; egress to the internet stays open), no Docker socket. This becomes the factory default — the mode that runs when no explicit preference is set.

A resolve_run_mode() helper + updated compose_files() gate select exactly one overlay via a 4-layer precedence stack (highest → lowest):

  1. Env vars DRYDOCK_DOOD=1 / DRYDOCK_CONTAIN=1
  2. Per-project sentinels ~/.config/drydock/{dood,contain}/<project>
  3. Global sentinel ~/.config/drydock/default-dood
  4. Factory default: contained

The logic is fail-closed: conflicting signals (both env vars set, both per-project sentinels present) abort with a clear error rather than silently choosing one.

Doctrine deltas shipped in this PR (they become active falsehoods the moment the base goes neutral):

  • INV-6 (CLAUDE.md) — repointed to docker-compose.dood.yml (the socket now lives in the DooD overlay, not the base)
  • §3 Docker rule (CLAUDE.md) — flipped from "socket always present" to "socket is opt-in via DooD overlay"
  • docs/security.md — network/socket paragraphs updated to reflect the split

Chain context

PR 1 of 3 — stacked to dev:

PR Branch Scope
1 (this) feat/dual-mode-compose-core Compose infrastructure + gate + doctrine deltas
2 feat/dual-mode-commands drydock default/dood/contain CLI subcommands + startup banner
3 feat/dual-mode-docs INV-9 full text, §4 threat-model note, README/troubleshooting migration

Part of #149.

Invariants touched

  • INV-6 — rule updated in CLAUDE.md: "Where this lives in code" now points to docker-compose.dood.yml (the DooD overlay) instead of the base docker-compose.yml. The socket still exists; it has moved to an explicit opt-in overlay, which strengthens rather than weakens the boundary.
  • INV-7 — not touched. The contained mode does NOT add adversarial protections; it removes the Docker socket from the default path. This is an accident-defence ergonomics change (accidentally running with a socket you didn't need), consistent with threat model A.
  • INV-8 — unaffected. Hardening defaults (cap_drop, no-new-privileges, tmpfs) live in docker-compose.hardening.yml and are independent of the DooD/contain split.

Test plan

  • 1077/1077 unit and render tests green (scripts/test.sh); shellcheck, shfmt, and lint-commits clean.
  • W1 — contained-mode egress verified empirically: a container running on a plain drydock_net bridge (no internal: true) reached api.anthropic.com → HTTP 405 (method not allowed, but TLS handshake + DNS completed). Egress works in contained mode.
  • W2 — integration migration verified: submount test PASS; lifecycle L-A / L-B / L-D PASS. L-C is a Docker Desktop /etc/hostname environment artifact unrelated to this change; integration tests are gated out of default CI.

Known follow-ups (not in this PR)

  • M2drydock doctor active-mode reporting → PR 2
  • N1 — ROADMAP.md stale "isolated internal bridge" reference → PR 3
  • Full up-the-stack contained-mode runtime test suite → later milestone

jraicr added 7 commits June 4, 2026 20:57
Adds 29 failing tests for the dual-mode network/socket containment feature:
- test/lib_compose.bats: 9 compose_files gate tests + 11 resolve_run_mode unit
  tests covering all precedence paths (factory default, env overrides, per-project
  sentinels, global default, fail-closed conflicts)
- test/compose_overlay_render.bats (new): 9 render assertions via docker compose
  config covering contain/dood mode correctness and hardening composition; includes
  the hard internal:true regression guard (R8.6)

All 29 tests fail (RED) — resolve_run_mode and overlay files don't exist yet.
…overlays + gate

Add resolve_run_mode() and COMPOSE_DOOD/COMPOSE_CONTAIN constants to lib/compose.sh.
Wire the compose_files() gate to include EXACTLY ONE of dood/contain per invocation
using 4-layer precedence (env > per-project sentinel > global default > factory).

docker-compose.yml becomes network-neutral: removes network_mode: host and the
docker.sock bind-mount (both delivered by mode overlay instead). Adds a pointer
comment directing to the overlay files and INV-9.

docker-compose.dood.yml (NEW): opt-in overlay that re-adds network_mode: host +
the Docker socket bind-mount. Carries INV-6 warning comment.

docker-compose.contain.yml (NEW): factory-default overlay that attaches the
drydock service to a drydock_net bridge (driver: bridge, NO internal: true —
egress open in Phase 1 by design). No socket, no host networking.

Atomicity: base-neutral + both overlays + gate land together; partial application
would produce a fatal 'network_mode and networks are mutually exclusive' compose error.

All 1076 tests green; shellcheck + shfmt clean.
test_projects_submount.sh: add explicit -f docker-compose.dood.yml after
the base -f in run_container(). This file hardcodes compose file paths and
never calls compose_files(), so DRYDOCK_DOOD=1 env alone has no effect.
network_mode: host now lives in the dood overlay, not the base.

session_lifecycle_compose_exec.bats: export DRYDOCK_DOOD=1 in setup() so
compose_files() resolves to the dood overlay. These tests run real compose
up -d + compose exec against the stack and require the Docker socket (for
exec) and host networking — both removed from the base and only available
in dood mode.
…r dual-mode

CLAUDE.md:
- INV-6 Rule: update socket description from docker-compose.yml:53 to
  docker-compose.dood.yml; note socket is absent in contained mode (default)
- INV-6 'Where this lives in code': repoint to docker-compose.dood.yml with
  note that socket is opt-in and absent in the factory default (R6.10)
- §3 Docker rule: replace 'DooD via the host Docker socket is the contract'
  with 'DooD is OPT-IN per session (dood mode); factory default is contained
  mode'. Consequence updated: make shell-api / Docker socket commands require
  dood mode (R6.11, R6.12)

docs/security.md:
- Socket paragraph: dood mode only carries the socket; contained mode (factory
  default) blocks the socket-escape class (R6.15)
- Network-exfiltration paragraph: dood = host network; contained = drydock-managed
  bridge, no socket, no host networking; PHASE 1 NOTE: egress open, not filtered
  (R6.14). Pre-existing third-party tool references unchanged.

No third-party project names in new prose (CC-1). No forbidden phrases (R6.7-R6.9).
Without an explicit name:, Compose namespaces the network as
drydock-<proj>-<disc>_drydock_net per session. drydock's teardown is
always `docker rm -f` (REQ-4 — never `compose down`), so each session
leaked one network. Docker's ~31-network bridge pool would exhaust and
ALL host docker networking would fail.

Fix: set name: drydock_net so all contained sessions reuse ONE shared
network. All sessions are isolated at the container level; network
sharing is acceptable under threat model A (accidents, not adversaries).

Regression guard: test/compose_overlay_render.bats — 'render: contain —
network named drydock_net (fixed, not per-session namespaced)'
… mechanisms (M1)

CLAUDE.md §3 Docker rule and docs/security.md referenced `drydock dood`
and `drydock default dood` subcommands that do not exist in PR1 — only
DRYDOCK_DOOD=1 env override and ~/.config/drydock/dood/<proj> sentinel
files are implemented. Shipped docs pointing at unknown commands would
mislead users immediately.

Replace the CLI references with the mechanisms that exist in PR1:
- DRYDOCK_DOOD=1 env override (per-invocation)
- ~/.config/drydock/dood/<proj> sentinel (per-project pin)
- ~/.config/drydock/default-dood sentinel (global default)

Add a brief parenthetical noting the ergonomic drydock dood / drydock
default CLI lands in a later slice (PR2). All See INV-9 pointers kept
as-is (forward INV reference is fine; a nonexistent command is not).
@jraicr jraicr added type:feat Feature work size:l Large: 400+ lines labels Jun 4, 2026
@jraicr jraicr merged commit 4061f60 into dev Jun 4, 2026
4 checks passed
@jraicr jraicr deleted the feat/dual-mode-compose-core branch June 4, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:l Large: 400+ lines type:feat Feature work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant