From a75800a9881b25017eee50f2cd82a7e8bc717da3 Mon Sep 17 00:00:00 2001 From: John Eikenberry Date: Thu, 2 Mar 2023 20:33:06 +0000 Subject: [PATCH] add provider ca support for jwt file base auth Adds support for a jwt token in a file. Simply reads the file and sends the read in jwt along to the vault login. It also supports a legacy mode with the jwt string being passed directly. In which case the path is made optional. --- .changelog/16266.txt | 3 + agent/connect/ca/provider_vault.go | 3 +- agent/connect/ca/provider_vault_auth_jwt.go | 50 +++++++++++++ agent/connect/ca/provider_vault_auth_test.go | 74 ++++++++++++++++++++ agent/connect/ca/provider_vault_test.go | 2 +- 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 .changelog/16266.txt create mode 100644 agent/connect/ca/provider_vault_auth_jwt.go diff --git a/.changelog/16266.txt b/.changelog/16266.txt new file mode 100644 index 000000000..72dbc3980 --- /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 7954e7ea0..1244c7945 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 000000000..e95481b55 --- /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 2b0a04a46..7a3872930 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 173d2c554..a834c7187 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"}},