Summary
backend/internal/orthrus/muzzle.go and agent/muzzle/muzzle.go are two independent Docker API allowlist filters enforcing the same read-only policy at two different points in the Orthrus tunnel (Charon-side reverse proxy, and the remote agent binary right before it touches the real Docker socket). They normalize incoming request paths in a different order before matching:
- Backend (
muzzle.go): strips the /vX.Y version prefix first, then runs path.Clean.
- Agent (
agent/muzzle/muzzle.go): runs path.Clean first, then strips the version prefix.
A synthetic crafted input (e.g. /foo/../v1.44/images/x/json) can normalize to a different result depending on which order is applied.
Why this isn't an active bug today
The backend muzzle always runs first in the real request pipeline (Charon proxies the request before it ever reaches the tunnel), so it already blocks the one input shape that exposes the divergence before the agent's filter is reached. Confirmed via Supervisor review during the Orthrus external-proxy hotfix (see commits 98a68b67, b71cbd62, eabf358d).
Why it's worth fixing anyway
Two files meant to enforce identical security policy should behave identically in isolation, not rely on one always running before the other. If the request path into the agent's filter ever changes (e.g. a future direct-to-agent access path, or a refactor that reorders backend proxying), this asymmetry could become exploitable without anyone noticing, since neither file's tests currently assert the two filters agree on edge-case inputs.
Suggested fix
Align the normalization order in both files (recommend: strip version prefix, then path.Clean, matching the backend's current order), and add a shared test fixture / property-based test that feeds the same corpus of inputs to both Muzzle.ServeHTTP (backend) and Filter.Allow (agent) and asserts identical accept/reject decisions.
Related
Discovered while fixing namespaced-image support in both muzzle filters for the Orthrus External Docker Proxy feature — see docs/reports/qa_report.md and docs/plans/current_spec.md for full context.
Summary
backend/internal/orthrus/muzzle.goandagent/muzzle/muzzle.goare two independent Docker API allowlist filters enforcing the same read-only policy at two different points in the Orthrus tunnel (Charon-side reverse proxy, and the remote agent binary right before it touches the real Docker socket). They normalize incoming request paths in a different order before matching:muzzle.go): strips the/vX.Yversion prefix first, then runspath.Clean.agent/muzzle/muzzle.go): runspath.Cleanfirst, then strips the version prefix.A synthetic crafted input (e.g.
/foo/../v1.44/images/x/json) can normalize to a different result depending on which order is applied.Why this isn't an active bug today
The backend muzzle always runs first in the real request pipeline (Charon proxies the request before it ever reaches the tunnel), so it already blocks the one input shape that exposes the divergence before the agent's filter is reached. Confirmed via Supervisor review during the Orthrus external-proxy hotfix (see commits
98a68b67,b71cbd62,eabf358d).Why it's worth fixing anyway
Two files meant to enforce identical security policy should behave identically in isolation, not rely on one always running before the other. If the request path into the agent's filter ever changes (e.g. a future direct-to-agent access path, or a refactor that reorders backend proxying), this asymmetry could become exploitable without anyone noticing, since neither file's tests currently assert the two filters agree on edge-case inputs.
Suggested fix
Align the normalization order in both files (recommend: strip version prefix, then
path.Clean, matching the backend's current order), and add a shared test fixture / property-based test that feeds the same corpus of inputs to bothMuzzle.ServeHTTP(backend) andFilter.Allow(agent) and asserts identical accept/reject decisions.Related
Discovered while fixing namespaced-image support in both muzzle filters for the Orthrus External Docker Proxy feature — see
docs/reports/qa_report.mdanddocs/plans/current_spec.mdfor full context.