client/internal/kubeconfig/validator.go (112 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. package kubeconfig //go:generate ../../bin/mockgen -copyright_file=../../../hack/copyright_header.txt -destination=./mocks/mock_validator.go -package=mocks github.com/Azure/aks-secure-tls-bootstrap/client/internal/kubeconfig Validator import ( "fmt" "net/http" "os" "time" internalhttp "github.com/Azure/aks-secure-tls-bootstrap/client/internal/http" "github.com/hashicorp/go-retryablehttp" "go.uber.org/zap" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/transport" certutil "k8s.io/client-go/util/cert" ) // kubeconfigLoader provides an interface for loading and unmarshaling a kubeconfig YAML from disk // and returning the corresponding REST client config. type clientConfigLoaderFunc func(kubeconfigPath string) (*restclient.Config, error) // clientsetLoaderFunc provides an interface for creating a kubernetes.Interface // from a specified REST client config. type clientsetLoaderFunc func(clientConfig *restclient.Config) (kubernetes.Interface, error) type Validator interface { Validate(kubeconfigPath string, ensureAuthorizedClient bool) error } type validator struct { clientConfigLoader clientConfigLoaderFunc clientsetLoader clientsetLoaderFunc logger *zap.Logger } var _ Validator = (*validator)(nil) func NewValidator(logger *zap.Logger) Validator { return &validator{ clientConfigLoader: func(kubeconfigPath string) (*restclient.Config, error) { if _, err := os.Stat(kubeconfigPath); err != nil { return nil, fmt.Errorf("failed to read specified kubeconfig: %w", err) } // Load structured kubeconfig data from the given path. loader := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath} loadedConfig, err := loader.Load() if err != nil { return nil, err } // Flatten the loaded data to a particular restclient.Config based on the current context. return clientcmd.NewNonInteractiveClientConfig( *loadedConfig, loadedConfig.CurrentContext, &clientcmd.ConfigOverrides{}, loader, ).ClientConfig() }, clientsetLoader: func(clientConfig *restclient.Config) (kubernetes.Interface, error) { return kubernetes.NewForConfig(clientConfig) }, logger: logger, } } func (v *validator) Validate(kubeconfigPath string, ensureAuthorizedClient bool) error { clientConfig, err := v.clientConfigLoader(kubeconfigPath) if err != nil { return fmt.Errorf("failed to create REST client config from kubeconfig: %w", err) } if err := validateClientConfig(clientConfig); err != nil { return fmt.Errorf("failed to validate client config contents: %w", err) } if !ensureAuthorizedClient { return nil } restclient.AddUserAgent(clientConfig, internalhttp.GetUserAgentValue()) clientConfig.Wrap(func(rt http.RoundTripper) http.RoundTripper { c := internalhttp.NewRetryableClient(v.logger) c.HTTPClient = &http.Client{Transport: rt} return &retryablehttp.RoundTripper{Client: c} }) clientset, err := v.clientsetLoader(clientConfig) if err != nil { return fmt.Errorf("failed to create clientset from REST client config: %w", err) } if err := ensureAuthorized(clientset); err != nil { return fmt.Errorf("failed to ensure client authorization: %w", err) } v.logger.Info("ensured existing clientset is authorized", zap.String("kubeconfig", kubeconfigPath)) return nil } // validateClientConfig returns a nil error iff the specified rest config contains a valid, unexpired client certificate. // Note that this function does NOT check whether the certificate signer is valid. func validateClientConfig(clientConfig *restclient.Config) error { transportConfig, err := clientConfig.TransportConfig() if err != nil { return fmt.Errorf("unable to load transport configuration from existing kubeconfig: %w", err) } if _, err := transport.TLSConfigFor(transportConfig); err != nil { return fmt.Errorf("unable to load TLS configuration from existing kubeconfig: %w", err) } certs, err := certutil.ParseCertsPEM(transportConfig.TLS.CertData) if err != nil { return fmt.Errorf("unable to load TLS certificates from existing kubeconfig: %w", err) } if len(certs) == 0 { return fmt.Errorf("no client certificates found within kubeconfig") } now := time.Now() for _, cert := range certs { if now.After(cert.NotAfter) { return fmt.Errorf("some part of the existing kubeconfig certificate has expired") } } return nil } // ensureAuthorized ensures that the provided clientset is authorized by making a call to get the apiserver's version. // An error is returned if the call fails, or if the server returns an unauthorized response. func ensureAuthorized(clientset kubernetes.Interface) error { _, err := clientset.Discovery().ServerVersion() switch { case err == nil: return nil case errors.IsUnauthorized(err): return fmt.Errorf("cannot make authorized request to list server version: %w", err) default: return fmt.Errorf("encountered an unexpected error when attempting to request server version info: %w", err) } }