github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/config/vault.go (about) 1 package config 2 3 import ( 4 "os" 5 "path" 6 "regexp" 7 "strings" 8 9 "github.com/SAP/jenkins-library/pkg/config/interpolation" 10 "github.com/SAP/jenkins-library/pkg/log" 11 "github.com/SAP/jenkins-library/pkg/piperutils" 12 "github.com/SAP/jenkins-library/pkg/vault" 13 "github.com/hashicorp/vault/api" 14 ) 15 16 const ( 17 vaultRootPaths = "vaultRootPaths" 18 vaultTestCredentialPath = "vaultTestCredentialPath" 19 vaultCredentialPath = "vaultCredentialPath" 20 vaultTestCredentialKeys = "vaultTestCredentialKeys" 21 vaultCredentialKeys = "vaultCredentialKeys" 22 vaultAppRoleID = "vaultAppRoleID" 23 vaultAppRoleSecretID = "vaultAppRoleSecreId" 24 vaultServerUrl = "vaultServerUrl" 25 vaultNamespace = "vaultNamespace" 26 vaultBasePath = "vaultBasePath" 27 vaultPipelineName = "vaultPipelineName" 28 vaultPath = "vaultPath" 29 skipVault = "skipVault" 30 vaultDisableOverwrite = "vaultDisableOverwrite" 31 vaultTestCredentialEnvPrefix = "vaultTestCredentialEnvPrefix" 32 vaultCredentialEnvPrefix = "vaultCredentialEnvPrefix" 33 vaultTestCredentialEnvPrefixDefault = "PIPER_TESTCREDENTIAL_" 34 VaultCredentialEnvPrefixDefault = "PIPER_VAULTCREDENTIAL_" 35 vaultSecretName = ".+VaultSecretName$" 36 ) 37 38 var ( 39 vaultFilter = []string{ 40 vaultRootPaths, 41 vaultAppRoleID, 42 vaultAppRoleSecretID, 43 vaultServerUrl, 44 vaultNamespace, 45 vaultBasePath, 46 vaultPipelineName, 47 vaultPath, 48 skipVault, 49 vaultDisableOverwrite, 50 vaultTestCredentialPath, 51 vaultTestCredentialKeys, 52 vaultTestCredentialEnvPrefix, 53 vaultCredentialPath, 54 vaultCredentialKeys, 55 vaultCredentialEnvPrefix, 56 vaultSecretName, 57 } 58 59 // VaultRootPaths are the lookup paths piper tries to use during the vault lookup. 60 // A path is only used if it's variables can be interpolated from the config 61 VaultRootPaths = []string{ 62 "$(vaultPath)", 63 "$(vaultBasePath)/$(vaultPipelineName)", 64 "$(vaultBasePath)/GROUP-SECRETS", 65 } 66 67 // VaultSecretFileDirectory holds the directory for the current step run to temporarily store secret files fetched from vault 68 VaultSecretFileDirectory = "" 69 ) 70 71 // VaultCredentials hold all the auth information needed to fetch configuration from vault 72 type VaultCredentials struct { 73 AppRoleID string 74 AppRoleSecretID string 75 VaultToken string 76 } 77 78 // vaultClient interface for mocking 79 type vaultClient interface { 80 GetKvSecret(string) (map[string]string, error) 81 MustRevokeToken() 82 } 83 84 func (s *StepConfig) mixinVaultConfig(parameters []StepParameters, configs ...map[string]interface{}) { 85 for _, config := range configs { 86 s.mixIn(config, vaultFilter) 87 // when an empty filter is returned we skip the mixin call since an empty filter will allow everything 88 if referencesFilter := getFilterForResourceReferences(parameters); len(referencesFilter) > 0 { 89 s.mixIn(config, referencesFilter) 90 } 91 } 92 } 93 94 func getVaultClientFromConfig(config StepConfig, creds VaultCredentials) (vaultClient, error) { 95 address, addressOk := config.Config["vaultServerUrl"].(string) 96 // if vault isn't used it's not an error 97 if !addressOk || creds.VaultToken == "" && (creds.AppRoleID == "" || creds.AppRoleSecretID == "") { 98 log.Entry().Debug("Vault not configured") 99 return nil, nil 100 } 101 log.Entry().Info("Logging into Vault") 102 log.Entry().Debugf(" with URL %s", address) 103 namespace := "" 104 // namespaces are only available in vault enterprise so using them should be optional 105 if config.Config["vaultNamespace"] != nil { 106 namespace = config.Config["vaultNamespace"].(string) 107 log.Entry().Debugf(" with namespace %s", namespace) 108 } 109 var client vaultClient 110 var err error 111 clientConfig := &vault.Config{Config: &api.Config{Address: address}, Namespace: namespace} 112 if creds.VaultToken != "" { 113 log.Entry().Debugf(" with Token authentication") 114 client, err = vault.NewClient(clientConfig, creds.VaultToken) 115 } else { 116 log.Entry().Debugf(" with AppRole authentication") 117 client, err = vault.NewClientWithAppRole(clientConfig, creds.AppRoleID, creds.AppRoleSecretID) 118 } 119 if err != nil { 120 log.Entry().Info(" failed") 121 return nil, err 122 } 123 log.Entry().Info(" succeeded") 124 return client, nil 125 } 126 127 func resolveAllVaultReferences(config *StepConfig, client vaultClient, params []StepParameters) { 128 for _, param := range params { 129 if ref := param.GetReference("vaultSecret"); ref != nil { 130 resolveVaultReference(ref, config, client, param) 131 } 132 if ref := param.GetReference("vaultSecretFile"); ref != nil { 133 resolveVaultReference(ref, config, client, param) 134 } 135 } 136 } 137 138 func resolveVaultReference(ref *ResourceReference, config *StepConfig, client vaultClient, param StepParameters) { 139 vaultDisableOverwrite, _ := config.Config["vaultDisableOverwrite"].(bool) 140 if _, ok := config.Config[param.Name].(string); vaultDisableOverwrite && ok { 141 log.Entry().Debugf("Not fetching '%s' from Vault since it has already been set", param.Name) 142 return 143 } 144 145 log.Entry().Infof("Resolving '%s'", param.Name) 146 147 var secretValue *string 148 for _, vaultPath := range getSecretReferencePaths(ref, config.Config) { 149 // it should be possible to configure the root path were the secret is stored 150 vaultPath, ok := interpolation.ResolveString(vaultPath, config.Config) 151 if !ok { 152 continue 153 } 154 155 secretValue = lookupPath(client, vaultPath, ¶m) 156 if secretValue != nil { 157 log.Entry().Infof(" succeeded with Vault path '%s'", vaultPath) 158 if ref.Type == "vaultSecret" { 159 config.Config[param.Name] = *secretValue 160 } else if ref.Type == "vaultSecretFile" { 161 filePath, err := createTemporarySecretFile(param.Name, *secretValue) 162 if err != nil { 163 log.Entry().WithError(err).Warnf("Couldn't create temporary secret file for '%s'", param.Name) 164 return 165 } 166 config.Config[param.Name] = filePath 167 } 168 break 169 } 170 } 171 if secretValue == nil { 172 log.Entry().Warn(" failed") 173 } 174 } 175 176 func resolveVaultTestCredentialsWrapper(config *StepConfig, client vaultClient) { 177 log.Entry().Infof("Resolving test credentials wrapper") 178 resolveVaultTestCredentialsWrapperBase(config, client, vaultTestCredentialPath, vaultTestCredentialKeys, resolveVaultTestCredentials) 179 } 180 181 func resolveVaultCredentialsWrapper(config *StepConfig, client vaultClient) { 182 log.Entry().Infof("Resolving credentials wrapper") 183 resolveVaultTestCredentialsWrapperBase(config, client, vaultCredentialPath, vaultCredentialKeys, resolveVaultCredentials) 184 } 185 186 func resolveVaultTestCredentialsWrapperBase( 187 config *StepConfig, client vaultClient, 188 vaultCredPath, vaultCredKeys string, 189 resolveVaultCredentials func(config *StepConfig, client vaultClient), 190 ) { 191 switch config.Config[vaultCredPath].(type) { 192 case string: 193 resolveVaultCredentials(config, client) 194 case []interface{}: 195 vaultCredentialPathCopy := config.Config[vaultCredPath] 196 vaultCredentialKeysCopy := config.Config[vaultCredKeys] 197 198 if _, ok := vaultCredentialKeysCopy.([]interface{}); !ok { 199 log.Entry().Debugf(" failed, unknown type of keys") 200 return 201 } 202 203 if len(vaultCredentialKeysCopy.([]interface{})) != len(vaultCredentialPathCopy.([]interface{})) { 204 log.Entry().Debugf(" failed, not same count of values and keys") 205 return 206 } 207 208 for i := 0; i < len(vaultCredentialPathCopy.([]interface{})); i++ { 209 config.Config[vaultCredPath] = vaultCredentialPathCopy.([]interface{})[i] 210 config.Config[vaultCredKeys] = vaultCredentialKeysCopy.([]interface{})[i] 211 resolveVaultCredentials(config, client) 212 } 213 214 config.Config[vaultCredPath] = vaultCredentialPathCopy 215 config.Config[vaultCredKeys] = vaultCredentialKeysCopy 216 default: 217 log.Entry().Debugf(" failed, unknown type of path") 218 return 219 } 220 } 221 222 // resolve test credential keys and expose as environment variables 223 func resolveVaultTestCredentials(config *StepConfig, client vaultClient) { 224 credPath, pathOk := config.Config[vaultTestCredentialPath].(string) 225 keys := getTestCredentialKeys(config) 226 if !(pathOk && keys != nil) || credPath == "" || len(keys) == 0 { 227 log.Entry().Debugf("Not fetching test credentials from Vault since they are not (properly) configured") 228 return 229 } 230 231 lookupPath := make([]string, 3) 232 lookupPath[0] = "$(vaultPath)/" + credPath 233 lookupPath[1] = "$(vaultBasePath)/$(vaultPipelineName)/" + credPath 234 lookupPath[2] = "$(vaultBasePath)/GROUP-SECRETS/" + credPath 235 236 for _, path := range lookupPath { 237 vaultPath, ok := interpolation.ResolveString(path, config.Config) 238 if !ok { 239 continue 240 } 241 242 secret, err := client.GetKvSecret(vaultPath) 243 if err != nil { 244 log.Entry().WithError(err).Debugf("Couldn't fetch secret at '%s'", vaultPath) 245 continue 246 } 247 if secret == nil { 248 continue 249 } 250 secretsResolved := false 251 secretsResolved = populateTestCredentialsAsEnvs(config, secret, keys) 252 if secretsResolved { 253 // prevent overwriting resolved secrets 254 // only allows vault test credentials on one / the same vault path 255 break 256 } 257 } 258 } 259 260 func resolveVaultCredentials(config *StepConfig, client vaultClient) { 261 credPath, pathOk := config.Config[vaultCredentialPath].(string) 262 keys := getCredentialKeys(config) 263 if !(pathOk && keys != nil) || credPath == "" || len(keys) == 0 { 264 log.Entry().Debugf("Not fetching credentials from vault since they are not (properly) configured") 265 return 266 } 267 268 lookupPath := make([]string, 3) 269 lookupPath[0] = "$(vaultPath)/" + credPath 270 lookupPath[1] = "$(vaultBasePath)/$(vaultPipelineName)/" + credPath 271 lookupPath[2] = "$(vaultBasePath)/GROUP-SECRETS/" + credPath 272 273 for _, path := range lookupPath { 274 vaultPath, ok := interpolation.ResolveString(path, config.Config) 275 if !ok { 276 continue 277 } 278 279 secret, err := client.GetKvSecret(vaultPath) 280 if err != nil { 281 log.Entry().WithError(err).Debugf("Couldn't fetch secret at '%s'", vaultPath) 282 continue 283 } 284 if secret == nil { 285 continue 286 } 287 secretsResolved := false 288 secretsResolved = populateCredentialsAsEnvs(config, secret, keys) 289 if secretsResolved { 290 // prevent overwriting resolved secrets 291 // only allows vault test credentials on one / the same vault path 292 break 293 } 294 } 295 } 296 297 func populateTestCredentialsAsEnvs(config *StepConfig, secret map[string]string, keys []string) (matched bool) { 298 299 vaultTestCredentialEnvPrefix, ok := config.Config["vaultTestCredentialEnvPrefix"].(string) 300 if !ok || len(vaultTestCredentialEnvPrefix) == 0 { 301 vaultTestCredentialEnvPrefix = vaultTestCredentialEnvPrefixDefault 302 } 303 for secretKey, secretValue := range secret { 304 for _, key := range keys { 305 if secretKey == key { 306 log.RegisterSecret(secretValue) 307 envVariable := vaultTestCredentialEnvPrefix + ConvertEnvVar(secretKey) 308 log.Entry().Debugf("Exposing test credential '%v' as '%v'", key, envVariable) 309 os.Setenv(envVariable, secretValue) 310 matched = true 311 } 312 } 313 } 314 return 315 } 316 317 func populateCredentialsAsEnvs(config *StepConfig, secret map[string]string, keys []string) (matched bool) { 318 319 vaultCredentialEnvPrefix, ok := config.Config["vaultCredentialEnvPrefix"].(string) 320 isCredentialEnvPrefixDefault := false 321 322 if !ok { 323 vaultCredentialEnvPrefix = VaultCredentialEnvPrefixDefault 324 isCredentialEnvPrefixDefault = true 325 } 326 for secretKey, secretValue := range secret { 327 for _, key := range keys { 328 if secretKey == key { 329 log.RegisterSecret(secretValue) 330 envVariable := vaultCredentialEnvPrefix + ConvertEnvVar(secretKey) 331 log.Entry().Debugf("Exposing general purpose credential '%v' as '%v'", key, envVariable) 332 os.Setenv(envVariable, secretValue) 333 334 log.RegisterSecret(piperutils.EncodeString(secretValue)) 335 envVariable = vaultCredentialEnvPrefix + ConvertEnvVar(secretKey) + "_BASE64" 336 log.Entry().Debugf("Exposing general purpose base64 encoded credential '%v' as '%v'", key, envVariable) 337 os.Setenv(envVariable, piperutils.EncodeString(secretValue)) 338 matched = true 339 } 340 } 341 } 342 343 // we always create a standard env variable with the default prefx so that 344 // we can always refer to it in steps if its to be hard-coded 345 if !isCredentialEnvPrefixDefault { 346 for secretKey, secretValue := range secret { 347 for _, key := range keys { 348 if secretKey == key { 349 log.RegisterSecret(secretValue) 350 envVariable := VaultCredentialEnvPrefixDefault + ConvertEnvVar(secretKey) 351 log.Entry().Debugf("Exposing general purpose credential '%v' as '%v'", key, envVariable) 352 os.Setenv(envVariable, secretValue) 353 354 log.RegisterSecret(piperutils.EncodeString(secretValue)) 355 envVariable = VaultCredentialEnvPrefixDefault + ConvertEnvVar(secretKey) + "_BASE64" 356 log.Entry().Debugf("Exposing general purpose base64 encoded credential '%v' as '%v'", key, envVariable) 357 os.Setenv(envVariable, piperutils.EncodeString(secretValue)) 358 matched = true 359 } 360 } 361 } 362 } 363 return 364 } 365 366 func getTestCredentialKeys(config *StepConfig) []string { 367 keysRaw, ok := config.Config[vaultTestCredentialKeys].([]interface{}) 368 if !ok { 369 return nil 370 } 371 keys := make([]string, 0, len(keysRaw)) 372 for _, keyRaw := range keysRaw { 373 key, ok := keyRaw.(string) 374 if !ok { 375 log.Entry().Warnf("%s needs to be an array of strings", vaultTestCredentialKeys) 376 return nil 377 } 378 keys = append(keys, key) 379 } 380 return keys 381 } 382 383 func getCredentialKeys(config *StepConfig) []string { 384 keysRaw, ok := config.Config[vaultCredentialKeys].([]interface{}) 385 if !ok { 386 log.Entry().Debugf("Not fetching general purpose credentials from vault since they are not (properly) configured") 387 return nil 388 } 389 keys := make([]string, 0, len(keysRaw)) 390 for _, keyRaw := range keysRaw { 391 key, ok := keyRaw.(string) 392 if !ok { 393 log.Entry().Warnf("%s is needs to be an array of strings", vaultCredentialKeys) 394 return nil 395 } 396 keys = append(keys, key) 397 } 398 return keys 399 } 400 401 // ConvertEnvVar converts to a valid environment variable string 402 func ConvertEnvVar(s string) string { 403 r := strings.ToUpper(s) 404 r = strings.ReplaceAll(r, "-", "_") 405 reg, err := regexp.Compile("[^a-zA-Z0-9_]*") 406 if err != nil { 407 log.Entry().Debugf("could not compile regex of convertEnvVar: %v", err) 408 } 409 replacedString := reg.ReplaceAllString(r, "") 410 return replacedString 411 } 412 413 // RemoveVaultSecretFiles removes all secret files that have been created during execution 414 func RemoveVaultSecretFiles() { 415 if VaultSecretFileDirectory != "" { 416 os.RemoveAll(VaultSecretFileDirectory) 417 } 418 } 419 420 func createTemporarySecretFile(namePattern string, content string) (string, error) { 421 if VaultSecretFileDirectory == "" { 422 var err error 423 fileUtils := &piperutils.Files{} 424 VaultSecretFileDirectory, err = fileUtils.TempDir("", "vault") 425 if err != nil { 426 return "", err 427 } 428 } 429 430 file, err := os.CreateTemp(VaultSecretFileDirectory, namePattern) 431 if err != nil { 432 return "", err 433 } 434 defer file.Close() 435 _, err = file.WriteString(content) 436 if err != nil { 437 return "", err 438 } 439 return file.Name(), nil 440 } 441 442 func lookupPath(client vaultClient, path string, param *StepParameters) *string { 443 log.Entry().Debugf(" with Vault path '%s'", path) 444 secret, err := client.GetKvSecret(path) 445 if err != nil { 446 log.Entry().WithError(err).Warnf("Couldn't fetch secret at '%s'", path) 447 return nil 448 } 449 if secret == nil { 450 return nil 451 } 452 453 field := secret[param.Name] 454 if field != "" { 455 log.RegisterSecret(field) 456 return &field 457 } 458 log.Entry().Debugf("Secret did not contain a field name '%s'", param.Name) 459 // try parameter aliases 460 for _, alias := range param.Aliases { 461 log.Entry().Debugf("Trying alias field name '%s'", alias.Name) 462 field := secret[alias.Name] 463 if field != "" { 464 log.RegisterSecret(field) 465 if alias.Deprecated { 466 log.Entry().WithField("package", "SAP/jenkins-library/pkg/config").Warningf("DEPRECATION NOTICE: old step config key '%s' used in Vault. Please switch to '%s'!", alias.Name, param.Name) 467 } 468 return &field 469 } 470 } 471 return nil 472 } 473 474 func getSecretReferencePaths(reference *ResourceReference, config map[string]interface{}) []string { 475 retPaths := make([]string, 0, len(VaultRootPaths)) 476 secretName := reference.Default 477 if providedName, ok := config[reference.Name].(string); ok && providedName != "" { 478 secretName = providedName 479 } 480 for _, rootPath := range VaultRootPaths { 481 fullPath := path.Join(rootPath, secretName) 482 retPaths = append(retPaths, fullPath) 483 } 484 return retPaths 485 } 486 487 func toStringSlice(interfaceSlice []interface{}) []string { 488 retSlice := make([]string, 0, len(interfaceSlice)) 489 for _, vRaw := range interfaceSlice { 490 if v, ok := vRaw.(string); ok { 491 retSlice = append(retSlice, v) 492 continue 493 } 494 log.Entry().Warnf("'%s' needs to be of type string or an array of strings but got %T (%[2]v)", vaultPath, vRaw) 495 } 496 return retSlice 497 }