Docker daemon is extensible by plugins, allowing network, volumes, authorization and (in last version) logs and metrics management to be plugged in and controlled from external services.
As I'm investigating Docker infrastructure security in the contexte of a CI/CD service, I've been looking for ways to lock down docker daemon so it only allows a subset of the Docker API. Typically, I want to disable bind mounts to ensure one can't just make crazy things like :
docker run -v /:/target alpine rm -rf /target/*
(don't try this at home)
There's various solutions to prevent bad things to happen, including enabling the user namespace, but a second protection barrier still makes sense #DefenseInDepth.
So I've been investigating authorization plugin for docker daemon.
Authorization plugin only implements 2 http endpoints, one to authorize the incoming API request, and the second to authorize the resulting output. This design allows to filter some API calls or to block some data to be exposed by the daemon. It's sometime simpler to let the daemon run some read-only API calls (like inspect) then check the result to ensure no sensible data is exposed.
To write such a plugin, you'll need to understand which API are actually used by a docker CLI command. Typically, docker run -it actually uses a bunch of them to pull image, create and start container, attach stdin/out. A simple way is to just look a docker daemon logs when debug is enabled.
Also note authorization plugin can't make changes to the incoming request, so you can't for sample remove some parameters or add some additional ones, like forcing a label to a container to track which user created it. I'd like to do this so I can prevent a later docker exec command to enter a container someone else created. To do this from this plugin I'll need to become stateful and store some container :: owner map.
I spent some time fighting with GOPATH and (lack of) dependency management in Go language. Typically, go-plugin-helped do depend on go-connections, and I've been hurt by this change as go-connection has some tags but go-plugin-helper doesn't, so dep I'm using to manage dependencies just pulled incompatible code. I like go development but dependency hells is really a terrible issue for this ecosystem.
Plugin's code is available on github. There's nothing magic here, just a whitelist for API calls I authorize and a specific parameter check for 'docker run' to ensure no bind mount is user, nor Privileged containers creation.
I could also have used twistlock authz plugin with a custom profile, but
To avoid breaking my local docker installation (sic) I created a virtualbox test environment using docker-machine (yes, despite docker4desktop supersede it for developer experience, this project still is useful). So we now have a sandbox, let's play
docker build -t myplugin . && docker plugin install myplugin
but ... that's not the case (yet - maybe this will be improved in a future release).
You first have to build your plugin and provide a root filesystem for it to run containerized. So the simpler solution is to build a docker image and export it's filesystem
One also have to create a JSON descriptor for the plugin. There's various options in this file, but for this simple plugin descriptor is trivial:As I'm investigating Docker infrastructure security in the contexte of a CI/CD service, I've been looking for ways to lock down docker daemon so it only allows a subset of the Docker API. Typically, I want to disable bind mounts to ensure one can't just make crazy things like :
docker run -v /:/target alpine rm -rf /target/*
(don't try this at home)
There's various solutions to prevent bad things to happen, including enabling the user namespace, but a second protection barrier still makes sense #DefenseInDepth.
Step 1 : Architecture
Authorization plugin are invoked by deamon for any API call, either from docker socket or http clients. User identity is based on TLS key, so one could create such a plugin with user profiles, so some super-admin with adequate TLS key could do anything, and all others would have restricted access to the API. You can chain authorization plugins, so depending your needs you can keep them simple, or just plug existing ones.Authorization plugin only implements 2 http endpoints, one to authorize the incoming API request, and the second to authorize the resulting output. This design allows to filter some API calls or to block some data to be exposed by the daemon. It's sometime simpler to let the daemon run some read-only API calls (like inspect) then check the result to ensure no sensible data is exposed.
To write such a plugin, you'll need to understand which API are actually used by a docker CLI command. Typically, docker run -it actually uses a bunch of them to pull image, create and start container, attach stdin/out. A simple way is to just look a docker daemon logs when debug is enabled.
Also note authorization plugin can't make changes to the incoming request, so you can't for sample remove some parameters or add some additional ones, like forcing a label to a container to track which user created it. I'd like to do this so I can prevent a later docker exec command to enter a container someone else created. To do this from this plugin I'll need to become stateful and store some container :: owner map.
Step 2 : Code
I've implemented my plugin in Go, using go-plugin-helper skeleton. There's not much done by this lib, but I'm lazy :PI spent some time fighting with GOPATH and (lack of) dependency management in Go language. Typically, go-plugin-helped do depend on go-connections, and I've been hurt by this change as go-connection has some tags but go-plugin-helper doesn't, so dep I'm using to manage dependencies just pulled incompatible code. I like go development but dependency hells is really a terrible issue for this ecosystem.
Plugin's code is available on github. There's nothing magic here, just a whitelist for API calls I authorize and a specific parameter check for 'docker run' to ensure no bind mount is user, nor Privileged containers creation.
I could also have used twistlock authz plugin with a custom profile, but
- I'm not sure this plugin is more than just a proof of concept (twistlock offers a proprietary more advanced security service, so don't expect this project to do more than just basic filtering)
- One learn more by getting hands dirty. Or maybe this is some sort of NIH syndrom :P
Step 3 : Test
You can ask my colleagues : I'm not used to write unit tests (and that's probably bad) but here I did at least for a simple one :P. But unit tests only cover local code execution, let's try to deploy this plugin on a real docker daemon and validate it actually does the job.To avoid breaking my local docker installation (sic) I created a virtualbox test environment using docker-machine (yes, despite docker4desktop supersede it for developer experience, this project still is useful). So we now have a sandbox, let's play
Step 4 : Deploy
Authorization plugin deployment is not a smooth process. I would expect I can run something like:docker build -t myplugin . && docker plugin install myplugin
but ... that's not the case (yet - maybe this will be improved in a future release).
You first have to build your plugin and provide a root filesystem for it to run containerized. So the simpler solution is to build a docker image and export it's filesystem
docker build -t authobot . (...) mkdir rootfs ID=$(docker run -d authobot) docker export $ID | tar -x -C rootfs docker kill $ID docker rm $ID
{ "Description": "Authorization plugin for Docker", "Documentation": "https://github.com/ndeloof/authobot/blob/master/README.md", "Entrypoint": [ "/bin/authobot" ], "Interface": { "Socket": "authobot.sock", "Types": [ "docker.authz/1.0" ] } }
We can now register this plugin on docker daemon and enable it
docker plugin create authobot . docker plugin enable authobot
Here, the funny thing is that the plugin is enabled, but not in use. #plugin channel on Docker slack was a great assistance for me to understand the issue. Thanks a lot to Brian Goff for taking time to assist a noob :P
Authorization plugins have to be explicitly set as docker daemon options, and can't be enabled/disabled from the API. It makes sense as one could then use the API to disable authorization :-\ but makes the code/deploy/test cycle bit more slow and annoying.
So let's now actually enable the plugin :
sudo vi /var/lib/boot2docker/profile(adjust to your docker installation if you don't use a boot2docker VM)
Add --authorization-plugin=authobot to docker daemon arguments
restart daemon :
sudo /etc/init.d/docker restart
Step 5 : Done!
My docker daemon now accepts the few API I've whitelisted, but nothing much$ docker run -it ubuntu echo 'hello world!' hello world! $ docker ps Error response from daemon: plugin authobot:latest failed with error: AuthZPlugin.AuthZReq: /v1.30/containers/json is not authorized
Step 6 : What's next?
Code is currently very minimalist, there's many thing I could improve.Generally speaking, it only partially cover my need : I will need to track the owner of each container to prevent one try to access other's containers.
Also, as an authorization plugin doesn't allow to change the API call payload, there's few things that will be harder to implement, like per user resource quota, and few other protections I'd like to implement. This plugin anyway will at least prevent the most obvious security issues.