diff --git a/.changelog/16266.txt b/.changelog/16266.txt new file mode 100644 index 0000000000..72dbc39800 --- /dev/null +++ b/.changelog/16266.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ca: support Vault agent auto-auth config for Vault CA provider using JWT authentication. +``` diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index 7954e7ea03..1244c79455 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -935,6 +935,8 @@ func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthent return NewAzureAuthClient(authMethod) case VaultAuthMethodTypeGCP: return NewGCPAuthClient(authMethod) + case VaultAuthMethodTypeJWT: + return NewJwtAuthClient(authMethod) case VaultAuthMethodTypeKubernetes: // For the Kubernetes Auth method, we will try to read the JWT token // from the default service account file location if jwt was not provided. @@ -972,7 +974,6 @@ func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthent VaultAuthMethodTypeAppRole, VaultAuthMethodTypeCloudFoundry, VaultAuthMethodTypeGitHub, - VaultAuthMethodTypeJWT, VaultAuthMethodTypeKerberos, VaultAuthMethodTypeTLS: return NewVaultAPIAuthClient(authMethod, loginPath), nil diff --git a/agent/connect/ca/provider_vault_auth_jwt.go b/agent/connect/ca/provider_vault_auth_jwt.go new file mode 100644 index 0000000000..e95481b55c --- /dev/null +++ b/agent/connect/ca/provider_vault_auth_jwt.go @@ -0,0 +1,50 @@ +package ca + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/consul/agent/structs" +) + +func NewJwtAuthClient(authMethod *structs.VaultAuthMethod) (*VaultAuthClient, error) { + params := authMethod.Params + + role, ok := params["role"].(string) + if !ok || strings.TrimSpace(role) == "" { + return nil, fmt.Errorf("missing 'role' value") + } + + authClient := NewVaultAPIAuthClient(authMethod, "") + if legacyCheck(params, "jwt") { + return authClient, nil + } + + // The path is required for the auto-auth config, but this auth provider + // seems to be used for jwt based auth by directly passing the jwt token. + // So we only require the token file path if the token string isn't + // present. + tokenPath, ok := params["path"].(string) + if !ok || strings.TrimSpace(tokenPath) == "" { + return nil, fmt.Errorf("missing 'path' value") + } + authClient.LoginDataGen = JwtLoginDataGen + return authClient, nil +} + +func JwtLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error) { + params := authMethod.Params + role := params["role"].(string) + + tokenPath := params["path"].(string) + rawToken, err := os.ReadFile(tokenPath) + if err != nil { + return nil, err + } + + return map[string]any{ + "role": role, + "jwt": strings.TrimSpace(string(rawToken)), + }, nil +} diff --git a/agent/connect/ca/provider_vault_auth_test.go b/agent/connect/ca/provider_vault_auth_test.go index 2b0a04a46f..7a38729307 100644 --- a/agent/connect/ca/provider_vault_auth_test.go +++ b/agent/connect/ca/provider_vault_auth_test.go @@ -428,3 +428,77 @@ func TestVaultCAProvider_AzureAuthClient(t *testing.T) { }) } } + +func TestVaultCAProvider_JwtAuthClient(t *testing.T) { + tokenF, err := os.CreateTemp("", "token-path") + require.NoError(t, err) + defer func() { os.Remove(tokenF.Name()) }() + _, err = tokenF.WriteString("test-token") + require.NoError(t, err) + err = tokenF.Close() + require.NoError(t, err) + + cases := map[string]struct { + authMethod *structs.VaultAuthMethod + expData map[string]any + expErr error + }{ + "base-case": { + authMethod: &structs.VaultAuthMethod{ + Type: "jwt", + Params: map[string]any{ + "role": "test-role", + "path": tokenF.Name(), + }, + }, + expData: map[string]any{ + "role": "test-role", + "jwt": "test-token", + }, + }, + "no-role": { + authMethod: &structs.VaultAuthMethod{ + Type: "jwt", + Params: map[string]any{}, + }, + expErr: fmt.Errorf("missing 'role' value"), + }, + "no-path": { + authMethod: &structs.VaultAuthMethod{ + Type: "jwt", + Params: map[string]any{ + "role": "test-role", + }, + }, + expErr: fmt.Errorf("missing 'path' value"), + }, + "no-path-but-jwt": { + authMethod: &structs.VaultAuthMethod{ + Type: "jwt", + Params: map[string]any{ + "role": "test-role", + "jwt": "test-jwt", + }, + }, + expData: map[string]any{ + "role": "test-role", + "jwt": "test-jwt", + }, + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + auth, err := NewJwtAuthClient(c.authMethod) + if c.expErr != nil { + 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 173d2c5549..a834c71877 100644 --- a/agent/connect/ca/provider_vault_test.go +++ b/agent/connect/ca/provider_vault_test.go @@ -111,7 +111,7 @@ func TestVaultCAProvider_configureVaultAuthMethod(t *testing.T) { "cf": {expLoginPath: "auth/cf/login"}, "github": {expLoginPath: "auth/github/login"}, "gcp": {expLoginPath: "auth/gcp/login", params: map[string]interface{}{"type": "iam", "role": "test-role"}}, - "jwt": {expLoginPath: "auth/jwt/login"}, + "jwt": {expLoginPath: "auth/jwt/login", params: map[string]any{"role": "test-role", "path": "test-path"}, hasLDG: true}, "kerberos": {expLoginPath: "auth/kerberos/login"}, "kubernetes": {expLoginPath: "auth/kubernetes/login", params: map[string]interface{}{"jwt": "fake"}}, "ldap": {expLoginPath: "auth/ldap/login/foo", params: map[string]interface{}{"username": "foo"}},