Restricting Docker Socket Proxy by Container
Further enhance security for socket-proxy usage with this one wierd trick
Intro
In the homelab it is common to find services that consume some part of the Docker API in order to provide easy discovery or monitoring of your docker containers/services.
Most of these services only require access to a specific part of the API, and it is usually read-only. However, the normal way of accessing the Docker API does not provide any access control for these services and so solutions like docker-socket-proxy have been created to restrict this access.
Unfortunately, even these solutions can be too broad in the access they expose: their access control is granular only to the api “category” level without (usually) providing any control over the individual routes and resources.
Effectively: a service that only needs access to one specific container still has access to read data for any container on that docker host.
In this post I introduce a new “proxy for your proxy”, foxxmd/docker-proxy-filter, that enables filtering Docker API responses to specific containers, to further enhance security and restrict access for specific usecases. (Skip to explanation)
What is the Docker API?
The Docker Daemon running on your host machine communicates with programs using an HTTP API (usually) served over unix socket, the Docker Engine API. The aforementioned socket is a network socket accessible through the filesystem, normally at /var/run/docker.sock.
Using this socket the Docker client, and any other program with access to docker.sock, can interact with any part of your Docker instance: start/stop container, create new containers, volumes, networks, get container logs and info, etc…
If you aren’t familiar with unix sockets all you need to know about communication through it is that it’s essentially the same as making normal HTTP calls.
For instance, to get a list of all containers running on a host, a call to the Docker API would be:
1 2 3 $ curl --unix-socket /var/run/docker.sock http:/v1.50/containers/json # response output: [{"Id":"2c97d3e28dafa0b9af160bcbc67c5785930cfa2fbef2802ffe0ff8a76285da47","Names":["/my-service-a"],"Image":"qmcgaw/gluetun", ... ]
This makes communication with the Docker Daemon easy but it also poses a problem: the Docker API has no authentication and no access control. Other than filesystem permissions applied to docker.sock, if a program can access docker.sock then it can do anything with Docker.
This is not an issue for the Docker client since that’s its intended use, but if another program or service wants to use the Docker API there is no way to control what it can do with that access.
Restricting Docker API with Socket Proxy
Smart folks realized this unrestricted access could be a problem and quickly came up with a good solution: an HTTP server that proxies requests to docker.sock for you.
Rather than giving your service direct access to docker.sock you can instead configure it to connect through a docker socket proxy to get the same interface to the Docker API. Then, the proxy can be configured to allow/disallow access to parts of the Docker API by blocking requests to routes by name.
As an example, your Service A only needs to get a list of containers to see which have the label
my.cool.label=foo:Our docker socket proxy can disallow all requests except those that are prefixed with
/containers. It can additionally only allowGETrequests to this route. Now, Service A only has access to the relevant part of the API for containers and has read-only access (cannot restart/stop/create containers).
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
services:
socket-proxy:
image: lscr.io/linuxserver/socket-proxy:latest
environment:
- ALLOW_START=0
- ALLOW_STOP=0
- ALLOW_RESTARTS=0
- CONTAINERS=1
- INFO=1
- POST=0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- 2375:2375
1
2
3
$ curl http://localhost:2375/v1.50/containers/json
# response output:
[{"Id":"2c97d3e28dafa0b9af160bcbc67c5785930cfa2fbef2802ffe0ff8a76285da47","Names":["/my-service-a"],"Image":"qmcgaw/gluetun", ... ]
1
2
3
4
5
$ curl http://localhost:2375/v1.50/volumes
# response output:
<html><body><h1>403 Forbidden</h1>
Request forbidden by administrative rules.
</body></html>
1
2
3
4
$ curl -X POST http://localhost:2380/containers/7a3390449a27211b6d792d3ed74cd2fdba1d23fcf6925fe8540e38f647597f7a/stop
<html><body><h1>403 Forbidden</h1>
Request forbidden by administrative rules.
</body></html>
There are many docker socket proxy implementations that exist today, but the most popular ones are:
Not Enough Restriction
The existing socket proxy implementations are great but they are still lacking more granular access controls, in my opinion.
Consider our example from above where we want to get a list of containers with the label my.cool.label=foo:
We can configure our socket proxy to allow only access to container endpoints, but when Service A makes a request to /containers/json it is still getting a response with all the containers on the host. Even though it may only need to be able to see 1 or 2 containers to do its job, it can read data about all other 50+ containers.
Additionally, Service A can still make any GET request for any container, even if it doesn’t need them. Concerningly, the Inspect a container endpoint returns all environmental variables for a container. What if those ENVs contain secrets and sensitive keys? It can also Get containers logs for any container which may also contain sensitive data.
While Service A may use the API as intended this won’t stop an attacker from exploiting it or accessing your network from another vector. Anyone who can access the port of the socket proxy can read any data about any container. Not great!
Mitigating Socket Proxy Access by running in the same Stack
Some of the attack vectors mentioned above can be mitigated by restricting how a socket proxy is exposed. If the service that needs docker access is on the same host that needs to be accessed then the socket proxy can be created in the same stack as the service and communication can happen on the isolated stack network, rather than over the docker bridge.
Example
❌ Using socket proxy over the docker bridge (don’t do this!)
1
2
3
4
5
6
7
8
9
10
11
services:
docker-socket-proxy:
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- POST=0
- CONTAINERS=1
ports:
# any client can connect to proxy using HOST_IP:2375, not good!
- 2375:2375
1
2
3
4
5
6
services:
serviceA:
image: myService
environment:
# connecting over docker bridge, uh oh!
- DOCKER_HOST=192.168.0.101:2375
✅ Using socket proxy inside an isolated stack network
1
2
3
4
5
6
7
8
9
10
11
12
13
14
services:
serviceA:
image: myService
environment:
# connecting via internal stack network
- DOCKER_HOST=docker-socket-proxy:2375
docker-socket-proxy:
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- POST=0
- CONTAINERS=1
# no ports required since connection stay inside stack network
This can be extended to remote hosts if you use docker overlay networks with Docker Swarm. However, this isn’t possible if you don’t have Swarm setup or there are other network factors that prevent overlay networks from working across hosts.
Restricting Docker API to Specific Containers
To address the problem of exposing all containers I have created docker-proxy-filter (DPF).
DPF is an additional proxy that sits in front of an existing socket proxy and enables you to filter Docker API responses and container specific routes based on container names and labels. It is used exactly the same as a regular socket proxy, as far as your services are concerned.
Using filters with DPF changes Docker API container routes like so:
- Filters List Containers responses so any container that does not match filters is excluded from the returned list
- Any other Container endpoints will return 404 if it does not match a filter
Now, in addition to the restrictions configured by your normal socket proxy, you can restrict the containers that are exposed by your socket-proxy.
Going back to our initial example of
Service A wants to get a list of containers that have label
my.cool.label=fooWe can configure DPF to only expose containers that have that exact label. The List Containers endpoint now returns 2 containers instead of 50+. Calls to
/containers/{id}/jsonreturn 404 if the container does not have our label attached. We’re only exposing what is needed!
docker-proxy-filter Usage
Like other socket proxies, DPF is configured through environmental variables passed to the container. This, and more, is covered in docker-proxy-filter’s repository README.
PROXY_URL- The URL of the existing socket proxy DPF will connect through EXhttp://socket-proxy:2375CONTAINER_NAMES(optional) - A comma-delimited list of values that should appear in valid container names. Any value matched will mark the container as valid.- EX
frigate,postgreswill match container names likefrigate-nvidiaandpostgres11
- EX
CONTAINER_LABELS(optional) - A comma-delimited listed of key-values that should appear in valid container labels. Any container label that matches any filter value will be marked as valid.- Values are optional so it is possible to search only for label keys.
- EX
CONTAINER_LABELS=foowill match any container labels whose key containsfoolikecom.mylabel.foo=bar
- EX
- Full key-values are matched together, but as “part” of larger strings.
- EX
CONTAINER_LABELS=com.foo=barwill match any container label where the key containscom.fooAND value containsbarlikecom.foo.fun=barstuff
- EX
- Values are optional so it is possible to search only for label keys.
SCRUB_ENVS(optional) - Replaces environmental variables list with an empty list in Container Inspect responses.
CONTAINER_NAMES and CONTAINER_LABELS are independent filters. Any container that matches either filter will be valid.
Example
Homepage Docker Integration
Scenario
Homepage, a popular startpage application, can use the Docker API to discover services automatically for its dashboard.
Homepage uses the Docker API to:
- Query the List Containers (
/containers/json) endpoint to find services by label. - Query the Inspect Container (
/containers/{id}/json) endpoint for service state, among other things.
It does not need access to any other Docker API endpoints, and does not need access to any container that does not have homepage in its label keys.
We have Homepage deployed on Server A and we want it to discover services running on Server B at 192.168.0.101.
Implementation
Deploy docker-proxy-filter with a socket-proxy implementation
I am choosing to use wollomatic/socket-proxy as the backing proxy service for DPF because it provides additional functionality by allowing routes to be restricted by regular expression. The configuration below for wollomatic/socket-proxy:
- Only allows connections from a container named
proxy-container(docker-proxy-filter) - Disallows all methods except
GET - Only allows routes to
containers/*
For docker-proxy-filter:
- we set
CONTAINER_LABELS=homepageso that only containers that containhomepagein their labels key are returned for Container List and for access to individual routes - we set
SCRUB_ENVS=trueto prevent Homepage from being able to read environmental variables when inspecting a container - expose port
2375so that Homepage can connect to it
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
services:
proxy-container:
image: foxxmd/docker-proxy-filter
environment:
- PROXY_URL=http://socket-proxy:2375
# only containers with a label key containing "homepage" will be returned or accessible
- CONTAINER_LABELS=homepage
# replace env variables in Docker Container api responses with an empty list
- SCRUB_ENVS=true
ports:
- 2375:2375
socket-proxy:
image: lscr.io/linuxserver/socket-proxy:latest
environment:
- ALLOW_START=0
- ALLOW_STOP=0
- ALLOW_RESTARTS=0
- CONTAINERS=1
- INFO=0
- POST=0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
Configure Homepage
Finally, we configure docker.yaml in our Homepage configuration to connect to docker-proxy-filter like a normal socket-proxy:
1
2
3
server-b:
host: 192.168.0.101
port: 2375
And we’re done! Now, Homepage (and any other actor) connecting to 192.168.0.101:2375 will only be able to get read-only access to containers that have homepage in their labels, rather than all containers as with a normal socket proxy.
Monitor All Services with scrubbed ENVs
Scenario
Maybe you want to be able to monitor all services for a host. Or can’t narrow down to a subset immediately. You can still expose container info while removing sensitive environmental variables and restricting routes to prevent inspect container contents in a way that might reveal sensitive data.
Implementation
allowGETallows only ping, info, version, and requests to List Container and Container InspectSCRUB_ENVSreplaces environmental variables in Container Inspect with an empty list- Omitting
CONTAINER_LABELSandCONTAINER_NAMESmeans all containers will be marked as valid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
services:
proxy-container:
image: foxxmd/docker-proxy-filter
environment:
- PROXY_URL=http://socket-proxy:2375
- SCRUB_ENVS=true
ports:
- 2375:2375
socket-proxy:
image: wollomatic/socket-proxy:1.10.0
restart: unless-stopped
user: 0:0
mem_limit: 64M
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges
command:
- '-loglevel=debug'
- '-listenip=0.0.0.0'
- '-allowfrom=proxy-container'
- '-allowHEAD=/_ping'
- '-allowGET=/_ping|/(v1\..{1,2}/)?((info|version)$|containers/(json$|.*/json$))'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro