From fdb36cd8a3bfb5d8f6ca243df4556b204c0fb882 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Mon, 20 Mar 2023 16:42:02 +0000 Subject: [PATCH] Use token from CSI TokenRequests (#163) --- CHANGELOG.md | 7 +++++++ Makefile | 7 ++++--- internal/config/config.go | 32 ++++++++++++++++++++++++++++---- internal/config/config_test.go | 34 ++++++++++++++++++---------------- internal/provider/provider.go | 17 +++++++++++------ main.go | 1 - test/bats/_helpers.bash | 7 +++++-- test/bats/provider.bats | 5 +++++ 8 files changed, 78 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 528060e..bde63c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,13 @@ CHANGES: CHANGES: * Duplicate object names now trigger an error instead of silently overwriting files. [[GH-148](https://github.com/hashicorp/vault-csi-provider/pull/148)] +* Vault CSI Provider will use service account tokens passed from the Secrets Store CSI Driver instead of generating one if an appropriate token is provided. [[GH-163](https://github.com/hashicorp/vault-csi-provider/pull/163)] + * The Secrets Store CSI driver needs to be configured to generate tokens with the correct audience for this feature. Vault CSI Provider + will look for a token with the audience specified in the SecretProviderClass, or otherwise "vault". To configure the driver to generate + a token with the correct audience, use the + [`tokenRequests`](https://github.com/kubernetes-sigs/secrets-store-csi-driver/tree/main/charts/secrets-store-csi-driver#configuration) + option from the _driver_ helm chart via the flag `--set tokenRequests[0].audience="vault"`. See + [CSI TokenRequests documentation](https://kubernetes-csi.github.io/docs/token-requests.html) for further details. BUGS: diff --git a/Makefile b/Makefile index 43beaed..2313b35 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,8 @@ PKG=github.com/hashicorp/vault-csi-provider/internal/version LDFLAGS?="-X '$(PKG).BuildVersion=$(VERSION)' \ -X '$(PKG).BuildDate=$(BUILD_DATE)' \ -X '$(PKG).GoVersion=$(shell go version)'" -CSI_DRIVER_VERSION=1.2.4 -VAULT_HELM_VERSION=0.22.1 +CSI_DRIVER_VERSION=1.3.1 +VAULT_HELM_VERSION=0.23.0 GOLANGCI_LINT_FORMAT?=colored-line-number .PHONY: default build test bootstrap fmt lint image e2e-image e2e-setup e2e-teardown e2e-test mod setup-kind promote-staging-manifest @@ -76,7 +76,8 @@ e2e-setup: --wait --timeout=5m \ --namespace=csi \ --set linux.image.pullPolicy="IfNotPresent" \ - --set syncSecret.enabled=true + --set syncSecret.enabled=true \ + --set tokenRequests[0].audience="vault" helm install vault-bootstrap test/bats/configs/vault \ --namespace=csi helm install vault vault \ diff --git a/internal/config/config.go b/internal/config/config.go index ae0d2ef..928b57a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -77,10 +77,11 @@ type Parameters struct { } type PodInfo struct { - Name string - UID types.UID - Namespace string - ServiceAccountName string + Name string + UID types.UID + Namespace string + ServiceAccountName string + ServiceAccountToken string } type Secret struct { @@ -151,6 +152,29 @@ func parseParameters(parametersStr string) (Parameters, error) { return Parameters{}, err } + tokensJSON := params["csi.storage.k8s.io/serviceAccount.tokens"] + if tokensJSON != "" { + // The csi.storage.k8s.io/serviceAccount.tokens field is a JSON object + // marshalled into a string. The object keys are audience name (string) + // and the values are embedded objects with "token" and + // "expirationTimestamp" fields for the corresponding audience. + var tokens map[string]struct { + Token string `json:"token"` + ExpirationTimestamp string `json:"expirationTimestamp"` + } + if err := json.Unmarshal([]byte(tokensJSON), &tokens); err != nil { + return Parameters{}, fmt.Errorf("failed to unmarshal service account tokens: %w", err) + } + + audience := "vault" + if parameters.Audience != "" { + audience = parameters.Audience + } + if token, ok := tokens[audience]; ok { + parameters.PodInfo.ServiceAccountToken = token.Token + } + } + return parameters, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index fbd373f..765b605 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -145,22 +145,23 @@ func TestParseConfig(t *testing.T) { name: "set all options", targetPath: targetPath, parameters: map[string]string{ - "roleName": "example-role", - "vaultSkipTLSVerify": "true", - "vaultAddress": "my-vault-address", - "vaultNamespace": "my-vault-namespace", - "vaultKubernetesMountPath": "my-mount-path", - "vaultCACertPath": "my-ca-cert-path", - "vaultCADirectory": "my-ca-directory", - "vaultTLSServerName": "mytls-server-name", - "vaultTLSClientCertPath": "my-tls-client-cert-path", - "vaultTLSClientKeyPath": "my-tls-client-key-path", - "csi.storage.k8s.io/pod.name": "my-pod-name", - "csi.storage.k8s.io/pod.uid": "my-pod-uid", - "csi.storage.k8s.io/pod.namespace": "my-pod-namespace", - "csi.storage.k8s.io/serviceAccount.name": "my-pod-sa-name", - "objects": objects, - "audience": "my-aud", + "roleName": "example-role", + "vaultSkipTLSVerify": "true", + "vaultAddress": "my-vault-address", + "vaultNamespace": "my-vault-namespace", + "vaultKubernetesMountPath": "my-mount-path", + "vaultCACertPath": "my-ca-cert-path", + "vaultCADirectory": "my-ca-directory", + "vaultTLSServerName": "mytls-server-name", + "vaultTLSClientCertPath": "my-tls-client-cert-path", + "vaultTLSClientKeyPath": "my-tls-client-key-path", + "csi.storage.k8s.io/pod.name": "my-pod-name", + "csi.storage.k8s.io/pod.uid": "my-pod-uid", + "csi.storage.k8s.io/pod.namespace": "my-pod-namespace", + "csi.storage.k8s.io/serviceAccount.name": "my-pod-sa-name", + "csi.storage.k8s.io/serviceAccount.tokens": `{"my-aud": {"token": "my-pod-sa-token", "expirationTimestamp": "bar"}, "other-aud": {"token": "unused-token"}}`, + "objects": objects, + "audience": "my-aud", }, expected: Config{ TargetPath: targetPath, @@ -186,6 +187,7 @@ func TestParseConfig(t *testing.T) { "my-pod-uid", "my-pod-namespace", "my-pod-sa-name", + "my-pod-sa-token", }, Audience: "my-aud", }, diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f941772..75cb529 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -84,19 +84,24 @@ func (p *provider) createJWTToken(ctx context.Context, podInfo config.PodInfo, a func (p *provider) login(ctx context.Context, client *api.Client, params config.Parameters) error { p.logger.Debug("performing vault login") - jwt, err := p.createJWTToken(ctx, params.PodInfo, params.Audience) - if err != nil { - return err + jwt := params.PodInfo.ServiceAccountToken + if jwt == "" { + p.logger.Debug("no suitable token found in mount request, falling back to generating service account JWT") + var err error + jwt, err = p.createJWTToken(ctx, params.PodInfo, params.Audience) + if err != nil { + return err + } } req := client.NewRequest(http.MethodPost, "/v1/auth/"+params.VaultKubernetesMountPath+"/login") - err = req.SetJSONBody(map[string]string{ + if err := req.SetJSONBody(map[string]string{ "role": params.VaultRoleName, "jwt": jwt, - }) - if err != nil { + }); err != nil { return err } + secret, err := vaultclient.Do(ctx, client, req) if err != nil { return fmt.Errorf("failed to login: %w", err) diff --git a/main.go b/main.go index fe79407..752d7a1 100644 --- a/main.go +++ b/main.go @@ -75,7 +75,6 @@ func realMain(logger hclog.Logger) error { grpc.UnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { startTime := time.Now() serverLogger.Info("Processing unary gRPC call", "grpc.method", info.FullMethod) - serverLogger.Debug("Request contents", "req", req) resp, err := handler(ctx, req) serverLogger.Info("Finished unary gRPC call", "grpc.method", info.FullMethod, "grpc.time", time.Since(startTime), "grpc.code", status.Code(err), "err", err) return resp, err diff --git a/test/bats/_helpers.bash b/test/bats/_helpers.bash index 5049f4a..c156b9d 100644 --- a/test/bats/_helpers.bash +++ b/test/bats/_helpers.bash @@ -14,13 +14,16 @@ wait_for_success() { } setup_postgres() { - # Setup postgres + # Setup postgres, pulling the image first to help avoid CI timeouts. + POSTGRES_IMAGE="$(awk '/image:/{print $NF}' $CONFIGS/postgres.yaml)" + docker pull "${POSTGRES_IMAGE}" + kind load docker-image "${POSTGRES_IMAGE}" POSTGRES_PASSWORD=$(openssl rand -base64 30) kubectl --namespace=test create secret generic postgres-root \ --from-literal=POSTGRES_USER="root" \ --from-literal=POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" kubectl --namespace=test apply -f $CONFIGS/postgres.yaml - kubectl wait --namespace=test --for=condition=Ready --timeout=5m pod -l app=postgres + kubectl wait --namespace=test --for=condition=Ready --timeout=10m pod -l app=postgres # Configure vault to manage postgres kubectl --namespace=csi exec vault-0 -- vault secrets enable database diff --git a/test/bats/provider.bats b/test/bats/provider.bats index 8c8d955..c7f5b6c 100644 --- a/test/bats/provider.bats +++ b/test/bats/provider.bats @@ -43,11 +43,13 @@ setup(){ kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/db-role \ bound_service_account_names=nginx-db \ bound_service_account_namespaces=test \ + audience=vault \ policies=db-policy \ ttl=20m kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/kv-role \ bound_service_account_names=nginx-kv \ bound_service_account_namespaces=test \ + audience=vault \ policies=kv-policy \ ttl=20m kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/kv-custom-audience-role \ @@ -59,16 +61,19 @@ setup(){ kubectl --namespace=csi exec vault-0 -- vault write -namespace=acceptance auth/kubernetes/role/kv-namespace-role \ bound_service_account_names=nginx-kv-namespace \ bound_service_account_namespaces=test \ + audience=vault \ policies=kv-namespace-policy \ ttl=20m kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/pki-role \ bound_service_account_names=nginx-pki \ bound_service_account_namespaces=test \ + audience=vault \ policies=pki-policy \ ttl=20m kubectl --namespace=csi exec vault-0 -- vault write auth/kubernetes/role/all-role \ bound_service_account_names=nginx-all \ bound_service_account_namespaces=test \ + audience=vault \ policies=db-policy,kv-policy,pki-policy \ ttl=20m