Skip to content

fix: judge POST /volumes/create against allow-bind-sources - #442

Merged
zcsizmadia merged 1 commit into
mainfrom
fix/volume-create-bind-sources
Sep 19, 2026
Merged

zcsizmadia merged 1 commit into
mainfrom
fix/volume-create-bind-sources

Conversation

@zcsizmadia

Copy link
Copy Markdown
Collaborator

Closes #419.

docs/policy.md said, for a long time: "Named volumes are not binds — -v myvol:/data has no host path to restrict and is never denied by this rule." True of the common case, false in general.

docker volume create -d local -o type=none -o o=bind -o device=/mnt/c/secrets esc
docker run -v esc:/out ubuntu cat /out/...

POST /volumes/create was judged by nothing, and the container create that follows carries only the volume's name — which bindSources deliberately skips, because a name is not a path. So with allow-bind-sources: [C:\work] in force, those two commands read C:\secrets. The rule was not weak here; it was absent. device=/ is the same trick against the whole guest filesystem.

How it is judged

The device is a guest path — dockerd is what opens it — so it is mapped back from /mnt/<drive>/... to Windows form before comparison. That is the inverse of the translation the bridge already applies to binds.

Two consequences, both documented:

  • A device under no Windows drive is refused (/, /etc, /var/lib/docker). Under no allowed root, and refusing is the point.
  • A third-party volume driver is refused while this rule is in force, because its options are its own vocabulary and cannot be checked. Reporting "checked" would be a lie. The local driver — the default — is unaffected.

Wired on both backends, in the same change

combinedGate gets DenyVolumeCreate and a compile-time VolumeGate guard.

Deliberate, and not speculative: deny-unattributable-builds shipped in this same release consulting both layers for create/pull/push and only the WSL layer for build, so the rule was inert on the wslc backend while policy show reported it active. A new gate method is exactly where that recurs.

Verification

Tested at both levels, because one is not enough and this release is the reason — deny-unattributable-builds passed every Rules test while being a no-op in the product, since nothing drove the path the bridge takes. So: Rules tests for the rule, Watcher tests for what the bridge actually installs, and an end-to-end test through RewriteBindsGuarded with the real policy.Watcher.

Negative control — route removed:

status = 201, want 403
the request reached the engine; a denial must stop at the bridge

The permitted cases are asserted too, so this cannot have bought safety by refusing ordinary named volumes: a plain volume and a device inside the allowed root both reach the engine and return 201.

Closes #419.

docs/policy.md said, for a long time: "Named volumes are not binds --
`-v myvol:/data` has no host path to restrict and is never denied by
this rule." True of the common case, false in general.

A local-driver volume can name a host path:

	docker volume create -d local -o type=none -o o=bind \
	    -o device=/mnt/c/secrets esc
	docker run -v esc:/out ubuntu cat /out/...

/volumes/create was judged by nothing, and the container create that
follows carries only the volume's NAME -- which bindSources
deliberately skips, because a name is not a path. So with
allow-bind-sources: [C:\work] in force, those two commands read
C:\secrets. The rule was not weak here; it was absent. device=/ is the
same trick against the whole guest filesystem.

The device is a GUEST path -- dockerd is what opens it -- so it is
mapped back from /mnt/<drive>/... to Windows form before comparison,
the inverse of the translation the bridge already applies to binds.
Two consequences, both documented:

  - a device under no Windows drive (/, /etc, /var/lib/docker) is
    refused. It is under no allowed root, and that is the point.
  - a third-party volume driver is refused while this rule is in
    force, because its options are its own vocabulary and cannot be
    checked. Reporting "checked" would be a lie. The local driver --
    the default -- is unaffected.

Wired on BOTH backends in the same change
-----------------------------------------
combinedGate gets DenyVolumeCreate and a compile-time VolumeGate guard.
This is deliberate and it is not speculative: deny-unattributable-builds
shipped in this same release consulting both layers for
create/pull/push and only the WSL layer for build, so the rule was inert
on the wslc backend while `policy show` reported it active. A new gate
method is exactly where that recurs.

Verification
------------
Tested at both levels, because one of them is not enough and this
release is the reason. deny-unattributable-builds passed every Rules
test while being a no-op in the product, since nothing drove the path
the bridge takes. So: Rules tests for the rule, Watcher tests for what
the bridge installs, and an end-to-end test through RewriteBindsGuarded
with the real policy.Watcher.

Negative control -- with the route removed:

	status = 201, want 403
	the request reached the engine; a denial must stop at the bridge

The permitted cases are asserted too, so the rule cannot have bought
safety by refusing ordinary named volumes: a plain volume and a device
inside the allowed root both reach the engine and return 201.
@zcsizmadia
zcsizmadia merged commit d8c2f9a into main Sep 19, 2026
7 checks passed
@zcsizmadia
zcsizmadia deleted the fix/volume-create-bind-sources branch September 19, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

policy: /volumes/create bypasses allow-bind-sources (local driver with o=bind names a host path)

1 participant