diff --git a/.changelog/16262.txt b/.changelog/16262.txt new file mode 100644 index 000000000..3a53c5b2c --- /dev/null +++ b/.changelog/16262.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ca: support Vault agent auto-auth config for Vault CA provider using Kubernetes authentication. +``` diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index 1244c7945..e8b14e412 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "os" "strings" "sync" "time" @@ -922,6 +921,14 @@ func vaultLogin(client *vaultapi.Client, authMethod *structs.VaultAuthMethod) (* return resp, nil } +// Note the authMethod's parameters (Params) is populated from a freeform map +// in the configuration where they could hardcode values to be passed directly +// to the `auth/*/login` endpoint. Each auth method's authentication code +// needs to handle two cases: +// - The legacy case (which should be deprecated) where the user has +// hardcoded login values directly (eg. a `jwt` string) +// - The case where they use the configuration option used in the +// vault agent's auth methods. func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthenticator, error) { if authMethod.MountPath == "" { authMethod.MountPath = authMethod.Type @@ -938,17 +945,7 @@ func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (VaultAuthent 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. - if jwt, ok := authMethod.Params["jwt"]; !ok || jwt == "" { - serviceAccountToken, err := os.ReadFile(defaultK8SServiceAccountTokenPath) - if err != nil { - return nil, err - } - - authMethod.Params["jwt"] = string(serviceAccountToken) - } - return NewVaultAPIAuthClient(authMethod, loginPath), nil + return NewK8sAuthClient(authMethod) // These auth methods require a username for the login API path. case VaultAuthMethodTypeLDAP, VaultAuthMethodTypeUserpass, VaultAuthMethodTypeOkta, VaultAuthMethodTypeRadius: // Get username from the params. diff --git a/agent/connect/ca/provider_vault_auth_k8s.go b/agent/connect/ca/provider_vault_auth_k8s.go new file mode 100644 index 000000000..983750cd5 --- /dev/null +++ b/agent/connect/ca/provider_vault_auth_k8s.go @@ -0,0 +1,47 @@ +package ca + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/consul/agent/structs" +) + +func NewK8sAuthClient(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") + } + // don't check for `token_path` as it is optional + + authClient := NewVaultAPIAuthClient(authMethod, "") + // Note the `jwt` can be passed directly in the authMethod as a Param value + // is a freeform map in the config where they could hardcode it. + if legacyCheck(params, "jwt") { + return authClient, nil + } + + authClient.LoginDataGen = K8sLoginDataGen + return authClient, nil +} + +func K8sLoginDataGen(authMethod *structs.VaultAuthMethod) (map[string]any, error) { + params := authMethod.Params + role := params["role"].(string) + + // read token from file on path + tokenPath, ok := params["token_path"].(string) + if !ok || strings.TrimSpace(tokenPath) == "" { + tokenPath = defaultK8SServiceAccountTokenPath + } + 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 7a3872930..6601f9f4b 100644 --- a/agent/connect/ca/provider_vault_auth_test.go +++ b/agent/connect/ca/provider_vault_auth_test.go @@ -502,3 +502,69 @@ func TestVaultCAProvider_JwtAuthClient(t *testing.T) { }) } } + +func TestVaultCAProvider_K8sAuthClient(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: "kubernetes", + Params: map[string]any{ + "role": "test-role", + "token_path": tokenF.Name(), + }, + }, + expData: map[string]any{ + "role": "test-role", + "jwt": "test-token", + }, + }, + "legacy-case": { + authMethod: &structs.VaultAuthMethod{ + Type: "kubernetes", + Params: map[string]any{ + "role": "test-role", + "jwt": "test-token", + }, + }, + expData: map[string]any{ + "role": "test-role", + "jwt": "test-token", + }, + }, + "no-role": { + authMethod: &structs.VaultAuthMethod{ + Type: "kubernetes", + Params: map[string]any{}, + }, + expErr: fmt.Errorf("missing 'role' value"), + }, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + auth, err := NewK8sAuthClient(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 a834c7187..80fbff45c 100644 --- a/agent/connect/ca/provider_vault_test.go +++ b/agent/connect/ca/provider_vault_test.go @@ -113,7 +113,7 @@ func TestVaultCAProvider_configureVaultAuthMethod(t *testing.T) { "gcp": {expLoginPath: "auth/gcp/login", params: map[string]interface{}{"type": "iam", "role": "test-role"}}, "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"}}, + "kubernetes": {expLoginPath: "auth/kubernetes/login", params: map[string]interface{}{"role": "test-role"}, hasLDG: true}, "ldap": {expLoginPath: "auth/ldap/login/foo", params: map[string]interface{}{"username": "foo"}}, "oci": {expLoginPath: "auth/oci/login/foo", params: map[string]interface{}{"role": "foo"}}, "okta": {expLoginPath: "auth/okta/login/foo", params: map[string]interface{}{"username": "foo"}},