diff --git a/Dockerfile b/Dockerfile index bff3e9c..11447ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,10 @@ FROM haproxy:3.4.2-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)