github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/kube/vault/vault_factory.go (about)

     1  package vault
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"time"
     8  
     9  	"github.com/olli-ai/jx/v2/pkg/vault"
    10  
    11  	"github.com/banzaicloud/bank-vaults/operator/pkg/client/clientset/versioned"
    12  	"github.com/hashicorp/vault/api"
    13  	"github.com/jenkins-x/jx-logging/pkg/log"
    14  	"github.com/olli-ai/jx/v2/pkg/kube/serviceaccount"
    15  	"github.com/olli-ai/jx/v2/pkg/util"
    16  	"github.com/pkg/errors"
    17  	"gopkg.in/AlecAivazis/survey.v1/terminal"
    18  	v1 "k8s.io/api/core/v1"
    19  	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/client-go/kubernetes"
    21  )
    22  
    23  const (
    24  
    25  	// maxRetries controls the maximum number of time retry when 5xx error occurs. Default to 2 (for a total
    26  	// of three retires)
    27  	maxRetries = 2
    28  
    29  	// healthReadyTimeout define the maximum duration to wait for vault to become initialized and unsealed
    30  	healthRetryTimeout = 10 * time.Minute
    31  
    32  	// healthInitialRetryDelay define the initial delay before starting the retries
    33  	healthInitialRetryDelay = 10 * time.Second
    34  
    35  	// authRetryTimeout define the maximum duration to wait for vault to authenticate
    36  	authRetryTimeout = 1 * time.Minute
    37  
    38  	// kvEngineConfigPath config path for KV secrets engine V2
    39  	kvEngineConfigPath = "config"
    40  
    41  	// kvEngineWriteCheckPath imaginary secret to check when the secrets engine is ready for write
    42  	kvEngineWriteCheckPath = "data/jx-write-check"
    43  
    44  	// kvEngineInitialRetryDelay define the initial delay before checking the kv engine configuration
    45  	kvEngineInitialRetryDelay = 1 * time.Second
    46  
    47  	// kvEngineRetryTimeout define the maximum duration to wait for KV engine to be properly configured
    48  	kvEngineRetryTimeout = 5 * time.Minute
    49  )
    50  
    51  // OptionsInterface is an interface to allow passing around of a CommonOptions object without dependencies on the whole of the cmd package
    52  type OptionsInterface interface {
    53  	KubeClientAndNamespace() (kubernetes.Interface, string, error)
    54  	VaultOperatorClient() (versioned.Interface, error)
    55  	GetIn() terminal.FileReader
    56  	GetOut() terminal.FileWriter
    57  	GetErr() io.Writer
    58  	GetIOFileHandles() util.IOFileHandles
    59  }
    60  
    61  // VaultClientFactory keeps the configuration required to build a new vault client factory
    62  type VaultClientFactory struct {
    63  	Options             OptionsInterface
    64  	Selector            Selector
    65  	kubeClient          kubernetes.Interface
    66  	defaultNamespace    string
    67  	DisableURLDiscovery bool
    68  }
    69  
    70  // NewInteractiveVaultClientFactory creates a VaultClientFactory that allows the user to pick vaults if necessary
    71  func NewInteractiveVaultClientFactory(options OptionsInterface) (*VaultClientFactory, error) {
    72  	factory := &VaultClientFactory{
    73  		Options: options,
    74  	}
    75  	var err error
    76  	factory.kubeClient, factory.defaultNamespace, err = options.KubeClientAndNamespace()
    77  	if err != nil {
    78  		return factory, err
    79  	}
    80  	factory.Selector, err = NewVaultSelector(options)
    81  	if err != nil {
    82  		return factory, err
    83  	}
    84  	return factory, nil
    85  }
    86  
    87  // NewVaultClientFactory creates a new VaultClientFactory with different options to the above. It doesnt' have CLI support so
    88  // will fail if it needs interactive input (unlikely)
    89  func NewVaultClientFactory(kubeClient kubernetes.Interface, vaultOperatorClient versioned.Interface, defaultNamespace string) (*VaultClientFactory, error) {
    90  	return &VaultClientFactory{
    91  		kubeClient:       kubeClient,
    92  		defaultNamespace: defaultNamespace,
    93  		Selector: &vaultSelector{
    94  			kubeClient:          kubeClient,
    95  			vaultOperatorClient: vaultOperatorClient,
    96  		},
    97  	}, nil
    98  }
    99  
   100  // NewVaultClientFactoryWithoutSelector creates a new VaultClientFactory.
   101  func NewVaultClientFactoryWithoutSelector(kubeClient kubernetes.Interface, defaultNamespace string) (*VaultClientFactory, error) {
   102  	return &VaultClientFactory{
   103  		kubeClient:       kubeClient,
   104  		defaultNamespace: defaultNamespace,
   105  	}, nil
   106  }
   107  
   108  // NewVaultClientFactoryWithSelector creates a new VaultClientFactory with a provided Selector.
   109  // This allows to use an external Vault instance using the custom selector.
   110  func NewVaultClientFactoryWithSelector(kubeClient kubernetes.Interface, selector Selector, defaultNamespace string) (*VaultClientFactory, error) {
   111  	return &VaultClientFactory{
   112  		kubeClient:       kubeClient,
   113  		defaultNamespace: defaultNamespace,
   114  		Selector:         selector,
   115  	}, nil
   116  }
   117  
   118  // NewVaultClient creates a new api.Client
   119  // if namespace is nil, then the default namespace of the factory will be used
   120  // if the name is nil, and only one vault is found, then that vault will be used. Otherwise the user will be prompted to
   121  // select a vault for the client.
   122  func (v *VaultClientFactory) NewVaultClient(name string, namespace string, useIngressURL, insecureSSLWebhook bool) (*api.Client, error) {
   123  	config, jwt, serviceAccountName, err := v.GetConfigData(name, namespace, useIngressURL, insecureSSLWebhook)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	vaultConfig := vault.Vault{
   129  		Name:                   name,
   130  		ServiceAccountName:     serviceAccountName,
   131  		Namespace:              namespace,
   132  		SecretEngineMountPoint: vault.DefaultKVEngineMountPoint,
   133  		KubernetesAuthPath:     vault.DefaultKubernetesAuthPath,
   134  	}
   135  
   136  	return v.createClient(config, vaultConfig, jwt)
   137  }
   138  
   139  // NewVaultClientForURL creates a new Vault api.Client.
   140  // If namespace is nil, then the default namespace of the factory will be used
   141  func (v *VaultClientFactory) NewVaultClientForURL(vaultConfig vault.Vault, insecureSSLWebhook bool) (*api.Client, error) {
   142  	serviceAccount, err := v.kubeClient.CoreV1().ServiceAccounts(vaultConfig.Namespace).Get(vaultConfig.ServiceAccountName, meta_v1.GetOptions{})
   143  	if err != nil {
   144  		return nil, errors.Wrapf(err, "unable to get service account '%s'", vaultConfig.ServiceAccountName)
   145  	}
   146  
   147  	jwt, err := serviceaccount.GetServiceAccountToken(v.kubeClient, vaultConfig.Namespace, serviceAccount.Name)
   148  	if err != nil {
   149  		return nil, errors.Wrapf(err, "unable to get service account token for '%s'", serviceAccount.Name)
   150  	}
   151  
   152  	config, err := v.vaultAPIClient(vaultConfig.URL, insecureSSLWebhook)
   153  	if err != nil {
   154  		return nil, errors.Wrapf(err, "unable to create Vault api client")
   155  	}
   156  
   157  	return v.createClient(config, vaultConfig, jwt)
   158  }
   159  
   160  func (v *VaultClientFactory) createClient(config *api.Config, vaultConfig vault.Vault, jwt string) (*api.Client, error) {
   161  	vaultClient, err := api.NewClient(config)
   162  	if err != nil {
   163  		return nil, errors.Wrap(err, "creating vault client")
   164  	}
   165  
   166  	// Wait for vault to be ready
   167  	log.Logger().Debugf("Connecting to vault on %s", vaultClient.Address())
   168  	err = waitForVault(vaultClient, healthInitialRetryDelay, healthRetryTimeout)
   169  	if err != nil {
   170  		return nil, errors.Wrap(err, "wait for vault to be initialized and unsealed")
   171  	}
   172  
   173  	token, err := getTokenFromVault(vaultConfig.ServiceAccountName, jwt, vaultConfig.KubernetesAuthPath, vaultClient, authRetryTimeout)
   174  	if err != nil {
   175  		return nil, errors.Wrapf(err, "getting Vault authentication token")
   176  	}
   177  	vaultClient.SetToken(token)
   178  
   179  	// Wait for KV secret engine V2 to be configured
   180  	err = waitForKVEngine(vaultClient, vaultConfig.SecretEngineMountPoint, kvEngineInitialRetryDelay, kvEngineRetryTimeout)
   181  	if err != nil {
   182  		return nil, errors.Wrap(err, "wait for vault kv engine to be configured")
   183  	}
   184  
   185  	return vaultClient, nil
   186  }
   187  
   188  // GetConfigData generates the information necessary to configure an api.Client object
   189  // Returns the api.Config object, the JWT needed to create the auth user in vault, and an error if present
   190  func (v *VaultClientFactory) GetConfigData(name string, namespace string, useIngressURL, insecureSSLWebhook bool) (config *api.Config, jwt string, saName string, err error) {
   191  	if namespace == "" {
   192  		namespace = v.defaultNamespace
   193  	}
   194  
   195  	vlt, err := v.Selector.GetVault(name, namespace, useIngressURL)
   196  	if err != nil {
   197  		return nil, "", "", err
   198  	}
   199  
   200  	if os.Getenv(vault.LocalVaultEnvVar) != "" && !useIngressURL {
   201  		vlt.URL = os.Getenv(vault.LocalVaultEnvVar)
   202  	}
   203  
   204  	serviceAccount, err := v.getServiceAccountFromVault(vlt)
   205  	token, err := serviceaccount.GetServiceAccountToken(v.kubeClient, namespace, serviceAccount.Name)
   206  	cfg, err := v.vaultAPIClient(vlt.URL, insecureSSLWebhook)
   207  	if err != nil {
   208  		return nil, "", "", errors.Wrapf(err, "unable to create Vault api client")
   209  	}
   210  
   211  	return cfg, token, serviceAccount.Name, err
   212  }
   213  
   214  func (v *VaultClientFactory) vaultAPIClient(url string, insecureSSLWebhook bool) (*api.Config, error) {
   215  	cfg := &api.Config{
   216  		Address:    url,
   217  		MaxRetries: maxRetries,
   218  	}
   219  
   220  	if insecureSSLWebhook {
   221  		t := api.TLSConfig{Insecure: true}
   222  		err := cfg.ConfigureTLS(&t)
   223  		if err != nil {
   224  			return nil, errors.Wrap(err, "unable to configure tls")
   225  		}
   226  	}
   227  
   228  	return cfg, nil
   229  }
   230  
   231  func (v *VaultClientFactory) getServiceAccountFromVault(vault *vault.Vault) (*v1.ServiceAccount, error) {
   232  	return v.kubeClient.CoreV1().ServiceAccounts(vault.Namespace).Get(vault.ServiceAccountName, meta_v1.GetOptions{})
   233  }
   234  
   235  func waitForVault(vaultClient *api.Client, initialDelay, timeout time.Duration) error {
   236  	return util.RetryWithInitialDelaySlower(initialDelay, timeout, func() error {
   237  		hr, err := vaultClient.Sys().Health()
   238  		if err == nil && hr != nil && hr.Initialized && !hr.Sealed {
   239  			return nil
   240  		}
   241  		log.Logger().Info("Waiting for vault to be initialized and unsealed...")
   242  		if err != nil {
   243  			return errors.Wrap(err, "reading vault health")
   244  		}
   245  		if hr != nil {
   246  			return fmt.Errorf("vault health: initialized=%t, sealed=%t", hr.Initialized, hr.Sealed)
   247  		}
   248  		return errors.New("failed to read vault health")
   249  	})
   250  }
   251  
   252  func waitForKVEngine(vaultClient *api.Client, secretEngineMountPoint string, initialDelay, timeout time.Duration) error {
   253  	return util.RetryWithInitialDelaySlower(initialDelay, timeout, func() error {
   254  		if _, err := vaultClient.Logical().Read(fmt.Sprintf("%s/%s", secretEngineMountPoint, kvEngineConfigPath)); err != nil {
   255  			log.Logger().Infof("Waiting for KV secrets engine on %s to be configured...", secretEngineMountPoint)
   256  			return errors.Wrap(err, "checking KV secrets engine config")
   257  		}
   258  
   259  		payload := map[string]interface{}{
   260  			"data": map[string]string{
   261  				"test": "write",
   262  			},
   263  		}
   264  		if _, err := vaultClient.Logical().Write(fmt.Sprintf("%s/%s", secretEngineMountPoint, kvEngineWriteCheckPath), payload); err != nil {
   265  			log.Logger().Info("Waiting for KV secrets engine to be ready for write...")
   266  			return errors.Wrap(err, "checking KV secrets engine ready for write")
   267  		}
   268  		return nil
   269  	})
   270  }
   271  
   272  func getTokenFromVault(role string, jwt string, kubernetesAuthPath string, vaultClient *api.Client, timeout time.Duration) (string, error) {
   273  	if role == "" {
   274  		return "", errors.New("role cannot be empty")
   275  	}
   276  	if jwt == "" {
   277  		return "", errors.New("JWT cannot be empty empty")
   278  	}
   279  	m := map[string]interface{}{
   280  		"jwt":  jwt,
   281  		"role": role,
   282  	}
   283  
   284  	clientToken := ""
   285  	err := util.Retry(timeout, func() error {
   286  		sec, err := vaultClient.Logical().Write(fmt.Sprintf("/auth/%s/login", kubernetesAuthPath), m)
   287  		if err == nil {
   288  			clientToken = sec.Auth.ClientToken
   289  			return nil
   290  		}
   291  		return errors.Wrapf(err, "auth with %s login", kubernetesAuthPath)
   292  	})
   293  
   294  	return clientToken, err
   295  }