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"},