diff --git a/config.go b/config.go index 85d0236..79fd9d3 100644 --- a/config.go +++ b/config.go @@ -27,6 +27,10 @@ type Config struct { dnsAddr string domain Domain dockerHost string + tlsVerify bool + tlsCaCert string + tlsCert string + tlsKey string verbose bool httpAddr string ttl int @@ -37,6 +41,11 @@ func NewConfig() *Config { if len(dockerHost) == 0 { dockerHost = "unix:///var/run/docker.sock" } + tlsVerify := len(os.Getenv("DOCKER_TLS_VERIFY")) != 0 + dockerCerts := os.Getenv("DOCKER_CERT_PATH") + if len(dockerCerts) == 0 { + dockerCerts = os.Getenv("HOME") + "/.docker" + } return &Config{ nameserver: "8.8.8.8:53", @@ -44,6 +53,10 @@ func NewConfig() *Config { domain: NewDomain("docker"), dockerHost: dockerHost, httpAddr: ":80", + tlsVerify: tlsVerify, + tlsCaCert: dockerCerts + "/ca.pem", + tlsCert: dockerCerts + "/cert.pem", + tlsKey: dockerCerts + "/key.pem", } } diff --git a/dnsserver.go b/dnsserver.go index 710aa6e..5f9af1a 100644 --- a/dnsserver.go +++ b/dnsserver.go @@ -18,10 +18,11 @@ type Service struct { Ip net.IP Ttl int Aliases []string + Manual bool } func NewService() (s *Service) { - s = &Service{Ttl: -1} + s = &Service{Ttl: -1, Manual: false} return } diff --git a/docker.go b/docker.go index 14c0bc4..0e31c4c 100644 --- a/docker.go +++ b/docker.go @@ -1,6 +1,7 @@ package main import ( + "crypto/tls" "errors" "log" "net" @@ -18,8 +19,8 @@ type DockerManager struct { docker *dockerclient.DockerClient } -func NewDockerManager(c *Config, list ServiceListProvider) (*DockerManager, error) { - docker, err := dockerclient.NewDockerClient(c.dockerHost, nil) +func NewDockerManager(c *Config, list ServiceListProvider, tlsConfig *tls.Config) (*DockerManager, error) { + docker, err := dockerclient.NewDockerClient(c.dockerHost, tlsConfig) if err != nil { return nil, err } @@ -70,7 +71,10 @@ func (d *DockerManager) Update() error { log.Println(err) continue } - d.list.AddService(container.Id, *service) + s, err := d.list.GetService(container.Id) + if err != nil || !s.Manual { + d.list.AddService(container.Id, *service) + } } return nil @@ -108,10 +112,14 @@ func (d *DockerManager) getService(id string) (*Service, error) { func (d *DockerManager) eventCallback(event *dockerclient.Event, ec chan error, args ...interface{}) { //log.Printf("Received event: %#v %#v\n", *event, args) + s, s_err := d.list.GetService(event.Id) + switch event.Status { case "die", "stop", "kill": // Errors can be ignored here because there can be no-op events. - d.list.RemoveService(event.Id) + if s_err == nil && !s.Manual { + d.list.RemoveService(event.Id) + } case "start", "restart": service, err := d.getService(event.Id) if err != nil { @@ -119,7 +127,9 @@ func (d *DockerManager) eventCallback(event *dockerclient.Event, ec chan error, return } - d.list.AddService(event.Id, *service) + if s_err != nil || !s.Manual { + d.list.AddService(event.Id, *service) + } } } diff --git a/http.go b/http.go index 321a682..9f6361d 100644 --- a/http.go +++ b/http.go @@ -96,6 +96,7 @@ func (s *HTTPServer) addService(w http.ResponseWriter, req *http.Request) { return } + service.Manual = true s.list.AddService(id, *service) } @@ -160,6 +161,7 @@ func (s *HTTPServer) updateService(w http.ResponseWriter, req *http.Request) { } } + service.Manual = true // todo: this probably needs to be moved. consider stop event in the // middle of sending PATCH. container would not be removed. s.list.AddService(id, service) diff --git a/http_test.go b/http_test.go index 9e782c4..46c0358 100644 --- a/http_test.go +++ b/http_test.go @@ -28,13 +28,13 @@ func TestServiceRequests(t *testing.T) { {"GET", "/services/foo", "", "", 404}, {"PUT", "/services/foo", `{"name": "foo"}`, "", 500}, {"PUT", "/services/foo", `{"name": "foo", "image": "bar", "ip": "127.0.0.1", "aliases": ["foo.docker"]}`, "", 200}, - {"GET", "/services/foo", "", `{"Name":"foo","Image":"bar","Ip":"127.0.0.1","Ttl":-1,"Aliases":["foo.docker"]}`, 200}, + {"GET", "/services/foo", "", `{"Name":"foo","Image":"bar","Ip":"127.0.0.1","Ttl":-1,"Aliases":["foo.docker"],"Manual":true}`, 200}, {"PUT", "/services/boo", `{"name": "baz", "image": "bar", "ip": "127.0.0.2"}`, "", 200}, - {"GET", "/services", "", `{"boo":{"Name":"baz","Image":"bar","Ip":"127.0.0.2","Ttl":-1,"Aliases":null},"foo":{"Name":"foo","Image":"bar","Ip":"127.0.0.1","Ttl":-1,"Aliases":["foo.docker"]}}`, 200}, + {"GET", "/services", "", `{"boo":{"Name":"baz","Image":"bar","Ip":"127.0.0.2","Ttl":-1,"Aliases":null,"Manual":true},"foo":{"Name":"foo","Image":"bar","Ip":"127.0.0.1","Ttl":-1,"Aliases":["foo.docker"],"Manual":true}}`, 200}, {"PATCH", "/services/boo", `{"name": "bar", "ttl": 20, "image": "bar"}`, "", 200}, - {"GET", "/services/boo", "", `{"Name":"bar","Image":"bar","Ip":"127.0.0.2","Ttl":20,"Aliases":null}`, 200}, + {"GET", "/services/boo", "", `{"Name":"bar","Image":"bar","Ip":"127.0.0.2","Ttl":20,"Aliases":null,"Manual":true}`, 200}, {"DELETE", "/services/foo", ``, "", 200}, - {"GET", "/services", "", `{"boo":{"Name":"bar","Image":"bar","Ip":"127.0.0.2","Ttl":20,"Aliases":null}}`, 200}, + {"GET", "/services", "", `{"boo":{"Name":"bar","Image":"bar","Ip":"127.0.0.2","Ttl":20,"Aliases":null,"Manual":true}}`, 200}, } for _, input := range tests { diff --git a/main.go b/main.go index 0c531ce..5c2e5ca 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,11 @@ package main import ( + "crypto/tls" + "crypto/x509" "flag" "fmt" + "io/ioutil" "log" "os" ) @@ -20,6 +23,10 @@ func main() { domain := flag.String("domain", config.domain.String(), "Domain that is appended to all requests") environment := flag.String("environment", "", "Optional context before domain suffix") flag.StringVar(&config.dockerHost, "docker", config.dockerHost, "Path to the docker socket") + flag.BoolVar(&config.tlsVerify, "tlsverify", false, "Enable mTLS when connecting to docker") + flag.StringVar(&config.tlsCaCert, "tlscacert", config.tlsCaCert, "Path to CA certificate") + flag.StringVar(&config.tlsCert, "tlscert", config.tlsCert, "Path to client certificate") + flag.StringVar(&config.tlsKey, "tlskey", config.tlsKey, "Path to client certificate private key") flag.BoolVar(&config.verbose, "verbose", true, "Verbose output") flag.IntVar(&config.ttl, "ttl", config.ttl, "TTL for matched requests") @@ -45,7 +52,26 @@ func main() { dnsServer := NewDNSServer(config) - docker, err := NewDockerManager(config, dnsServer) + var tlsConfig *tls.Config = nil + if config.tlsVerify { + clientCert, err := tls.LoadX509KeyPair(config.tlsCert, config.tlsKey) + if err != nil { + log.Fatal(err) + } + tlsConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{clientCert}, + } + pemData, err := ioutil.ReadFile(config.tlsCaCert) + if err == nil { + rootCert := x509.NewCertPool() + rootCert.AppendCertsFromPEM(pemData) + tlsConfig.RootCAs = rootCert + } else { + log.Print(err) + } + } + docker, err := NewDockerManager(config, dnsServer, tlsConfig) if err != nil { log.Fatal(err) } diff --git a/readme.md b/readme.md index 7c74d38..785a767 100644 --- a/readme.md +++ b/readme.md @@ -85,6 +85,10 @@ Additional configuration options to dnsdock command: -nameserver="8.8.8.8:53": DNS server for unmatched requests -ttl=0: TTL for matched requests -verbose=true: Verbose output +-tlsverify=false: enable mutual TLS between dnsdock and Docker +-tlscacert="$HOME/.docker/ca.pem": Path to CA certificate +-tlscert="$HOME/.docker/cert.pem": Path to client certificate +-tlskey="$HOME/.docker/key.pem": Path to client certificate private key ``` If you also want to let the host machine discover the containers add `nameserver 172.17.42.1` to your `/etc/resolv.conf`. @@ -94,6 +98,23 @@ If you also want to let the host machine discover the containers add `nameserver Mounting docker daemon's unix socket may not work with default configuration on these platforms. Please use [selinux-dockersock](https://github.com/dpw/selinux-dockersock) to fix this. More information in [#11](https://github.com/tonistiigi/dnsdock/issues/11). +#### TLS Authentication + +Instead of connecting to the Docker daemon's UNIX socket, you may prefer to connect via a TLS-protected TCP socket (for example, if you are running Swarm). The `-tlsverify` option enables TLS, and the three additional options (`-tlscacert`, `-tlscert` and `-tlskey`) must also be specified. Alternatively, you may set the `DOCKER_TLS_VERIFY` environment variable to a non-empty value and the `DOCKER_CERTS` to a directory containing files named `ca.pem`, `cert.pem` and `key.pem`. + +You may build this into your own container with this example Dockerfile: + +``` +FROM tonistiigi/dnsdock + +ENV DOCKER_TLS_VERIFY 1 +ENV DOCKER_CERTS /certs + +CMD ["-docker=tcp://172.17.42.1:2376"] +``` + +Use a volume (`-v /path/to/certs:/certs`) to give the container access to the certificate files, or build the certificates into the image if you have access to a secure private image registry. + #### HTTP Server For easy overview and manual control dnsdock also includes HTTP server that lets you configure the server using a JSON API.