The hardened policy matches paths with equality on input.PathPlain (R-17). That field carries the API version prefix in production, so every equality-keyed rule is dead on a live host. A legitimate project create is denied.
Evidence
The plugin builds the input from the raw request URL — main.go:257 of open-policy-agent/opa-docker-authz:
"PathPlain": u.Path,
"PathArr": strings.Split(u.Path, "/"),
Nothing strips a version prefix (-skip-ping only bypasses HEAD /_ping). The plugin's own decision log on a live host, taken while running this policy under the plugin image (v0.10, embedded OPA v1.3.0) on Docker 29.6.1:
"Path":"/v1.56/containers/json","PathPlain":"/v1.56/containers/json",
"PathArr":["","v1.56","containers","json"],"Method":"GET","User":""
What it does
The documented input shape in SCHEMATIC.md reads "PathPlain": "/containers/create" for "Path": "/v1.47/containers/create?name=...". The 71-row probe table in skeleton/agent.rego.schema was run with that version-less value. With the value a real plugin sends, the rules decide differently:
$ opa eval -f raw --data agent.rego --input documented-shape.json data.docker.authz.allow
true
$ opa eval -f raw --data agent.rego --input plugin-real-shape.json data.docker.authz.allow
false
Both inputs are the same request — POST /v1.56/containers/create, the project label, an empty HostConfig — and differ only in PathPlain (/containers/create against /v1.56/containers/create).
On a live daemon the result is a denial of work the policy intends to allow: docker run --rm --label com.docker.compose.project=<project> alpine echo hello returns authorization denied by plugin opa-docker-authz, while the same request is allowed under the documented shape. Reads and lifecycle calls keep working, because the pre-existing rules match them by prefix, which is version-agnostic.
So the hardening closed the host-access holes but also removed the grants: the sandbox can no longer create a container, volume, or network for its own project. The failure direction is safe (deny), which is why no test caught it — a failing-closed sandbox looks secure.
What must change
- The rules must match a version-less path. Either derive it in the policy (
regex.replace(input.PathPlain, "^/v[0-9]+\\.[0-9]+", ""), or PathArr with the first two elements dropped), or the plugin must be given one — it has no option for that today.
- The documented input shape in
SCHEMATIC.md and skeleton/agent.rego.schema must be corrected: PathPlain is the raw request path, not the path without the version.
- The probe table must be re-run with the real shape. The version-less table cannot catch this class again.
Found while building this schematic on a snap-installed daemon (phoenix), by running the deployed policy under the plugin image rather than under opa eval alone.
The hardened policy matches paths with equality on
input.PathPlain(R-17). That field carries the API version prefix in production, so every equality-keyed rule is dead on a live host. A legitimate project create is denied.Evidence
The plugin builds the input from the raw request URL —
main.go:257ofopen-policy-agent/opa-docker-authz:Nothing strips a version prefix (
-skip-pingonly bypassesHEAD /_ping). The plugin's own decision log on a live host, taken while running this policy under the plugin image (v0.10, embedded OPAv1.3.0) on Docker 29.6.1:What it does
The documented input shape in
SCHEMATIC.mdreads"PathPlain": "/containers/create"for"Path": "/v1.47/containers/create?name=...". The 71-row probe table inskeleton/agent.rego.schemawas run with that version-less value. With the value a real plugin sends, the rules decide differently:Both inputs are the same request —
POST /v1.56/containers/create, the project label, an emptyHostConfig— and differ only inPathPlain(/containers/createagainst/v1.56/containers/create).On a live daemon the result is a denial of work the policy intends to allow:
docker run --rm --label com.docker.compose.project=<project> alpine echo helloreturnsauthorization denied by plugin opa-docker-authz, while the same request is allowed under the documented shape. Reads and lifecycle calls keep working, because the pre-existing rules match them by prefix, which is version-agnostic.So the hardening closed the host-access holes but also removed the grants: the sandbox can no longer create a container, volume, or network for its own project. The failure direction is safe (deny), which is why no test caught it — a failing-closed sandbox looks secure.
What must change
regex.replace(input.PathPlain, "^/v[0-9]+\\.[0-9]+", ""), orPathArrwith the first two elements dropped), or the plugin must be given one — it has no option for that today.SCHEMATIC.mdandskeleton/agent.rego.schemamust be corrected:PathPlainis the raw request path, not the path without the version.Found while building this schematic on a snap-installed daemon (phoenix), by running the deployed policy under the plugin image rather than under
opa evalalone.