From b73c2c10c8f75c20db0d746cb969be000ddaa0d9 Mon Sep 17 00:00:00 2001 From: Ben Einaudi Date: Fri, 7 Feb 2020 17:18:16 +0100 Subject: [PATCH] Allow user to provide registry certificate Fixes #1100 Fixes #1101 --- cmd/executor/cmd/root.go | 2 + deploy/Dockerfile | 2 +- deploy/Dockerfile_debug | 2 +- deploy/Dockerfile_warmer | 2 +- pkg/config/args.go | 31 +++++++++++++++ pkg/config/args_test.go | 47 ++++++++++++++++++++++ pkg/config/options.go | 1 + pkg/executor/push.go | 53 ++++++++++++++++++++++-- pkg/executor/push_test.go | 84 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 pkg/config/args_test.go diff --git a/cmd/executor/cmd/root.go b/cmd/executor/cmd/root.go index efda975b5f..62f10d60e2 100644 --- a/cmd/executor/cmd/root.go +++ b/cmd/executor/cmd/root.go @@ -154,6 +154,8 @@ func addKanikoOptionsFlags() { RootCmd.PersistentFlags().DurationVarP(&opts.CacheTTL, "cache-ttl", "", time.Hour*336, "Cache timeout in hours. Defaults to two weeks.") RootCmd.PersistentFlags().VarP(&opts.InsecureRegistries, "insecure-registry", "", "Insecure registry using plain HTTP to push and pull. Set it repeatedly for multiple registries.") RootCmd.PersistentFlags().VarP(&opts.SkipTLSVerifyRegistries, "skip-tls-verify-registry", "", "Insecure registry ignoring TLS verify to push and pull. Set it repeatedly for multiple registries.") + opts.RegistriesCertificates = make(map[string]string) + RootCmd.PersistentFlags().VarP(&opts.RegistriesCertificates, "registry-certificate", "", "Use the provided certificate for TLS communication with the given registry. Expected format is 'my.registry.url=/path/to/the/server/certificate'.") RootCmd.PersistentFlags().StringVarP(&opts.RegistryMirror, "registry-mirror", "", "", "Registry mirror to use has pull-through cache instead of docker.io.") RootCmd.PersistentFlags().BoolVarP(&opts.WhitelistVarRun, "whitelist-var-run", "", true, "Ignore /var/run directory when taking image snapshot. Set it to false to preserve /var/run/ in destination image. (Default true).") } diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 1ed34a4d8e..2e1b1fb9ba 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -14,7 +14,7 @@ # Builds the static Go image to execute in a Kubernetes job -FROM golang:1.12 +FROM golang:1.13 ARG GOARCH=amd64 WORKDIR /go/src/github.com/GoogleContainerTools/kaniko # Get GCR credential helper diff --git a/deploy/Dockerfile_debug b/deploy/Dockerfile_debug index a57151babc..e9ccf15e3f 100644 --- a/deploy/Dockerfile_debug +++ b/deploy/Dockerfile_debug @@ -15,7 +15,7 @@ # Builds the static Go image to execute in a Kubernetes job # Stage 0: Build the executor binary and get credential helpers -FROM golang:1.12 +FROM golang:1.13 ARG GOARCH=amd64 WORKDIR /go/src/github.com/GoogleContainerTools/kaniko # Get GCR credential helper diff --git a/deploy/Dockerfile_warmer b/deploy/Dockerfile_warmer index b81df44130..d33d2d74b1 100644 --- a/deploy/Dockerfile_warmer +++ b/deploy/Dockerfile_warmer @@ -14,7 +14,7 @@ # Builds the static Go image to execute in a Kubernetes job -FROM golang:1.12 +FROM golang:1.13 ARG GOARCH=amd64 WORKDIR /go/src/github.com/GoogleContainerTools/kaniko # Get GCR credential helper diff --git a/pkg/config/args.go b/pkg/config/args.go index 36bcff0a9f..46ec51f7cd 100644 --- a/pkg/config/args.go +++ b/pkg/config/args.go @@ -17,6 +17,7 @@ limitations under the License. package config import ( + "fmt" "strings" "github.com/sirupsen/logrus" @@ -39,6 +40,7 @@ func (b *multiArg) Set(value string) error { return nil } +// The third is Type() string func (b *multiArg) Type() string { return "multi-arg type" } @@ -51,3 +53,32 @@ func (b *multiArg) Contains(v string) bool { } return false } + +// This type is used to supported passing in multiple key=value flags +type keyValueArg map[string]string + +// Now, for our new type, implement the two methods of +// the flag.Value interface... +// The first method is String() string +func (a *keyValueArg) String() string { + var result []string + for key := range *a { + result = append(result, fmt.Sprintf("%s=%s", key, (*a)[key])) + } + return strings.Join(result, ",") +} + +// The second method is Set(value string) error +func (a *keyValueArg) Set(value string) error { + valueSplit := strings.SplitN(value, "=", 2) + if len(valueSplit) < 2 { + return fmt.Errorf("invalid argument value. expect key=value, got %s", value) + } + (*a)[valueSplit[0]] = valueSplit[1] + return nil +} + +// The third is Type() string +func (a *keyValueArg) Type() string { + return "key-value-arg type" +} diff --git a/pkg/config/args_test.go b/pkg/config/args_test.go new file mode 100644 index 0000000000..12cf839770 --- /dev/null +++ b/pkg/config/args_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2020 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import "testing" + +func TestMultiArg_Set_shouldAppendValue(t *testing.T) { + var arg multiArg + arg.Set("value1") + if len(arg) != 1 || arg[0] != "value1" { + t.Error("Fist value was not appended") + } + arg.Set("value2") + if len(arg) != 2 || arg[1] != "value2" { + t.Error("Second value was not appended") + } +} + +func Test_KeyValueArg_Set_shouldSplitArgument(t *testing.T) { + arg := make(keyValueArg) + arg.Set("key=value") + if arg["key"] != "value" { + t.Error("Invalid split. key=value should be split to key=>value") + } +} + +func Test_KeyValueArg_Set_shouldAcceptEqualAsValue(t *testing.T) { + arg := make(keyValueArg) + arg.Set("key=value=something") + if arg["key"] != "value=something" { + t.Error("Invalid split. key=value=something should be split to key=>value=something") + } +} diff --git a/pkg/config/options.go b/pkg/config/options.go index bf34f3b5cc..b0ba6e8d16 100644 --- a/pkg/config/options.go +++ b/pkg/config/options.go @@ -44,6 +44,7 @@ type KanikoOptions struct { BuildArgs multiArg InsecureRegistries multiArg SkipTLSVerifyRegistries multiArg + RegistriesCertificates keyValueArg Insecure bool SkipTLSVerify bool InsecurePull bool diff --git a/pkg/executor/push.go b/pkg/executor/push.go index 3999c49734..1141563bc5 100644 --- a/pkg/executor/push.go +++ b/pkg/executor/push.go @@ -18,6 +18,7 @@ package executor import ( "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "io/ioutil" @@ -62,6 +63,41 @@ func (w *withUserAgent) RoundTrip(r *http.Request) (*http.Response, error) { return w.t.RoundTrip(r) } +type CertPool interface { + value() *x509.CertPool + append(path string) error +} + +type X509CertPool struct { + inner x509.CertPool +} + +func (p *X509CertPool) value() *x509.CertPool { + return &p.inner +} + +func (p *X509CertPool) append(path string) error { + pem, err := ioutil.ReadFile(path) + if err != nil { + return err + } + p.inner.AppendCertsFromPEM(pem) + return nil +} + +type systemCertLoader func() CertPool + +var defaultX509Handler systemCertLoader = func() CertPool { + systemCertPool, err := x509.SystemCertPool() + if err != nil { + logrus.Warn("Failed to load system cert pool. Loading empty one instead.") + systemCertPool = x509.NewCertPool() + } + return &X509CertPool{ + inner: *systemCertPool, + } +} + // CheckPushPermissions checks that the configured credentials can be used to // push to every specified destination. func CheckPushPermissions(opts *config.KanikoOptions) error { @@ -87,7 +123,7 @@ func CheckPushPermissions(opts *config.KanikoOptions) error { } destRef.Repository.Registry = newReg } - tr := makeTransport(opts, registryName) + tr := makeTransport(opts, registryName, defaultX509Handler) if err := remote.CheckPushPermission(destRef, creds.GetKeychain(), tr); err != nil { return errors.Wrapf(err, "checking push permission for %q", destRef) } @@ -184,7 +220,7 @@ func DoPush(image v1.Image, opts *config.KanikoOptions) error { return errors.Wrap(err, "resolving pushAuth") } - tr := makeTransport(opts, registryName) + tr := makeTransport(opts, registryName, defaultX509Handler) rt := &withUserAgent{t: tr} if err := remote.Write(destRef, image, remote.WithAuth(pushAuth), remote.WithTransport(rt)); err != nil { @@ -228,13 +264,22 @@ func writeImageOutputs(image v1.Image, destRefs []name.Tag) error { return nil } -func makeTransport(opts *config.KanikoOptions, registryName string) http.RoundTripper { +func makeTransport(opts *config.KanikoOptions, registryName string, loader systemCertLoader) http.RoundTripper { // Create a transport to set our user-agent. - tr := http.DefaultTransport + var tr http.RoundTripper = http.DefaultTransport.(*http.Transport).Clone() if opts.SkipTLSVerify || opts.SkipTLSVerifyRegistries.Contains(registryName) { tr.(*http.Transport).TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } + } else if certificatePath := opts.RegistriesCertificates[registryName]; certificatePath != "" { + systemCertPool := loader() + if err := systemCertPool.append(certificatePath); err != nil { + logrus.WithError(err).Warnf("Failed to load certificate %s for %s\n", certificatePath, registryName) + } else { + tr.(*http.Transport).TLSClientConfig = &tls.Config{ + RootCAs: systemCertPool.value(), + } + } } return tr } diff --git a/pkg/executor/push_test.go b/pkg/executor/push_test.go index 838561dde5..3659f85148 100644 --- a/pkg/executor/push_test.go +++ b/pkg/executor/push_test.go @@ -18,6 +18,8 @@ package executor import ( "bytes" + "crypto/tls" + "crypto/x509" "fmt" "io/ioutil" "net/http" @@ -223,3 +225,85 @@ func TestImageNameDigestFile(t *testing.T) { testutil.CheckErrorAndDeepEqual(t, false, err, want, got) } + +type mockedCertPool struct { + certificatesPath []string +} + +func (m *mockedCertPool) value() *x509.CertPool { + return &x509.CertPool{} +} + +func (m *mockedCertPool) append(path string) error { + m.certificatesPath = append(m.certificatesPath, path) + return nil +} + +func Test_makeTransport(t *testing.T) { + registryName := "my.registry.name" + + tests := []struct { + name string + opts *config.KanikoOptions + check func(*tls.Config, *mockedCertPool) + }{ + { + name: "SkipTLSVerify set", + opts: &config.KanikoOptions{SkipTLSVerify: true}, + check: func(config *tls.Config, pool *mockedCertPool) { + if !config.InsecureSkipVerify { + t.Errorf("makeTransport().TLSClientConfig.InsecureSkipVerify not set while SkipTLSVerify set") + } + }, + }, + { + name: "SkipTLSVerifyRegistries set with expected registry", + opts: &config.KanikoOptions{SkipTLSVerifyRegistries: []string{registryName}}, + check: func(config *tls.Config, pool *mockedCertPool) { + if !config.InsecureSkipVerify { + t.Errorf("makeTransport().TLSClientConfig.InsecureSkipVerify not set while SkipTLSVerifyRegistries set with registry name") + } + }, + }, + { + name: "SkipTLSVerifyRegistries set with other registry", + opts: &config.KanikoOptions{SkipTLSVerifyRegistries: []string{fmt.Sprintf("other.%s", registryName)}}, + check: func(config *tls.Config, pool *mockedCertPool) { + if config.InsecureSkipVerify { + t.Errorf("makeTransport().TLSClientConfig.InsecureSkipVerify set while SkipTLSVerifyRegistries not set with registry name") + } + }, + }, + { + name: "RegistriesCertificates set for registry", + opts: &config.KanikoOptions{RegistriesCertificates: map[string]string{registryName: "/path/to/the/certificate.cert"}}, + check: func(config *tls.Config, pool *mockedCertPool) { + if len(pool.certificatesPath) != 1 || pool.certificatesPath[0] != "/path/to/the/certificate.cert" { + t.Errorf("makeTransport().RegistriesCertificates certificate not appended to system certificates") + } + }, + }, + { + name: "RegistriesCertificates set for another registry", + opts: &config.KanikoOptions{RegistriesCertificates: map[string]string{fmt.Sprintf("other.%s=", registryName): "/path/to/the/certificate.cert"}}, + check: func(config *tls.Config, pool *mockedCertPool) { + if len(pool.certificatesPath) != 0 { + t.Errorf("makeTransport().RegistriesCertificates certificate appended to system certificates while added for other registry") + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var certificatesPath []string + certPool := mockedCertPool{ + certificatesPath: certificatesPath, + } + var mockedSystemCertLoader systemCertLoader = func() CertPool { + return &certPool + } + transport := makeTransport(tt.opts, registryName, mockedSystemCertLoader) + tt.check(transport.(*http.Transport).TLSClientConfig, &certPool) + }) + } +}