├── .gitignore ├── AUTHORS ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── build-deb.sh ├── config-example.json ├── docker-image-policy.service ├── main.go └── plugin.go /.gitignore: -------------------------------------------------------------------------------- 1 | github.com 2 | golang.org 3 | docker-image-policy 4 | src 5 | pkg 6 | bin 7 | *.deb 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | ======= 2 | Authors 3 | ======= 4 | 5 | Simon Pirschel is the main developer of docker-image-policy-plugin. He is the 6 | founder, owner, maintainer and lead of the docker-image-policy-plugin project, 7 | as well as author of the docker-image-policy-plugin code and documentation. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Simon Pirschel 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prefix = /usr/local 2 | VERSION = $(shell cat VERSION) 3 | 4 | all: build 5 | 6 | build: 7 | go build -o docker-image-policy -ldflags "-X main.version=$(VERSION)" . 8 | 9 | install: build 10 | install -D docker-image-policy $(DESTDIR)$(prefix)/bin/docker-image-policy 11 | 12 | clean: 13 | -rm -f docker-image-policy 14 | go clean 15 | 16 | distclean: clean 17 | 18 | uninstall: 19 | -rm -f $(DESTDIR)$(prefix)/bin/docker-image-policy 20 | 21 | .PHONY: all install clean distclean uninstall 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Image policy plugin 2 | 3 | `docker-image-policy` is a Docker *Access authorization plugin* written Go to control 4 | which Images are allowed to be pulled by your Docker daemon. The plugin is using 5 | the [AuthZPlugin API](https://docs.docker.com/engine/extend/plugins_authorization/) 6 | by Docker. Black and Whitelistings are expressed through regular expression. 7 | A default policy if no listing matched can be defined also. 8 | 9 | **Supported: Docker Engine >= 1.11** 10 | 11 | ## Building 12 | 13 | To build this plugin Go >= 1.7 and proper GOPATH setup is required. 14 | 15 | ```sh 16 | $ make 17 | ``` 18 | 19 | ## Build Debian Package 20 | 21 | **PACKAGES WILL BE INSTALLED** 22 | 23 | *Please consider using a Docker container for building the Debian package.* 24 | 25 | ```sh 26 | $ sudo sh build-deb.sh 27 | ``` 28 | 29 | Example with Docker container: 30 | 31 | ```sh 32 | $ git clone git@github.com:freach/docker-image-policy-plugin.git ~/docker-image-policy-plugin 33 | $ docker run -it --rm -v ~/docker-image-policy-plugin:/go golang bash 34 | $ sh build-deb.sh 35 | $ ls *.deb 36 | ``` 37 | 38 | ## Get started 39 | 40 | ### Plugin configuration 41 | 42 | Add a config file (default: /etc/docker/docker-image-policy.json), and configure the plugin like so: 43 | 44 | ```json 45 | { 46 | "whitelist": [ 47 | "^alpine:", 48 | "^docker\\.elastic\\.co/beats/filebeat:", 49 | "^gcr\\.io/google_containers", 50 | "^mysql:", 51 | "^nginx:", 52 | "^php:", 53 | "^apache:", 54 | "^quay\\.io/calico/cni", 55 | "^quay\\.io/calico/node", 56 | "^quay\\.io/coreos/flannel" 57 | ], 58 | "blacklist": [ 59 | "^docker:" 60 | ], 61 | "defaultAllow": false 62 | } 63 | ``` 64 | The *whitelist* and *blacklist* array expect strings in regex format. Image pull requests will be checked by applying the compiled regular expressions on the full image, *< repository >:< tag >*. 65 | **Certain characters in a regular expression like "." have special meaning and need to be escaped. The JSON format requires you to double escape**. 66 | 67 | Image pull requests will be handled in the following order: 68 | 69 | 1. Whitelist: Allow explicitly white listed images 70 | 1. Blacklist: Reject explicitly black listed images 71 | 1. defaultAllow: Default policy, if true allow, if false reject 72 | 73 | If one of the steps matched, the plugin will return accordingly. If whitelist and blacklist did not match, the default policy `defaultAllow` will allow or reject the request. 74 | 75 | ### Docker configuration 76 | 77 | Edit your `/etc/docker/daemon.json` 78 | 79 | ``` 80 | { 81 | "authorization-plugins": ["docker-image-policy"] 82 | } 83 | ``` 84 | 85 | ### Running 86 | 87 | Start `docker-image-policy` and restart Docker daemon. 88 | 89 | ```sh 90 | $ docker-image-policy & 91 | $ curl localhost:5006/health 92 | $ service docker restart 93 | ``` 94 | 95 | **Please consider using the systemd service file for running docker-image-policy** 96 | 97 | ## API Endpoints 98 | 99 | Besides the plugin API for Docker a second API provided through *127.0.0.1:5006* (default) is available to monitor the plugin or check the current state. 100 | 101 | * `/health` -> Health check 102 | * `/config` -> Current config 103 | * `/version` -> Current version 104 | 105 | ``` 106 | $ curl localhost:5006/health 107 | HEALTHY 108 | ``` 109 | 110 | ## Author 111 | 112 | * Simon Pirschel 113 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.4 2 | -------------------------------------------------------------------------------- /build-deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Install requirements 4 | apt update 5 | apt install -y ruby-dev build-essential 6 | gem install fpm 7 | 8 | # Build plugin 9 | make clean 10 | go get github.com/Sirupsen/logrus 11 | go get github.com/docker/go-plugins-helpers/authorization 12 | go get github.com/docker/docker/api 13 | go get github.com/docker/engine-api/client 14 | make 15 | 16 | mkdir -p _tmp/usr/bin _tmp/lib/systemd/system _tmp/etc/docker 17 | cp docker-image-policy _tmp/usr/bin 18 | cp docker-image-policy.service _tmp/lib/systemd/system 19 | chmod 755 _tmp/usr/bin/docker-image-policy 20 | cat < _tmp/etc/docker/docker-image-policy.json 21 | { 22 | "whitelist": [], 23 | "blacklist": [], 24 | "defaultAllow": false 25 | } 26 | EOF 27 | 28 | cat < post-install.sh 29 | #!/bin/sh 30 | systemctl enable docker-image-policy 31 | systemctl start docker-image-policy 32 | EOF 33 | 34 | cat < post-remove.sh 35 | #!/bin/sh 36 | systemctl stop docker-image-policy 37 | systemctl disable docker-image-policy 38 | EOF 39 | 40 | 41 | chmod 755 post-install.sh post-remove.sh 42 | 43 | fpm \ 44 | -s dir\ 45 | -t deb\ 46 | --description "Docker authentication plugin to enforce a image pull policy"\ 47 | --url https://github.com/freach/docker-image-policy-plugin\ 48 | --license "Apache License, Version 2.0"\ 49 | -m "Simon Pirschel "\ 50 | -v "$(cat VERSION)"\ 51 | -n docker-image-policy-plugin\ 52 | --category admin\ 53 | --after-install ./post-install.sh\ 54 | --before-remove ./post-remove.sh\ 55 | -C _tmp\ 56 | -p "$(pwd)" 57 | 58 | rm -rf _tmp post-install.sh post-remove.sh 59 | -------------------------------------------------------------------------------- /config-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "whitelist": [ 3 | "^alpine:", 4 | "^docker\\.elastic\\.co/beats/filebeat:", 5 | "^gcr\\.io/google_containers", 6 | "^mysql:", 7 | "^nginx:", 8 | "^php:", 9 | "^apache:", 10 | "^quay\\.io/calico/cni", 11 | "^quay\\.io/calico/node", 12 | "^quay\\.io/coreos/flannel" 13 | ], 14 | "blacklist": [ 15 | "^docker:" 16 | ], 17 | "defaultAllow": false 18 | } 19 | -------------------------------------------------------------------------------- /docker-image-policy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Docker Image Policy Plugin 3 | Before=docker.service 4 | After=network-online.target syslog.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | Type=simple 9 | ExecStart=/usr/bin/docker-image-policy 10 | SyslogIdentifier=docker-image-policy 11 | StandardOutput=syslog 12 | StandardError=syslog 13 | Restart=always 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | "os" 7 | "regexp" 8 | "fmt" 9 | "encoding/json" 10 | "bytes" 11 | 12 | "github.com/Sirupsen/logrus" 13 | "github.com/docker/go-plugins-helpers/authorization" 14 | ) 15 | 16 | const ( 17 | defaultDockerHost = "unix:///var/run/docker.sock" 18 | pluginSocket = "/run/docker/plugins/docker-image-policy.sock" 19 | defaultConfig = "/etc/docker/docker-image-policy.json" 20 | defaultAddr = "127.0.0.1:5006" 21 | ) 22 | 23 | type Config struct { 24 | Whitelist []string `json:"whitelist"` 25 | Blacklist []string `json:"blacklist"` 26 | DefaultAllow bool `json:"defaultAllow"` 27 | } 28 | 29 | // Globals 30 | var ( 31 | version string 32 | reWhitelist []*regexp.Regexp 33 | reBlacklist []*regexp.Regexp 34 | configuration Config 35 | ) 36 | 37 | // Command line options 38 | var ( 39 | flDockerHost = flag.String("host", defaultDockerHost, "Docker daemon host") 40 | flCertPath = flag.String("cert-path", "", "Path to Docker certificates (cert.pem, key.pem)") 41 | flTLSVerify = flag.Bool("tls-verify", false, "Verify certificates") 42 | flDebug = flag.Bool("debug", false, "Enable debug logging") 43 | flVersion = flag.Bool("version", false, "Print version") 44 | flAddr = flag.String("addr", defaultAddr, "Plugin API [HOSTNAME:PORT]") 45 | flConfig = flag.String("config", defaultConfig, "Path to plugin config file") 46 | ) 47 | 48 | func readConfig(configFile string) error { 49 | file, err := os.Open(configFile) 50 | if err != nil { 51 | return err 52 | } 53 | defer file.Close() 54 | 55 | // Decode JSON 56 | decoder := json.NewDecoder(file) 57 | if err := decoder.Decode(&configuration); err != nil { 58 | return err 59 | } 60 | 61 | // Build whitelist 62 | for _, v := range configuration.Whitelist { 63 | re, err := regexp.Compile(v) 64 | if err != nil { 65 | return err 66 | } 67 | reWhitelist = append(reWhitelist, re) 68 | } 69 | 70 | // Build blacklist 71 | for _, v := range configuration.Blacklist { 72 | re, err := regexp.Compile(v) 73 | if err != nil { 74 | return err 75 | } 76 | reBlacklist = append(reBlacklist, re) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func healthHandler(w http.ResponseWriter, r *http.Request) { 83 | fmt.Fprint(w, "HEALTHY") 84 | } 85 | 86 | func versionHandler(w http.ResponseWriter, r *http.Request) { 87 | fmt.Fprint(w, version) 88 | } 89 | 90 | func configHandler(w http.ResponseWriter, r *http.Request) { 91 | b, err := json.Marshal(configuration) 92 | if err != nil { 93 | fmt.Fprint(w, err) 94 | } 95 | 96 | var out bytes.Buffer 97 | json.Indent(&out, b, "", " ") 98 | out.WriteTo(w) 99 | } 100 | 101 | func main() { 102 | logrus.SetLevel(logrus.InfoLevel) 103 | flag.Parse() 104 | 105 | // Print version and exit 106 | if *flVersion { 107 | fmt.Printf("Version: %s\n", version) 108 | os.Exit(0) 109 | } 110 | 111 | if *flDebug { 112 | logrus.SetLevel(logrus.DebugLevel) 113 | } 114 | 115 | logrus.Infof("Docker Image policy plugin started (version: %s)", version) 116 | 117 | if err := readConfig(*flConfig); err != nil { 118 | logrus.Fatal(err) 119 | } 120 | 121 | logrus.Infof("%d entries in whitelist.", len(reWhitelist)) 122 | logrus.Infof("%d entries in blacklist.", len(reBlacklist)) 123 | logrus.Infof("Default allow: %t", configuration.DefaultAllow) 124 | 125 | // Add additional handlers 126 | http.HandleFunc("/health", healthHandler) 127 | http.HandleFunc("/config", configHandler) 128 | http.HandleFunc("/version", versionHandler) 129 | 130 | go func() { 131 | logrus.Debugf("Server running on %s", *flAddr) 132 | if err := http.ListenAndServe(*flAddr, nil); err != nil { 133 | logrus.Fatal(err) 134 | } 135 | }() 136 | 137 | plugin, err := newPlugin(*flDockerHost, *flCertPath, *flTLSVerify) 138 | if err != nil { 139 | logrus.Fatal(err) 140 | } 141 | 142 | h := authorization.NewHandler(plugin) 143 | 144 | logrus.Debugf("Plugin running on %s", pluginSocket) 145 | if err := h.ServeUnix(pluginSocket, 0); err != nil { 146 | logrus.Fatal(err) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/Sirupsen/logrus" 12 | dockerapi "github.com/docker/docker/api" 13 | dockerclient "github.com/docker/engine-api/client" 14 | "github.com/docker/go-plugins-helpers/authorization" 15 | ) 16 | 17 | func newPlugin(dockerHost, certPath string, tlsVerify bool) (*authPlugin, error) { 18 | var httpClient *http.Client 19 | if certPath != "" { 20 | tlsc := &tls.Config{} 21 | 22 | cert, err := tls.LoadX509KeyPair( 23 | filepath.Join(certPath, "cert.pem"), 24 | filepath.Join(certPath, "key.pem"), 25 | ) 26 | 27 | if err != nil { 28 | return nil, fmt.Errorf("Error loading x509 key pair: %s", err) 29 | } 30 | 31 | tlsc.Certificates = append(tlsc.Certificates, cert) 32 | tlsc.InsecureSkipVerify = !tlsVerify 33 | transport := &http.Transport{TLSClientConfig: tlsc} 34 | httpClient = &http.Client{Transport: transport} 35 | } 36 | 37 | client, err := dockerclient.NewClient( 38 | dockerHost, dockerapi.DefaultVersion, httpClient, nil, 39 | ) 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return &authPlugin{client: client}, nil 46 | } 47 | 48 | type authPlugin struct { 49 | client *dockerclient.Client 50 | } 51 | 52 | func (p *authPlugin) AuthZReq(req authorization.Request) authorization.Response { 53 | if req.RequestMethod == "POST" && strings.Contains(req.RequestURI, "/images/create") { 54 | uri, err := url.ParseRequestURI(req.RequestURI) 55 | if err != nil { 56 | errMsg := fmt.Sprintf("Error while parsing request URI: %s", err) 57 | logrus.Error(errMsg) 58 | return authorization.Response{Allow: false, Msg: errMsg} 59 | } 60 | 61 | query, err := url.ParseQuery(uri.RawQuery) 62 | if err != nil { 63 | errMsg := fmt.Sprintf("Error while parsing request query string: %s", err) 64 | logrus.Error(errMsg) 65 | return authorization.Response{Allow: false, Msg: errMsg} 66 | } 67 | 68 | var image string 69 | if _, exists := query["tag"]; exists { 70 | image = fmt.Sprintf("%s:%s", query["fromImage"][0], query["tag"][0]) 71 | } else { 72 | // Docker < 17.xx 73 | image = query["fromImage"][0] 74 | } 75 | 76 | bImage := []byte(image) 77 | 78 | for _, v := range reWhitelist { 79 | if v.Match(bImage) { 80 | return authorization.Response{Allow: true} 81 | } 82 | } 83 | 84 | for _, v := range reBlacklist { 85 | if v.Match(bImage) { 86 | logrus.Infof("Image %s blocked, because blacklisted.", image) 87 | return authorization.Response{ 88 | Allow: false, 89 | Msg: fmt.Sprintf("Image %s is blacklisted on this server.", image), 90 | } 91 | } 92 | } 93 | 94 | if configuration.DefaultAllow == true { 95 | return authorization.Response{Allow: true} 96 | } 97 | 98 | logrus.Infof("Image %s blocked, because default is reject.", image) 99 | return authorization.Response{ 100 | Allow: false, 101 | Msg: fmt.Sprintf("Image %s is not allowed on this server.", image), 102 | } 103 | } 104 | 105 | return authorization.Response{Allow: true} 106 | } 107 | 108 | func (p *authPlugin) AuthZRes(req authorization.Request) authorization.Response { 109 | return authorization.Response{Allow: true} 110 | } 111 | --------------------------------------------------------------------------------