diff --git a/internal/providers/vault/README.md b/internal/providers/vault/README.md index 90b2e3eae..38e0274cd 100644 --- a/internal/providers/vault/README.md +++ b/internal/providers/vault/README.md @@ -6,6 +6,9 @@ on the [Vault API client](https://pkg.go.dev/github.com/hashicorp/vault/api) in Go. It reads the secret object from the configured path and returns the value navigated to by the configured fields (or default field otherwise). +Please note that Vault API client uses the environment variable `VAULT_ADDR` to +determine the URL to the Vault API base endpoint, e.g. `https://localhost:8200`. + ## Usage Documentation The Vault provider is configured in the `secretless.yml` using: @@ -86,9 +89,44 @@ services: ... ``` +## Auth Methods + +Vault supports various auth methods. To direct the provider to use a specific +auth method, the environment variable `VAULT_AUTH_METHOD` must be set to a +valid value. This is currently `AppRole` or `Token`. If the variable is not set, +the auth method `Token` is implied, for backwards compatibility. + +### Token + +Token auth method is the default and minimal auth method in Vault. It requires +the token to use in the environment variable `VAULT_TOKEN`. No additional +authentication steps are performed. + +Configure the the Token auth method as follows: + +``` +VAULT_AUTH_METHOD=Token +VAULT_TOKEN=... # some valid Vault token +``` + +### AppRole + +AppRole auth method requires a role ID and a secret ID. The latter may be +wrapped, requiring an unwrapping first. This is referred to as the pull mode. +The provider uses the following environment variables for the AppRole auth +method: + +``` +VAULT_AUTH_METHOD=AppRole +VAULT_APPROLE_ROLE_ID=... # some valid AppRole role ID +VAULT_APPROLE_SECRET_ID=... # some valid AppRole secret ID, may be wrapped +VAULT_APPROLE_UNWRAP=0 # some truthy value, true indicates unwrapping +``` + ## Limitations -- Only token-based login to Vault supported at the moment. +- Only Token and basic AppRole auth methods to Vault supported at the moment. +- Only environment variables are supported o configure auth methods. - Only secrets that are "read" in Vault are supported at the moment. Backends that require "writes" to obtain the secret (e.g. PKI, dynamic database credentials) are not supported at the moment. diff --git a/internal/providers/vault/auth/approle.go b/internal/providers/vault/auth/approle.go new file mode 100644 index 000000000..fe12a8ee9 --- /dev/null +++ b/internal/providers/vault/auth/approle.go @@ -0,0 +1,73 @@ +package auth + +import ( + "fmt" + vault "github.com/hashicorp/vault/api" + "os" + "strconv" +) + +// DefaultAppRoleLoginPath path to AppRole login in Vault. +const DefaultAppRoleLoginPath = "auth/approle/login" + +// EnvVaultAppRoleRoleID environment variable name of role ID used in AppRole auth method. +const EnvVaultAppRoleRoleID = "VAULT_APPROLE_ROLE_ID" + +// EnvVaultAppRoleSecretID environment variable name of (possibly wrapped) secret ID used in AppRole auth method. +const EnvVaultAppRoleSecretID = "VAULT_APPROLE_SECRET_ID" + +// EnvVaultAppRoleUnwrap environment variable name of unwrapping of secret ID in AppRole auth method. +const EnvVaultAppRoleUnwrap = "VAULT_APPROLE_UNWRAP" + +// AppRoleConfig holds AppRole auth method configuration. +type AppRoleConfig struct { + LoginPath string + RoleID string + SecretID string + Unwrap bool +} + +// AppRoleAuthMethodFactory performs AppRole auth method for given client. +func AppRoleAuthMethodFactory(client *vault.Client) (*vault.Secret, error) { + config := getAppRoleConfigFromEnv() + + var secret *vault.Secret + var err error + if secret, err = getTokenFromAppRoleLogin(client, config); err != nil { + return nil, err + } + + return secret, nil +} + +// getAppRoleConfigFromEnv returns AppRole auth method configuration from environment variables. +func getAppRoleConfigFromEnv() *AppRoleConfig { + config := &AppRoleConfig{} + config.RoleID = os.Getenv(EnvVaultAppRoleRoleID) + config.SecretID = os.Getenv(EnvVaultAppRoleSecretID) + // Ignore error, default unwrap = false + config.Unwrap, _ = strconv.ParseBool(os.Getenv(EnvVaultAppRoleUnwrap)) + config.LoginPath = getLoginPathFromEnv(DefaultAppRoleLoginPath) + return config +} + +// getTokenFromAppRoleLogin performs AppRole login for client using given AppRole auth method configuration. +func getTokenFromAppRoleLogin(client *vault.Client, config *AppRoleConfig) (*vault.Secret, error) { + // Conditionally unwrap secret ID (referred to as pull mode in Vault) + if config.Unwrap { + secret, err := client.Logical().Unwrap(config.SecretID) + if err != nil { + return nil, fmt.Errorf("HashiCorp Vault provider unwrapping failed: %s", err) + } + + // Replace with unwrapped secret ID in config + config.SecretID = secret.Data["secret_id"].(string) + } + + data := map[string]interface{}{ + "role_id": config.RoleID, + "secret_id": config.SecretID, + } + + return client.Logical().Write(config.LoginPath, data) +} \ No newline at end of file diff --git a/internal/providers/vault/auth/auth.go b/internal/providers/vault/auth/auth.go new file mode 100644 index 000000000..2df9794c7 --- /dev/null +++ b/internal/providers/vault/auth/auth.go @@ -0,0 +1,68 @@ +package auth + +import ( + "fmt" + vault "github.com/hashicorp/vault/api" + "os" +) + +// EnvVarVaultAuthMethod environment variable name of Vault auth method to use. +const EnvVarVaultAuthMethod = "VAULT_AUTH_METHOD" + +// EnvVarVaultLoginPath path to auth method's login in Vault. +const EnvVarVaultLoginPath = "VAULT_LOGIN_PATH" + +// VaultAuthMethodFactories mapping of supported Vault auth methods by their IDs. Must have either an auth method +// factory or `nil` to signify no action is required, e.g. for Token auth method. +// NOTE: the "Token" auth methods is added for consistency. Token auth method requires no additional authentication +// steps, unlike other auth methods in Vault. +var VaultAuthMethodFactories = map[string]func(*vault.Client) (*vault.Secret, error) { + "AppRole": AppRoleAuthMethodFactory, + "Token": nil, +} + +// Authenticate a Vault client using any of the supported auth methods. +// NOTE: All auth methods require an initialized Vault API client. +func Authenticate(client *vault.Client) error { + authMethod := getAuthMethodFromEnv() + applyAuthMethod, ok := VaultAuthMethodFactories[authMethod] + if !ok { + return fmt.Errorf("HashiCorp Vault provider unsupported auth method: %s", authMethod) + } + + // No (additional) authentication steps required, hence done + if applyAuthMethod == nil { + return nil + } + + secret, err := applyAuthMethod(client) + if err != nil { + return fmt.Errorf("HashiCorp Vault provider auth method failed: %s", err) + } + if secret == nil || secret.Auth == nil { + return fmt.Errorf("HashiCorp Vault provider login failed (no secret or auth info)") + } + + client.SetToken(secret.Auth.ClientToken) + return nil +} + +// getAuthMethodFromEnv returns the auth method from environment variable. If not set, it defaults to "Token" for +// backwards compatibility. +func getAuthMethodFromEnv() string { + authMethod, ok := os.LookupEnv(EnvVarVaultAuthMethod) + if !ok { + return "Token" + } + return authMethod +} + +// getLoginPathFromEnv returns login path from environment variable or returns given default value otherwise. +func getLoginPathFromEnv(defaultLoginPath string) string { + // Optional path to login, otherwise default path of Vault is used + path := os.Getenv(EnvVarVaultLoginPath) + if path != "" { + return path + } + return defaultLoginPath +} diff --git a/internal/providers/vault/provider.go b/internal/providers/vault/provider.go index 788a76b7a..a39d64cc6 100644 --- a/internal/providers/vault/provider.go +++ b/internal/providers/vault/provider.go @@ -2,6 +2,7 @@ package vault import ( "fmt" + "github.com/cyberark/secretless-broker/internal/providers/vault/auth" "strings" vault "github.com/hashicorp/vault/api" @@ -15,18 +16,18 @@ type Provider struct { Client *vault.Client } -// ProviderFactory constructs a Provider. The API client is configured from -// environment variables. Underlying Vault API client by default uses: -// - VAULT_ADDR: endpoint of Vault, e.g. http://vault:8200/ -// - VAULT_TOKEN: token to login to Vault +// ProviderFactory constructs a Provider for Vault. It uses the Vault API client. // See Vault API docs at https://godoc.org/github.com/hashicorp/vault/api func ProviderFactory(options plugin_v1.ProviderOptions) (plugin_v1.Provider, error) { config := vault.DefaultConfig() - var client *vault.Client var err error if client, err = vault.NewClient(config); err != nil { - return nil, fmt.Errorf("ERROR: Could not create Vault provider: %s", err) + return nil, fmt.Errorf("HashiCorp Vault provider client failed: %s", err) + } + + if err = auth.Authenticate(client); err != nil { + return nil, err } provider := &Provider{