diff --git a/Dockerfile b/Dockerfile index 1ec174c..8b74c15 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,11 @@ ENV ALLOW_RESTARTS=0 \ SYSTEM=0 \ TASKS=0 \ VERSION=1 \ - VOLUMES=0 + VOLUMES=0 \ + DELETE=0 \ + ALLOW_IMAGES_DELETE=0 \ + ALLOW_NETWORKS_DELETE=1 \ + ALLOW_CONTAINERS_DELETE=0 COPY docker-entrypoint.sh /usr/local/bin/ COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg.template USER root diff --git a/haproxy.cfg b/haproxy.cfg index 9d0da3e..91df4b0 100644 --- a/haproxy.cfg +++ b/haproxy.cfg @@ -45,36 +45,76 @@ backend docker-events frontend dockerfrontend bind ${BIND_CONFIG} - http-request deny unless METH_GET || { env(POST) -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 } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/stop } { env(ALLOW_STOP) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/auth } { env(AUTH) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/build } { env(BUILD) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/commit } { env(COMMIT) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/configs } { env(CONFIGS) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers } { env(CONTAINERS) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/distribution } { env(DISTRIBUTION) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } { env(EVENTS) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec } { env(EXEC) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/grpc } { env(GRPC) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images } { env(IMAGES) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/info } { env(INFO) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/networks } { env(NETWORKS) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/nodes } { env(NODES) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } { env(PING) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/plugins } { env(PLUGINS) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/secrets } { env(SECRETS) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/services } { env(SERVICES) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/session } { env(SESSION) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/swarm } { env(SWARM) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/system } { env(SYSTEM) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/tasks } { env(TASKS) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/version } { env(VERSION) -m bool } - http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes } { env(VOLUMES) -m bool } + # --- Method ACLs --- + acl method_GET method GET + acl method_HEAD method HEAD + acl method_POST method POST + acl method_DELETE method DELETE + acl method_PUT method PUT + acl method_PATCH method PATCH + + # --- Allow only enabled methods --- + http-request deny unless METH_GET || method_POST { env(POST) -m bool } || method_DELETE { env(DELETE) -m bool } || method_PUT { env(PUT) -m bool } || method_PATCH { env(PATCH) -m bool } + + # --- Allowed endpoints --- + # GET, HEAD, PUT & PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/auth } { env(AUTH) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/build } { env(BUILD) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/commit } { env(COMMIT) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/configs } { env(CONFIGS) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers } { env(CONTAINERS) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/distribution } { env(DISTRIBUTION) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } { env(EVENTS) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec } { env(EXEC) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/grpc } { env(GRPC) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images } { env(IMAGES) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/info } { env(INFO) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/networks } { env(NETWORKS) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/nodes } { env(NODES) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } { env(PING) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/plugins } { env(PLUGINS) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/secrets } { env(SECRETS) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/services } { env(SERVICES) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/session } { env(SESSION) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/swarm } { env(SWARM) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/system } { env(SYSTEM) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/tasks } { env(TASKS) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/version } { env(VERSION) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes } { env(VOLUMES) -m bool } method_GET || method_HEAD || method_PUT || method_PATCH + + # POST and DELETE + http-request allow if method_POST { 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 method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/start } { env(ALLOW_START) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/stop } { env(ALLOW_STOP) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/auth } { env(AUTH) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/build } { env(BUILD) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/commit } { env(COMMIT) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/configs } { env(CONFIGS) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers } { env(CONTAINERS) -m bool } + http-request allow if method_DELETE { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers }{ env(ALLOW_CONTAINERS_DELETE) -m bool } { env(CONTAINERS) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/distribution }{ env(DISTRIBUTION) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } { env(EVENTS) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec } { env(EXEC) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/grpc } { env(GRPC) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/images } { env(IMAGES) -m bool } + http-request allow if method_DELETE { path,url_dec -m reg -i ^(/v[\d\.]+)?/images } { env(ALLOW_IMAGES_DELETE) -m bool } { env(IMAGES) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/networks } { env(NETWORKS) -m bool } + http-request allow if method_DELETE { path,url_dec -m reg -i ^(/v[\d\.]+)?/networks } { env(ALLOW_NETWORKS_DELETE) -m bool } { env(NETWORKS) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/nodes } { env(NODES) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } { env(PING) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/plugins } { env(PLUGINS) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/secrets } { env(SECRETS) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/services } { env(SERVICES) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/session } { env(SESSION) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/swarm } { env(SWARM) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/system } { env(SYSTEM) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/tasks } { env(TASKS) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/version } { env(VERSION) -m bool } + http-request allow if method_POST { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes } { env(VOLUMES) -m bool } + + # --- Default deny everything else --- http-request deny - default_backend dockerbackend + default_backend dockerbackend use_backend docker-events if { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } diff --git a/tests/test_service.py b/tests/test_service.py index 4724d3f..f0dbb20 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -72,12 +72,34 @@ def test_network_post_permissions(proxy_factory): allowed_calls = [ ("network", "ls"), ("network", "create", "foo"), - ("network", "rm", "foo"), ] forbidden_calls = [] _check_permissions(allowed_calls, forbidden_calls) +def test_network_delete_permissions(proxy_factory): + with proxy_factory(NETWORKS=1, DELETE=1): + allowed_calls = [ + ("network", "rm", "foo"), + ("network", "rm", "-f", "foobarfoo"), + ] + forbidden_calls = [ + ("network", "create", "foobarfoo"), + ] + _check_permissions(allowed_calls, forbidden_calls) + + +def test_network_delete_permissions_v2(proxy_factory): + with proxy_factory(NETWORKS=1, POST=1): + allowed_calls = [ + ("network", "create", "foobarfoo"), + ] + forbidden_calls = [ + ("network", "rm", "foobarfoo"), + ] + _check_permissions(allowed_calls, forbidden_calls) + + def test_exec_permissions(proxy_factory): with proxy_factory(CONTAINERS=1, EXEC=1, POST=1) as container_id: allowed_calls = [ @@ -85,3 +107,50 @@ def test_exec_permissions(proxy_factory): ] forbidden_calls = [] _check_permissions(allowed_calls, forbidden_calls) + + +def test_image_delete_permissions_v1(proxy_factory): + with proxy_factory(DELETE=1, ALLOW_IMAGES_DELETE=1, IMAGES=1, POST=1): + allowed_calls = [ + ("pull", "alpine"), + ("image", "rmi", "alpine"), + ] + forbidden_calls = [] + _check_permissions(allowed_calls, forbidden_calls) + + +def test_image_delete_permissions_v2(proxy_factory): + with proxy_factory(IMAGES=1, POST=1, DELETE=1): + allowed_calls = [ + ("pull", "alpine"), + ("image", "ls"), + ("image", "inspect", "alpine"), + ] + forbidden_calls = [ + ("image", "rmi", "alpine"), + ("image", "rmi", "-f", "alpine"), + ] + _check_permissions(allowed_calls, forbidden_calls) + + +def test_container_delete_permissions_v2(proxy_factory): + with proxy_factory( + CONTAINERS=1, + DELETE=1, + ALLOW_START=1, + ALLOW_CONTAINERS_DELETE=1, + IMAGES=1, + POST=1, + ): + allowed_calls = [ + ("pull", "alpine"), + ("container", "run", "-dt", "--rm", "--name", "alpine", "alpine"), + ("container", "rm", "-f", "alpine"), + # ("image", "rmi", "alpine"), + # ("image", "rmi", "-f", "alpine"), + ] + forbidden_calls = [ + ("image", "rmi", "alpine"), + ("image", "rmi", "-f", "alpine"), + ] + _check_permissions(allowed_calls, forbidden_calls)