Browse Source

feat: add vault auth namespace option (#3157)

* feat: add vault auth namespace option

Signed-off-by: Blair Drummond <blaird@liatrio.com>

* fix: appease the linter

Signed-off-by: Blair Drummond <blaird@liatrio.com>

* feat: add tests for auth namespace

Signed-off-by: Blair Drummond <blaird@liatrio.com>

* fix: add make reviewable output

Signed-off-by: Blair Drummond <blaird@liatrio.com>

---------

Signed-off-by: Blair Drummond <blaird@liatrio.com>
Blair Drummond 2 years ago
parent
commit
731c0ed736

+ 9 - 1
apis/externalsecrets/v1beta1/secretstore_vault_types.go

@@ -104,8 +104,16 @@ type VaultClientTLS struct {
 
 // VaultAuth is the configuration used to authenticate with a Vault server.
 // Only one of `tokenSecretRef`, `appRole`,  `kubernetes`, `ldap`, `userPass`, `jwt` or `cert`
-// can be specified.
+// can be specified. A namespace to authenticate against can optionally be specified.
 type VaultAuth struct {
+	// Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+	// Namespaces is a set of features within Vault Enterprise that allows
+	// Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+	// More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+	// This will default to Vault.Namespace field if set, or empty otherwise
+	// +optional
+	Namespace *string `json:"namespace,omitempty"`
+
 	// TokenSecretRef authenticates with Vault by presenting a token.
 	// +optional
 	TokenSecretRef *esmeta.SecretKeySelector `json:"tokenSecretRef,omitempty"`

+ 5 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -2569,6 +2569,11 @@ func (in *VaultAppRole) DeepCopy() *VaultAppRole {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VaultAuth) DeepCopyInto(out *VaultAuth) {
 	*out = *in
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
 	if in.TokenSecretRef != nil {
 		in, out := &in.TokenSecretRef, &out.TokenSecretRef
 		*out = new(metav1.SecretKeySelector)

+ 8 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -3918,6 +3918,14 @@ spec:
                             - path
                             - username
                             type: object
+                          namespace:
+                            description: |-
+                              Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                              Namespaces is a set of features within Vault Enterprise that allows
+                              Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                              More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                              This will default to Vault.Namespace field if set, or empty otherwise
+                            type: string
                           tokenSecretRef:
                             description: TokenSecretRef authenticates with Vault by
                               presenting a token.

+ 8 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -3918,6 +3918,14 @@ spec:
                             - path
                             - username
                             type: object
+                          namespace:
+                            description: |-
+                              Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                              Namespaces is a set of features within Vault Enterprise that allows
+                              Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                              More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                              This will default to Vault.Namespace field if set, or empty otherwise
+                            type: string
                           tokenSecretRef:
                             description: TokenSecretRef authenticates with Vault by
                               presenting a token.

+ 8 - 0
config/crds/bases/generators.external-secrets.io_vaultdynamicsecrets.yaml

@@ -491,6 +491,14 @@ spec:
                         - path
                         - username
                         type: object
+                      namespace:
+                        description: |-
+                          Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                          Namespaces is a set of features within Vault Enterprise that allows
+                          Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                          More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                          This will default to Vault.Namespace field if set, or empty otherwise
+                        type: string
                       tokenSecretRef:
                         description: TokenSecretRef authenticates with Vault by presenting
                           a token.

+ 24 - 0
deploy/crds/bundle.yaml

@@ -4247,6 +4247,14 @@ spec:
                                 - path
                                 - username
                               type: object
+                            namespace:
+                              description: |-
+                                Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                                Namespaces is a set of features within Vault Enterprise that allows
+                                Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                                More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                                This will default to Vault.Namespace field if set, or empty otherwise
+                              type: string
                             tokenSecretRef:
                               description: TokenSecretRef authenticates with Vault by presenting a token.
                               properties:
@@ -9495,6 +9503,14 @@ spec:
                                 - path
                                 - username
                               type: object
+                            namespace:
+                              description: |-
+                                Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                                Namespaces is a set of features within Vault Enterprise that allows
+                                Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                                More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                                This will default to Vault.Namespace field if set, or empty otherwise
+                              type: string
                             tokenSecretRef:
                               description: TokenSecretRef authenticates with Vault by presenting a token.
                               properties:
@@ -11062,6 +11078,14 @@ spec:
                             - path
                             - username
                           type: object
+                        namespace:
+                          description: |-
+                            Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+                            Namespaces is a set of features within Vault Enterprise that allows
+                            Vault environments to support Secure Multi-tenancy. e.g: "ns1".
+                            More about namespaces can be found here https://www.vaultproject.io/docs/enterprise/namespaces
+                            This will default to Vault.Namespace field if set, or empty otherwise
+                          type: string
                         tokenSecretRef:
                           description: TokenSecretRef authenticates with Vault by presenting a token.
                           properties:

+ 17 - 1
docs/api/spec.md

@@ -6876,7 +6876,7 @@ resource is used as the app role secret.</p>
 <p>
 <p>VaultAuth is the configuration used to authenticate with a Vault server.
 Only one of <code>tokenSecretRef</code>, <code>appRole</code>,  <code>kubernetes</code>, <code>ldap</code>, <code>userPass</code>, <code>jwt</code> or <code>cert</code>
-can be specified.</p>
+can be specified. A namespace to authenticate against can optionally be specified.</p>
 </p>
 <table>
 <thead>
@@ -6888,6 +6888,22 @@ can be specified.</p>
 <tbody>
 <tr>
 <td>
+<code>namespace</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Name of the vault namespace to authenticate to. This can be different than the namespace your secret is in.
+Namespaces is a set of features within Vault Enterprise that allows
+Vault environments to support Secure Multi-tenancy. e.g: &ldquo;ns1&rdquo;.
+More about namespaces can be found here <a href="https://www.vaultproject.io/docs/enterprise/namespaces">https://www.vaultproject.io/docs/enterprise/namespaces</a>
+This will default to Vault.Namespace field if set, or empty otherwise</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>tokenSecretRef</code></br>
 <em>
 <a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">

+ 24 - 0
docs/provider/hashicorp-vault.md

@@ -278,6 +278,8 @@ We support five different modes for authentication:
 [tlsCert](https://developer.hashicorp.com/vault/docs/auth/cert), each one comes with it's own
 trade-offs. Depending on the authentication method you need to adapt your environment.
 
+If you're using Vault namespaces, you can authenticate into one namespace and use the vault token against a different namespace, if desired.
+
 #### Token-based authentication
 
 A static token is stored in a `Kind=Secret` and is used to authenticate with vault.
@@ -461,6 +463,28 @@ spec:
         # ...
 ```
 
+##### Authenticating into a different namespace
+
+In some situations your authentication backend may be in one namespace, and your secrets in another. You can authenticate into one namespace, and use that token against another, by setting `provider.vault.namespace` and `provider.vault.auth.namespace` to different values. If `provider.vault.auth.namespace` is unset but `provider.vault.namespace` is, it will default to the `provider.vault.namespace` value.
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: vault-backend
+spec:
+  provider:
+    vault:
+      server: "http://my.vault.server:8200"
+      # See https://www.vaultproject.io/docs/enterprise/namespaces
+      namespace: "app-team"
+      path: "secret"
+      version: "v2"
+      auth:
+        namespace: "kubernetes-team"
+        # ...
+```
+
 #### Read Your Writes
 
 Vault 1.10.0 and later encodes information in the token to detect the case

+ 28 - 0
pkg/provider/vault/auth.go

@@ -42,6 +42,10 @@ const (
 // setAuth gets a new token using the configured mechanism.
 // If there's already a valid token, does nothing.
 func (c *client) setAuth(ctx context.Context, cfg *vault.Config) error {
+	// Switch to auth namespace if different from the provider namespace
+	restoreNamespace := c.useAuthNamespace(ctx)
+	defer restoreNamespace()
+
 	tokenExists := false
 	var err error
 	if c.client.Token() != "" {
@@ -169,3 +173,27 @@ func revokeTokenIfValid(ctx context.Context, client util.Client) error {
 	}
 	return nil
 }
+
+func (c *client) useAuthNamespace(_ context.Context) func() {
+	ns := ""
+	if c.store.Namespace != nil {
+		ns = *c.store.Namespace
+	}
+
+	if c.store.Auth.Namespace != nil {
+		// Different Auth Vault Namespace than Secret Vault Namespace
+		// Switch namespaces then switch back at the end
+		if c.store.Auth.Namespace != nil && *c.store.Auth.Namespace != ns {
+			c.log.V(1).Info("Using namespace=%s for the vault login", *c.store.Auth.Namespace)
+			c.client.SetNamespace(*c.store.Auth.Namespace)
+			// use this as a defer to reset the namespace
+			return func() {
+				c.log.V(1).Info("Restoring client namespace to namespace=%s", ns)
+				c.client.SetNamespace(ns)
+			}
+		}
+	}
+
+	// no-op
+	return func() {}
+}

+ 167 - 0
pkg/provider/vault/auth_test.go

@@ -0,0 +1,167 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package vault
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/ptr"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/vault/fake"
+)
+
+// Test Vault Namespace logic.
+func TestSetAuthNamespace(t *testing.T) {
+	store := makeValidSecretStore()
+
+	kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "vault-secret",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"key": []byte("token"),
+		},
+	}).Build()
+
+	store.Spec.Provider.Vault.Auth.Kubernetes.ServiceAccountRef = nil
+	store.Spec.Provider.Vault.Auth.Kubernetes.SecretRef = &esmeta.SecretKeySelector{
+		Name:      "vault-secret",
+		Namespace: ptr.To("default"),
+		Key:       "key",
+	}
+
+	adminNS := "admin"
+	teamNS := "admin/team-a"
+
+	type result struct {
+		Before string
+		During string
+		After  string
+	}
+
+	type args struct {
+		store    *esv1beta1.SecretStore
+		expected result
+	}
+	cases := map[string]struct {
+		reason string
+		args   args
+	}{
+		"StoreNoNamespace": {
+			reason: "no namespace should ever be set",
+			args: args{
+				store:    store,
+				expected: result{Before: "", During: "", After: ""},
+			},
+		},
+		"StoreWithNamespace": {
+			reason: "use the team namespace throughout",
+			args: args{
+				store: func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+					s := store.DeepCopy()
+					s.Spec.Provider.Vault.Namespace = ptr.To(teamNS)
+					return s
+				}(store),
+				expected: result{Before: teamNS, During: teamNS, After: teamNS},
+			},
+		},
+		"StoreWithAuthNamespace": {
+			reason: "switch to the auth namespace during login then revert",
+			args: args{
+				store: func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+					s := store.DeepCopy()
+					s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
+					return s
+				}(store),
+				expected: result{Before: "", During: adminNS, After: ""},
+			},
+		},
+		"StoreWithSameNamespace": {
+			reason: "the admin namespace throughout",
+			args: args{
+				store: func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+					s := store.DeepCopy()
+					s.Spec.Provider.Vault.Namespace = ptr.To(adminNS)
+					s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
+					return s
+				}(store),
+				expected: result{Before: adminNS, During: adminNS, After: adminNS},
+			},
+		},
+		"StoreWithDistinctNamespace": {
+			reason: "switch from team namespace, to admin, then back",
+			args: args{
+				store: func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+					s := store.DeepCopy()
+					s.Spec.Provider.Vault.Namespace = ptr.To(teamNS)
+					s.Spec.Provider.Vault.Auth.Namespace = ptr.To(adminNS)
+					return s
+				}(store),
+				expected: result{Before: teamNS, During: adminNS, After: teamNS},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			prov := &Provider{
+				NewVaultClient: fake.ClientWithLoginMock,
+			}
+
+			c, cfg, err := prov.prepareConfig(context.Background(), kube, nil, tc.args.store.Spec.Provider.Vault, nil, "default", store.GetObjectKind().GroupVersionKind().Kind)
+			if err != nil {
+				t.Errorf(err.Error())
+			}
+
+			client, err := getVaultClient(prov, tc.args.store, cfg)
+			if err != nil {
+				t.Errorf("vault.useAuthNamespace: failed to create client: %s", err.Error())
+			}
+
+			_, err = prov.initClient(context.Background(), c, client, cfg, tc.args.store.Spec.Provider.Vault)
+			if err != nil {
+				t.Errorf("vault.useAuthNamespace: failed to init client: %s", err.Error())
+			}
+
+			c.client = client
+
+			// before auth
+			actual := result{
+				Before: c.client.Namespace(),
+			}
+
+			// during authentication (getting a token)
+			resetNS := c.useAuthNamespace(context.Background())
+			actual.During = c.client.Namespace()
+			resetNS()
+
+			// after getting the token
+			actual.After = c.client.Namespace()
+
+			if diff := cmp.Diff(tc.args.expected, actual, cmpopts.EquateComparable()); diff != "" {
+				t.Errorf("\n%s\nvault.useAuthNamepsace(...): -want namespace, +got namespace:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}

+ 18 - 5
pkg/provider/vault/fake/vault.go

@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"reflect"
 	"strings"
+	"sync"
 
 	vault "github.com/hashicorp/vault/api"
 
@@ -155,6 +156,8 @@ type MockTokenFn func() string
 
 type MockClearTokenFn func()
 
+type MockNamespaceFn func() string
+
 type MockSetNamespaceFn func(namespace string)
 
 type MockAddHeaderFn func(key, value string)
@@ -188,10 +191,6 @@ func NewClearTokenFn() MockClearTokenFn {
 	return func() {}
 }
 
-func NewSetNamespaceFn() MockSetNamespaceFn {
-	return func(namespace string) {}
-}
-
 func NewAddHeaderFn() MockAddHeaderFn {
 	return func(key, value string) {}
 }
@@ -203,8 +202,12 @@ type VaultClient struct {
 	MockSetToken     MockSetTokenFn
 	MockToken        MockTokenFn
 	MockClearToken   MockClearTokenFn
+	MockNamespace    MockNamespaceFn
 	MockSetNamespace MockSetNamespaceFn
 	MockAddHeader    MockAddHeaderFn
+
+	namespace string
+	lock      sync.RWMutex
 }
 
 func (c *VaultClient) Logical() Logical {
@@ -253,8 +256,17 @@ func (c *VaultClient) ClearToken() {
 	c.MockClearToken()
 }
 
+func (c *VaultClient) Namespace() string {
+	c.lock.RLock()
+	defer c.lock.RUnlock()
+	ns := c.namespace
+	return ns
+}
+
 func (c *VaultClient) SetNamespace(namespace string) {
-	c.MockSetNamespace(namespace)
+	c.lock.Lock()
+	defer c.lock.Unlock()
+	c.namespace = namespace
 }
 
 func (c *VaultClient) AddHeader(key, value string) {
@@ -277,6 +289,7 @@ func ClientWithLoginMock(_ *vault.Config) (util.Client, error) {
 		AuthField:        cl.Auth(),
 		AuthTokenField:   cl.AuthToken(),
 		LogicalField:     cl.Logical(),
+		NamespaceFunc:    cl.Namespace,
 		SetNamespaceFunc: cl.SetNamespace,
 		AddHeaderFunc:    cl.AddHeader,
 	}, nil

+ 1 - 0
pkg/provider/vault/provider.go

@@ -71,6 +71,7 @@ func NewVaultClient(config *vault.Config) (util.Client, error) {
 		AuthField:        vaultClient.Auth(),
 		AuthTokenField:   vaultClient.Auth().Token(),
 		LogicalField:     vaultClient.Logical(),
+		NamespaceFunc:    vaultClient.Namespace,
 		SetNamespaceFunc: vaultClient.SetNamespace,
 		AddHeaderFunc:    vaultClient.AddHeader,
 	}, nil

+ 6 - 0
pkg/provider/vault/util/vault.go

@@ -46,6 +46,7 @@ type Client interface {
 	Auth() Auth
 	Logical() Logical
 	AuthToken() Token
+	Namespace() string
 	SetNamespace(namespace string)
 	AddHeader(key, value string)
 }
@@ -57,6 +58,7 @@ type VaultClient struct {
 	AuthField        Auth
 	LogicalField     Logical
 	AuthTokenField   Token
+	NamespaceFunc    func() string
 	SetNamespaceFunc func(namespace string)
 	AddHeaderFunc    func(key, value string)
 }
@@ -65,6 +67,10 @@ func (v VaultClient) AddHeader(key, value string) {
 	v.AddHeaderFunc(key, value)
 }
 
+func (v VaultClient) Namespace() string {
+	return v.NamespaceFunc()
+}
+
 func (v VaultClient) SetNamespace(namespace string) {
 	v.SetNamespaceFunc(namespace)
 }