From 03f5f7ec85f36cd70e29ce99910bfd55f43cd14e Mon Sep 17 00:00:00 2001 From: Dmitry Emelyanov Date: Sun, 24 May 2026 06:10:04 +0300 Subject: [PATCH] Add ALLOW_EXEC / ALLOW_KILL / ALLOW_DELETE granular flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #114. Until now, setting CONTAINERS=1 + POST=1 implicitly granted three high-impact operations: * POST /containers//exec — create new exec sessions * POST /containers//kill — kill a container * DELETE /containers/ — remove a container In a typical sandboxing setup (an untrusted workload reaching the host Docker socket only via this proxy) these are exactly the operations one wants to deny by default, while still allowing inspect/logs/create-only flows via CONTAINERS=1. The existing EXEC env controls only /exec//start|resize|inspect (operations on already-created sessions), not creation of new ones — see issue #114. This change introduces three new env flags following the same pattern as the existing ALLOW_START / ALLOW_STOP / ALLOW_RESTARTS: * ALLOW_EXEC — POST /containers//exec * ALLOW_KILL — POST /containers//kill * ALLOW_DELETE — DELETE /containers/ All three default to 0 (deny). The deny rules are evaluated before the generic CONTAINERS allow rule, so existing deployments that wanted these operations under CONTAINERS=1 must now opt in explicitly — this is the intended behavior change. Tests cover the deny path (default behavior) for each new flag. The existing test_exec_permissions is updated to set ALLOW_EXEC=1 alongside the existing CONTAINERS=1 EXEC=1 POST=1, documenting the new requirement for callers that genuinely need exec. --- Dockerfile | 5 ++++- README.md | 10 +++++++++- haproxy.cfg | 8 ++++++++ tests/test_service.py | 40 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 25aca9f..4566e71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,10 @@ FROM haproxy:3.2.4-alpine EXPOSE 2375 -ENV ALLOW_RESTARTS=0 \ +ENV ALLOW_DELETE=0 \ + ALLOW_EXEC=0 \ + ALLOW_KILL=0 \ + ALLOW_RESTARTS=0 \ ALLOW_STOP=0 \ ALLOW_START=0 \ AUTH=0 \ diff --git a/README.md b/README.md index 8c1aceb..3df1217 100644 --- a/README.md +++ b/README.md @@ -130,8 +130,16 @@ extremely critical but can expose some information that your service does not ne - `ALLOW_RESTARTS` (containers/`id`/`stop`|`restart`|`kill`) - `ALLOW_PAUSE` (containers/`id`/`pause`) - `ALLOW_UNPAUSE` (containers/`id`/`unpause`) +- `ALLOW_EXEC` (containers/`id`/`exec`) — required IN ADDITION to `CONTAINERS=1` to create + new exec instances. Without it, even with `CONTAINERS=1 POST=1`, `docker exec` is denied. +- `ALLOW_KILL` (containers/`id`/`kill`) — required IN ADDITION to `CONTAINERS=1` to kill a + container. Without it, `docker kill` is denied even with `CONTAINERS=1 POST=1`. +- `ALLOW_DELETE` (`DELETE` containers/`id`) — required IN ADDITION to `CONTAINERS=1` to + remove a container. Without it, `docker rm` is denied even with `CONTAINERS=1 POST=1`. - `DISTRIBUTION` -- `EXEC` +- `EXEC` — controls only `/exec/{id}/start|resize|inspect` (operations on + *existing* exec sessions). Creation of new exec instances via + `POST /containers/{id}/exec` is controlled by `ALLOW_EXEC` (above). - `GRPC` - `IMAGES` - `INFO` diff --git a/haproxy.cfg b/haproxy.cfg index d61b1d4..395b13f 100644 --- a/haproxy.cfg +++ b/haproxy.cfg @@ -47,6 +47,14 @@ frontend dockerfrontend bind ${BIND_CONFIG} http-request deny unless METH_GET || { env(POST) -m bool } + # Granular container-mutating operations. Checked BEFORE generic CONTAINERS + # to keep them disabled by default even when CONTAINERS=1 is set — same + # pattern as the existing ALLOW_START / ALLOW_STOP / ALLOW_RESTARTS. See + # https://github.com/Tecnativa/docker-socket-proxy/issues/114. + http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/exec } !{ env(ALLOW_EXEC) -m bool } + http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/kill } !{ env(ALLOW_KILL) -m bool } + http-request deny if METH_DELETE { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+$ } !{ env(ALLOW_DELETE) -m bool } + # Allowed endpoints http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/((stop)|(restart)|(kill)) } { env(ALLOW_RESTARTS) -m bool } http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/start } { env(ALLOW_START) -m bool } diff --git a/tests/test_service.py b/tests/test_service.py index 4724d3f..8ff0831 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -79,9 +79,47 @@ def test_network_post_permissions(proxy_factory): def test_exec_permissions(proxy_factory): - with proxy_factory(CONTAINERS=1, EXEC=1, POST=1) as container_id: + # ALLOW_EXEC=1 is required IN ADDITION to CONTAINERS+EXEC+POST to actually + # create new exec sessions, see issue #114. EXEC controls only operations + # on already-created exec sessions (/exec//start|resize|inspect). + with proxy_factory(CONTAINERS=1, EXEC=1, POST=1, ALLOW_EXEC=1) as container_id: allowed_calls = [ ("exec", container_id, "ls"), ] forbidden_calls = [] _check_permissions(allowed_calls, forbidden_calls) + + +def test_exec_denied_without_allow_exec(proxy_factory): + """CONTAINERS=1 + POST=1 must NOT be enough to create new exec sessions. + + Regression test for https://github.com/Tecnativa/docker-socket-proxy/issues/114. + EXEC controls /exec//start (existing sessions); creating a new exec + instance via POST /containers//exec requires ALLOW_EXEC. + """ + with proxy_factory(CONTAINERS=1, EXEC=1, POST=1) as container_id: + forbidden_calls = [ + ("exec", container_id, "ls"), + ] + _check_permissions((), forbidden_calls) + + +def test_delete_denied_without_allow_delete(proxy_factory): + """CONTAINERS=1 + POST=1 must NOT allow DELETE /containers/. + + Regression test for https://github.com/Tecnativa/docker-socket-proxy/issues/114. + """ + with proxy_factory(CONTAINERS=1, POST=1) as container_id: + forbidden_calls = [ + ("rm", "-f", container_id), + ] + _check_permissions((), forbidden_calls) + + +def test_kill_denied_without_allow_kill(proxy_factory): + """CONTAINERS=1 + POST=1 must NOT allow `docker kill`.""" + with proxy_factory(CONTAINERS=1, POST=1) as container_id: + forbidden_calls = [ + ("kill", container_id), + ] + _check_permissions((), forbidden_calls)