consul/agent/connect/ca/provider_vault_auth_test.go

780 lines
20 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package ca
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"testing"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/hashicorp/vault/api/auth/gcp"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/stretchr/testify/require"
)
func TestVaultCAProvider_GCPAuthClient(t *testing.T) {
cases := map[string]struct {
authMethod *structs.VaultAuthMethod
isExplicit bool
expErr error
}{
"explicit config": {
authMethod: &structs.VaultAuthMethod{
Type: "gcp",
Params: map[string]interface{}{
"role": "test-role",
"jwt": "test-jwt",
},
},
isExplicit: true,
},
"derived iam auth": {
authMethod: &structs.VaultAuthMethod{
Type: "gcp",
Params: map[string]interface{}{
"type": "iam",
"role": "test-role",
"service_account_email": "test@google.cloud",
},
},
},
"derived gce auth": {
authMethod: &structs.VaultAuthMethod{
Type: "gcp",
Params: map[string]interface{}{
"type": "gce",
"role": "test-role",
},
},
},
"derived without role": {
authMethod: &structs.VaultAuthMethod{
Type: "gcp",
Params: map[string]interface{}{
"type": "gce",
},
},
expErr: fmt.Errorf("failed to create a new Vault GCP auth client"),
},
"invalid config": {
authMethod: &structs.VaultAuthMethod{
Type: "gcp",
Params: map[string]interface{}{
"invalid": true,
},
},
expErr: fmt.Errorf("misconfiguration of GCP auth parameters: invalid type for field"),
},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
auth, err := NewGCPAuthClient(c.authMethod)
if c.expErr != nil {
require.Error(t, err)
require.Contains(t, err.Error(), c.expErr.Error())
return
}
require.NoError(t, err)
require.NotNil(t, auth)
if c.isExplicit {
// in this case a JWT is provided so we'll call the login API directly using a VaultAuthClient.
_ = auth.(*VaultAuthClient)
} else {
// in this case we delegate to gcp.GCPAuth to perform the login.
_ = auth.(*gcp.GCPAuth)
}
})
}
}
func TestVaultCAProvider_AWSAuthClient(t *testing.T) {
cases := map[string]struct {
authMethod *structs.VaultAuthMethod
expLoginPath string
hasLDG bool
}{
"explicit aws ec2 identity": {
authMethod: &structs.VaultAuthMethod{
Type: "aws",
Params: map[string]interface{}{
"role": "test-role",
"identity": "test-identity",
"signature": "test-signature",
},
},
expLoginPath: "auth/aws/login",
},
"explicit aws ec2 pkcs7": {
authMethod: &structs.VaultAuthMethod{
Type: "aws",
MountPath: "custom-aws",
Params: map[string]interface{}{
"role": "test-role",
"pkcs7": "test-pkcs7",
},
},
expLoginPath: "auth/custom-aws/login",
},
"derived aws login data": {
authMethod: &structs.VaultAuthMethod{
Type: "aws",
Params: map[string]interface{}{
"role": "test-role",
"type": "ec2",
"region": "test-region",
},
},
expLoginPath: "auth/aws/login",
hasLDG: true,
},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
if c.authMethod.MountPath == "" {
c.authMethod.MountPath = c.authMethod.Type
}
auth := NewAWSAuthClient(c.authMethod)
require.Equal(t, c.authMethod, auth.AuthMethod)
require.Equal(t, c.expLoginPath, auth.LoginPath)
if c.hasLDG {
require.NotNil(t, auth.LoginDataGen)
} else {
require.Nil(t, auth.LoginDataGen)
}
})
}
}
func TestVaultCAProvider_AWSCredentialsConfig(t *testing.T) {
cases := map[string]struct {
params map[string]interface{}
envVars map[string]string
expCreds *awsutil.CredentialsConfig
expErr error
expRegion string
}{
"valid config": {
params: map[string]interface{}{
"access_key": "access key",
"secret_key": "secret key",
"session_token": "session token",
"iam_endpoint": "iam endpoint",
"sts_endpoint": "sts endpoint",
"region": "region",
"filename": "filename",
"profile": "profile",
"role_arn": "role arn",
"role_session_name": "role session name",
"web_identity_token_file": "web identity token file",
"header_value": "header value",
"max_retries": "13",
},
expCreds: &awsutil.CredentialsConfig{
AccessKey: "access key",
SecretKey: "secret key",
SessionToken: "session token",
IAMEndpoint: "iam endpoint",
STSEndpoint: "sts endpoint",
Region: "region",
Filename: "filename",
Profile: "profile",
RoleARN: "role arn",
RoleSessionName: "role session name",
WebIdentityTokenFile: "web identity token file",
},
},
"default region": {
params: map[string]interface{}{},
expCreds: &awsutil.CredentialsConfig{},
expRegion: "us-east-1",
},
"env AWS_REGION": {
params: map[string]interface{}{},
envVars: map[string]string{"AWS_REGION": "us-west-1"},
expCreds: &awsutil.CredentialsConfig{},
expRegion: "us-west-1",
},
"env AWS_DEFAULT_REGION": {
params: map[string]interface{}{},
envVars: map[string]string{"AWS_DEFAULT_REGION": "us-west-2"},
expCreds: &awsutil.CredentialsConfig{},
expRegion: "us-west-2",
},
"both AWS_REGION and AWS_DEFAULT_REGION": {
params: map[string]interface{}{},
envVars: map[string]string{
"AWS_REGION": "us-west-1",
"AWS_DEFAULT_REGION": "us-west-2",
},
expCreds: &awsutil.CredentialsConfig{},
expRegion: "us-west-1",
},
"invalid config": {
params: map[string]interface{}{
"invalid": true,
},
expErr: fmt.Errorf("misconfiguration of AWS auth parameters: invalid type for field"),
},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
if c.envVars != nil {
for k, v := range c.envVars {
require.NoError(t, os.Setenv(k, v))
}
t.Cleanup(func() {
for k := range c.envVars {
os.Unsetenv(k)
}
})
}
creds, headerValue, err := newAWSCredentialsConfig(c.params)
if c.expErr != nil {
require.Error(t, err)
require.Contains(t, err.Error(), c.expErr.Error())
return
}
// If a header value was provided in the params then make sure it was returned.
if val, ok := c.params["header_value"]; ok {
require.Equal(t, val, headerValue)
} else {
require.Empty(t, headerValue)
}
if val, ok := c.params["max_retries"]; ok {
mr, err := strconv.Atoi(val.(string))
require.NoError(t, err)
c.expCreds.MaxRetries = &mr
} else {
creds.MaxRetries = nil
}
require.NotNil(t, creds.HTTPClient)
creds.HTTPClient = nil
if c.expRegion != "" {
c.expCreds.Region = c.expRegion
}
require.Equal(t, *c.expCreds, *creds)
})
}
}
func TestVaultCAProvider_AWSLoginDataGenerator(t *testing.T) {
cases := map[string]struct {
expErr error
authMethod structs.VaultAuthMethod
}{
"valid login data": {
authMethod: structs.VaultAuthMethod{},
},
"with role": {
expErr: nil,
authMethod: structs.VaultAuthMethod{Type: "aws", MountPath: "", Params: map[string]interface{}{"role": "test-role"}},
},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
ldg := &AWSLoginDataGenerator{credentials: credentials.AnonymousCredentials}
loginData, err := ldg.GenerateLoginData(&c.authMethod)
if c.expErr != nil {
require.Error(t, err)
require.Contains(t, err.Error(), c.expErr.Error())
return
}
require.NoError(t, err)
keys := []string{
"iam_http_request_method",
"iam_request_url",
"iam_request_headers",
"iam_request_body",
}
for _, key := range keys {
val, exists := loginData[key]
require.True(t, exists, "missing expected key: %s", key)
require.NotEmpty(t, val, "expected non-empty value for key: %s", key)
}
if c.authMethod.Params["role"] != nil {
require.Equal(t, c.authMethod.Params["role"], loginData["role"])
}
})
}
}
func TestVaultCAProvider_AzureAuthClient(t *testing.T) {
instance := instanceData{Compute: Compute{
Name: "a", ResourceGroupName: "b", SubscriptionID: "c", VMScaleSetName: "d",
}}
instanceJSON, err := json.Marshal(instance)
require.NoError(t, err)
identity := identityData{AccessToken: "a-jwt-token"}
identityJSON, err := json.Marshal(identity)
require.NoError(t, err)
msi := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
url := r.URL.Path
switch url {
case "/metadata/instance":
w.Write(instanceJSON)
case "/metadata/identity/oauth2/token":
w.Write(identityJSON)
default:
t.Errorf("unexpected testing URL: %s", url)
}
}))
origIn, origId := instanceEndpoint, identityEndpoint
instanceEndpoint = msi.URL + "/metadata/instance"
identityEndpoint = msi.URL + "/metadata/identity/oauth2/token"
defer func() {
instanceEndpoint, identityEndpoint = origIn, origId
}()
t.Run("get-metadata-instance-info", func(t *testing.T) {
md, err := getMetadataInfo(instanceEndpoint, nil)
require.NoError(t, err)
var testInstance instanceData
err = jsonutil.DecodeJSON(md, &testInstance)
require.NoError(t, err)
require.Equal(t, testInstance, instance)
})
t.Run("get-metadata-identity-info", func(t *testing.T) {
md, err := getMetadataInfo(identityEndpoint, nil)
require.NoError(t, err)
var testIdentity identityData
err = jsonutil.DecodeJSON(md, &testIdentity)
require.NoError(t, err)
require.Equal(t, testIdentity, identity)
})
cases := map[string]struct {
authMethod *structs.VaultAuthMethod
expData map[string]any
expErr error
}{
"legacy-case": {
authMethod: &structs.VaultAuthMethod{
Type: "azure",
Params: map[string]interface{}{
"role": "a",
"vm_name": "b",
"vmss_name": "c",
"resource_group_name": "d",
"subscription_id": "e",
"jwt": "f",
},
},
expData: map[string]any{
"role": "a",
"vm_name": "b",
"vmss_name": "c",
"resource_group_name": "d",
"subscription_id": "e",
"jwt": "f",
},
},
"base-case": {
authMethod: &structs.VaultAuthMethod{
Type: "azure",
Params: map[string]interface{}{
"role": "a-role",
"resource": "b-resource",
},
},
expData: map[string]any{
"role": "a-role",
"jwt": "a-jwt-token",
},
},
"no-role": {
authMethod: &structs.VaultAuthMethod{
Type: "azure",
Params: map[string]interface{}{
"resource": "b-resource",
},
},
expErr: fmt.Errorf("missing 'role' value"),
},
"no-resource": {
authMethod: &structs.VaultAuthMethod{
Type: "azure",
Params: map[string]interface{}{
"role": "a-role",
},
},
expErr: fmt.Errorf("missing 'resource' value"),
},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
auth, err := NewAzureAuthClient(c.authMethod)
if c.expErr != nil {
require.EqualError(t, err, c.expErr.Error())
return
}
require.NoError(t, err)
if auth.LoginDataGen != nil {
data, err := auth.LoginDataGen(c.authMethod)
require.NoError(t, err)
require.Subset(t, data, c.expData)
}
})
}
}
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)
}
})
}
}
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)
}
})
}
}
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)
}
})
}
}
func TestVaultCAProvider_AliCloudAuthClient(t *testing.T) {
// required as login parameters, will hang if not set
os.Setenv("ALICLOUD_ACCESS_KEY", "test-access-key")
os.Setenv("ALICLOUD_SECRET_KEY", "test-secret-key")
os.Setenv("ALICLOUD_ACCESS_KEY_STS_TOKEN", "test-access-token")
defer func() {
os.Unsetenv("ALICLOUD_ACCESS_KEY")
os.Unsetenv("ALICLOUD_SECRET_KEY")
os.Unsetenv("ALICLOUD_ACCESS_KEY_STS_TOKEN")
}()
cases := map[string]struct {
authMethod *structs.VaultAuthMethod
expQry map[string][]string
expErr error
}{
"base-case": {
authMethod: &structs.VaultAuthMethod{
Type: VaultAuthMethodTypeAliCloud,
Params: map[string]interface{}{
"role": "test-role",
"region": "test-region",
},
},
expQry: map[string][]string{
"Action": {"GetCallerIdentity"},
"AccessKeyId": {"test-access-key"},
"RegionId": {"test-region"},
},
},
"no-role": {
authMethod: &structs.VaultAuthMethod{
Type: VaultAuthMethodTypeAliCloud,
Params: map[string]interface{}{
"region": "test-region",
},
},
expErr: fmt.Errorf("role is required for AliCloud login"),
},
"no-region": {
authMethod: &structs.VaultAuthMethod{
Type: VaultAuthMethodTypeAliCloud,
Params: map[string]interface{}{
"role": "test-role",
},
},
expErr: fmt.Errorf("region is required for AliCloud login"),
},
"legacy-case": {
authMethod: &structs.VaultAuthMethod{
Type: VaultAuthMethodTypeAliCloud,
Params: map[string]interface{}{
"access_key": "test-key",
"access_token": "test-token",
"secret_key": "test-secret-key",
},
},
},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
auth, err := NewAliCloudAuthClient(c.authMethod)
if c.expErr != nil {
require.Error(t, err)
require.EqualError(t, c.expErr, err.Error())
return
}
require.NotNil(t, auth)
if auth.LoginDataGen != nil {
encodedData, err := auth.LoginDataGen(c.authMethod)
require.NoError(t, err)
// identity_request_headers (json encoded headers)
rawheaders, err := base64.StdEncoding.DecodeString(
encodedData["identity_request_headers"].(string))
require.NoError(t, err)
headers := string(rawheaders)
require.Contains(t, headers, "User-Agent")
require.Contains(t, headers, "AlibabaCloud")
require.Contains(t, headers, "Content-Type")
require.Contains(t, headers, "x-acs-action")
require.Contains(t, headers, "GetCallerIdentity")
// identity_request_url (w/ query params)
rawurl, err := base64.StdEncoding.DecodeString(
encodedData["identity_request_url"].(string))
require.NoError(t, err)
requrl, err := url.Parse(string(rawurl))
require.NoError(t, err)
queries := requrl.Query()
require.Subset(t, queries, c.expQry, "query missing fields")
require.Equal(t, requrl.Hostname(), "sts.test-region.aliyuncs.com")
}
})
}
}