From 2e51536dae431c9719b3c0f3f3a64091f633fba2 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Fri, 3 Jun 2022 16:50:18 -0500 Subject: [PATCH] Support connecting to TLS secured docker host When we create a docker cli instance using just the docker/cli library, some important configuration doesn't happen, namely reading DOCKER_TLS_VERIFY and DOCKER_CERT_PATH. Unlike DOCKER_HOST, these other variables for connecting to a TLS secured docker host are not configured in the main library functions but instead just in the CLI's main package when the flags (for stuff like --tlsverify) are bound. This means we need to configure this ourselves when using that library. I've added a function that consolidates all the necessary configuration steps to make a docker cli client that is configured the same as if running the docker CLI directly on your computer. I've tested this manually against a remote docker host secured with a self-signed certificate and I'm able to build, push and run bundles on a remote host with this fix. Signed-off-by: Carolyn Van Slyck --- driver/docker/client.go | 77 ++++++++++++++++++++++++++++++++++++ driver/docker/client_test.go | 76 +++++++++++++++++++++++++++++++++++ driver/docker/docker.go | 8 ++-- 3 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 driver/docker/client.go create mode 100644 driver/docker/client_test.go diff --git a/driver/docker/client.go b/driver/docker/client.go new file mode 100644 index 00000000..b347374b --- /dev/null +++ b/driver/docker/client.go @@ -0,0 +1,77 @@ +package docker + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/docker/cli/cli/command" + cliconfig "github.com/docker/cli/cli/config" + cliflags "github.com/docker/cli/cli/flags" + "github.com/docker/go-connections/tlsconfig" +) + +const ( + // DockerTLSVerifyEnvVar is the Docker environment variable that indicates that + // Docker socket is protected with TLS. + DockerTLSVerifyEnvVar = "DOCKER_TLS_VERIFY" + + // DockerCertPathEnvVar is the Docker environment variable that specifies a + // custom path to the TLS certificates for the Docker socket. + DockerCertPathEnvVar = "DOCKER_CERT_PATH" +) + +// GetDockerClient creates a Docker CLI client that uses the user's Docker configuration +// such as environment variables and the Docker home directory to initialize the client. +func GetDockerClient() (*command.DockerCli, error) { + cli, err := command.NewDockerCli() + if err != nil { + return nil, fmt.Errorf("could not create new docker client: %w", err) + } + opts := buildDockerClientOptions() + if err = cli.Initialize(opts); err != nil { + return nil, fmt.Errorf("error initializing docker client: %w", err) + } + return cli, nil +} + +// manually handle DOCKER_TLS_VERIFY and DOCKER_CERT_PATH because the docker cli +// library only binds these values when initializing its cli flags. There isn't +// other parts of the library that we can take advantage of to get these values +// for "free". +// +// DOCKER_HOST however is retrieved dynamically later so that doesn't +// require additional configuration. +func buildDockerClientOptions() *cliflags.ClientOptions { + cliOpts := cliflags.NewClientOptions() + cliOpts.ConfigDir = cliconfig.Dir() + + // Check if TLS is enabled Docker configures TLS settings if DOCKER_TLS_VERIFY is + // set to anything, so it could be false and that still means we should use TLS + // (but don't check the certs). + tlsVerify, tlsConfigured := os.LookupEnv(DockerTLSVerifyEnvVar) + if tlsConfigured && tlsVerify != "" { + cliOpts.Common.TLS = true + + // Check if we should verify certs or allow self-signed certs (insecure) + verify, _ := strconv.ParseBool(tlsVerify) + cliOpts.Common.TLSVerify = verify + + // Check if the TLS certs have been overridden + var certPath string + if certPathOverride, ok := os.LookupEnv(DockerCertPathEnvVar); ok && certPathOverride != "" { + certPath = certPathOverride + } else { + certPath = cliOpts.ConfigDir + } + + cliOpts.Common.TLSOptions = &tlsconfig.Options{ + CAFile: filepath.Join(certPath, cliflags.DefaultCaFile), + CertFile: filepath.Join(certPath, cliflags.DefaultCertFile), + KeyFile: filepath.Join(certPath, cliflags.DefaultKeyFile), + } + } + + return cliOpts +} diff --git a/driver/docker/client_test.go b/driver/docker/client_test.go new file mode 100644 index 00000000..1a476a77 --- /dev/null +++ b/driver/docker/client_test.go @@ -0,0 +1,76 @@ +package docker + +import ( + "os" + "testing" + + "github.com/docker/go-connections/tlsconfig" + "github.com/stretchr/testify/assert" +) + +func Test_buildDockerClientOptions(t *testing.T) { + // Tell Docker where its config is located, so that we have repeatable paths in the tests + os.Setenv("DOCKER_CONFIG", "/home/me/.docker") + defer os.Unsetenv("DOCKER_CONFIG") + + defaultTLSOptions := &tlsconfig.Options{ + CAFile: "/home/me/.docker/ca.pem", + CertFile: "/home/me/.docker/cert.pem", + KeyFile: "/home/me/.docker/key.pem", + } + + customTLSOptions := &tlsconfig.Options{ + CAFile: "/mycerts/ca.pem", + CertFile: "/mycerts/cert.pem", + KeyFile: "/mycerts/key.pem", + } + + t.Run("tls disabled", func(t *testing.T) { + os.Unsetenv(DockerTLSVerifyEnvVar) + opts := buildDockerClientOptions() + assert.False(t, opts.Common.TLS, "expected TLS to be disabled") + assert.False(t, opts.Common.TLSVerify, "expected TLSVerify to be disabled") + assert.Nil(t, opts.Common.TLSOptions, "expected TLSOptions to be unset") + }) + + t.Run("tls enabled without certs", func(t *testing.T) { + os.Setenv(DockerTLSVerifyEnvVar, "true") + os.Unsetenv(DockerCertPathEnvVar) + defer func() { + os.Unsetenv(DockerTLSVerifyEnvVar) + }() + + opts := buildDockerClientOptions() + assert.True(t, opts.Common.TLS, "expected TLS to be enabled") + assert.True(t, opts.Common.TLSVerify, "expected the certs to be verified") + assert.Equal(t, defaultTLSOptions, opts.Common.TLSOptions, "expected TLSOptions to be initialized to the default TLS settings") + }) + + t.Run("tls enabled with custom certs", func(t *testing.T) { + os.Setenv(DockerTLSVerifyEnvVar, "true") + os.Setenv(DockerCertPathEnvVar, "/mycerts") + defer func() { + os.Unsetenv(DockerTLSVerifyEnvVar) + os.Unsetenv(DockerCertPathEnvVar) + }() + + opts := buildDockerClientOptions() + assert.True(t, opts.Common.TLS, "expected TLS to be enabled") + assert.True(t, opts.Common.TLSVerify, "expected the certs to be verified") + assert.Equal(t, customTLSOptions, opts.Common.TLSOptions, "expected TLSOptions to use the custom DOCKER_CERT_PATH set") + }) + + t.Run("tls enabled with self-signed certs", func(t *testing.T) { + os.Setenv(DockerTLSVerifyEnvVar, "false") + os.Setenv(DockerCertPathEnvVar, "/mycerts") + defer func() { + os.Unsetenv(DockerTLSVerifyEnvVar) + os.Unsetenv(DockerCertPathEnvVar) + }() + + opts := buildDockerClientOptions() + assert.True(t, opts.Common.TLS, "expected TLS to be enabled") + assert.False(t, opts.Common.TLSVerify, "expected TLSVerify to be false") + assert.Equal(t, customTLSOptions, opts.Common.TLSOptions, "expected TLSOptions to use the custom DOCKER_CERT_PATH set") + }) +} diff --git a/driver/docker/docker.go b/driver/docker/docker.go index 50849b3f..9cb61f55 100644 --- a/driver/docker/docker.go +++ b/driver/docker/docker.go @@ -12,7 +12,6 @@ import ( "strings" "github.com/docker/cli/cli/command" - cliflags "github.com/docker/cli/cli/flags" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -167,16 +166,15 @@ func (d *Driver) initializeDockerCli() (command.Cli, error) { return d.dockerCli, nil } - cli, err := command.NewDockerCli() + cli, err := GetDockerClient() if err != nil { return nil, err } + if d.config["DOCKER_DRIVER_QUIET"] == "1" { cli.Apply(command.WithCombinedStreams(ioutil.Discard)) } - if err := cli.Initialize(cliflags.NewClientOptions()); err != nil { - return nil, err - } + d.dockerCli = cli return cli, nil }