From e3e77cbb0561bbdea22c4aa7f19551ad3da8a14c Mon Sep 17 00:00:00 2001 From: suryatech27-cloud Date: Mon, 28 Mar 2022 15:35:16 +0530 Subject: [PATCH] Two-way TLS support added for oci pull. Signed-off-by: Sunil Kumar Signed-off-by: suryatech27-cloud --- internal/tlsutil/cfg.go | 80 +++++++++++++++++++++++++++++ pkg/action/pull.go | 21 +++++++- pkg/cli/environment.go | 50 +++++++++--------- pkg/registry/client.go | 110 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 226 insertions(+), 35 deletions(-) diff --git a/internal/tlsutil/cfg.go b/internal/tlsutil/cfg.go index 8b9d4329f..a4dc82228 100644 --- a/internal/tlsutil/cfg.go +++ b/internal/tlsutil/cfg.go @@ -19,7 +19,13 @@ package tlsutil import ( "crypto/tls" "crypto/x509" + "fmt" + "io/ioutil" "os" + "os/exec" + "path/filepath" + "runtime" + "strings" "github.com/pkg/errors" ) @@ -56,3 +62,77 @@ 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) { + if runtime.GOOS == "windows" || runtime.GOOS == "unix" { + fmt.Printf("%v OS not supported for this oci pull. Contact your administrator for more information !!!", runtime.GOOS) + os.Exit(1) + } else { + cmd, err := exec.Command("helm", "env", "HELM_CLIENT_TLS_CERT_DIR").Output() + if err != nil { + fmt.Printf("Error : %s", err) + os.Exit(1) + } + clientCertDir := strings.TrimSuffix(string(cmd), "\n") + if clientCertDir == "" { + fmt.Printf("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) + } else { + clientCertDir = fmt.Sprintf("%s%s", clientCertDir, host) + } + if _, err := os.Stat(clientCertDir); err != nil { + if os.IsNotExist(err) { + os.MkdirAll(clientCertDir, os.ModePerm) + return opts, errors.Wrapf(err, clientCertDir, "%v\n%v Directory created.") + } + } else { + + if files, err := ioutil.ReadDir(clientCertDir); err == nil { + for _, file := range files { + if filepath.Ext(file.Name()) == ".crt" { + opts.CaCertFile = fmt.Sprintf("%s/%s", clientCertDir, file.Name()) + } else if filepath.Ext(file.Name()) == ".pem" { + opts.CertFile = fmt.Sprintf("%s/%s", clientCertDir, file.Name()) + } else if filepath.Ext(file.Name()) == ".key" { + opts.KeyFile = fmt.Sprintf("%s/%s", clientCertDir, file.Name()) + } + } + } else { + fmt.Printf(" 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/pkg/action/pull.go b/pkg/action/pull.go index b4018869e..87cbd4380 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -125,7 +125,26 @@ func (p *Pull) Run(chartRef string) (string, error) { saved, v, err := c.DownloadTo(chartRef, p.Version, dest) if err != nil { - return out.String(), err + if registry.IsOCI(chartRef) && strings.Contains(fmt.Sprint(err), "remote error: tls: handshake failure") { + registryClient, err := registry.NewCrosClient(chartRef, + registry.ClientOptDebug(p.Settings.Debug), + registry.ClientOptCredentialsFile(p.Settings.RegistryConfig), + registry.ClientOptWriter(&out), + ) + if err != nil { + return out.String(), err + } + c.Options = append(c.Options, + getter.WithRegistryClient(registryClient), + getter.WithTagName(p.Version)) + + saved, v, err = c.DownloadTo(chartRef, p.Version, dest) + if err != nil { + return out.String(), err + } + } else { + return out.String(), err + } } if p.Verify { diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index d5b208015..d9851be66 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -68,22 +68,25 @@ type EnvSettings struct { PluginsDirectory string // MaxHistory is the max release history maintained. MaxHistory 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")), + 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")), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) @@ -146,17 +149,18 @@ 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_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_CLIENT_TLS_CERT_DIR": s.ClientSecCertDirectory, // broken, these are populated from helm flags and not kubeconfig. "HELM_KUBECONTEXT": s.KubeContext, diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 679cd690d..8be7b4c71 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" @@ -131,6 +135,94 @@ func NewClient(options ...ClientOption) (*Client, error) { return client, nil } +func NewCrosClient(chartref string, options ...ClientOption) (*Client, error) { + client := &Client{ + out: ioutil.Discard, + } + for _, option := range options { + option(client) + } + if client.credentialsFile == "" { + client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename) + } + if client.authorizer == nil { + authClient, err := dockerauth.NewClientWithDockerFallback(client.credentialsFile) + if err != nil { + return nil, err + } + client.authorizer = authClient + } + if client.resolver == nil { + host, err := urlutil.ExtractHostname(chartref) + 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(3 * 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 + } + + if client.registryAuthorizer == nil { + client.registryAuthorizer = ®istryauth.Client{ + Header: http.Header{ + "User-Agent": {version.GetUserAgent()}, + }, + Cache: registryauth.DefaultCache, + Credential: func(ctx context.Context, reg string) (registryauth.Credential, error) { + dockerClient, ok := client.authorizer.(*dockerauth.Client) + if !ok { + return registryauth.EmptyCredential, errors.New("unable to obtain docker client") + } + + username, password, err := dockerClient.Credential(reg) + if err != nil { + return registryauth.EmptyCredential, errors.New("unable to retrieve credentials") + } + + // A blank returned username and password value is a bearer token + if username == "" && password != "" { + return registryauth.Credential{ + RefreshToken: password, + }, nil + } + + return registryauth.Credential{ + Username: username, + Password: password, + }, nil + + }, + } + + } + return client, nil +} + // ClientOptDebug returns a function that sets the debug setting on client options set func ClientOptDebug(debug bool) ClientOption { return func(client *Client) { @@ -303,9 +395,8 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { numDescriptors := len(descriptors) if numDescriptors < minNumDescriptors { - return nil, errors.New( - fmt.Sprintf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d", - minNumDescriptors, numDescriptors)) + return nil, fmt.Errorf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d", + minNumDescriptors, numDescriptors) } var configDescriptor *ocispec.Descriptor var chartDescriptor *ocispec.Descriptor @@ -325,22 +416,19 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { } } if configDescriptor == nil { - return nil, errors.New( - fmt.Sprintf("could not load config with mediatype %s", ConfigMediaType)) + return nil, fmt.Errorf("could not load config with mediatype %s", ConfigMediaType) } if operation.withChart && chartDescriptor == nil { - return nil, errors.New( - fmt.Sprintf("manifest does not contain a layer with mediatype %s", - ChartLayerMediaType)) + return nil, fmt.Errorf("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, errors.New( - fmt.Sprintf("manifest does not contain a layer with mediatype %s", - ProvLayerMediaType)) + return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s", + ProvLayerMediaType) } } result := &PullResult{