├── .gitignore ├── Dockerfile ├── Gopkg.toml ├── README.md ├── config.json ├── logo.jpg ├── main.go ├── plugin.go └── plugin_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | authobot 4 | 5 | # use `dep ensure` to manage vendor content 6 | vendor/ 7 | Gopkg.lock 8 | 9 | rootfs/ 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as build 2 | RUN mkdir -p /go/src/app 3 | WORKDIR /go/src/app 4 | COPY . /go/src/app 5 | RUN go build 6 | 7 | FROM golang:alpine 8 | COPY --from=build /go/src/app/app /bin/authobot 9 | 10 | # this is where authobot.sock will be created on startup 11 | RUN mkdir -p /run/docker/plugins/ 12 | 13 | ENTRYPOINT ["/bin/authobot"] -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | [[override]] 3 | name = "github.com/docker/go-connections" 4 | revision = "3ede32e2033de7505e6500d6c868c2b9ed9f169d" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authobot 2 | 3 | ![logo](logo.jpg) 4 | 5 | 6 | Authobot is a simple authorization plugin for docker to prevent some API usages 7 | that we know will expose hosts data and are not required for a legitimate use of docker 8 | from a containerized jenkins build agent. 9 | 10 | We prevent 11 | - running container with bind mounts 12 | - running privileged container 13 | - _more to come_ 14 | 15 | We also provide a whitelist of authorized API URIs, based on docker-pipeline / declarative-pipeline requirements. 16 | Any other API call will be rejected. 17 | 18 | ## Samples 19 | 20 | ``` 21 | ➜ # 22 | ➜ # let's run a container 23 | ➜ # 24 | ➜ docker run --rm -t ubuntu echo Hello 25 | Hello 26 | 27 | ➜ # 28 | ➜ # let's now run a _privileged_ container (can access hosts' devices) 29 | ➜ # 30 | ➜ docker run --rm -t --privileged ubuntu echo Hello 31 | docker: Error response from daemon: authorization denied by plugin authobot:latest: use of Privileged contianers is not allowed. 32 | See 'docker run --help'. 33 | 34 | ➜ # 35 | ➜ # hum, let's bind mount host filesystem to hack all it's secrets 36 | ➜ # 37 | ➜ docker run --rm -t -v /:/host ubuntu echo Hello 38 | docker: Error response from daemon: authorization denied by plugin authobot:latest: use of bind mounts is not allowed. 39 | See 'docker run --help'. 40 | 41 | ➜ # 42 | ➜ # ok, let's just mount a volume to persist our data 43 | ➜ # 44 | ➜ docker run --rm -t -v some_volume:/host ubuntu echo Hello 45 | Hello 46 | ``` 47 | 48 | 49 | 50 | ## Contribute / hack 51 | 52 | we use [dep](https://github.com/golang/dep) to manage dependencies. 53 | run `dep ensure` to generate a local `vendor` folder so you can hack and build the plugin. 54 | 55 | build with `docker build -t authobot .` 56 | 57 | create rootfs directory, and export container filesystem 58 | ``` 59 | rm -rf rootfs 60 | mkdir rootfs 61 | ID=$(docker run -d authobot:latest) 62 | docker export $ID | tar -x -C rootfs 63 | docker kill $ID 64 | docker rm $ID 65 | ``` 66 | 67 | Then, install and enable plugin on your local docker daemon 68 | ``` 69 | docker plugin create authobot $(pwd) 70 | docker plugin enable authobot 71 | ``` 72 | 73 | change docker daemon configuration to include `--authorization-plugin=authobot` and restart daemon. 74 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Authorization plugin for Docker", 3 | "Documentation": "https://github.com/ndeloof/authobot/blob/master/README.md", 4 | "Entrypoint": [ 5 | "/bin/authobot" 6 | ], 7 | 8 | "Interface": { 9 | "Socket": "authobot.sock", 10 | "Types": [ 11 | "docker.authz/1.0" 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndeloof/authobot/2b09b01c1e879bc597b486fa1d8e908f868fe54e/logo.jpg -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "flag" 6 | "github.com/Sirupsen/logrus" 7 | "github.com/docker/go-plugins-helpers/authorization" 8 | ) 9 | 10 | func main() { 11 | fmt.Println("hello") 12 | 13 | flag.Parse() 14 | 15 | authobot, err := newPlugin() 16 | if err != nil { 17 | logrus.Fatal(err) 18 | } 19 | 20 | h := authorization.NewHandler(authobot) 21 | 22 | if err := h.ServeUnix("authobot", 0); err != nil { 23 | logrus.Fatal(err) 24 | } 25 | } -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/url" 7 | "regexp" 8 | 9 | "github.com/docker/go-plugins-helpers/authorization" 10 | "github.com/docker/engine-api/types/container" 11 | "github.com/docker/engine-api/types/mount" 12 | "github.com/pkg/errors" 13 | "fmt" 14 | "strings" 15 | ) 16 | 17 | func newPlugin() (*authobot, error) { 18 | return &authobot{}, nil 19 | } 20 | 21 | var ( 22 | create = regexp.MustCompile(`/v.*/containers/create`) 23 | 24 | whitelist = []regexp.Regexp{ 25 | *regexp.MustCompile(`/_ping`), 26 | *regexp.MustCompile(`/v.*/version`), 27 | 28 | *create, 29 | *regexp.MustCompile(`/v.*/containers/.+/start`), 30 | *regexp.MustCompile(`/v.*/containers/.+/stop`), 31 | *regexp.MustCompile(`/v.*/containers/.+/kill`), 32 | *regexp.MustCompile(`/v.*/containers/.+/json`), // inspect 33 | *regexp.MustCompile(`/v.*/containers/.+/exec`), 34 | *regexp.MustCompile(`/v.*/containers/.+/attach`), 35 | *regexp.MustCompile(`/v.*/containers/.+/wait`), 36 | *regexp.MustCompile(`/v.*/containers/.+/resize`), 37 | *regexp.MustCompile(`/v.*/exec/.+/start`), 38 | *regexp.MustCompile(`/v.*/exec/.+/json`), 39 | 40 | *regexp.MustCompile(`/v.*/build`), 41 | *regexp.MustCompile(`/v.*/images/create`), // pull 42 | *regexp.MustCompile(`/v.*/images/.+/json`), // inspect 43 | *regexp.MustCompile(`/v.*/images/.+/push`), 44 | *regexp.MustCompile(`/v.*/images/.+/tag`), 45 | *regexp.MustCompile(`/v.*/images/.+`), // remove 46 | } 47 | ) 48 | 49 | type authobot struct { 50 | } 51 | 52 | type configWrapper struct { 53 | *container.Config 54 | HostConfig *container.HostConfig 55 | } 56 | 57 | // --- implement authorization.Plugin 58 | 59 | func (p *authobot) AuthZReq(req authorization.Request) authorization.Response { 60 | 61 | uri, err := url.QueryUnescape(req.RequestURI) 62 | if err != nil { 63 | return authorization.Response{Err: err.Error()} 64 | } 65 | 66 | // Remove query parameters 67 | i := strings.Index(uri, "?") 68 | if i > 0 { 69 | uri = uri[:i] 70 | } 71 | 72 | fmt.Println("checking "+req.RequestMethod+" request to '"+uri+"' from user : "+req.User) 73 | 74 | err = p.Authorized(uri) 75 | if err != nil { 76 | return authorization.Response{Err: err.Error()} 77 | } 78 | 79 | 80 | if req.RequestMethod == "POST" && create.MatchString(uri) { 81 | if req.RequestBody != nil { 82 | body := &configWrapper{} 83 | if err := json.NewDecoder(bytes.NewReader(req.RequestBody)).Decode(body); err != nil { 84 | return authorization.Response{Err: err.Error()} 85 | } 86 | 87 | if body.HostConfig.Privileged { 88 | return authorization.Response{Msg: "use of Privileged contianers is not allowed"} 89 | } 90 | 91 | // Binds is the old API 92 | for _, b := range body.HostConfig.Binds { 93 | if (b[:1] == "/") { 94 | return authorization.Response{Msg: "use of bind mounts is not allowed"} 95 | } 96 | } 97 | 98 | // Mounts is the new API 99 | for _, m := range body.HostConfig.Mounts { 100 | if m.Type == mount.TypeBind { 101 | return authorization.Response{Msg: "use of bind mounts is not allowed"} 102 | } 103 | 104 | } 105 | } 106 | } 107 | 108 | return authorization.Response{Allow: true} 109 | } 110 | 111 | func (p *authobot) AuthZRes(req authorization.Request) authorization.Response { 112 | return authorization.Response{Allow: true} 113 | } 114 | 115 | // --- 116 | 117 | func (p *authobot) Authorized(uri string) error { 118 | for _, m := range whitelist { 119 | if m.MatchString(uri) { 120 | return nil; 121 | } 122 | } 123 | return errors.New(uri + " is not authorized") 124 | } 125 | 126 | -------------------------------------------------------------------------------- /plugin_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestDockerVersionWhitelisted(t *testing.T) { 6 | authobot, _ := newPlugin() 7 | err := authobot.Authorized("/v1.30/version") 8 | if err != nil { 9 | t.Error("/version was not authorized") 10 | } 11 | } 12 | 13 | func TestDockerInspectWhitelisted(t *testing.T) { 14 | authobot, _ := newPlugin() 15 | err := authobot.Authorized("/v1.30/containers/1234abcd/json") 16 | if err != nil { 17 | t.Error("/containers/1234abcd/json was not authorized") 18 | } 19 | 20 | } 21 | 22 | func TestDockerPSBlacklisted(t *testing.T) { 23 | authobot, _ := newPlugin() 24 | err := authobot.Authorized("/v1.30/containers/json") 25 | if err == nil { 26 | t.Error("/containers/json should have been rejected") 27 | } 28 | } 29 | --------------------------------------------------------------------------------