diff --git a/SECURITY.md b/SECURITY.md index 46285397..1cd34737 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -81,7 +81,7 @@ An embargo period applies between fix and disclosure. The reporter will be notif The CashPilot worker (`drumsergio/cashpilot-worker`) creates and manages containers, so it needs the Docker API. Raw access to the Docker socket (`/var/run/docker.sock`) is root on the host. The UI never touches Docker; it commands workers over their authenticated API. -**The shipped compose files do not give the worker the socket.** A Docker socket proxy, [sockguard](https://github.com/CodesWhat/sockguard) `codeswhat/sockguard:2.4.5` pinned by digest, holds it read-only and serves the worker a filtered socket. It refuses every Docker API call the worker does not make, and it reads request bodies, so it refuses privileged containers, binds of `/` or of the socket, capabilities and devices outside the catalog's, host namespaces, a container without `no-new-privileges`, and exec of anything but the catalog's reader commands. [Security defaults: Docker socket proxy](docs/security-defaults.md#docker-socket-proxy) has the full policy, how it is tested, and what a compromised worker can still do through it. It is not yet a full root boundary: on sockguard 2.4.5 a compromised worker can still reach root in two steps, by joining another root container's process namespace or through a symlink planted in a host folder the proxy allows, so it narrows the risk rather than removing it. +**The shipped compose files do not give the worker the socket.** A Docker socket proxy, [sockguard](https://github.com/CodesWhat/sockguard) `codeswhat/sockguard:2.4.6` pinned by digest, holds it read-only and serves the worker a filtered socket. It refuses every Docker API call the worker does not make, and it reads request bodies, so it refuses privileged containers, binds of `/` or of the socket, capabilities and devices outside the catalog's, host namespaces, a container without `no-new-privileges`, and exec of anything but the catalog's reader commands. [Security defaults: Docker socket proxy](docs/security-defaults.md#docker-socket-proxy) has the full policy, how it is tested, and what a compromised worker can still do through it. It is not yet a full root boundary: on sockguard 2.4.6 a compromised worker can still reach root in two steps, through a symlink planted in a host folder the proxy allows (owner isolation closes the other one, joining another container's process namespace), so it narrows the risk rather than removing it. Other mitigations: diff --git a/app/socket_policy.py b/app/socket_policy.py index dd593adc..1924c0ff 100644 --- a/app/socket_policy.py +++ b/app/socket_policy.py @@ -74,7 +74,7 @@ ("GET", "/networks"), ("GET", "/networks/*"), # The LAN fence (app/lan_fence.py) creates cashpilot-isolated, moves - # earners onto it and recreates it after a prune. sockguard 2.4.5 cannot + # earners onto it and recreates it after a prune. sockguard 2.4.6 cannot # limit these by network name, so they are allowed for any network. ("POST", "/networks/create"), ("POST", "/networks/*/connect"), diff --git a/contrib/app-stores/casaos/CashPilot/docker-compose.yml b/contrib/app-stores/casaos/CashPilot/docker-compose.yml index 74d02772..180877fc 100644 --- a/contrib/app-stores/casaos/CashPilot/docker-compose.yml +++ b/contrib/app-stores/casaos/CashPilot/docker-compose.yml @@ -85,7 +85,7 @@ services: # Same service, same policy as the repository's docker-compose.yml; # scripts/socket_proxy_policy.py writes the policy into both. cashpilot-socket-proxy: - image: codeswhat/sockguard:2.4.5@sha256:d15f93e856626ff21f48400034d887874a4dafbbf6bb1e31799511beea07d84c + image: codeswhat/sockguard:2.4.6@sha256:9a7142d9f8254e7fde7543e046eff920463f5df27b36d959915b44fb4878d06f container_name: cashpilot-socket-proxy # Root with no capabilities: uid 0 opens the root-owned host socket, and # group 1000 (the worker's) lets it give the worker its socket. diff --git a/contrib/app-stores/runtipi/cashpilot/docker-compose.yml b/contrib/app-stores/runtipi/cashpilot/docker-compose.yml index 79b812ce..f217b5fc 100644 --- a/contrib/app-stores/runtipi/cashpilot/docker-compose.yml +++ b/contrib/app-stores/runtipi/cashpilot/docker-compose.yml @@ -51,7 +51,7 @@ services: # Same service, same policy as the repository's docker-compose.yml; # scripts/socket_proxy_policy.py writes the policy into both. cashpilot-socket-proxy: - image: codeswhat/sockguard:2.4.5@sha256:d15f93e856626ff21f48400034d887874a4dafbbf6bb1e31799511beea07d84c + image: codeswhat/sockguard:2.4.6@sha256:9a7142d9f8254e7fde7543e046eff920463f5df27b36d959915b44fb4878d06f container_name: cashpilot-socket-proxy # Root with no capabilities: uid 0 opens the root-owned host socket, and # group 1000 (the worker's) lets it give the worker its socket. diff --git a/docker-compose.build.yml b/docker-compose.build.yml index 62e9dfe7..0f2ee66c 100644 --- a/docker-compose.build.yml +++ b/docker-compose.build.yml @@ -95,7 +95,7 @@ services: # Pinned by digest and bumped on purpose; the in-app update never recreates # it (it runs `up --no-deps` on the UI and worker only). cashpilot-socket-proxy: - image: codeswhat/sockguard:2.4.5@sha256:d15f93e856626ff21f48400034d887874a4dafbbf6bb1e31799511beea07d84c + image: codeswhat/sockguard:2.4.6@sha256:9a7142d9f8254e7fde7543e046eff920463f5df27b36d959915b44fb4878d06f container_name: cashpilot-socket-proxy # Root with no capabilities: uid 0 opens the root-owned host socket, and # group 1000 (the worker's) lets it give the worker its socket. diff --git a/docker-compose.fleet.yml b/docker-compose.fleet.yml index fda0f4e3..49beb609 100644 --- a/docker-compose.fleet.yml +++ b/docker-compose.fleet.yml @@ -102,7 +102,7 @@ services: # Pinned by digest and bumped on purpose; the in-app update never recreates # it (it runs `up --no-deps` on the UI and worker only). cashpilot-socket-proxy: - image: codeswhat/sockguard:2.4.5@sha256:d15f93e856626ff21f48400034d887874a4dafbbf6bb1e31799511beea07d84c + image: codeswhat/sockguard:2.4.6@sha256:9a7142d9f8254e7fde7543e046eff920463f5df27b36d959915b44fb4878d06f container_name: cashpilot-socket-proxy # Root with no capabilities: uid 0 opens the root-owned host socket, and # group 1000 (the worker's) lets it give the worker its socket. diff --git a/docker-compose.worker.yml b/docker-compose.worker.yml index 4081a520..61499bd1 100644 --- a/docker-compose.worker.yml +++ b/docker-compose.worker.yml @@ -54,7 +54,7 @@ services: # Pinned by digest and bumped on purpose; the in-app update never recreates # it (it runs `up --no-deps` on the UI and worker only). cashpilot-socket-proxy: - image: codeswhat/sockguard:2.4.5@sha256:d15f93e856626ff21f48400034d887874a4dafbbf6bb1e31799511beea07d84c + image: codeswhat/sockguard:2.4.6@sha256:9a7142d9f8254e7fde7543e046eff920463f5df27b36d959915b44fb4878d06f container_name: cashpilot-socket-proxy # Root with no capabilities: uid 0 opens the root-owned host socket, and # group 1000 (the worker's) lets it give the worker its socket. diff --git a/docker-compose.yml b/docker-compose.yml index 2ce2af05..5b322d12 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,7 +117,7 @@ services: # Pinned by digest and bumped on purpose; the in-app update never recreates # it (it runs `up --no-deps` on the UI and worker only). cashpilot-socket-proxy: - image: codeswhat/sockguard:2.4.5@sha256:d15f93e856626ff21f48400034d887874a4dafbbf6bb1e31799511beea07d84c + image: codeswhat/sockguard:2.4.6@sha256:9a7142d9f8254e7fde7543e046eff920463f5df27b36d959915b44fb4878d06f container_name: cashpilot-socket-proxy # Root with no capabilities: uid 0 opens the root-owned host socket, and # group 1000 (the worker's) lets it give the worker its socket. diff --git a/docs/security-defaults.md b/docs/security-defaults.md index 589bdddd..b966eb8d 100644 --- a/docs/security-defaults.md +++ b/docs/security-defaults.md @@ -101,9 +101,9 @@ machine. In the shipped compose files that holder is a proxy, not the worker. The shipped compose files (`docker-compose.yml`, `docker-compose.fleet.yml`, `docker-compose.worker.yml`, `docker-compose.build.yml`) run -[sockguard](https://github.com/CodesWhat/sockguard) 2.4.5 as +[sockguard](https://github.com/CodesWhat/sockguard) 2.4.6 as `cashpilot-socket-proxy`, pinned by digest -(`codeswhat/sockguard:2.4.5@sha256:d15f93e856626ff21f48400034d887874a4dafbbf6bb1e31799511beea07d84c`). +(`codeswhat/sockguard:2.4.6@sha256:9a7142d9f8254e7fde7543e046eff920463f5df27b36d959915b44fb4878d06f`). It holds the socket read-only, runs as root with every capability dropped and no network, and serves a filtered socket in the `cashpilot-docker-api-owned` volume. The worker mounts that volume and sets `DOCKER_HOST` to it. It has no Docker socket @@ -164,7 +164,7 @@ those. Images are not scoped: a custom service can still pull any image. privileged container, a bind of `/` or of the socket, an added capability. Owner isolation closes joining another container's process namespace, which read a root container's files through `/proc/1/root`. One two-step way is -left, because sockguard 2.4.5 has no setting for it: +left, because sockguard 2.4.6 has no setting for it: - **A symlink in an allowed folder.** sockguard checks a bind's source as a string; Docker follows symlinks when it mounts. A container that binds an @@ -198,7 +198,7 @@ stands between the worker and root. folder is not bindable at all. - Create Docker networks, and attach, detach and delete the ones it created. The LAN fence needs this to build `cashpilot-isolated` and move earners onto - it, and sockguard 2.4.5 cannot limit it to that one network. The proxy lets bridge driver options and + it, and sockguard 2.4.6 cannot limit it to that one network. The proxy lets bridge driver options and a forced disconnect through, because the fence uses both; custom drivers, swarm scope, custom address plans and endpoint settings stay refused, except network aliases, which sockguard always lets through. It adds little reach, since the worker