From a460b9ac6eb2519e4ff61575aa5d4ee85e576ebc Mon Sep 17 00:00:00 2001 From: suryatech27-cloud Date: Thu, 9 Jun 2022 17:20:48 +0530 Subject: [PATCH] Changes for two-way tls client validation --- cmd/helm/flags.go | 1 + cmd/helm/root.go | 1 + internal/tlsutil/cfg.go | 86 +++++++++++++++++++++++++++++++++++ internal/urlutil/urlutil.go | 6 ++- pkg/action/install.go | 1 + pkg/action/pull.go | 22 ++++++++- pkg/cli/environment.go | 54 +++++++++++----------- pkg/getter/getter.go | 7 +++ pkg/registry/client.go | 89 +++++++++++++++++++++++++++++++------ 9 files changed, 225 insertions(+), 42 deletions(-) diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index 0cc0564e2..c5983b9a2 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -61,6 +61,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains") + f.BoolVar(&c.TlsEnabled, "tls-enabled", false, "if two-way tls authentication enabled then trying to send client certificate") } // bindOutputFlag will add the output flag to the given command and bind the diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 53b0c0ce8..5f7aba02f 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -68,6 +68,7 @@ Environment variables: | $HELM_KUBECONTEXT | set the name of the kubeconfig context. | | $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. | | $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable)| +| $HELM_CLIENT_TLS_CERT_DIR | set the certificate directory for 2-way tls support for oci pull. | Helm stores cache, configuration, and data based on the following configuration order: diff --git a/internal/tlsutil/cfg.go b/internal/tlsutil/cfg.go index 8b9d4329f..8c7b22178 100644 --- a/internal/tlsutil/cfg.go +++ b/internal/tlsutil/cfg.go @@ -19,7 +19,14 @@ package tlsutil import ( "crypto/tls" "crypto/x509" + "fmt" + "io/ioutil" + "log" "os" + "os/exec" + "path/filepath" + "runtime" + "strings" "github.com/pkg/errors" ) @@ -56,3 +63,82 @@ func ClientConfig(opts Options) (cfg *tls.Config, err error) { cfg = &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify, Certificates: []tls.Certificate{*cert}, RootCAs: pool} return cfg, nil } + +func ReadCertFromSecDir(host string) (opts Options, err error) { + //fmt.Println("Final Host Name : ", host) + if runtime.GOOS == "windows" || runtime.GOOS == "unix" { + log.Fatalf("%v OS not supported for this oci pull.", runtime.GOOS) + os.Exit(1) + } else { + cmd, err := exec.Command("helm", "env", "HELM_CLIENT_TLS_CERT_DIR").Output() + if err != nil { + log.Fatalf("Error : %s", err) + os.Exit(1) + } + clientCertDir := strings.TrimSuffix(string(cmd), "\n") + if clientCertDir == "" { + log.Fatalf("Please configure client certificate directory for tls connection set/export HELM_CLIENT_TLS_CERT_DIR='/etc/docker/certs.d/'\n") + os.Exit(1) + } + + if clientCertDir[len(clientCertDir)-1] != '/' { + clientCertDir = fmt.Sprintf("%s/%s", clientCertDir, host) + //fmt.Println("clientCertDir1", clientCertDir) + } else { + clientCertDir = fmt.Sprintf("%s%s", clientCertDir, host) + //fmt.Println("clientCertDir2", clientCertDir) + } + if _, err := os.Stat(clientCertDir); err != nil { + if os.IsNotExist(err) { + return opts, errors.Wrapf(err, clientCertDir, "%v\nPlease Create a directory same as hostname [%v] .") + } + } else { + + if files, err := ioutil.ReadDir(clientCertDir); err == nil { + for _, file := range files { + if filepath.Ext(file.Name()) == ".pem" { + opts.CaCertFile = fmt.Sprintf("%s/%s", clientCertDir, file.Name()) + //fmt.Println("Root ca file : ", opts.CaCertFile) + } else if filepath.Ext(file.Name()) == ".cert" { + opts.CertFile = fmt.Sprintf("%s/%s", clientCertDir, file.Name()) + //fmt.Println("client cert file : ", opts.CertFile) + } else if filepath.Ext(file.Name()) == ".key" { + opts.KeyFile = fmt.Sprintf("%s/%s", clientCertDir, file.Name()) + //fmt.Println("client key file", opts.KeyFile) + } + } + } else { + log.Fatalf(" Certificate not found in current directory - %v\n ", err) + os.Exit(1) + } + switch { + case opts.CaCertFile == "" && opts.CertFile == "" && opts.KeyFile == "": + fmt.Printf("Error : Missing certificate (cacerts.crt,client.pem,client.key) required !!\n") + os.Exit(1) + case opts.CaCertFile == "" && opts.CertFile == "": + fmt.Printf("Error : Missing certificate : Root-CA and client certificate (cacerts.crt,client.pem) required !!\n") + os.Exit(1) + case opts.CaCertFile == "" && opts.KeyFile == "": + fmt.Printf("Error : Missing Certificate : Root-CA and and client key (cacerts.crt,client.key) required.\n") + os.Exit(1) + + case opts.CertFile == "" && opts.KeyFile == "": + fmt.Printf("Error : Missing Certificate : Client certificate and client key (client.pem,client.key) required.\n") + os.Exit(1) + } + switch { + case opts.CaCertFile == "": + fmt.Printf("Error : Missing Certificate : Client Root-CA (cacerts.crt) required.\n") + os.Exit(1) + case opts.CertFile == "": + fmt.Printf("Error : Missing Certificate : Client certificate(client.pem) required.\n") + os.Exit(1) + case opts.KeyFile == "": + fmt.Printf("Error : Missing Certificate : Client keyfile (client.key) required.\n") + os.Exit(1) + + } + } + } + return opts, nil +} diff --git a/internal/urlutil/urlutil.go b/internal/urlutil/urlutil.go index a8cf7398c..9331fecd4 100644 --- a/internal/urlutil/urlutil.go +++ b/internal/urlutil/urlutil.go @@ -69,5 +69,9 @@ func ExtractHostname(addr string) (string, error) { if err != nil { return "", err } - return u.Hostname(), nil + if u.Port() != "" { + return u.Hostname() + ":" + u.Port(), nil + } else { + return u.Hostname(), nil + } } diff --git a/pkg/action/install.go b/pkg/action/install.go index cd202ccab..a01023395 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -117,6 +117,7 @@ type ChartPathOptions struct { Username string // --username Verify bool // --verify Version string // --version + TlsEnabled bool // --tls-enabled // registryClient provides a registry client but is not added with // options from a flag diff --git a/pkg/action/pull.go b/pkg/action/pull.go index b4018869e..a7fb9028a 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -86,6 +86,7 @@ func (p *Pull) Run(chartRef string) (string, error) { getter.WithPassCredentialsAll(p.PassCredentialsAll), getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile), getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify), + getter.WithTwoWayTLSEnable(p.TlsEnabled), }, RegistryClient: p.cfg.RegistryClient, RepositoryConfig: p.Settings.RepositoryConfig, @@ -93,8 +94,25 @@ func (p *Pull) Run(chartRef string) (string, error) { } if registry.IsOCI(chartRef) { - c.Options = append(c.Options, - getter.WithRegistryClient(p.cfg.RegistryClient)) + //fmt.Println("pull.go :===> tls enabled", p.TlsEnabled) + //fmt.Println("pull.go : ====> ", chartRef) + if !p.TlsEnabled { + c.Options = append(c.Options, + getter.WithRegistryClient(p.cfg.RegistryClient), + ) + } else { + registryClient, err := registry.NewClient( + registry.ClientOptDebug(p.Settings.Debug), + registry.ClientOptCredentialsFile(p.Settings.RegistryConfig), + registry.ClientOptWriter(&out), + registry.ClientOptTwoWayTLSEnable(p.TlsEnabled), + registry.ClientOptChartRef(chartRef), + ) + if err != nil { + return out.String(), err + } + c.Options = append(c.Options, getter.WithRegistryClient(registryClient)) + } } if p.Verify { diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index b17172db4..418cf3726 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -74,23 +74,26 @@ type EnvSettings struct { MaxHistory int // BurstLimit is the default client-side throttling limit. BurstLimit int + // Secondary Certificate directory for helm oci pull + ClientSecCertDirectory string } func New() *EnvSettings { env := &EnvSettings{ - namespace: os.Getenv("HELM_NAMESPACE"), - MaxHistory: envIntOr("HELM_MAX_HISTORY", defaultMaxHistory), - KubeContext: os.Getenv("HELM_KUBECONTEXT"), - KubeToken: os.Getenv("HELM_KUBETOKEN"), - KubeAsUser: os.Getenv("HELM_KUBEASUSER"), - KubeAsGroups: envCSV("HELM_KUBEASGROUPS"), - KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"), - KubeCaFile: os.Getenv("HELM_KUBECAFILE"), - PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), - RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry/config.json")), - RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), - RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), - BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), + namespace: os.Getenv("HELM_NAMESPACE"), + MaxHistory: envIntOr("HELM_MAX_HISTORY", defaultMaxHistory), + KubeContext: os.Getenv("HELM_KUBECONTEXT"), + KubeToken: os.Getenv("HELM_KUBETOKEN"), + KubeAsUser: os.Getenv("HELM_KUBEASUSER"), + KubeAsGroups: envCSV("HELM_KUBEASGROUPS"), + KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"), + KubeCaFile: os.Getenv("HELM_KUBECAFILE"), + ClientSecCertDirectory: envOr("HELM_CLIENT_TLS_CERT_DIR", ""), + PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), + RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry/config.json")), + RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), + RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), + BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) @@ -158,18 +161,19 @@ func envCSV(name string) (ls []string) { func (s *EnvSettings) EnvVars() map[string]string { envvars := map[string]string{ - "HELM_BIN": os.Args[0], - "HELM_CACHE_HOME": helmpath.CachePath(""), - "HELM_CONFIG_HOME": helmpath.ConfigPath(""), - "HELM_DATA_HOME": helmpath.DataPath(""), - "HELM_DEBUG": fmt.Sprint(s.Debug), - "HELM_PLUGINS": s.PluginsDirectory, - "HELM_REGISTRY_CONFIG": s.RegistryConfig, - "HELM_REPOSITORY_CACHE": s.RepositoryCache, - "HELM_REPOSITORY_CONFIG": s.RepositoryConfig, - "HELM_NAMESPACE": s.Namespace(), - "HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory), - "HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit), + "HELM_BIN": os.Args[0], + "HELM_CACHE_HOME": helmpath.CachePath(""), + "HELM_CONFIG_HOME": helmpath.ConfigPath(""), + "HELM_DATA_HOME": helmpath.DataPath(""), + "HELM_DEBUG": fmt.Sprint(s.Debug), + "HELM_PLUGINS": s.PluginsDirectory, + "HELM_REGISTRY_CONFIG": s.RegistryConfig, + "HELM_REPOSITORY_CACHE": s.RepositoryCache, + "HELM_REPOSITORY_CONFIG": s.RepositoryConfig, + "HELM_NAMESPACE": s.Namespace(), + "HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory), + "HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit), + "HELM_CLIENT_TLS_CERT_DIR": s.ClientSecCertDirectory, // broken, these are populated from helm flags and not kubeconfig. "HELM_KUBECONTEXT": s.KubeContext, diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 653b032fe..b8b1252cb 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -42,6 +42,7 @@ type options struct { passCredentialsAll bool userAgent string version string + tlsEnabled bool registryClient *registry.Client timeout time.Duration transport *http.Transport @@ -87,6 +88,12 @@ func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option { } } +func WithTwoWayTLSEnable(tlsEnabled bool) Option { + return func(opts *options) { + opts.tlsEnabled = tlsEnabled + } +} + // WithTLSClientConfig sets the client auth with the provided credentials. func WithTLSClientConfig(certFile, keyFile, caFile string) Option { return func(opts *options) { diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 0107136d3..471c3724e 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -22,9 +22,11 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" "sort" "strings" + "time" "github.com/Masterminds/semver/v3" "github.com/containerd/containerd/remotes" @@ -38,6 +40,8 @@ import ( registryremote "oras.land/oras-go/pkg/registry/remote" registryauth "oras.land/oras-go/pkg/registry/remote/auth" + "helm.sh/helm/v3/internal/tlsutil" + "helm.sh/helm/v3/internal/urlutil" "helm.sh/helm/v3/internal/version" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/helmpath" @@ -60,6 +64,8 @@ type ( authorizer auth.Client registryAuthorizer *registryauth.Client resolver remotes.Resolver + tlsEnabled bool + chartRef string } // ClientOption allows specifying various settings configurable by the user for overriding the defaults @@ -85,15 +91,53 @@ func NewClient(options ...ClientOption) (*Client, error) { } client.authorizer = authClient } + if client.resolver == nil { - headers := http.Header{} - headers.Set("User-Agent", version.GetUserAgent()) - opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)} - resolver, err := client.authorizer.ResolverWithOpts(opts...) - if err != nil { - return nil, err + if client.tlsEnabled { + host, err := urlutil.ExtractHostname(client.chartRef) + fmt.Println("host name : ", host) + if err != nil { + fmt.Printf("error :%v\n", err) + } + clientOpts, err := tlsutil.ReadCertFromSecDir(host) + if err != nil { + return client, errors.Wrapf(err, "Client certificate/directory Not Exist !!") + } + cfgtls, err := tlsutil.ClientConfig(clientOpts) + if err != nil { + fmt.Printf("error :%v\n", err) + + } + var rt http.RoundTripper = &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 30 * time.Second, + TLSClientConfig: cfgtls, + ResponseHeaderTimeout: time.Duration(30 * time.Second), + DisableKeepAlives: true, + } + crosclient := http.Client{Transport: rt, Timeout: 30 * time.Second} + headers := http.Header{} + headers.Set("User-Agent", version.GetUserAgent()) + opts := []auth.ResolverOption{auth.WithResolverHeaders(headers), auth.WithResolverClient(&crosclient)} + resolver, err := client.authorizer.ResolverWithOpts(opts...) + if err != nil { + return nil, err + } + client.resolver = resolver + } else { + headers := http.Header{} + headers.Set("User-Agent", version.GetUserAgent()) + opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)} + resolver, err := client.authorizer.ResolverWithOpts(opts...) + if err != nil { + return nil, err + } + client.resolver = resolver } - client.resolver = resolver + } if client.registryAuthorizer == nil { client.registryAuthorizer = ®istryauth.Client{ @@ -145,6 +189,12 @@ func ClientOptWriter(out io.Writer) ClientOption { } } +func ClientOptChartRef(chartRef string) ClientOption { + return func(client *Client) { + client.chartRef = chartRef + } +} + // ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set func ClientOptCredentialsFile(credentialsFile string) ClientOption { return func(client *Client) { @@ -152,6 +202,13 @@ func ClientOptCredentialsFile(credentialsFile string) ClientOption { } } +//ClientOptTwoWayTLSEnable returns a function that sets the client certificate when two-way tls authentication enable +func ClientOptTwoWayTLSEnable(tlsEnabled bool) ClientOption { + return func(client *Client) { + client.tlsEnabled = tlsEnabled + } +} + type ( // LoginOption allows specifying various settings on login LoginOption func(*loginOperation) @@ -303,8 +360,9 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { numDescriptors := len(descriptors) if numDescriptors < minNumDescriptors { - return nil, fmt.Errorf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d", - minNumDescriptors, numDescriptors) + return nil, errors.New( + fmt.Sprintf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d", + minNumDescriptors, numDescriptors)) } var configDescriptor *ocispec.Descriptor var chartDescriptor *ocispec.Descriptor @@ -324,19 +382,22 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { } } if configDescriptor == nil { - return nil, fmt.Errorf("could not load config with mediatype %s", ConfigMediaType) + return nil, errors.New( + fmt.Sprintf("could not load config with mediatype %s", ConfigMediaType)) } if operation.withChart && chartDescriptor == nil { - return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s", - ChartLayerMediaType) + return nil, errors.New( + fmt.Sprintf("manifest does not contain a layer with mediatype %s", + ChartLayerMediaType)) } var provMissing bool if operation.withProv && provDescriptor == nil { if operation.ignoreMissingProv { provMissing = true } else { - return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s", - ProvLayerMediaType) + return nil, errors.New( + fmt.Sprintf("manifest does not contain a layer with mediatype %s", + ProvLayerMediaType)) } } result := &PullResult{