فهرست منبع

feat(openbao): add `auth.userPass` auth method (#6492)

Signed-off-by: Philipp Stehle <philipp.stehle@secretz.io>
Philipp Stehle 1 هفته پیش
والد
کامیت
0a8439031f

+ 31 - 0
apis/externalsecrets/v1/secretstore_openbao_types.go

@@ -66,8 +66,39 @@ type OpenBaoProvider struct {
 // OpenBaoAuth is the configuration used to authenticate with an OpenBao server.
 // Currently only token-based authentication is supported via `tokenSecretRef`.
 // Additional authentication methods are planned for future releases.
+//
+// +kubebuilder:validation:MaxProperties=1
 type OpenBaoAuth struct {
 	// TokenSecretRef authenticates with OpenBao by presenting a token.
 	// +optional
 	TokenSecretRef *esmeta.SecretKeySelector `json:"tokenSecretRef,omitempty"`
+
+	// UserPass authenticates with OpenBao by passing a username/password pair
+	// +optional
+	UserPass *OpenBaoUserPassAuth `json:"userPass,omitempty"`
+}
+
+// OpenBaoUserPassAuth authenticates with OpenBao using [UserPass authentication
+// method], with the username and password stored in a Kubernetes Secret
+// resource.
+//
+// [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+type OpenBaoUserPassAuth struct {
+	// Path where the UserPassword authentication backend is mounted
+	// in OpenBao, e.g: "userpass"
+	// +kubebuilder:default=userpass
+	Path string `json:"path"`
+
+	// Username is a username used to authenticate using the [UserPass
+	// authentication method]
+	//
+	// [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+	Username string `json:"username"`
+
+	// SecretRef to a key in a Secret resource containing password for the user
+	// used to authenticate with OpenBao using the [UserPass authentication
+	// method]
+	//
+	// [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+	SecretRef esmeta.SecretKeySelector `json:"secretRef,omitempty"`
 }

+ 21 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -3034,6 +3034,11 @@ func (in *OpenBaoAuth) DeepCopyInto(out *OpenBaoAuth) {
 		*out = new(apismetav1.SecretKeySelector)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.UserPass != nil {
+		in, out := &in.UserPass, &out.UserPass
+		*out = new(OpenBaoUserPassAuth)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenBaoAuth.
@@ -3081,6 +3086,22 @@ func (in *OpenBaoProvider) DeepCopy() *OpenBaoProvider {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OpenBaoUserPassAuth) DeepCopyInto(out *OpenBaoUserPassAuth) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenBaoUserPassAuth.
+func (in *OpenBaoUserPassAuth) DeepCopy() *OpenBaoUserPassAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(OpenBaoUserPassAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OracleAuth) DeepCopyInto(out *OracleAuth) {
 	*out = *in

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

@@ -4175,6 +4175,7 @@ spec:
                       auth:
                         description: Auth configures how secret-manager authenticates
                           with the OpenBao server.
+                        maxProperties: 1
                         properties:
                           tokenSecretRef:
                             description: TokenSecretRef authenticates with OpenBao
@@ -4204,6 +4205,59 @@ spec:
                                 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                 type: string
                             type: object
+                          userPass:
+                            description: UserPass authenticates with OpenBao by passing
+                              a username/password pair
+                            properties:
+                              path:
+                                default: userpass
+                                description: |-
+                                  Path where the UserPassword authentication backend is mounted
+                                  in OpenBao, e.g: "userpass"
+                                type: string
+                              secretRef:
+                                description: |-
+                                  SecretRef to a key in a Secret resource containing password for the user
+                                  used to authenticate with OpenBao using the [UserPass authentication
+                                  method]
+
+                                  [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              username:
+                                description: |-
+                                  Username is a username used to authenticate using the [UserPass
+                                  authentication method]
+
+                                  [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+                                type: string
+                            required:
+                            - path
+                            - username
+                            type: object
                         type: object
                       caBundle:
                         description: |-

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

@@ -4175,6 +4175,7 @@ spec:
                       auth:
                         description: Auth configures how secret-manager authenticates
                           with the OpenBao server.
+                        maxProperties: 1
                         properties:
                           tokenSecretRef:
                             description: TokenSecretRef authenticates with OpenBao
@@ -4204,6 +4205,59 @@ spec:
                                 pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                 type: string
                             type: object
+                          userPass:
+                            description: UserPass authenticates with OpenBao by passing
+                              a username/password pair
+                            properties:
+                              path:
+                                default: userpass
+                                description: |-
+                                  Path where the UserPassword authentication backend is mounted
+                                  in OpenBao, e.g: "userpass"
+                                type: string
+                              secretRef:
+                                description: |-
+                                  SecretRef to a key in a Secret resource containing password for the user
+                                  used to authenticate with OpenBao using the [UserPass authentication
+                                  method]
+
+                                  [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              username:
+                                description: |-
+                                  Username is a username used to authenticate using the [UserPass
+                                  authentication method]
+
+                                  [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+                                type: string
+                            required:
+                            - path
+                            - username
+                            type: object
                         type: object
                       caBundle:
                         description: |-

+ 104 - 0
deploy/crds/bundle.yaml

@@ -6140,6 +6140,7 @@ spec:
                       properties:
                         auth:
                           description: Auth configures how secret-manager authenticates with the OpenBao server.
+                          maxProperties: 1
                           properties:
                             tokenSecretRef:
                               description: TokenSecretRef authenticates with OpenBao by presenting a token.
@@ -6167,6 +6168,57 @@ spec:
                                   pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                   type: string
                               type: object
+                            userPass:
+                              description: UserPass authenticates with OpenBao by passing a username/password pair
+                              properties:
+                                path:
+                                  default: userpass
+                                  description: |-
+                                    Path where the UserPassword authentication backend is mounted
+                                    in OpenBao, e.g: "userpass"
+                                  type: string
+                                secretRef:
+                                  description: |-
+                                    SecretRef to a key in a Secret resource containing password for the user
+                                    used to authenticate with OpenBao using the [UserPass authentication
+                                    method]
+
+                                    [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                username:
+                                  description: |-
+                                    Username is a username used to authenticate using the [UserPass
+                                    authentication method]
+
+                                    [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+                                  type: string
+                              required:
+                                - path
+                                - username
+                              type: object
                           type: object
                         caBundle:
                           description: |-
@@ -18647,6 +18699,7 @@ spec:
                       properties:
                         auth:
                           description: Auth configures how secret-manager authenticates with the OpenBao server.
+                          maxProperties: 1
                           properties:
                             tokenSecretRef:
                               description: TokenSecretRef authenticates with OpenBao by presenting a token.
@@ -18674,6 +18727,57 @@ spec:
                                   pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                   type: string
                               type: object
+                            userPass:
+                              description: UserPass authenticates with OpenBao by passing a username/password pair
+                              properties:
+                                path:
+                                  default: userpass
+                                  description: |-
+                                    Path where the UserPassword authentication backend is mounted
+                                    in OpenBao, e.g: "userpass"
+                                  type: string
+                                secretRef:
+                                  description: |-
+                                    SecretRef to a key in a Secret resource containing password for the user
+                                    used to authenticate with OpenBao using the [UserPass authentication
+                                    method]
+
+                                    [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                username:
+                                  description: |-
+                                    Username is a username used to authenticate using the [UserPass
+                                    authentication method]
+
+                                    [UserPass authentication method]: https://openbao.org/docs/auth/userpass/
+                                  type: string
+                              required:
+                                - path
+                                - username
+                              type: object
                           type: object
                         caBundle:
                           description: |-

+ 74 - 0
docs/api/spec.md

@@ -8332,6 +8332,20 @@ External Secrets meta/v1.SecretKeySelector
 <p>TokenSecretRef authenticates with OpenBao by presenting a token.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>userPass</code></br>
+<em>
+<a href="#external-secrets.io/v1.OpenBaoUserPassAuth">
+OpenBaoUserPassAuth
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>UserPass authenticates with OpenBao by passing a username/password pair</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1.OpenBaoKVStoreVersion">OpenBaoKVStoreVersion
@@ -8458,6 +8472,66 @@ OpenBaoKVStoreVersion
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1.OpenBaoUserPassAuth">OpenBaoUserPassAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.OpenBaoAuth">OpenBaoAuth</a>)
+</p>
+<p>
+<p>OpenBaoUserPassAuth authenticates with OpenBao using <a href="https://openbao.org/docs/auth/userpass/">UserPass authentication
+method</a>, with the username and password stored in a Kubernetes Secret
+resource.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>path</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Path where the UserPassword authentication backend is mounted
+in OpenBao, e.g: &ldquo;userpass&rdquo;</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>username</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Username is a username used to authenticate using the <a href="https://openbao.org/docs/auth/userpass/">UserPass
+authentication method</a></p>
+</td>
+</tr>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<p>SecretRef to a key in a Secret resource containing password for the user
+used to authenticate with OpenBao using the <a href="https://openbao.org/docs/auth/userpass/">UserPass authentication
+method</a></p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1.OracleAuth">OracleAuth
 </h3>
 <p>

+ 1 - 0
go.mod

@@ -332,6 +332,7 @@ require (
 	github.com/nebius/gosdk v0.0.0-20260204094009-511fd4d4f7a1 // indirect
 	github.com/ngrok/ngrok-api-go/v9 v9.0.0 // indirect
 	github.com/oapi-codegen/runtime v1.1.2 // indirect
+	github.com/openbao/openbao/api/auth/userpass/v2 v2.5.1 // indirect
 	github.com/openbao/openbao/api/v2 v2.5.1-0.20260603121413-a08669ff09ec // indirect
 	github.com/opentracing/basictracer-go v1.1.0 // indirect
 	github.com/ovh/okms-sdk-go v0.5.1 // indirect

+ 2 - 0
go.sum

@@ -944,6 +944,8 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4
 github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
 github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/openbao/openbao/api/auth/userpass/v2 v2.5.1 h1:81YQNOT/0wZJtp6zka9KAdUSADShkhocgb35CJH7r28=
+github.com/openbao/openbao/api/auth/userpass/v2 v2.5.1/go.mod h1:uOtBhWrhgDf++LLD+XtdxKzDFv8cHUurnFz6kRw0+nE=
 github.com/openbao/openbao/api/v2 v2.5.1-0.20260603121413-a08669ff09ec h1:Cka9sTUAqBQBtTYSsOvkG99ojxUp1nlhixAbYl1wRYA=
 github.com/openbao/openbao/api/v2 v2.5.1-0.20260603121413-a08669ff09ec/go.mod h1:FyY+uFxQHGVlkGZilVrWaxJNk39gH3CRqA+g2/0LvU8=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=

+ 58 - 0
providers/v1/openbao/DEV.md

@@ -0,0 +1,58 @@
+# OpenBao Provider Development
+
+## API
+
+When implementing new functionality, please check the Vault provider first. If
+the same functionality is implemented in the Vault provider: Use the same API if
+there is no good reason to deviate.
+
+## Testing Strategy
+
+Much of the provider is tested using recorded HTTP traffic from interactions
+with a real OpenBao server (stored in `testdata/http/<TestName>.yaml`). This
+gives us very realistic tests, which still run in a few milliseconds.
+
+### Re-recording Traffic
+
+To re-record the traffic, run:
+
+```bash
+ESO_PROVIDER_OPENBAO_RERECORD=true go test .
+```
+
+This will:
+
+- delete all previous recordings
+- start an OpenBao Dev Server (requires `bao` to be on your `PATH`)
+- run the tests while proxying all requests to OpenBao
+- store the HTTP traffic
+- stop the Dev Server
+
+Before storing the HTTP traffic some cleanup is applied (see `getRecorder`),
+this replaces values that are random (e.g. OpenBao "mount accessors") or
+timestamp based (e.g. creation timestamps) with predictable values. While this
+is not technically necessary and adds some complexity to the tests, it greatly
+improves the readability of the `git diff`. When you have to rerecord the
+traffic, it is recommended to:
+
+1. run with `ESO_PROVIDER_OPENBAO_RERECORD=true`
+1. `git add` the recordings
+1. run again with `ESO_PROVIDER_OPENBAO_RERECORD=true`, if there are changes in
+   the recording files, tweak the cleanup logic and go to step 1.
+1. run again without `ESO_PROVIDER_OPENBAO_RERECORD=true` and see if the tests
+   still pass.
+
+### Limits of Traffic Recording/Replay
+
+While this strategy is good for testing the CRUD operations on secrets, it will
+not work very well for many Authentication methods, which often work with random
+strings and might even have explicit replay protections, therefore:
+
+- We will only apply the HTTP recording based tests to the UserPass auth method,
+  which is rather static.
+- For other auth methods, we will only validate that the OpenBao client has been
+  configured as expected.
+
+With this setup, we can still run extensive tests on our logic (e.g. token
+caching) against the UserPass auth method and relying on the OpenBao client to
+call whatever authentication method we have configured.

+ 22 - 1
providers/v1/openbao/client.go

@@ -27,6 +27,7 @@ import (
 	"strconv"
 	"time"
 
+	"github.com/openbao/openbao/api/auth/userpass/v2"
 	"github.com/openbao/openbao/api/v2"
 	v1 "k8s.io/api/core/v1"
 	k8sClient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -106,13 +107,33 @@ func (c *client) setupAuth(ctx context.Context, kube k8sClient.Client, namespace
 		return nil
 	}
 
-	if c.store.Auth.TokenSecretRef != nil {
+	switch {
+	case c.store.Auth.TokenSecretRef != nil:
 		token, err := resolvers.SecretKeyRef(ctx, kube, c.storeKind, namespace, c.store.Auth.TokenSecretRef)
 		if err != nil {
 			return err
 		}
 
 		c.client.SetToken(token)
+
+	case c.store.Auth.UserPass != nil:
+		userPass := c.store.Auth.UserPass
+		password, err := resolvers.SecretKeyRef(ctx, kube, c.storeKind, namespace, &userPass.SecretRef)
+		if err != nil {
+			return err
+		}
+
+		auth, err := userpass.NewUserpassAuth(userPass.Username, &userpass.Password{
+			FromString: password,
+		}, userpass.WithMountPath(userPass.Path))
+		if err != nil {
+			return err
+		}
+
+		_, err = c.client.Auth().Login(ctx, auth)
+		if err != nil {
+			return err
+		}
 	}
 
 	return nil

+ 1 - 0
providers/v1/openbao/go.mod

@@ -7,6 +7,7 @@ require (
 	github.com/external-secrets/external-secrets/runtime v0.0.0-00010101000000-000000000000
 	github.com/go-viper/mapstructure/v2 v2.5.0
 	github.com/onsi/gomega v1.39.1
+	github.com/openbao/openbao/api/auth/userpass/v2 v2.5.1
 	github.com/openbao/openbao/api/v2 v2.5.1-0.20260603121413-a08669ff09ec
 	gopkg.in/dnaeon/go-vcr.v4 v4.0.6
 	k8s.io/api v0.35.2

+ 2 - 0
providers/v1/openbao/go.sum

@@ -162,6 +162,8 @@ github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc
 github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
 github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
 github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
+github.com/openbao/openbao/api/auth/userpass/v2 v2.5.1 h1:81YQNOT/0wZJtp6zka9KAdUSADShkhocgb35CJH7r28=
+github.com/openbao/openbao/api/auth/userpass/v2 v2.5.1/go.mod h1:uOtBhWrhgDf++LLD+XtdxKzDFv8cHUurnFz6kRw0+nE=
 github.com/openbao/openbao/api/v2 v2.5.1-0.20260603121413-a08669ff09ec h1:Cka9sTUAqBQBtTYSsOvkG99ojxUp1nlhixAbYl1wRYA=
 github.com/openbao/openbao/api/v2 v2.5.1-0.20260603121413-a08669ff09ec/go.mod h1:FyY+uFxQHGVlkGZilVrWaxJNk39gH3CRqA+g2/0LvU8=
 github.com/oracle/oci-go-sdk/v65 v65.103.0 h1:HfyZx+JefCPK3At0Xt45q+wr914jDXuoyzOFX3XCbno=

+ 4 - 0
providers/v1/openbao/provider.go

@@ -70,6 +70,10 @@ func isReferentSpec(prov *esv1.OpenBaoProvider) bool {
 		if auth.TokenSecretRef != nil && auth.TokenSecretRef.Namespace == nil {
 			return true
 		}
+
+		if auth.UserPass != nil && auth.UserPass.SecretRef.Namespace == nil {
+			return true
+		}
 	}
 
 	if prov.CAProvider != nil && prov.CAProvider.Namespace == nil {

+ 73 - 16
providers/v1/openbao/provider_test.go

@@ -46,11 +46,14 @@ import (
 )
 
 const recordDir = "testdata/http"
+const fakeToken = "s.fakeTOKEN123"
 
 var (
-	requestIdReg = regexp.MustCompile(`id":"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}"`)
-	timeReg      = regexp.MustCompile(`_time":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z"`)
-	accessorReg  = regexp.MustCompile(`accessor":"([a-z]+)_[0-9a-f]+"`)
+	requestIdReg     = regexp.MustCompile(`id":"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}"`)
+	timeReg          = regexp.MustCompile(`_time":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z"`)
+	namedAccessorReg = regexp.MustCompile(`accessor":"([a-z]+)_[0-9a-f]+"`)
+	accessorReg      = regexp.MustCompile(`accessor":"([A-Za-z0-9]+)"`)
+	tokenReg         = regexp.MustCompile(`client_token":"s\.([A-Za-z0-9]+)"`)
 )
 
 func getRecorder(t *testing.T) *recorder.Recorder {
@@ -60,7 +63,14 @@ func getRecorder(t *testing.T) *recorder.Recorder {
 		i.Response.Duration = 0
 		i.Response.Body = requestIdReg.ReplaceAllString(i.Response.Body, `id":"00000000-0000-0000-0000-000000000000"`)
 		i.Response.Body = timeReg.ReplaceAllString(i.Response.Body, `_time":"2099-09-09T09:09:09.09Z"`)
-		i.Response.Body = accessorReg.ReplaceAllString(i.Response.Body, `accessor":"${1}_01234567"`)
+		i.Response.Body = namedAccessorReg.ReplaceAllString(i.Response.Body, `accessor":"${1}_01234567"`)
+		i.Response.Body = accessorReg.ReplaceAllString(i.Response.Body, `accessor":"AbCdEfGHiJk123"`)
+		i.Response.Body = tokenReg.ReplaceAllString(i.Response.Body, `client_token":"`+fakeToken+`"`)
+
+		token := i.Request.Headers.Get("X-Vault-Token")
+		if token != "" && token != "root" {
+			i.Request.Headers.Set("X-Vault-Token", fakeToken)
+		}
 
 		var body map[string]any
 		err := json.Unmarshal([]byte(i.Response.Body), &body)
@@ -347,10 +357,57 @@ func TestProvider_KVv1(t *testing.T) {
 	})
 }
 
+func TestProvider_Auth_UserPass(t *testing.T) {
+	RegisterTestingT(t)
+	kube, provider := setupProvider(t, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "password-of-alice",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"pw": []byte("bob4ever"),
+		},
+	})
+
+	store := makeValidSecretStoreWithVersion(esv1.OpenBaoKVStoreV2)
+	store.Spec.Provider.OpenBao.Auth = &esv1.OpenBaoAuth{
+		UserPass: &esv1.OpenBaoUserPassAuth{
+			Path:     "customuserpasspath",
+			Username: "alice",
+			SecretRef: esmeta.SecretKeySelector{
+				Name: "password-of-alice",
+				Key:  "pw",
+			},
+		},
+	}
+
+	client, err := provider.NewClient(t.Context(), store, kube, "default")
+	Expect(err).NotTo(HaveOccurred())
+	Expect(client).NotTo(BeNil())
+	t.Cleanup(func() {
+		client.Close(t.Context())
+	})
+
+	data, err := client.GetSecret(t.Context(), esv1.ExternalSecretDataRemoteRef{
+		Key:      "foo",
+		Property: "bar",
+	})
+	Expect(err).NotTo(HaveOccurred())
+	Expect(data).To(BeEquivalentTo("bazz"))
+}
+
 func TestProvider_Validate(t *testing.T) {
 	RegisterTestingT(t)
 
-	kube, provider := setupProvider(t)
+	kube, provider := setupProvider(t, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "bao-token",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("root"),
+		},
+	})
 
 	store := makeValidSecretStoreWithVersion(esv1.OpenBaoKVStoreV1)
 	client, err := provider.NewClient(t.Context(), store, kube, "default")
@@ -498,7 +555,15 @@ func TestProvider_CustomCA(t *testing.T) {
 }
 
 func setupClient(t *testing.T, v esv1.OpenBaoKVStoreVersion) esv1.SecretsClient {
-	kube, provider := setupProvider(t)
+	kube, provider := setupProvider(t, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "bao-token",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("root"),
+		},
+	})
 
 	client, err := provider.NewClient(t.Context(), makeValidSecretStoreWithVersion(v), kube, "default")
 	Expect(err).NotTo(HaveOccurred())
@@ -509,18 +574,10 @@ func setupClient(t *testing.T, v esv1.OpenBaoKVStoreVersion) esv1.SecretsClient
 	return client
 }
 
-func setupProvider(t *testing.T) (client.WithWatch, *openbao.Provider) {
+func setupProvider(t *testing.T, objects ...client.Object) (client.WithWatch, *openbao.Provider) {
 	r := getRecorder(t)
 
-	kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "bao-token",
-			Namespace: "default",
-		},
-		Data: map[string][]byte{
-			"token": []byte("root"),
-		},
-	}).Build()
+	kube := clientfake.NewClientBuilder().WithObjects(objects...).Build()
 
 	provider := openbao.NewProvider().(*openbao.Provider)
 	provider.HTTPClientFactory = r.GetDefaultClient

+ 119 - 0
providers/v1/openbao/testdata/http/TestProvider_Auth_UserPass.yaml

@@ -0,0 +1,119 @@
+---
+version: 2
+interactions:
+    - id: 0
+      request:
+        proto: HTTP/1.1
+        proto_major: 1
+        proto_minor: 1
+        content_length: 23
+        host: localhost:8200
+        body: '{"password":"bob4ever"}'
+        headers:
+            X-Vault-Request:
+                - "true"
+        url: http://localhost:8200/v1/auth/customuserpasspath/login/alice
+        method: PUT
+      response:
+        proto: HTTP/1.1
+        proto_major: 1
+        proto_minor: 1
+        content_length: 511
+        body: |-
+            {
+              "auth": {
+                "accessor": "AbCdEfGHiJk123",
+                "client_token": "s.fakeTOKEN123",
+                "entity_id": "00000000-0000-0000-0000-000000000000",
+                "lease_duration": 2764800,
+                "metadata": {
+                  "username": "alice"
+                },
+                "mfa_requirement": null,
+                "num_uses": 0,
+                "orphan": true,
+                "policies": [
+                  "default",
+                  "read-kv"
+                ],
+                "renewable": true,
+                "token_policies": [
+                  "default",
+                  "read-kv"
+                ],
+                "token_type": "service"
+              },
+              "data": null,
+              "lease_duration": 0,
+              "lease_id": "",
+              "renewable": false,
+              "request_id": "00000000-0000-0000-0000-000000000000",
+              "warnings": null,
+              "wrap_info": null
+            }
+        headers:
+            Cache-Control:
+                - no-store
+            Content-Length:
+                - "511"
+            Content-Type:
+                - application/json
+            Strict-Transport-Security:
+                - max-age=31536000; includeSubDomains
+        status: 200 OK
+        code: 200
+        duration: 0s
+    - id: 1
+      request:
+        proto: HTTP/1.1
+        proto_major: 1
+        proto_minor: 1
+        content_length: 0
+        host: localhost:8200
+        headers:
+            X-Vault-Request:
+                - "true"
+            X-Vault-Token:
+                - s.fakeTOKEN123
+        url: http://localhost:8200/v1/secret/data/foo
+        method: GET
+      response:
+        proto: HTTP/1.1
+        proto_major: 1
+        proto_minor: 1
+        content_length: 330
+        body: |-
+            {
+              "auth": null,
+              "data": {
+                "data": {
+                  "bar": "bazz",
+                  "lorem": "ipsum"
+                },
+                "metadata": {
+                  "created_time": "2099-09-09T09:09:09.09Z",
+                  "custom_metadata": null,
+                  "deletion_time": "",
+                  "destroyed": false,
+                  "version": 2
+                }
+              },
+              "lease_duration": 0,
+              "lease_id": "",
+              "renewable": false,
+              "request_id": "00000000-0000-0000-0000-000000000000",
+              "warnings": null,
+              "wrap_info": null
+            }
+        headers:
+            Cache-Control:
+                - no-store
+            Content-Length:
+                - "330"
+            Content-Type:
+                - application/json
+            Strict-Transport-Security:
+                - max-age=31536000; includeSubDomains
+        status: 200 OK
+        code: 200
+        duration: 0s

+ 6 - 0
providers/v1/openbao/testdata/init-bao.sh

@@ -1,5 +1,7 @@
 #!/usr/bin/env bash
 
+set -euo pipefail
+
 export BAO_TOKEN='root'
 export BAO_ADDR='http://localhost:8200'
 
@@ -8,3 +10,7 @@ bao kv put -mount=secret foo bar=bazz lorem=ipsum
 
 bao secrets enable -version=1 -path=secret_v1 kv
 bao kv put -mount=secret_v1 foo bar=bazz_v1 lorem=ipsum_v1
+
+bao policy write read-kv testdata/policy-read-kv.hcl
+bao auth enable --path=customuserpasspath userpass
+bao write auth/customuserpasspath/users/alice password=bob4ever token_policies=read-kv

+ 3 - 0
providers/v1/openbao/testdata/policy-read-kv.hcl

@@ -0,0 +1,3 @@
+path "secret/*" {
+  capabilities = ["read"]
+}

+ 14 - 7
providers/v1/openbao/validate.go

@@ -27,11 +27,12 @@ import (
 )
 
 const (
-	errInvalidStore       = "invalid store"
-	errInvalidStoreSpec   = "invalid store spec"
-	errInvalidStoreProv   = "invalid store provider"
-	errInvalidOpenBaoProv = "invalid OpenBao provider"
-	errInvalidTokenRef    = "invalid Auth.TokenSecretRef: %w"
+	errInvalidStore             = "invalid store"
+	errInvalidStoreSpec         = "invalid store spec"
+	errInvalidStoreProv         = "invalid store provider"
+	errInvalidOpenBaoProv       = "invalid OpenBao provider"
+	errInvalidTokenRef          = "invalid Auth.TokenSecretRef: %w"
+	errInvalidUserPassSecretRef = "invalid Auth.UserPass.SecretRef: %w"
 )
 
 // ValidateStore validates the OpenBao provider configuration in the SecretStore.
@@ -51,11 +52,17 @@ func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, e
 		return nil, errors.New(errInvalidOpenBaoProv)
 	}
 	if baoProvider.Auth != nil {
-		if baoProvider.Auth.TokenSecretRef != nil {
-			if err := esutils.ValidateReferentSecretSelector(store, *baoProvider.Auth.TokenSecretRef); err != nil {
+		auth := baoProvider.Auth
+		if auth.TokenSecretRef != nil {
+			if err := esutils.ValidateReferentSecretSelector(store, *auth.TokenSecretRef); err != nil {
 				return nil, fmt.Errorf(errInvalidTokenRef, err)
 			}
 		}
+		if auth.UserPass != nil {
+			if err := esutils.ValidateReferentSecretSelector(store, auth.UserPass.SecretRef); err != nil {
+				return nil, fmt.Errorf(errInvalidUserPassSecretRef, err)
+			}
+		}
 	}
 
 	return nil, nil

+ 7 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -623,6 +623,13 @@ spec:
           key: string
           name: string
           namespace: string
+        userPass:
+          path: "userpass"
+          secretRef:
+            key: string
+            name: string
+            namespace: string
+          username: string
       caBundle: c3RyaW5n
       caProvider:
         key: string

+ 7 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -623,6 +623,13 @@ spec:
           key: string
           name: string
           namespace: string
+        userPass:
+          path: "userpass"
+          secretRef:
+            key: string
+            name: string
+            namespace: string
+          username: string
       caBundle: c3RyaW5n
       caProvider:
         key: string