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 }