From cf8c193b875b671bc458dcf42419ebc29a68f744 Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Tue, 5 Mar 2019 18:20:27 +0100 Subject: [PATCH] Allow to read OpenStack config from the secret Currently OpenStack cloud provider reads user credentials from config file, where data is stored in clear text. This approach is not recommended, as it is a serious security issue. This commit add an ability to read the config from secrets, if necessary. To do so, two new parameters are added to the config: SecretNamespace and SecretName. If they are specified, the provider will try to read config from the secret. --- pkg/cloudprovider/providers/openstack/BUILD | 3 + .../providers/openstack/openstack.go | 96 ++++++++++++++++--- pkg/volume/cinder/cinder_test.go | 24 ++--- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/pkg/cloudprovider/providers/openstack/BUILD b/pkg/cloudprovider/providers/openstack/BUILD index 934af2f750..3348196794 100644 --- a/pkg/cloudprovider/providers/openstack/BUILD +++ b/pkg/cloudprovider/providers/openstack/BUILD @@ -23,10 +23,13 @@ go_library( "//pkg/util/mount:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//staging/src/k8s.io/client-go/kubernetes:go_default_library", + "//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library", "//staging/src/k8s.io/client-go/util/cert:go_default_library", "//staging/src/k8s.io/cloud-provider:go_default_library", "//staging/src/k8s.io/cloud-provider/node/helpers:go_default_library", diff --git a/pkg/cloudprovider/providers/openstack/openstack.go b/pkg/cloudprovider/providers/openstack/openstack.go index bc17f41a39..775fc7899b 100644 --- a/pkg/cloudprovider/providers/openstack/openstack.go +++ b/pkg/cloudprovider/providers/openstack/openstack.go @@ -42,8 +42,11 @@ import ( "gopkg.in/gcfg.v1" "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" netutil "k8s.io/apimachinery/pkg/util/net" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" certutil "k8s.io/client-go/util/cert" cloudprovider "k8s.io/cloud-provider" nodehelpers "k8s.io/cloud-provider/node/helpers" @@ -145,17 +148,19 @@ type OpenStack struct { // Config is used to read and store information from the cloud configuration file type Config struct { Global struct { - AuthURL string `gcfg:"auth-url"` - Username string - UserID string `gcfg:"user-id"` - Password string - TenantID string `gcfg:"tenant-id"` - TenantName string `gcfg:"tenant-name"` - TrustID string `gcfg:"trust-id"` - DomainID string `gcfg:"domain-id"` - DomainName string `gcfg:"domain-name"` - Region string - CAFile string `gcfg:"ca-file"` + AuthURL string `gcfg:"auth-url"` + Username string + UserID string `gcfg:"user-id"` + Password string + TenantID string `gcfg:"tenant-id"` + TenantName string `gcfg:"tenant-name"` + TrustID string `gcfg:"trust-id"` + DomainID string `gcfg:"domain-id"` + DomainName string `gcfg:"domain-name"` + Region string + CAFile string `gcfg:"ca-file"` + SecretName string `gcfg:"secret-name"` + SecretNamespace string `gcfg:"secret-namespace"` } LoadBalancer LoadBalancerOpts BlockStorage BlockStorageOpts @@ -231,6 +236,9 @@ func configFromEnv() (cfg Config, ok bool) { cfg.Global.DomainName = os.Getenv("OS_USER_DOMAIN_NAME") } + cfg.Global.SecretName = os.Getenv("SECRET_NAME") + cfg.Global.SecretNamespace = os.Getenv("SECRET_NAMESPACE") + ok = cfg.Global.AuthURL != "" && cfg.Global.Username != "" && cfg.Global.Password != "" && @@ -245,6 +253,58 @@ func configFromEnv() (cfg Config, ok bool) { return } +func createKubernetesClient() (*kubernetes.Clientset, error) { + klog.Info("Creating kubernetes API client.") + + // create in-cluster config + cfg, err := clientcmd.BuildConfigFromFlags("", "") + if err != nil { + return nil, err + } + + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + + v, err := client.Discovery().ServerVersion() + if err != nil { + return nil, err + } + + klog.Infof("Kubernetes API client created, server version %s", fmt.Sprintf("v%v.%v", v.Major, v.Minor)) + return client, nil +} + +// setConfigFromSecret allows setting up the config from k8s secret +func setConfigFromSecret(cfg *Config) error { + secretName := cfg.Global.SecretName + secretNamespace := cfg.Global.SecretNamespace + + k8sClient, err := createKubernetesClient() + if err != nil { + return fmt.Errorf("failed to get kubernetes client: %v", err) + } + + secret, err := k8sClient.CoreV1().Secrets(secretNamespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + klog.Warningf("Cannot get secret %s in namespace %s. error: %q", secretName, secretNamespace, err) + return err + } + + if content, ok := secret.Data["clouds.conf"]; ok { + err = gcfg.ReadStringInto(cfg, string(content)) + if err != nil { + klog.Errorf("Cannot parse data from the secret.") + return fmt.Errorf("cannot parse data from the secret") + } + return nil + } + + klog.Errorf("Cannot find \"clouds.conf\" key in the secret.") + return fmt.Errorf("cannot find \"clouds.conf\" key in the secret") +} + func readConfig(config io.Reader) (Config, error) { if config == nil { return Config{}, fmt.Errorf("no OpenStack cloud provider config file given") @@ -259,7 +319,19 @@ func readConfig(config io.Reader) (Config, error) { cfg.Metadata.SearchOrder = fmt.Sprintf("%s,%s", configDriveID, metadataID) err := gcfg.ReadInto(&cfg, config) - return cfg, err + if err != nil { + return cfg, err + } + + if cfg.Global.SecretName != "" && cfg.Global.SecretNamespace != "" { + klog.Infof("Set credentials from secret %s in namespace %s", cfg.Global.SecretName, cfg.Global.SecretNamespace) + err = setConfigFromSecret(&cfg) + if err != nil { + return cfg, err + } + } + + return cfg, nil } // caller is a tiny helper for conditional unwind logic diff --git a/pkg/volume/cinder/cinder_test.go b/pkg/volume/cinder/cinder_test.go index d75d629d3b..b502c470b0 100644 --- a/pkg/volume/cinder/cinder_test.go +++ b/pkg/volume/cinder/cinder_test.go @@ -304,17 +304,19 @@ func getOpenstackCloudProvider() (*openstack.OpenStack, error) { func getOpenstackConfig() openstack.Config { cfg := openstack.Config{ Global: struct { - AuthURL string `gcfg:"auth-url"` - Username string - UserID string `gcfg:"user-id"` - Password string - TenantID string `gcfg:"tenant-id"` - TenantName string `gcfg:"tenant-name"` - TrustID string `gcfg:"trust-id"` - DomainID string `gcfg:"domain-id"` - DomainName string `gcfg:"domain-name"` - Region string - CAFile string `gcfg:"ca-file"` + AuthURL string `gcfg:"auth-url"` + Username string + UserID string `gcfg:"user-id"` + Password string + TenantID string `gcfg:"tenant-id"` + TenantName string `gcfg:"tenant-name"` + TrustID string `gcfg:"trust-id"` + DomainID string `gcfg:"domain-id"` + DomainName string `gcfg:"domain-name"` + Region string + CAFile string `gcfg:"ca-file"` + SecretName string `gcfg:"secret-name"` + SecretNamespace string `gcfg:"secret-namespace"` }{ Username: "user", Password: "pass",