From a240b733a7627eb49977c5db7cb22d4f05732a74 Mon Sep 17 00:00:00 2001 From: Thomas Rucker Date: Mon, 13 May 2019 15:55:25 +0200 Subject: [PATCH] Add support for TLS connections to tiller (#13) Add possibility for TLS connections to tiller --- Gopkg.lock | 5 +- main.go | 21 ++++- vendor/k8s.io/helm/pkg/tlsutil/cfg.go | 89 +++++++++++++++++++++ vendor/k8s.io/helm/pkg/tlsutil/tls.go | 97 +++++++++++++++++++++++ vendor/k8s.io/helm/pkg/urlutil/urlutil.go | 87 ++++++++++++++++++++ 5 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 vendor/k8s.io/helm/pkg/tlsutil/cfg.go create mode 100644 vendor/k8s.io/helm/pkg/tlsutil/tls.go create mode 100644 vendor/k8s.io/helm/pkg/urlutil/urlutil.go diff --git a/Gopkg.lock b/Gopkg.lock index 902dd6d..ff57895 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -285,7 +285,7 @@ revision = "2a7c9300402896b3c073f2f47df85527c94f83a0" [[projects]] - digest = "1:afde29e1a327b6aa723131a5cb199a14462922a1f03e3f3d37de67f27839deed" + digest = "1:ad942673f5b07b6d444e6b838f53bdb3163204596f68c3f340c784ccf06c10f7" name = "k8s.io/helm" packages = [ "pkg/chartutil", @@ -301,6 +301,8 @@ "pkg/renderutil", "pkg/storage/errors", "pkg/sympath", + "pkg/tlsutil", + "pkg/urlutil", "pkg/version", ] pruneopts = "UT" @@ -317,6 +319,7 @@ "github.com/prometheus/client_golang/prometheus/promhttp", "k8s.io/helm/pkg/helm", "k8s.io/helm/pkg/proto/hapi/release", + "k8s.io/helm/pkg/tlsutil", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/main.go b/main.go index 28f4d12..bbfbdbd 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/tlsutil" "github.com/facebookgo/flagenv" "github.com/prometheus/client_golang/prometheus" @@ -32,6 +33,10 @@ var ( localTiller = "127.0.0.1:44134" tillerNamespace = flag.String("tiller-namespaces", "kube-system", "namespaces of Tillers , separated list kube-system,dev") + tillerTLSEnable = flag.Bool("tiller-tls-enable", false, "enable TLS communication with tiller (default false)") + tillerTLSKey = flag.String("tiller-tls-key", "/etc/helm-exporter/tls.key", "path to private key file used to communicate with tiller") + tillerTLSCert = flag.String("tiller-tls-cert", "/etc/helm-exporter/tls.crt", "path to certificate key file used to communicate with tiller") + tillerTLSVerify = flag.Bool("tiller-tls-verify", false, "enable verification of the remote tiller certificate (default false)") statusCodes = []release.Status_Code{ release.Status_UNKNOWN, @@ -52,7 +57,21 @@ var ( func newHelmClient(tillerEndpoint string) (*helm.Client, error) { log.Printf("Attempting to connect to %s", tillerEndpoint) - client := helm.NewClient(helm.Host(tillerEndpoint)) + options := []helm.Option{helm.Host(tillerEndpoint)} + if *tillerTLSEnable { + tlsopts := tlsutil.Options{ + KeyFile: *tillerTLSKey, + CertFile: *tillerTLSCert, + InsecureSkipVerify: !(*tillerTLSVerify), + } + tlscfg, err := tlsutil.ClientConfig(tlsopts) + if err != nil { + return nil, err + } + options = append(options, helm.WithTLS(tlscfg)) + } + + client := helm.NewClient(options...) err := client.PingTiller() return client, err } diff --git a/vendor/k8s.io/helm/pkg/tlsutil/cfg.go b/vendor/k8s.io/helm/pkg/tlsutil/cfg.go new file mode 100644 index 0000000..2c1dfd3 --- /dev/null +++ b/vendor/k8s.io/helm/pkg/tlsutil/cfg.go @@ -0,0 +1,89 @@ +/* +Copyright The Helm Authors. + +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 tlsutil + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" +) + +// Options represents configurable options used to create client and server TLS configurations. +type Options struct { + CaCertFile string + // If either the KeyFile or CertFile is empty, ClientConfig() will not load them, + // preventing Helm from authenticating to Tiller. They are required to be non-empty + // when calling ServerConfig, otherwise an error is returned. + KeyFile string + CertFile string + // Client-only options + InsecureSkipVerify bool + // Overrides the server name used to verify the hostname on the returned + // certificates from the server. + ServerName string + // Server-only options + ClientAuth tls.ClientAuthType +} + +// ClientConfig retusn a TLS configuration for use by a Helm client. +func ClientConfig(opts Options) (cfg *tls.Config, err error) { + var cert *tls.Certificate + var pool *x509.CertPool + + if opts.CertFile != "" || opts.KeyFile != "" { + if cert, err = CertFromFilePair(opts.CertFile, opts.KeyFile); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("could not load x509 key pair (cert: %q, key: %q): %v", opts.CertFile, opts.KeyFile, err) + } + return nil, fmt.Errorf("could not read x509 key pair (cert: %q, key: %q): %v", opts.CertFile, opts.KeyFile, err) + } + } + if !opts.InsecureSkipVerify && opts.CaCertFile != "" { + if pool, err = CertPoolFromFile(opts.CaCertFile); err != nil { + return nil, err + } + } + cfg = &tls.Config{ + InsecureSkipVerify: opts.InsecureSkipVerify, + Certificates: []tls.Certificate{*cert}, + ServerName: opts.ServerName, + RootCAs: pool, + } + return cfg, nil +} + +// ServerConfig returns a TLS configuration for use by the Tiller server. +func ServerConfig(opts Options) (cfg *tls.Config, err error) { + var cert *tls.Certificate + var pool *x509.CertPool + + if cert, err = CertFromFilePair(opts.CertFile, opts.KeyFile); err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("could not load x509 key pair (cert: %q, key: %q): %v", opts.CertFile, opts.KeyFile, err) + } + return nil, fmt.Errorf("could not read x509 key pair (cert: %q, key: %q): %v", opts.CertFile, opts.KeyFile, err) + } + if opts.ClientAuth >= tls.VerifyClientCertIfGiven && opts.CaCertFile != "" { + if pool, err = CertPoolFromFile(opts.CaCertFile); err != nil { + return nil, err + } + } + + cfg = &tls.Config{MinVersion: tls.VersionTLS12, ClientAuth: opts.ClientAuth, Certificates: []tls.Certificate{*cert}, ClientCAs: pool} + return cfg, nil +} diff --git a/vendor/k8s.io/helm/pkg/tlsutil/tls.go b/vendor/k8s.io/helm/pkg/tlsutil/tls.go new file mode 100644 index 0000000..6b0052a --- /dev/null +++ b/vendor/k8s.io/helm/pkg/tlsutil/tls.go @@ -0,0 +1,97 @@ +/* +Copyright The Helm Authors. + +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 tlsutil + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "k8s.io/helm/pkg/urlutil" +) + +func newTLSConfigCommon(certFile, keyFile, caFile string) (*tls.Config, error) { + config := tls.Config{} + + if certFile != "" && keyFile != "" { + cert, err := CertFromFilePair(certFile, keyFile) + if err != nil { + return nil, err + } + config.Certificates = []tls.Certificate{*cert} + } + + if caFile != "" { + cp, err := CertPoolFromFile(caFile) + if err != nil { + return nil, err + } + config.RootCAs = cp + } + + return &config, nil +} + +// NewClientTLS returns tls.Config appropriate for client auth. +func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) { + return newTLSConfigCommon(certFile, keyFile, caFile) +} + +// NewTLSConfig returns tls.Config appropriate for client and/or server auth. +func NewTLSConfig(url, certFile, keyFile, caFile string) (*tls.Config, error) { + config, err := newTLSConfigCommon(certFile, keyFile, caFile) + if err != nil { + return nil, err + } + config.BuildNameToCertificate() + + serverName, err := urlutil.ExtractHostname(url) + if err != nil { + return nil, err + } + config.ServerName = serverName + + return config, nil +} + +// CertPoolFromFile returns an x509.CertPool containing the certificates +// in the given PEM-encoded file. +// Returns an error if the file could not be read, a certificate could not +// be parsed, or if the file does not contain any certificates +func CertPoolFromFile(filename string) (*x509.CertPool, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("can't read CA file: %v", filename) + } + cp := x509.NewCertPool() + if !cp.AppendCertsFromPEM(b) { + return nil, fmt.Errorf("failed to append certificates from file: %s", filename) + } + return cp, nil +} + +// CertFromFilePair returns an tls.Certificate containing the +// certificates public/private key pair from a pair of given PEM-encoded files. +// Returns an error if the file could not be read, a certificate could not +// be parsed, or if the file does not contain any certificates +func CertFromFilePair(certFile, keyFile string) (*tls.Certificate, error) { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, fmt.Errorf("can't load key pair from cert %s and key %s: %s", certFile, keyFile, err) + } + return &cert, err +} diff --git a/vendor/k8s.io/helm/pkg/urlutil/urlutil.go b/vendor/k8s.io/helm/pkg/urlutil/urlutil.go new file mode 100644 index 0000000..272907d --- /dev/null +++ b/vendor/k8s.io/helm/pkg/urlutil/urlutil.go @@ -0,0 +1,87 @@ +/* +Copyright The Helm Authors. + +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 urlutil + +import ( + "net/url" + "path" + "path/filepath" + "strings" +) + +// URLJoin joins a base URL to one or more path components. +// +// It's like filepath.Join for URLs. If the baseURL is pathish, this will still +// perform a join. +// +// If the URL is unparsable, this returns an error. +func URLJoin(baseURL string, paths ...string) (string, error) { + u, err := url.Parse(baseURL) + if err != nil { + return "", err + } + // We want path instead of filepath because path always uses /. + all := []string{u.Path} + all = append(all, paths...) + u.Path = path.Join(all...) + return u.String(), nil +} + +// Equal normalizes two URLs and then compares for equality. +func Equal(a, b string) bool { + au, err := url.Parse(a) + if err != nil { + a = filepath.Clean(a) + b = filepath.Clean(b) + // If urls are paths, return true only if they are an exact match + return a == b + } + bu, err := url.Parse(b) + if err != nil { + return false + } + + for _, u := range []*url.URL{au, bu} { + if u.Path == "" { + u.Path = "/" + } + u.Path = filepath.Clean(u.Path) + } + return au.String() == bu.String() +} + +// ExtractHostname returns hostname from URL +func ExtractHostname(addr string) (string, error) { + u, err := url.Parse(addr) + if err != nil { + return "", err + } + return stripPort(u.Host), nil +} + +// Backported from Go 1.8 because Circle is still on 1.7 +func stripPort(hostport string) string { + colon := strings.IndexByte(hostport, ':') + if colon == -1 { + return hostport + } + if i := strings.IndexByte(hostport, ']'); i != -1 { + return strings.TrimPrefix(hostport[:i], "[") + } + return hostport[:colon] + +}