Skip to content

authorize-docker-requests: P-16 extra bind roots, and the host facts a snap install teaches (#38, #39) - #49

Open
phoenix-server wants to merge 2 commits into
mainfrom
feat/authorize-docker-requests-extra-roots
Open

phoenix-server wants to merge 2 commits into
mainfrom
feat/authorize-docker-requests-extra-roots

Conversation

@phoenix-server

@phoenix-server phoenix-server commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #38. Closes #39.

Two commits, one per issue: e179f69 implements the parameterised extra roots (#38), 73cca64 adds the host-path and client-consequence facts (#39). Frontmatter: version: 0.5.0 → 0.6.0, updated: 2026-09-17.

#38 — the allow-list was narrower than real deployments

The bind allow-list accepted the project directory plus named volumes, so a host whose media, library or document trees live elsewhere could not create those containers at all — the only route was editing the deployed Rego by hand, which is what happened on the host this was reported from (18 of 45 bind sources covered; 42 → 45 containers after the hand-edit).

P-16 EXTRA_BIND_ROOTS — comma-separated absolute directories, empty by default — is the knob:

  • Each root is a prefix grant matched as a path boundary, never as a string prefix: /srv/media accepts /srv/media/x, and /srv/mediaX is still refused.
  • A single file can be named exactly, without granting the directory that holds it — the case that made a hand-written root list necessary (a known_hosts in, the directory holding private keys out).
  • Traversal stays refused under every root: /srv/media/../../etc starts with the root and is denied. That was the point of keeping the check where it is; a prefix grant is exactly what path.. reaches.
  • A trailing / or /** is accepted and means the directory itself; / and empty entries are dropped rather than accepted, so a root cannot make the gate decorative.
  • The token joins the substitution list and the leftover check (Phase 6, the schema, scripts/reload-opa-policy.sh), so a copy deployed without it is caught by the check rather than silently deployed.

The limitation is documented as well as fixed — SCHEMATIC.md's Limitations, modules/opa-policy.md's Limitations, and P-16's own row all say the same thing: the allow-list is exactly as wide as P-15 plus P-16, a root is a prefix grant, and the operator's real layout is the one input to R-15 that the API's shape does not decide.

#38 — proof

Eight probe rows were added to skeleton/agent.rego.schema (78 → 86), and the new rows were run on the engines the plugin releases embed, plus a newer one — openpolicyagent/opa:1.3.0 (what v0.10 embeds), 0.60.0 (what v0.9 embeds), 1.7.1.

With P-16 = /srv/media,/srv/data/known_hosts — deliberately a directory and a single file:

=== 1.3.0 / extra roots set ===
  data.probe.test_er_dir_bind: PASS        /srv/media/downloads           -> allow
  data.probe.test_er_root_itself: PASS     /srv/media                     -> allow
  data.probe.test_er_mount_bind: PASS      Mounts[{bind,/srv/media/lib}]  -> allow
  data.probe.test_er_file_exact: PASS      /srv/data/known_hosts          -> allow
  data.probe.test_er_file_dir: PASS        /srv/data/other                -> deny
  data.probe.test_er_string_prefix: PASS   /srv/mediaX                    -> deny
  data.probe.test_er_traversal: PASS       /srv/media/../../etc           -> deny
  data.probe.test_er_symlink: PASS         Resolved:"/etc"                -> deny
  data.probe.test_regr_project_traversal: PASS                          -> deny
  data.probe.test_regr_privileged: PASS                                    -> deny
  PASS: 11/11          (identical on 0.60.0 and 1.7.1)

With P-16 empty — the default — the same twelve inputs decide 12/12 as the table says, all four extra-root grants denied, every denial still denied.

And the check that matters for the existing contract: the shipped template and this one decide identically on all twelve inputs when P-16 is empty, on all three engines:

=== openpolicyagent/opa:1.3.0 ===
  shipped template vs this one, P-16 empty: IDENTICAL on all 12 inputs
=== openpolicyagent/opa:0.60.0 ===
  shipped template vs this one, P-16 empty: IDENTICAL on all 12 inputs
=== openpolicyagent/opa:1.7.1 ===
  shipped template vs this one, P-16 empty: IDENTICAL on all 12 inputs

With P-16 set, the four extra-root shapes flip to allow while er-traversal, er-string-prefix, er-symlink, er-file-dir, regr-privileged and regr-project-traversal stay denied.

The rows were run with the plugin's real input shape (Path/PathPlain/PathArr carrying the API version prefix, Query parsed, identity User: sandbox-agent). The probe inputs and the comparison harness are throwaway — not committed.

#39 — three facts from a live snap deployment

  • The configuration file is found from the daemon's own command line. ps -o args= -C dockerd carries --config-file; a snap install names a revision-numbered directory there (/var/snap/docker/<revision>/config/daemon.json) that a snap refresh replaces, with /var/snap/docker/current/config/daemon.json as its stable alias. P-13's discovery cell, Phase 1 step 6 and the daemon-config module now say that, and record that writing the wrong file produces no error at all.
  • The paths around the daemon are snap-layout too. The plugin mounts the snap's /etc/docker at /opa, so the host-side policy path is under the snap root while every path inside the daemon and the plugin is /etc/docker. The daemon-config module gains that section, and the policy-reload module now says why --discover beats any documented path — it resolves the plugin's own argument through its own mount.
  • Client consequences of a policy that grants neither exec nor attach, measured as the sandbox client: docker run cannot complete (create granted, attach refused → docker create + docker start, or docker compose up -d); a build needs DOCKER_BUILDKIT=0, because BuildKit's POST /grpc is denied and its builder container is refused by the project-name rules; exec-based tooling stops working, and the denial message is what tells the operator it is a decision rather than a fault. SCHEMATIC.md's Limitations and modules/opa-policy.md carry it. The second-daemon trap is now a failure mode: a dind --privileged --network host trial takes docker0 down, and with it the network of every container already running.

Notes

  • improve-docker-security pins this package at 6775510 (v0.5.0) in improve-docker-security: pin D-3 at the revision whose policy is verified (#34, #37) #48, which is unaffected: a pin names a revision, and P-16 arrives in 0.6.0. When that composition wants the parameter, its pin moves with its own change.
  • Validator: BASE_REF=main bash scripts/validate-catalog.shfrontmatter ok: 18 specs, catalog ok: 19 entries … 21 pins verified, exit 0.
  • Diff: 4 files in commit 1 and 4 in commit 2, no test fixtures, no harness committed.

The bind allow-list was the project directory plus named volumes, which covers a
stack whose data lives under its own directory and nothing else: a host whose
media, library or document trees sit elsewhere cannot create those containers at
all, and the operator's only route was to edit the deployed policy by hand.

P-16 names further roots, comma-separated, empty by default. Each is a prefix
grant matched as a path boundary — a directory accepts its whole subtree, and a
single file is named exactly without granting the directory that holds it — and
a `..` segment stays refused under every root, which is what a prefix grant
otherwise reaches. With P-16 empty the policy decides exactly as the shipped one
does, verified row by row on the plugin's own engines.
…nap install teaches

Three facts a deployment rediscovers the hard way, now stated where they are
needed.

The daemon's configuration file is found from the daemon's own command line:
`ps -o args= -C dockerd` carries `--config-file`, which on a snap install names a
revision-numbered directory that a `snap refresh` replaces, with the `current`
symlink as its stable alias. The paths around the daemon are snap-layout too —
the plugin mounts the snap's `/etc/docker` at `/opa` — so the host-side policy
path is under the snap root while every path inside the daemon is `/etc/docker`,
and the plugin's own report (`--discover`) is what resolves it.

And a policy that grants neither exec nor attach changes what a client command
can do: `docker run` never completes its attach, a build needs
`DOCKER_BUILDKIT=0`, and exec-based tooling stops working — each of those is a
decision the client reports as a denial, which is worth saying because it
otherwise reads as a broken tool. A second daemon started with `dind
--privileged --network host` on a host that is already running containers is
called out as the way a working stack loses `docker0`.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying agentic-schematics with  Cloudflare Pages  Cloudflare Pages

Latest commit: 73cca64
Status: ✅  Deploy successful!
Preview URL: https://031167b1.agentic-schematics.pages.dev
Branch Preview URL: https://feat-authorize-docker-reques.agentic-schematics.pages.dev

View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant