From da08dbb8f365f627efb6e39150963f011d6b8c83 Mon Sep 17 00:00:00 2001 From: LifetimeMistake <57274704+LifetimeMistake@users.noreply.github.com> Date: Sun, 14 Apr 2024 15:43:18 +0200 Subject: [PATCH 1/3] Add more granular access control --- Dockerfile | 41 +++++++++++++++++++++++++++-------------- haproxy.cfg | 48 ++++++++++++++++++++++++------------------------ verify.lua | 12 ++++++++++++ 3 files changed, 63 insertions(+), 38 deletions(-) create mode 100644 verify.lua diff --git a/Dockerfile b/Dockerfile index 6e8ec04..2fa281c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,32 +4,45 @@ EXPOSE 2375 ENV ALLOW_RESTARTS=0 \ ALLOW_STOP=0 \ ALLOW_START=0 \ + CONTAINERS_READ=0 \ + CONTAINERS_WRITE=0 \ + EXEC_READ=0 \ + EXEC_WRITE=0 \ + IMAGES_READ=0 \ + IMAGES_WRITE=0 \ + NETWORKS_READ=0 \ + NETWORKS_WRITE=0 \ + SECRETS_READ=0 \ + SECRETS_WRITE=0 \ + SERVICES_READ=0 \ + SERVICES_WRITE=0 \ + VOLUMES_READ=0 \ + VOLUMES_WRITE=0 \ + SWARM_READ=0 \ + SWARM_WRITE=0 \ + NODES_READ=0 \ + NODES_WRITE=0 \ + CONFIGS_READ=0 \ + CONFIGS_WRITE=0 \ + PLUGINS_READ=0 \ + PLUGINS_WRITE=0 \ + SYSTEM_READ=0 \ + SYSTEM_WRITE=0 \ AUTH=0 \ BUILD=0 \ COMMIT=0 \ - CONFIGS=0 \ - CONTAINERS=0 \ DISABLE_IPV6=0 \ DISTRIBUTION=0 \ EVENTS=1 \ - EXEC=0 \ GRPC=0 \ - IMAGES=0 \ INFO=0 \ LOG_LEVEL=info \ - NETWORKS=0 \ - NODES=0 \ PING=1 \ - PLUGINS=0 \ - POST=0 \ - SECRETS=0 \ - SERVICES=0 \ SESSION=0 \ SOCKET_PATH=/var/run/docker.sock \ - SWARM=0 \ - SYSTEM=0 \ TASKS=0 \ - VERSION=1 \ - VOLUMES=0 + VERSION=1 + COPY docker-entrypoint.sh /usr/local/bin/ COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg.template +COPY verify.lua /usr/local/etc/haproxy/verify.lua diff --git a/haproxy.cfg b/haproxy.cfg index 43e3526..05dc8f1 100644 --- a/haproxy.cfg +++ b/haproxy.cfg @@ -6,6 +6,7 @@ global # Turn on stats unix socket server-state-file /var/lib/haproxy/server-state + lua-load /usr/local/etc/haproxy/verify.lua defaults mode http @@ -45,33 +46,32 @@ backend docker-events frontend dockerfrontend bind ${BIND_CONFIG} - http-request deny unless METH_GET || { env(POST) -m bool } 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 } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers } { lua.verify_access(CONTAINERS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec } { lua.verify_access(EXEC) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images } { lua.verify_access(IMAGES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/networks } { lua.verify_access(NETWORKS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/secrets } { lua.verify_access(SECRETS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/services } { lua.verify_access(SERVICES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes } { lua.verify_access(VOLUMES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/swarm } { lua.verify_access(SWARM) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/nodes } { lua.verify_access(NODES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/configs } { lua.verify_access(CONFIGS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/plugins } { lua.verify_access(PLUGINS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/system } { lua.verify_access(SYSTEM) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/auth } { lua.verify_access(AUTH) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/build } { lua.verify_access(BUILD) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/commit } { lua.verify_access(COMMIT) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/distribution } { lua.verify_access(DISTRIBUTION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } { lua.verify_access(EVENTS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/grpc } { lua.verify_access(GRPC) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/info } { lua.verify_access(INFO) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } { lua.verify_access(PING) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/session } { lua.verify_access(SESSION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/tasks } { lua.verify_access(TASKS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/version } { lua.verify_access(VERSION) -m bool } http-request deny default_backend dockerbackend diff --git a/verify.lua b/verify.lua new file mode 100644 index 0000000..5782393 --- /dev/null +++ b/verify.lua @@ -0,0 +1,12 @@ +core.register_fetches("verify_access", function(txn, api) + -- env(api) check is kept for backwards compatibility + local read_allowed = txn.f:env(api) == "1" or txn.f:env(api .. "_READ") == "1" + -- env(POST) check is kept for backwards compatibility + local write_allowed = txn.f:env(api .. "_WRITE") == "1" or txn.f:env("POST") == "1" + local method = txn.f:method() + + local result = ((method == "GET" or method == "HEAD") and read_allowed) + or ((method ~= "GET" and method ~= "HEAD") and write_allowed) + + return result +end) From eb128120ed157809aff1be706d893b83fdc08bc1 Mon Sep 17 00:00:00 2001 From: LifetimeMistake <57274704+LifetimeMistake@users.noreply.github.com> Date: Sun, 14 Apr 2024 15:57:31 +0200 Subject: [PATCH 2/3] Revert unnecessary changes --- Dockerfile | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2fa281c..79c43d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,44 +4,33 @@ EXPOSE 2375 ENV ALLOW_RESTARTS=0 \ ALLOW_STOP=0 \ ALLOW_START=0 \ - CONTAINERS_READ=0 \ - CONTAINERS_WRITE=0 \ - EXEC_READ=0 \ - EXEC_WRITE=0 \ - IMAGES_READ=0 \ - IMAGES_WRITE=0 \ - NETWORKS_READ=0 \ - NETWORKS_WRITE=0 \ - SECRETS_READ=0 \ - SECRETS_WRITE=0 \ - SERVICES_READ=0 \ - SERVICES_WRITE=0 \ - VOLUMES_READ=0 \ - VOLUMES_WRITE=0 \ - SWARM_READ=0 \ - SWARM_WRITE=0 \ - NODES_READ=0 \ - NODES_WRITE=0 \ - CONFIGS_READ=0 \ - CONFIGS_WRITE=0 \ - PLUGINS_READ=0 \ - PLUGINS_WRITE=0 \ - SYSTEM_READ=0 \ - SYSTEM_WRITE=0 \ AUTH=0 \ BUILD=0 \ COMMIT=0 \ + CONFIGS=0 \ + CONTAINERS=0 \ DISABLE_IPV6=0 \ DISTRIBUTION=0 \ EVENTS=1 \ + EXEC=0 \ GRPC=0 \ + IMAGES=0 \ INFO=0 \ LOG_LEVEL=info \ + NETWORKS=0 \ + NODES=0 \ PING=1 \ + PLUGINS=0 \ + POST=0 \ + SECRETS=0 \ + SERVICES=0 \ SESSION=0 \ SOCKET_PATH=/var/run/docker.sock \ + SWARM=0 \ + SYSTEM=0 \ TASKS=0 \ - VERSION=1 + VERSION=1 \ + VOLUMES=0 COPY docker-entrypoint.sh /usr/local/bin/ COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg.template From 7275202d5e4969949df99e4c8d9a841472ec5ec6 Mon Sep 17 00:00:00 2001 From: LifetimeMistake <57274704+LifetimeMistake@users.noreply.github.com> Date: Sun, 14 Apr 2024 16:09:11 +0200 Subject: [PATCH 3/3] Fix backwards compatibility code The original code will not allow write access (set via the POST var) to endpoints to which read access is not provided. Before this fix, verify_access would allow write-only access to all endpoints if the POST var was set regardless of read access. --- verify.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verify.lua b/verify.lua index 5782393..c7169bc 100644 --- a/verify.lua +++ b/verify.lua @@ -2,7 +2,7 @@ core.register_fetches("verify_access", function(txn, api) -- env(api) check is kept for backwards compatibility local read_allowed = txn.f:env(api) == "1" or txn.f:env(api .. "_READ") == "1" -- env(POST) check is kept for backwards compatibility - local write_allowed = txn.f:env(api .. "_WRITE") == "1" or txn.f:env("POST") == "1" + local write_allowed = txn.f:env(api .. "_WRITE") == "1" or (read_allowed and txn.f:env("POST") == "1") local method = txn.f:method() local result = ((method == "GET" or method == "HEAD") and read_allowed)