diff --git a/CHANGELOG.md b/CHANGELOG.md index c4da5ac97e9..d37d817b8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio - **General**: Enable support on s390x for KEDA ([#6543](https://github.com/kedacore/keda/issues/6543)) - **General**: Introduce new Solace Direct Messaging scaler ([#6545](https://github.com/kedacore/keda/issues/6545)) - **General**: Introduce new Sumo Logic Scaler ([#6734](https://github.com/kedacore/keda/issues/6734)) +- **General**: Vault authentication via cross-namespace service accounts ([#6153](https://github.com/kedacore/keda/issues/6153)) #### Experimental diff --git a/apis/keda/v1alpha1/triggerauthentication_types.go b/apis/keda/v1alpha1/triggerauthentication_types.go index 3c9154d21ab..85b9ad74acb 100644 --- a/apis/keda/v1alpha1/triggerauthentication_types.go +++ b/apis/keda/v1alpha1/triggerauthentication_types.go @@ -239,6 +239,9 @@ type Credential struct { // +optional ServiceAccount string `json:"serviceAccount,omitempty"` + + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` } // VaultAuthentication contains the list of Hashicorp Vault authentication methods diff --git a/config/crd/bases/keda.sh_clustertriggerauthentications.yaml b/config/crd/bases/keda.sh_clustertriggerauthentications.yaml index bbd2e34b920..35069d7db73 100644 --- a/config/crd/bases/keda.sh_clustertriggerauthentications.yaml +++ b/config/crd/bases/keda.sh_clustertriggerauthentications.yaml @@ -451,6 +451,8 @@ spec: properties: serviceAccount: type: string + serviceAccountName: + type: string token: type: string type: object diff --git a/config/crd/bases/keda.sh_triggerauthentications.yaml b/config/crd/bases/keda.sh_triggerauthentications.yaml index c81f78a4d79..e2b931eb9c9 100644 --- a/config/crd/bases/keda.sh_triggerauthentications.yaml +++ b/config/crd/bases/keda.sh_triggerauthentications.yaml @@ -450,6 +450,8 @@ spec: properties: serviceAccount: type: string + serviceAccountName: + type: string token: type: string type: object diff --git a/pkg/scaling/resolver/hashicorpvault_handler.go b/pkg/scaling/resolver/hashicorpvault_handler.go index 7e57c7013f3..48e95e84e00 100644 --- a/pkg/scaling/resolver/hashicorpvault_handler.go +++ b/pkg/scaling/resolver/hashicorpvault_handler.go @@ -17,6 +17,7 @@ limitations under the License. package resolver import ( + "context" "errors" "fmt" "os" @@ -26,19 +27,24 @@ import ( vaultapi "github.com/hashicorp/vault/api" kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + "github.com/kedacore/keda/v2/pkg/scalers/authentication" ) // HashicorpVaultHandler is a specification of HashiCorp Vault type HashicorpVaultHandler struct { - vault *kedav1alpha1.HashiCorpVault - client *vaultapi.Client - stopCh chan struct{} + vault *kedav1alpha1.HashiCorpVault + client *vaultapi.Client + acs *authentication.AuthClientSet + namespace string + stopCh chan struct{} } // NewHashicorpVaultHandler creates a HashicorpVaultHandler object -func NewHashicorpVaultHandler(v *kedav1alpha1.HashiCorpVault) *HashicorpVaultHandler { +func NewHashicorpVaultHandler(v *kedav1alpha1.HashiCorpVault, acs *authentication.AuthClientSet, namespace string) *HashicorpVaultHandler { return &HashicorpVaultHandler{ - vault: v, + vault: v, + acs: acs, + namespace: namespace, } } @@ -87,6 +93,8 @@ func (vh *HashicorpVaultHandler) Initialize(logger logr.Logger) error { // token Extract a vault token from the Authentication method func (vh *HashicorpVaultHandler) token(client *vaultapi.Client) (string, error) { var token string + var jwt []byte + var err error switch vh.vault.Authentication { case kedav1alpha1.VaultAuthenticationToken: @@ -115,14 +123,18 @@ func (vh *HashicorpVaultHandler) token(client *vaultapi.Client) (string, error) vh.vault.Credential = &defaultCred } - if len(vh.vault.Credential.ServiceAccount) == 0 { - return token, errors.New("k8s SA file not in config") + if vh.vault.Credential.ServiceAccountName == "" && vh.vault.Credential.ServiceAccount == "" { + return token, errors.New("k8s SA file not in config or serviceAccountName not supplied") } - // Get the JWT from POD - jwt, err := os.ReadFile(vh.vault.Credential.ServiceAccount) - if err != nil { - return token, err + if vh.vault.Credential.ServiceAccountName != "" { + jwt = []byte(GenerateBoundServiceAccountToken(context.Background(), vh.vault.Credential.ServiceAccountName, vh.namespace, vh.acs)) + } else if len(vh.vault.Credential.ServiceAccount) != 0 { + // Get the JWT from POD + jwt, err = os.ReadFile(vh.vault.Credential.ServiceAccount) + if err != nil { + return token, err + } } data := map[string]interface{}{"jwt": string(jwt), "role": vh.vault.Role} @@ -130,8 +142,8 @@ func (vh *HashicorpVaultHandler) token(client *vaultapi.Client) (string, error) if err != nil { return token, err } - token = secret.Auth.ClientToken + default: return token, fmt.Errorf("vault auth method %s is not supported", vh.vault.Authentication) } diff --git a/pkg/scaling/resolver/hashicorpvault_handler_test.go b/pkg/scaling/resolver/hashicorpvault_handler_test.go index 919b2951725..9bd5b6bec7d 100644 --- a/pkg/scaling/resolver/hashicorpvault_handler_test.go +++ b/pkg/scaling/resolver/hashicorpvault_handler_test.go @@ -27,9 +27,14 @@ import ( vaultapi "github.com/hashicorp/vault/api" "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + authv1 "k8s.io/api/authentication/v1" logf "sigs.k8s.io/controller-runtime/pkg/log" kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + mock_secretlister "github.com/kedacore/keda/v2/pkg/mock/mock_secretlister" + mock_serviceaccounts "github.com/kedacore/keda/v2/pkg/mock/mock_serviceaccounts" + "github.com/kedacore/keda/v2/pkg/scalers/authentication" ) const ( @@ -115,7 +120,15 @@ var pkiRequestTestDataset = []pkiRequestTestData{ } func TestGetPkiRequest(t *testing.T) { - vault := NewHashicorpVaultHandler(nil) + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } + + vault := NewHashicorpVaultHandler(nil, authClientSet, "default") for _, testData := range pkiRequestTestDataset { var secret kedav1alpha1.VaultSecret @@ -136,6 +149,7 @@ func TestGetPkiRequest(t *testing.T) { func mockVault(t *testing.T, useRootToken bool) *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var data map[string]interface{} + var auth *vaultapi.SecretAuth switch r.URL.Path { case "/v1/auth/token/lookup-self": data = vaultTokenSelf @@ -165,7 +179,10 @@ func mockVault(t *testing.T, useRootToken bool) *httptest.Server { "private_key_type": "rsa", "serial_number": "4c:79:c6:2c:23:65:77:73:c2:79:49:8c:c8:fe:ad:e3:78:68:0f:86", } - + case "/v1/auth/kubernetes/login": + auth = &vaultapi.SecretAuth{ + ClientToken: vaultTestToken, + } default: t.Logf("Got request at path %s", r.URL.Path) w.WriteHeader(404) @@ -178,7 +195,7 @@ func mockVault(t *testing.T, useRootToken bool) *httptest.Server { Data: data, Renewable: false, Warnings: nil, - Auth: nil, + Auth: auth, WrapInfo: nil, } var out, _ = json.Marshal(secret) @@ -190,6 +207,13 @@ func mockVault(t *testing.T, useRootToken bool) *httptest.Server { func TestHashicorpVaultHandler_getSecretValue_specify_secret_type(t *testing.T) { server := mockVault(t, false) defer server.Close() + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } vault := kedav1alpha1.HashiCorpVault{ Address: server.URL, @@ -198,7 +222,7 @@ func TestHashicorpVaultHandler_getSecretValue_specify_secret_type(t *testing.T) Token: vaultTestToken, }, } - vaultHandler := NewHashicorpVaultHandler(&vault) + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") err := vaultHandler.Initialize(logf.Log.WithName("test")) defer vaultHandler.Stop() assert.Nil(t, err) @@ -330,6 +354,13 @@ var resolveRequestTestDataSet = []resolveRequestTestData{ func TestHashicorpVaultHandler_ResolveSecret(t *testing.T) { server := mockVault(t, false) defer server.Close() + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } vault := kedav1alpha1.HashiCorpVault{ Address: server.URL, @@ -338,7 +369,7 @@ func TestHashicorpVaultHandler_ResolveSecret(t *testing.T) { Token: vaultTestToken, }, } - vaultHandler := NewHashicorpVaultHandler(&vault) + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") err := vaultHandler.Initialize(logf.Log.WithName("test")) defer vaultHandler.Stop() assert.Nil(t, err) @@ -374,7 +405,15 @@ func TestHashicorpVaultHandler_ResolveSecret_UsingRootToken(t *testing.T) { Token: vaultTestToken, }, } - vaultHandler := NewHashicorpVaultHandler(&vault) + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } + + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") err := vaultHandler.Initialize(logf.Log.WithName("test")) defer vaultHandler.Stop() assert.Nil(t, err) @@ -403,6 +442,13 @@ func TestHashicorpVaultHandler_DefaultKubernetesVaultRole(t *testing.T) { defaultServiceAccountPath := "/var/run/secrets/kubernetes.io/serviceaccount/token" server := mockVault(t, false) defer server.Close() + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } vault := kedav1alpha1.HashiCorpVault{ Address: server.URL, @@ -411,7 +457,7 @@ func TestHashicorpVaultHandler_DefaultKubernetesVaultRole(t *testing.T) { Role: "my-role", } - vaultHandler := NewHashicorpVaultHandler(&vault) + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") err := vaultHandler.Initialize(logf.Log.WithName("test")) defer vaultHandler.Stop() assert.Errorf(t, err, "open %s : no such file or directory", defaultServiceAccountPath) @@ -421,6 +467,13 @@ func TestHashicorpVaultHandler_DefaultKubernetesVaultRole(t *testing.T) { func TestHashicorpVaultHandler_ResolveSecrets_SameCertAndKey(t *testing.T) { server := mockVault(t, false) defer server.Close() + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } vault := kedav1alpha1.HashiCorpVault{ Address: server.URL, @@ -429,7 +482,7 @@ func TestHashicorpVaultHandler_ResolveSecrets_SameCertAndKey(t *testing.T) { Token: vaultTestToken, }, } - vaultHandler := NewHashicorpVaultHandler(&vault) + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") err := vaultHandler.Initialize(logf.Log.WithName("test")) defer vaultHandler.Stop() assert.Nil(t, err) @@ -490,6 +543,14 @@ func TestHashicorpVaultHandler_fetchSecret(t *testing.T) { server := mockVault(t, false) defer server.Close() + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } + vault := kedav1alpha1.HashiCorpVault{ Address: server.URL, Authentication: kedav1alpha1.VaultAuthenticationToken, @@ -497,7 +558,8 @@ func TestHashicorpVaultHandler_fetchSecret(t *testing.T) { Token: vaultTestToken, }, } - vaultHandler := NewHashicorpVaultHandler(&vault) + + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") err := vaultHandler.Initialize(logf.Log.WithName("test")) defer vaultHandler.Stop() assert.Nil(t, err) @@ -543,6 +605,14 @@ func TestHashicorpVaultHandler_Initialize(t *testing.T) { server := mockVault(t, false) defer server.Close() + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } + for _, testData := range initialiseTestDataSet { func() { vault := kedav1alpha1.HashiCorpVault{ @@ -553,7 +623,7 @@ func TestHashicorpVaultHandler_Initialize(t *testing.T) { }, Namespace: testData.namespace, } - vaultHandler := NewHashicorpVaultHandler(&vault) + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, testData.namespace) err := vaultHandler.Initialize(logf.Log.WithName("test")) defer vaultHandler.Stop() assert.Nil(t, err) @@ -618,6 +688,14 @@ func TestHashicorpVaultHandler_Token_VaultTokenAuth(t *testing.T) { server := mockVault(t, false) defer server.Close() + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } + for _, testData := range tokenTestDataSet { func() { vault := kedav1alpha1.HashiCorpVault{ @@ -627,7 +705,7 @@ func TestHashicorpVaultHandler_Token_VaultTokenAuth(t *testing.T) { Role: testData.role, Mount: testData.mount, } - vaultHandler := NewHashicorpVaultHandler(&vault) + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") defer vaultHandler.Stop() config := vaultapi.DefaultConfig() @@ -644,3 +722,48 @@ func TestHashicorpVaultHandler_Token_VaultTokenAuth(t *testing.T) { }() } } + +func TestHashicorpVaultHandler_Token_ServiceAccountAuth(t *testing.T) { + server := mockVault(t, false) + defer server.Close() + + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } + + defer ctrl.Finish() + + mockServiceAccountInterface := mockCoreV1Interface.GetServiceAccountInterface() + tokenRequest := &authv1.TokenRequest{ + Status: authv1.TokenRequestStatus{ + Token: bsatData, + }, + } + mockServiceAccountInterface.EXPECT().CreateToken(gomock.Any(), gomock.Eq(bsatSAName), gomock.Any(), gomock.Any()).Return(tokenRequest, nil).AnyTimes() + + vault := kedav1alpha1.HashiCorpVault{ + Address: server.URL, + Authentication: kedav1alpha1.VaultAuthenticationKubernetes, + Mount: "kubernetes", + Role: "keda-role", + Credential: &kedav1alpha1.Credential{ + ServiceAccountName: bsatSAName, + }, + } + + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") + defer vaultHandler.Stop() + + config := vaultapi.DefaultConfig() + config.Address = server.URL + client, err := vaultapi.NewClient(config) + assert.NoError(t, err) + + token, err := vaultHandler.token(client) + assert.NoError(t, err) + assert.NotEmpty(t, token) +} diff --git a/pkg/scaling/resolver/scale_resolvers.go b/pkg/scaling/resolver/scale_resolvers.go index 6e772d951e4..8a2cbe33902 100644 --- a/pkg/scaling/resolver/scale_resolvers.go +++ b/pkg/scaling/resolver/scale_resolvers.go @@ -274,7 +274,7 @@ func resolveAuthRef(ctx context.Context, client client.Client, logger logr.Logge } } if triggerAuthSpec.HashiCorpVault != nil && len(triggerAuthSpec.HashiCorpVault.Secrets) > 0 { - vault := NewHashicorpVaultHandler(triggerAuthSpec.HashiCorpVault) + vault := NewHashicorpVaultHandler(triggerAuthSpec.HashiCorpVault, authClientSet, namespace) err := vault.Initialize(logger) defer vault.Stop() if err != nil { @@ -628,12 +628,12 @@ func resolveBoundServiceAccountToken(ctx context.Context, client client.Client, logger.Error(err, "error trying to get service account from namespace", "ServiceAccount.Namespace", namespace, "ServiceAccount.Name", serviceAccountName) return "" } - return generateBoundServiceAccountToken(ctx, serviceAccountName, namespace, acs) + return GenerateBoundServiceAccountToken(ctx, serviceAccountName, namespace, acs) } -// generateBoundServiceAccountToken creates a Kubernetes token for a namespaced service account with a runtime-configurable expiration time and returns the token string. -func generateBoundServiceAccountToken(ctx context.Context, serviceAccountName, namespace string, acs *authentication.AuthClientSet) string { - expirationSeconds := ptr.To[int64](int64(boundServiceAccountTokenExpiry.Seconds())) +// GenerateBoundServiceAccountToken creates a Kubernetes token for a namespaced service account with a runtime-configurable expiration time and returns the token string. +func GenerateBoundServiceAccountToken(ctx context.Context, serviceAccountName, namespace string, acs *authentication.AuthClientSet) string { + expirationSeconds := ptr.To(int64(boundServiceAccountTokenExpiry.Seconds())) token, err := acs.CoreV1Interface.ServiceAccounts(namespace).CreateToken( ctx, serviceAccountName, diff --git a/tests/secret-providers/hashicorp_vault/hashicorp_vault_test.go b/tests/secret-providers/hashicorp_vault/hashicorp_vault_test.go index cd135e50d4a..fe672b2fb68 100644 --- a/tests/secret-providers/hashicorp_vault/hashicorp_vault_test.go +++ b/tests/secret-providers/hashicorp_vault/hashicorp_vault_test.go @@ -43,33 +43,39 @@ var ( postgreSQLDatabase = "test_db" postgreSQLConnectionString = fmt.Sprintf("postgresql://%s:%s@postgresql.%s.svc.cluster.local:5432/%s?sslmode=disable", postgreSQLUsername, postgreSQLPassword, testNamespace, postgreSQLDatabase) - prometheusServerName = fmt.Sprintf("%s-prom-server", testName) - minReplicaCount = 0 - maxReplicaCount = 1 + prometheusServerName = fmt.Sprintf("%s-prom-server", testName) + minReplicaCount = 0 + maxReplicaCount = 1 + serviceAccountTokenCreationRole = fmt.Sprintf("%s-sa-role", testName) + serviceAccountTokenCreationRoleBinding = fmt.Sprintf("%s-sa-role-binding", testName) ) type templateData struct { - TestNamespace string - DeploymentName string - VaultNamespace string - ScaledObjectName string - TriggerAuthenticationName string - VaultSecretPath string - VaultPromDomain string - SecretName string - HashiCorpAuthentication string - HashiCorpToken string - PostgreSQLStatefulSetName string - PostgreSQLConnectionStringBase64 string - PostgreSQLUsername string - PostgreSQLPassword string - PostgreSQLDatabase string - MinReplicaCount int - MaxReplicaCount int - PublishDeploymentName string - MonitoredAppName string - PrometheusServerName string - VaultPkiCommonName string + TestNamespace string + DeploymentName string + VaultNamespace string + ScaledObjectName string + TriggerAuthenticationName string + VaultSecretPath string + VaultPromDomain string + SecretName string + HashiCorpAuthentication string + HashiCorpToken string + PostgreSQLStatefulSetName string + PostgreSQLConnectionStringBase64 string + PostgreSQLUsername string + PostgreSQLPassword string + PostgreSQLDatabase string + MinReplicaCount int + MaxReplicaCount int + PublishDeploymentName string + MonitoredAppName string + PrometheusServerName string + VaultPkiCommonName string + VaultRole string + VaultServiceAccountName string + ServiceAccountTokenCreationRole string + ServiceAccountTokenCreationRoleBinding string } const ( @@ -128,9 +134,12 @@ metadata: spec: hashiCorpVault: address: http://vault.{{.VaultNamespace}}:8200 - authentication: token + authentication: {{.HashiCorpAuthentication}} + role: {{.VaultRole}} + mount: kubernetes credential: token: {{.HashiCorpToken}} + serviceAccountName: {{.VaultServiceAccountName}} secrets: - parameter: connection key: connectionString @@ -413,6 +422,44 @@ spec: pkiPolicyTemplate = `path "pki*" { capabilities = [ "create", "read", "update", "delete", "list", "sudo" ] }` + + secretReadPolicyTemplate = `path "secret/data/keda" { + capabilities = ["read"] +} +path "secret/metadata/keda" { + capabilities = ["read", "list"] +}` + + serviceAccountTokenCreationRoleTemplate = ` +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{.ServiceAccountTokenCreationRole}} + namespace: {{.TestNamespace}} +rules: +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create + - get +` + serviceAccountTokenCreationRoleBindingTemplate = ` +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{.ServiceAccountTokenCreationRoleBinding}} + namespace: {{.TestNamespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{.ServiceAccountTokenCreationRole}} +subjects: +- kind: ServiceAccount + name: keda-operator + namespace: keda +` ) func TestPkiSecretsEngine(t *testing.T) { @@ -432,7 +479,7 @@ func TestPkiSecretsEngine(t *testing.T) { // Create kubernetes resources kc := GetKubernetesClient(t) useKubernetesAuth := test.authentication == "kubernetes" - hashiCorpToken, promPkiData := setupHashiCorpVault(t, kc, 2, useKubernetesAuth, true) + hashiCorpToken, promPkiData := setupHashiCorpVault(t, kc, 2, useKubernetesAuth, true, false) prometheus.Install(t, kc, prometheusServerName, testNamespace, promPkiData) // Create kubernetes resources for testing @@ -460,16 +507,29 @@ func TestSecretsEngine(t *testing.T) { name string vaultEngineVersion uint vaultSecretPath string + useKubernetesAuth bool + useDelegatesSAAuth bool }{ { name: "vault kv engine v1", vaultEngineVersion: 1, vaultSecretPath: "secret/keda", + useKubernetesAuth: false, + useDelegatesSAAuth: false, }, { name: "vault kv engine v2", vaultEngineVersion: 2, vaultSecretPath: "secret/data/keda", + useKubernetesAuth: false, + useDelegatesSAAuth: false, + }, + { + name: "vault kv engine v2", + vaultEngineVersion: 2, + vaultSecretPath: "secret/data/keda", + useKubernetesAuth: true, + useDelegatesSAAuth: true, }, } @@ -480,7 +540,7 @@ func TestSecretsEngine(t *testing.T) { data, postgreSQLtemplates := getPostgreSQLTemplateData() CreateKubernetesResources(t, kc, testNamespace, data, postgreSQLtemplates) - hashiCorpToken, _ := setupHashiCorpVault(t, kc, test.vaultEngineVersion, false, false) + hashiCorpToken, _ := setupHashiCorpVault(t, kc, test.vaultEngineVersion, test.useKubernetesAuth, false, test.useDelegatesSAAuth) assert.True(t, WaitForStatefulsetReplicaReadyCount(t, kc, postgreSQLStatefulSetName, testNamespace, 1, 60, 3), "replica count should be %d after 3 minutes", 1) @@ -493,8 +553,19 @@ func TestSecretsEngine(t *testing.T) { // Create kubernetes resources for testing data, templates := getTemplateData() - data.HashiCorpToken = RemoveANSI(hashiCorpToken) data.VaultSecretPath = test.vaultSecretPath + data.VaultRole = "keda" + if test.useKubernetesAuth { + data.HashiCorpAuthentication = "kubernetes" + } else { + data.HashiCorpAuthentication = "token" + data.HashiCorpToken = RemoveANSI(hashiCorpToken) + } + + if test.useDelegatesSAAuth { + data.VaultRole = "vault-delegated-sa" + data.VaultServiceAccountName = "default" + } KubectlApplyMultipleWithTemplate(t, data, templates) assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, minReplicaCount, 60, 3), @@ -548,7 +619,7 @@ func setupHashiCorpVaultPki(t *testing.T, podName string, nameSpace string) *pro return &pkiData } -func setupHashiCorpVault(t *testing.T, kc *kubernetes.Clientset, kvVersion uint, useKubernetesAuth, pki bool) (string, *prometheus.VaultPkiData) { +func setupHashiCorpVault(t *testing.T, kc *kubernetes.Clientset, kvVersion uint, useKubernetesAuth, pki, delegatedAuth bool) (string, *prometheus.VaultPkiData) { CreateNamespace(t, kc, vaultNamespace) _, err := ExecuteCommand("helm repo add hashicorp https://helm.releases.hashicorp.com") @@ -572,7 +643,7 @@ func setupHashiCorpVault(t *testing.T, kc *kubernetes.Clientset, kvVersion uint, // Enable Kubernetes auth if useKubernetesAuth { if pki { - remoteFile := "/tmp/policy.hcl" + remoteFile := "/tmp/pki_policy.hcl" KubectlCopyToPod(t, pkiPolicyTemplate, remoteFile, podName, vaultNamespace) assert.NoErrorf(t, err, "cannot create policy file in hashicorp vault - %s", err) _, _, err = ExecCommandOnSpecificPod(t, podName, vaultNamespace, fmt.Sprintf("vault policy write pkiPolicy %s", remoteFile)) @@ -584,7 +655,18 @@ func setupHashiCorpVault(t *testing.T, kc *kubernetes.Clientset, kvVersion uint, assert.NoErrorf(t, err, "cannot set kubernetes host in hashicorp vault - %s", err) _, _, err = ExecCommandOnSpecificPod(t, podName, vaultNamespace, "vault write auth/kubernetes/role/keda bound_service_account_names=keda-operator bound_service_account_namespaces=keda policies=pkiPolicy ttl=1h") assert.NoErrorf(t, err, "cannot cerate keda role in hashicorp vault - %s", err) + if delegatedAuth { + remoteFile := "/tmp/secret_read_policy.hcl" + KubectlCopyToPod(t, secretReadPolicyTemplate, remoteFile, podName, vaultNamespace) + assert.NoErrorf(t, err, "cannot create policy file in hashicorp vault - %s", err) + _, _, err = ExecCommandOnSpecificPod(t, podName, vaultNamespace, fmt.Sprintf("vault policy write secretReadPolicy %s", remoteFile)) + assert.NoErrorf(t, err, "cannot create policy in hashicorp vault - %s", err) + + _, _, err = ExecCommandOnSpecificPod(t, podName, vaultNamespace, fmt.Sprintf("vault write auth/kubernetes/role/vault-delegated-sa bound_service_account_names=default bound_service_account_namespaces=%s policies=secretReadPolicy ttl=1h", testNamespace)) + assert.NoErrorf(t, err, "cannot cerate keda role in hashicorp vault - %s", err) + } } + // Create kv secret if !pki { _, _, err = ExecCommandOnSpecificPod(t, podName, vaultNamespace, fmt.Sprintf("vault kv put secret/keda connectionString=%s", postgreSQLConnectionString)) @@ -633,24 +715,26 @@ func testScaleOut(t *testing.T, kc *kubernetes.Clientset, data templateData) { } var data = templateData{ - TestNamespace: testNamespace, - PostgreSQLStatefulSetName: postgreSQLStatefulSetName, - DeploymentName: deploymentName, - ScaledObjectName: scaledObjectName, - MinReplicaCount: minReplicaCount, - MaxReplicaCount: maxReplicaCount, - TriggerAuthenticationName: triggerAuthenticationName, - SecretName: secretName, - PostgreSQLUsername: postgreSQLUsername, - PostgreSQLPassword: postgreSQLPassword, - PostgreSQLDatabase: postgreSQLDatabase, - PostgreSQLConnectionStringBase64: b64.StdEncoding.EncodeToString([]byte(postgreSQLConnectionString)), - PrometheusServerName: prometheusServerName, - MonitoredAppName: monitoredAppName, - PublishDeploymentName: publishDeploymentName, - VaultNamespace: vaultNamespace, - VaultPromDomain: vaultPromDomain, - VaultPkiCommonName: fmt.Sprintf("keda.%s.svc", testNamespace), + TestNamespace: testNamespace, + PostgreSQLStatefulSetName: postgreSQLStatefulSetName, + DeploymentName: deploymentName, + ScaledObjectName: scaledObjectName, + MinReplicaCount: minReplicaCount, + MaxReplicaCount: maxReplicaCount, + TriggerAuthenticationName: triggerAuthenticationName, + SecretName: secretName, + PostgreSQLUsername: postgreSQLUsername, + PostgreSQLPassword: postgreSQLPassword, + PostgreSQLDatabase: postgreSQLDatabase, + PostgreSQLConnectionStringBase64: b64.StdEncoding.EncodeToString([]byte(postgreSQLConnectionString)), + PrometheusServerName: prometheusServerName, + MonitoredAppName: monitoredAppName, + PublishDeploymentName: publishDeploymentName, + VaultNamespace: vaultNamespace, + VaultPromDomain: vaultPromDomain, + VaultPkiCommonName: fmt.Sprintf("keda.%s.svc", testNamespace), + ServiceAccountTokenCreationRole: serviceAccountTokenCreationRole, + ServiceAccountTokenCreationRoleBinding: serviceAccountTokenCreationRoleBinding, } func getPostgreSQLTemplateData() (templateData, []Template) { @@ -676,5 +760,8 @@ func getTemplateData() (templateData, []Template) { {Name: "deploymentTemplate", Config: deploymentTemplate}, {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + // required for the keda to request token creations for the service account + {Name: "serviceAccountTokenCreationRoleTemplate", Config: serviceAccountTokenCreationRoleTemplate}, + {Name: "serviceAccountTokenCreationRoleBindingTemplate", Config: serviceAccountTokenCreationRoleBindingTemplate}, } }