From 95288615faeecf07b40f105f91f641052df22677 Mon Sep 17 00:00:00 2001 From: John Eikenberry Date: Fri, 3 Mar 2023 19:29:53 +0000 Subject: [PATCH] add provider ca support for approle auth-method Adds support for the approle auth-method. Only handles using the approle role/secret to auth and it doesn't support the agent's extra management configuration options (wrap and delete after read) as they are not required as part of the auth (ie. they are vault agent things). --- .changelog/16259.txt | 3 + agent/connect/ca/provider_vault.go | 3 +- .../connect/ca/provider_vault_auth_approle.go | 66 +++++++++++++ agent/connect/ca/provider_vault_auth_test.go | 94 +++++++++++++++++++ agent/connect/ca/provider_vault_test.go | 2 +- 5 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 .changelog/16259.txt create mode 100644 agent/connect/ca/provider_vault_auth_approle.go diff --git a/.changelog/16259.txt b/.changelog/16259.txt new file mode 100644 index 000000000..dd73aaf6e --- /dev/null +++ b/.changelog/16259.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ca: support Vault agent auto-auth config for Vault CA provider using AppRole authentication. +``` diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index e8b14e412..d14fd0a4d 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -944,6 +944,8 @@ func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthent return NewGCPAuthClient(authMethod) case VaultAuthMethodTypeJWT: return NewJwtAuthClient(authMethod) + case VaultAuthMethodTypeAppRole: + return NewAppRoleAuthClient(authMethod) case VaultAuthMethodTypeKubernetes: return NewK8sAuthClient(authMethod) // These auth methods require a username for the login API path. @@ -968,7 +970,6 @@ func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthent "please provide the token with the 'token' parameter in the CA configuration") // The rest of the auth methods use auth/ login API path. case VaultAuthMethodTypeAliCloud, - VaultAuthMethodTypeAppRole, VaultAuthMethodTypeCloudFoundry, VaultAuthMethodTypeGitHub, VaultAuthMethodTypeKerberos, diff --git a/agent/connect/ca/provider_vault_auth_approle.go b/agent/connect/ca/provider_vault_auth_approle.go new file mode 100644 index 000000000..fad6011fc --- /dev/null +++ b/agent/connect/ca/provider_vault_auth_approle.go @@ -0,0 +1,66 @@ +package ca + +import ( + "bytes" + "fmt" + "os" + "strings" + + "github.com/hashicorp/consul/agent/structs" +) + +// left out 2 config options as we are re-using vault agent's auth config. +// Why? +// remove_secret_id_file_after_reading - don't remove what we don't own +// secret_id_response_wrapping_path - wrapping the secret before writing to disk +// (which we don't need to do) + +func NewAppRoleAuthClient(authMethod *structs.VaultAuthMethod) (*VaultAuthClient, error) { + authClient := NewVaultAPIAuthClient(authMethod, "") + // check for hardcoded /login params + if legacyCheck(authMethod.Params, "role_id", "secret_id") { + return authClient, nil + } + + // check for required config params + key := "role_id_file_path" + if val, ok := authMethod.Params[key].(string); !ok { + return nil, fmt.Errorf("missing '%s' value", key) + } else if strings.TrimSpace(val) == "" { + return nil, fmt.Errorf("'%s' value is empty", key) + } + authClient.LoginDataGen = ArLoginDataGen + + return authClient, nil +} + +func ArLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error) { + // don't need to check for legacy params as this func isn't used in that case + params := authMethod.Params + // role_id is required + roleIdFilePath := params["role_id_file_path"].(string) + // secret_id is optional (secret_ok is used in check below) + // secretIdFilePath, secret_ok := params["secret_id_file_path"].(string) + secretIdFilePath, hasSecret := params["secret_id_file_path"].(string) + if hasSecret && strings.TrimSpace(secretIdFilePath) == "" { + hasSecret = false + } + + var err error + var rawRoleID, rawSecretID []byte + data := make(map[string]any) + if rawRoleID, err = os.ReadFile(roleIdFilePath); err != nil { + return nil, err + } + data["role_id"] = string(rawRoleID) + if hasSecret { + switch rawSecretID, err = os.ReadFile(secretIdFilePath); { + case err != nil: + return nil, err + case len(bytes.TrimSpace(rawSecretID)) > 0: + data["secret_id"] = strings.TrimSpace(string(rawSecretID)) + } + } + + return data, nil +} diff --git a/agent/connect/ca/provider_vault_auth_test.go b/agent/connect/ca/provider_vault_auth_test.go index 6601f9f4b..45377b03a 100644 --- a/agent/connect/ca/provider_vault_auth_test.go +++ b/agent/connect/ca/provider_vault_auth_test.go @@ -568,3 +568,97 @@ func TestVaultCAProvider_K8sAuthClient(t *testing.T) { }) } } + +func TestVaultCAProvider_AppRoleAuthClient(t *testing.T) { + roleID, secretID := "test_role_id", "test_secret_id" + + roleFd, err := os.CreateTemp("", "role") + require.NoError(t, err) + _, err = roleFd.WriteString(roleID) + require.NoError(t, err) + err = roleFd.Close() + require.NoError(t, err) + + secretFd, err := os.CreateTemp("", "secret") + require.NoError(t, err) + _, err = secretFd.WriteString(secretID) + require.NoError(t, err) + err = secretFd.Close() + require.NoError(t, err) + + roleIdPath := roleFd.Name() + secretIdPath := secretFd.Name() + + defer func() { + os.Remove(secretFd.Name()) + os.Remove(roleFd.Name()) + }() + + cases := map[string]struct { + authMethod *structs.VaultAuthMethod + expData map[string]any + expErr error + }{ + "base-case": { + authMethod: &structs.VaultAuthMethod{ + Type: "approle", + Params: map[string]any{ + "role_id_file_path": roleIdPath, + "secret_id_file_path": secretIdPath, + }, + }, + expData: map[string]any{ + "role_id": roleID, + "secret_id": secretID, + }, + }, + "optional-secret-left-out": { + authMethod: &structs.VaultAuthMethod{ + Type: "approle", + Params: map[string]any{ + "role_id_file_path": roleIdPath, + }, + }, + expData: map[string]any{ + "role_id": roleID, + }, + }, + "missing-role-id-file-path": { + authMethod: &structs.VaultAuthMethod{ + Type: "approle", + Params: map[string]any{}, + }, + expErr: fmt.Errorf("missing '%s' value", "role_id_file_path"), + }, + "legacy-direct-values": { + authMethod: &structs.VaultAuthMethod{ + Type: "approle", + Params: map[string]any{ + "role_id": "test-role", + "secret_id": "test-secret", + }, + }, + expData: map[string]any{ + "role_id": "test-role", + "secret_id": "test-secret", + }, + }, + } + + for k, c := range cases { + t.Run(k, func(t *testing.T) { + auth, err := NewAppRoleAuthClient(c.authMethod) + if c.expErr != nil { + require.Error(t, err) + require.EqualError(t, c.expErr, err.Error()) + return + } + require.NoError(t, err) + if auth.LoginDataGen != nil { + data, err := auth.LoginDataGen(c.authMethod) + require.NoError(t, err) + require.Equal(t, c.expData, data) + } + }) + } +} diff --git a/agent/connect/ca/provider_vault_test.go b/agent/connect/ca/provider_vault_test.go index 80fbff45c..7242dda99 100644 --- a/agent/connect/ca/provider_vault_test.go +++ b/agent/connect/ca/provider_vault_test.go @@ -105,7 +105,7 @@ func TestVaultCAProvider_configureVaultAuthMethod(t *testing.T) { hasLDG bool }{ "alicloud": {expLoginPath: "auth/alicloud/login"}, - "approle": {expLoginPath: "auth/approle/login"}, + "approle": {expLoginPath: "auth/approle/login", params: map[string]any{"role_id_file_path": "test-path"}, hasLDG: true}, "aws": {expLoginPath: "auth/aws/login", params: map[string]interface{}{"type": "iam"}, hasLDG: true}, "azure": {expLoginPath: "auth/azure/login", params: map[string]interface{}{"role": "test-role", "resource": "test-resource"}, hasLDG: true}, "cf": {expLoginPath: "auth/cf/login"},