From f8a9755fcd9f238678d87bcc977aa4a220472bba Mon Sep 17 00:00:00 2001 From: Manuel Vogel Date: Wed, 26 Sep 2018 18:27:04 +0200 Subject: [PATCH] Fix/cert material (#91) * Fixes connection via TLS to docker host with file contents. Closes #86 * Fixes skip of TLS verification if ca_material is not present. Closes #14 --- CHANGELOG.md | 5 +++ docker/config.go | 75 ++++++++++++++++++++++++++++++-- website/docs/index.html.markdown | 26 +++++++++-- 3 files changed, 99 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac02e7c8..b37d18e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ ## 1.0.2 (Unreleased) + +BUG FIXES +* Fixes connection via TLS to docker host with file contents [GH-86] +* Skips TLS verification if `ca_material` is not set [GH-14] + ## 1.0.1 (August 06, 2018) BUG FIXES diff --git a/docker/config.go b/docker/config.go index 384e0843..d914d0fd 100644 --- a/docker/config.go +++ b/docker/config.go @@ -1,9 +1,16 @@ package docker import ( + "crypto/tls" + "crypto/x509" + "errors" "fmt" + "net" + "net/http" "path/filepath" + "runtime" "strings" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/client" @@ -21,20 +28,80 @@ type Config struct { CertPath string } +// buildHTTPClientFromBytes builds the http client from bytes (content of the files) +func buildHTTPClientFromBytes(caPEMCert, certPEMBlock, keyPEMBlock []byte) (*http.Client, error) { + tlsConfig := &tls.Config{} + if certPEMBlock != nil && keyPEMBlock != nil { + tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{tlsCert} + } + + if caPEMCert == nil || len(caPEMCert) == 0 { + tlsConfig.InsecureSkipVerify = true + } else { + caPool := x509.NewCertPool() + if !caPool.AppendCertsFromPEM(caPEMCert) { + return nil, errors.New("Could not add RootCA pem") + } + tlsConfig.RootCAs = caPool + } + + tr := defaultTransport() + tr.TLSClientConfig = tlsConfig + return &http.Client{Transport: tr}, nil +} + +// defaultTransport returns a new http.Transport with similar default values to +// http.DefaultTransport, but with idle connections and keepalives disabled. +func defaultTransport() *http.Transport { + transport := defaultPooledTransport() + transport.DisableKeepAlives = true + transport.MaxIdleConnsPerHost = -1 + return transport +} + +// defaultPooledTransport returns a new http.Transport with similar default +// values to http.DefaultTransport. +func defaultPooledTransport() *http.Transport { + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, + } + return transport +} + // NewClient returns a new Docker client. func (c *Config) NewClient() (*client.Client, error) { - if c.Ca != "" || c.Cert != "" || c.Key != "" { - if c.Ca == "" || c.Cert == "" || c.Key == "" { - return nil, fmt.Errorf("ca_material, cert_material, and key_material must be specified") + if c.Cert != "" || c.Key != "" { + if c.Cert == "" || c.Key == "" { + return nil, fmt.Errorf("cert_material, and key_material must be specified") } if c.CertPath != "" { return nil, fmt.Errorf("cert_path must not be specified") } + httpClient, err := buildHTTPClientFromBytes([]byte(c.Ca), []byte(c.Cert), []byte(c.Key)) + if err != nil { + return nil, err + } + + // Note: don't change the order here, because the custom client + // needs to be set first them we overwrite the other options: host, version return client.NewClientWithOpts( + client.WithHTTPClient(httpClient), client.WithHost(c.Host), - client.WithTLSClientConfig(c.Ca, c.Cert, c.Key), client.WithVersion(apiVersion), ) } diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 3898c72d..7a397811 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -91,6 +91,25 @@ and paste it into `~/.docker/config.json`: } ``` +## Certificate information + +Specify certificate information either with a directory or +directly with the content of the files for connecting to the Docker host via TLS. + +```hcl +provider "docker" { + host = "tcp://your-host-ip:2376/" + + # -> specify either + cert_path = "${pathexpand("~/.docker")}" + + # -> or the following + ca_material = "${file(pathexpand("~/.docker/ca.pem"))}" # this can be omitted + cert_material = "${file(pathexpand("~/.docker/cert.pem"))}" + key_material = "${file(pathexpand("~/.docker/key.pem"))}" +} +``` + ## Argument Reference The following arguments are supported: @@ -99,11 +118,12 @@ The following arguments are supported: blank, the `DOCKER_HOST` environment variable will also be read. * `cert_path` - (Optional) Path to a directory with certificate information - for connecting to the Docker host via TLS. If this is blank, the - `DOCKER_CERT_PATH` will also be checked. + for connecting to the Docker host via TLS. It is expected that the 3 files `{ca, cert, key}.pem` + are present in the path. If the path is blank, the `DOCKER_CERT_PATH` will also be checked. * `ca_material`, `cert_material`, `key_material`, - (Optional) Content of `ca.pem`, `cert.pem`, and `key.pem` files - for TLS authentication. Cannot be used together with `cert_path`. + for TLS authentication. Cannot be used together with `cert_path`. If `ca_material` is omitted + the client does not check the servers certificate chain and host name. * `registry_auth` - (Optional) A block specifying the credentials for a target v2 Docker registry.