Browse Source

feat(provider): add devolutions server provider support (#5712)

Co-authored-by: Gergely Bräutigam <skarlso777@gmail.com>
Richard Boisvert 3 months ago
parent
commit
bdfa94e92a

+ 56 - 0
apis/externalsecrets/v1/secretstore_dvls_types.go

@@ -0,0 +1,56 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+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
+
+    https://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 v1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// DVLSProvider configures a store to sync secrets using Devolutions Server.
+type DVLSProvider struct {
+	// ServerURL is the DVLS instance URL (e.g., https://dvls.example.com).
+	// +kubebuilder:validation:Required
+	ServerURL string `json:"serverUrl"`
+
+	// Insecure allows connecting to DVLS over plain HTTP.
+	// This is NOT RECOMMENDED for production use.
+	// Set to true only if you understand the security implications.
+	// +optional
+	Insecure bool `json:"insecure,omitempty"`
+
+	// Auth defines the authentication method to use.
+	// +kubebuilder:validation:Required
+	Auth DVLSAuth `json:"auth"`
+}
+
+// DVLSAuth defines the authentication method for the DVLS provider.
+type DVLSAuth struct {
+	// SecretRef contains the Application ID and Application Secret for authentication.
+	// +kubebuilder:validation:Required
+	SecretRef DVLSAuthSecretRef `json:"secretRef"`
+}
+
+// DVLSAuthSecretRef defines the secret references for DVLS authentication credentials.
+type DVLSAuthSecretRef struct {
+	// AppID is the reference to the secret containing the Application ID.
+	// +kubebuilder:validation:Required
+	AppID esmeta.SecretKeySelector `json:"appId"`
+
+	// AppSecret is the reference to the secret containing the Application Secret.
+	// +kubebuilder:validation:Required
+	AppSecret esmeta.SecretKeySelector `json:"appSecret"`
+}

+ 4 - 0
apis/externalsecrets/v1/secretstore_types.go

@@ -200,6 +200,10 @@ type SecretStoreProvider struct {
 	// +optional
 	Device42 *Device42Provider `json:"device42,omitempty"`
 
+	// DVLS configures this store to sync secrets using Devolutions Server provider
+	// +optional
+	DVLS *DVLSProvider `json:"dvls,omitempty"`
+
 	// Infisical configures this store to sync secrets using the Infisical provider
 	// +optional
 	Infisical *InfisicalProvider `json:"infisical,omitempty"`

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

@@ -1277,6 +1277,55 @@ func (in *ConjurProvider) DeepCopy() *ConjurProvider {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DVLSAuth) DeepCopyInto(out *DVLSAuth) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DVLSAuth.
+func (in *DVLSAuth) DeepCopy() *DVLSAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(DVLSAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DVLSAuthSecretRef) DeepCopyInto(out *DVLSAuthSecretRef) {
+	*out = *in
+	in.AppID.DeepCopyInto(&out.AppID)
+	in.AppSecret.DeepCopyInto(&out.AppSecret)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DVLSAuthSecretRef.
+func (in *DVLSAuthSecretRef) DeepCopy() *DVLSAuthSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(DVLSAuthSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DVLSProvider) DeepCopyInto(out *DVLSProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DVLSProvider.
+func (in *DVLSProvider) DeepCopy() *DVLSProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(DVLSProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *DelineaProvider) DeepCopyInto(out *DelineaProvider) {
 	*out = *in
 	if in.ClientID != nil {
@@ -3525,6 +3574,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(Device42Provider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.DVLS != nil {
+		in, out := &in.DVLS, &out.DVLS
+		*out = new(DVLSProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Infisical != nil {
 		in, out := &in.Infisical, &out.Infisical
 		*out = new(InfisicalProvider)

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

@@ -1955,6 +1955,93 @@ spec:
                     required:
                     - auth
                     type: object
+                  dvls:
+                    description: DVLS configures this store to sync secrets using
+                      Devolutions Server provider
+                    properties:
+                      auth:
+                        description: Auth defines the authentication method to use.
+                        properties:
+                          secretRef:
+                            description: SecretRef contains the Application ID and
+                              Application Secret for authentication.
+                            properties:
+                              appId:
+                                description: AppID is the reference to the secret
+                                  containing the Application ID.
+                                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
+                              appSecret:
+                                description: AppSecret is the reference to the secret
+                                  containing the Application Secret.
+                                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
+                            required:
+                            - appId
+                            - appSecret
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      insecure:
+                        description: |-
+                          Insecure allows connecting to DVLS over plain HTTP.
+                          This is NOT RECOMMENDED for production use.
+                          Set to true only if you understand the security implications.
+                        type: boolean
+                      serverUrl:
+                        description: ServerURL is the DVLS instance URL (e.g., https://dvls.example.com).
+                        type: string
+                    required:
+                    - auth
+                    - serverUrl
+                    type: object
                   fake:
                     description: Fake configures a store with static key/value pairs
                     properties:

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

@@ -1955,6 +1955,93 @@ spec:
                     required:
                     - auth
                     type: object
+                  dvls:
+                    description: DVLS configures this store to sync secrets using
+                      Devolutions Server provider
+                    properties:
+                      auth:
+                        description: Auth defines the authentication method to use.
+                        properties:
+                          secretRef:
+                            description: SecretRef contains the Application ID and
+                              Application Secret for authentication.
+                            properties:
+                              appId:
+                                description: AppID is the reference to the secret
+                                  containing the Application ID.
+                                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
+                              appSecret:
+                                description: AppSecret is the reference to the secret
+                                  containing the Application Secret.
+                                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
+                            required:
+                            - appId
+                            - appSecret
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      insecure:
+                        description: |-
+                          Insecure allows connecting to DVLS over plain HTTP.
+                          This is NOT RECOMMENDED for production use.
+                          Set to true only if you understand the security implications.
+                        type: boolean
+                      serverUrl:
+                        description: ServerURL is the DVLS instance URL (e.g., https://dvls.example.com).
+                        type: string
+                    required:
+                    - auth
+                    - serverUrl
+                    type: object
                   fake:
                     description: Fake configures a store with static key/value pairs
                     properties:

+ 162 - 0
deploy/crds/bundle.yaml

@@ -3910,6 +3910,87 @@ spec:
                       required:
                         - auth
                       type: object
+                    dvls:
+                      description: DVLS configures this store to sync secrets using Devolutions Server provider
+                      properties:
+                        auth:
+                          description: Auth defines the authentication method to use.
+                          properties:
+                            secretRef:
+                              description: SecretRef contains the Application ID and Application Secret for authentication.
+                              properties:
+                                appId:
+                                  description: AppID is the reference to the secret containing the Application ID.
+                                  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
+                                appSecret:
+                                  description: AppSecret is the reference to the secret containing the Application Secret.
+                                  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
+                              required:
+                                - appId
+                                - appSecret
+                              type: object
+                          required:
+                            - secretRef
+                          type: object
+                        insecure:
+                          description: |-
+                            Insecure allows connecting to DVLS over plain HTTP.
+                            This is NOT RECOMMENDED for production use.
+                            Set to true only if you understand the security implications.
+                          type: boolean
+                        serverUrl:
+                          description: ServerURL is the DVLS instance URL (e.g., https://dvls.example.com).
+                          type: string
+                      required:
+                        - auth
+                        - serverUrl
+                      type: object
                     fake:
                       description: Fake configures a store with static key/value pairs
                       properties:
@@ -15598,6 +15679,87 @@ spec:
                       required:
                         - auth
                       type: object
+                    dvls:
+                      description: DVLS configures this store to sync secrets using Devolutions Server provider
+                      properties:
+                        auth:
+                          description: Auth defines the authentication method to use.
+                          properties:
+                            secretRef:
+                              description: SecretRef contains the Application ID and Application Secret for authentication.
+                              properties:
+                                appId:
+                                  description: AppID is the reference to the secret containing the Application ID.
+                                  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
+                                appSecret:
+                                  description: AppSecret is the reference to the secret containing the Application Secret.
+                                  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
+                              required:
+                                - appId
+                                - appSecret
+                              type: object
+                          required:
+                            - secretRef
+                          type: object
+                        insecure:
+                          description: |-
+                            Insecure allows connecting to DVLS over plain HTTP.
+                            This is NOT RECOMMENDED for production use.
+                            Set to true only if you understand the security implications.
+                          type: boolean
+                        serverUrl:
+                          description: ServerURL is the DVLS instance URL (e.g., https://dvls.example.com).
+                          type: string
+                      required:
+                        - auth
+                        - serverUrl
+                      type: object
                     fake:
                       description: Fake configures a store with static key/value pairs
                       properties:

+ 148 - 0
docs/api/spec.md

@@ -3314,6 +3314,140 @@ ConjurAuth
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1.DVLSAuth">DVLSAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.DVLSProvider">DVLSProvider</a>)
+</p>
+<p>
+<p>DVLSAuth defines the authentication method for the DVLS provider.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+<a href="#external-secrets.io/v1.DVLSAuthSecretRef">
+DVLSAuthSecretRef
+</a>
+</em>
+</td>
+<td>
+<p>SecretRef contains the Application ID and Application Secret for authentication.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.DVLSAuthSecretRef">DVLSAuthSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.DVLSAuth">DVLSAuth</a>)
+</p>
+<p>
+<p>DVLSAuthSecretRef defines the secret references for DVLS authentication credentials.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>appId</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>AppID is the reference to the secret containing the Application ID.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>appSecret</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>AppSecret is the reference to the secret containing the Application Secret.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.DVLSProvider">DVLSProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>DVLSProvider configures a store to sync secrets using Devolutions Server.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>serverUrl</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>ServerURL is the DVLS instance URL (e.g., <a href="https://dvls.example.com">https://dvls.example.com</a>).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>insecure</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Insecure allows connecting to DVLS over plain HTTP.
+This is NOT RECOMMENDED for production use.
+Set to true only if you understand the security implications.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1.DVLSAuth">
+DVLSAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth defines the authentication method to use.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1.DelineaProvider">DelineaProvider
 </h3>
 <p>
@@ -9575,6 +9709,20 @@ Device42Provider
 </tr>
 <tr>
 <td>
+<code>dvls</code></br>
+<em>
+<a href="#external-secrets.io/v1.DVLSProvider">
+DVLSProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>DVLS configures this store to sync secrets using Devolutions Server provider</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>infisical</code></br>
 <em>
 <a href="#external-secrets.io/v1.InfisicalProvider">

+ 2 - 0
docs/introduction/stability-support.md

@@ -97,6 +97,7 @@ The following table describes the stability level of each provider and who's res
 | [Volcengine](https://external-secrets.io/latest/provider/volcengine)                                       | alpha     | [@kevinyancn](https://github.com/kevinyancn)                                                        |
 | [ngrok](https://external-secrets.io/latest/provider/ngrok)                                                 | alpha     | [@jonstacks](https://github.com/jonstacks)                                                          |
 | [Barbican](https://external-secrets.io/latest/provider/barbican)                                           | alpha     | [@rkferreira](https://github.com/rkferreira)                                                        |
+| [Devolutions Server](https://external-secrets.io/latest/provider/devolutions-server)                       | alpha     | [@rbstp](https://github.com/rbstp)                                                                  |
 
 
 ## Provider Feature Support
@@ -138,6 +139,7 @@ The following table show the support for features across different providers.
 | Volcengine                |              |              |                      |                         |        x         |             |                             |
 | ngrok                     |              |              |                      |                         |        x         |      x      |                             |
 | Barbican                  |      x       |              |                      |                         |        x         |             |                             |
+| Devolutions Server        |              |              |                      |                         |        x         |      x      |                             |
 
 ## Support Policy
 

+ 120 - 0
docs/provider/devolutions-server.md

@@ -0,0 +1,120 @@
+## Devolutions Server (DVLS)
+
+External Secrets Operator integrates with [Devolutions Server](https://devolutions.net/server/) (DVLS) for secret management.
+
+DVLS is a self-hosted privileged access management solution that provides secure password management, role-based access control, and credential injection for teams and enterprises.
+
+## Authentication
+
+DVLS authentication uses Application ID and Application Secret credentials.
+
+### Creating an Application in DVLS
+
+1. Log into your DVLS web interface
+2. Navigate to **Administration > Applications identities**
+3. Click **+ (Add)** to create a new application
+4. Configure the application with appropriate permissions to access the vaults and entries you need
+5. Save the **Application ID** and **Application Secret**
+
+### Creating the Kubernetes Secret
+
+Create a Kubernetes secret containing your DVLS credentials:
+
+```bash
+kubectl create secret generic dvls-credentials \
+  --from-literal=app-id="your-application-id" \
+  --from-literal=app-secret="your-application-secret"
+```
+
+### Creating a SecretStore
+
+```yaml
+{% include 'dvls-secret-store.yaml' %}
+```
+
+| Field | Description |
+|-------|-------------|
+| `serverUrl` | The URL of your DVLS instance (e.g., `https://dvls.example.com`) |
+| `insecure` | (Optional) Set to `true` to allow plain HTTP connections. **Not recommended for production.** |
+| `auth.secretRef.appId` | Reference to the secret containing the Application ID |
+| `auth.secretRef.appSecret` | Reference to the secret containing the Application Secret |
+
+**NOTE:** For `ClusterSecretStore`, ensure you specify the `namespace` in the secret references.
+
+## Referencing Secrets
+
+Secrets are referenced using the format: `<vault-id>/<entry-id>`
+
+- **vault-id**: The UUID of the vault containing the entry
+- **entry-id**: The UUID of the credential entry
+
+You can find these UUIDs in the DVLS web interface by viewing the entry properties.
+
+## Supported Credential Types
+
+DVLS supports multiple credential types. The provider maps each type to specific properties:
+
+| Credential Type | DVLS Entry Type | Available Properties |
+|-----------------|-----------------|---------------------|
+| **Default** | Credential | `username`, `password`, `domain` |
+| **Access Code** | Secret | `password` |
+| **API Key** | Credential | `api-id`, `api-key`, `tenant-id` |
+| **Azure Service Principal** | Credential | `client-id`, `client-secret`, `tenant-id` |
+| **Connection String** | Credential | `connection-string` |
+| **Private Key** | Credential | `username`, `password`, `private-key`, `public-key`, `passphrase` |
+
+All entries also include `entry-id` and `entry-name` metadata properties.
+
+**Note:** When no `property` is specified, the `password` field is returned by default.
+
+**Note:** In the DVLS web interface, "Secret" entries appear as a distinct entry type and are mapped to the Access Code credential subtype internally.
+
+## Examples
+
+### Fetching Individual Properties
+
+To fetch specific properties from a credential entry:
+
+```yaml
+{% include 'dvls-external-secret.yaml' %}
+```
+
+### Using dataFrom to Extract All Fields
+
+When using `dataFrom.extract`, all available properties from the credential entry will be synced to the Kubernetes secret.
+
+## Push Secrets
+
+The DVLS provider supports pushing secrets back to DVLS:
+
+```yaml
+{% include 'dvls-push-secret.yaml' %}
+```
+
+**Note:** Push secret updates an existing entry's password field. The entry must already exist in DVLS.
+
+## Limitations
+
+- **GetAllSecrets**: The `find` operation for discovering secrets is not currently supported
+- **Custom CA Certificates**: Custom TLS certificates for self-signed DVLS instances are not yet supported. Use the `SSL_CERT_FILE` environment variable as a workaround
+- **Name-based lookups**: Currently only UUID-based references (`vault-id/entry-id`) are supported. Path/name-based lookups are planned for future releases
+- **Certificate entries**: Certificate entry types (`Document/Certificate`) are not currently supported. Only Credential entries are supported
+
+## Troubleshooting
+
+### Authentication Errors
+
+If you receive authentication errors:
+
+1. Verify the Application ID and Secret are correct
+2. Ensure the application has the necessary permissions in DVLS
+3. Check that the DVLS server URL is accessible from your Kubernetes cluster
+
+### Entry Not Found
+
+If an entry cannot be found:
+
+1. Verify the vault UUID and entry UUID are correct
+2. Ensure the application has at least read access to the vault
+3. Check that the entry exists and is a Credential or Secret type entry
+4. Ensure the application has at least read, view password, and connect (execute) permissions on the entry.

+ 59 - 0
docs/snippets/dvls-external-secret.yaml

@@ -0,0 +1,59 @@
+---
+# Fetch a single property from a credential entry
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: database-credentials
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: dvls-store
+  target:
+    name: database-secret
+    creationPolicy: Owner
+  data:
+    - secretKey: username
+      remoteRef:
+        key: 'vault-uuid/entry-uuid'
+        property: username
+    - secretKey: password
+      remoteRef:
+        key: 'vault-uuid/entry-uuid'
+        property: password
+---
+# Fetch all fields from a credential entry
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: api-credentials
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: dvls-store
+  target:
+    name: api-secret
+    creationPolicy: Owner
+  dataFrom:
+    - extract:
+        key: 'vault-uuid/entry-uuid'
+---
+# Fetch a Secret entry (Access Code type)
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: app-secret
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: dvls-store
+  target:
+    name: app-secret
+    creationPolicy: Owner
+  data:
+    - secretKey: secret
+      remoteRef:
+        key: 'vault-uuid/secret-entry-uuid'
+        property: password

+ 17 - 0
docs/snippets/dvls-push-secret.yaml

@@ -0,0 +1,17 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: push-to-dvls
+spec:
+  refreshInterval: 1h
+  secretStoreRefs:
+    - name: dvls-store
+      kind: SecretStore
+  selector:
+    secret:
+      name: my-k8s-secret
+  data:
+    - match:
+        secretKey: password
+        remoteRef:
+          remoteKey: 'vault-uuid/entry-uuid'

+ 17 - 0
docs/snippets/dvls-secret-store.yaml

@@ -0,0 +1,17 @@
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: dvls-store
+  namespace: default
+spec:
+  provider:
+    dvls:
+      serverUrl: 'https://dvls.example.com'
+      auth:
+        secretRef:
+          appId:
+            name: dvls-credentials
+            key: app-id
+          appSecret:
+            name: dvls-credentials
+            key: app-secret

+ 11 - 8
go.mod

@@ -32,6 +32,7 @@ replace (
 	github.com/external-secrets/external-secrets/providers/v1/delinea => ./providers/v1/delinea
 	github.com/external-secrets/external-secrets/providers/v1/device42 => ./providers/v1/device42
 	github.com/external-secrets/external-secrets/providers/v1/doppler => ./providers/v1/doppler
+	github.com/external-secrets/external-secrets/providers/v1/dvls => ./providers/v1/dvls
 	github.com/external-secrets/external-secrets/providers/v1/fake => ./providers/v1/fake
 	github.com/external-secrets/external-secrets/providers/v1/fortanix => ./providers/v1/fortanix
 	github.com/external-secrets/external-secrets/providers/v1/gcp => ./providers/v1/gcp
@@ -97,7 +98,7 @@ require (
 	github.com/yandex-cloud/go-sdk v0.27.0 // indirect
 	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
 	go.uber.org/zap v1.27.0
-	golang.org/x/crypto v0.43.0 // indirect
+	golang.org/x/crypto v0.46.0 // indirect
 	golang.org/x/oauth2 v0.32.0 // indirect
 	google.golang.org/api v0.254.0 // indirect
 	google.golang.org/genproto v0.0.0-20251029180050-ab9386a59fda // indirect
@@ -145,6 +146,7 @@ require (
 	github.com/external-secrets/external-secrets/providers/v1/delinea v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/device42 v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/doppler v0.0.0-00010101000000-000000000000
+	github.com/external-secrets/external-secrets/providers/v1/dvls v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/fake v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/fortanix v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/gcp v0.0.0-20251104073127-4d2c8fd13e10
@@ -193,6 +195,7 @@ require (
 	github.com/BurntSushi/toml v1.5.0 // indirect
 	github.com/DelineaXPM/dsv-sdk-go/v2 v2.2.0 // indirect
 	github.com/DelineaXPM/tss-sdk-go/v3 v3.0.1 // indirect
+	github.com/Devolutions/go-dvls v0.15.0 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
 	github.com/Onboardbase/go-cryptojs-aes-decrypt v0.0.0-20230430095000-27c0d3a9016d // indirect
 	github.com/ProtonMail/go-crypto v1.3.0 // indirect
@@ -375,7 +378,7 @@ require (
 	go.yaml.in/yaml/v2 v2.4.3 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
-	golang.org/x/sync v0.17.0 // indirect
+	golang.org/x/sync v0.19.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
@@ -473,13 +476,13 @@ require (
 	go.mongodb.org/mongo-driver v1.17.6 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/mod v0.29.0 // indirect
-	golang.org/x/net v0.46.0 // indirect
-	golang.org/x/sys v0.37.0 // indirect
-	golang.org/x/term v0.36.0 // indirect
-	golang.org/x/text v0.30.0 // indirect
+	golang.org/x/mod v0.30.0 // indirect
+	golang.org/x/net v0.47.0 // indirect
+	golang.org/x/sys v0.39.0 // indirect
+	golang.org/x/term v0.38.0 // indirect
+	golang.org/x/text v0.32.0 // indirect
 	golang.org/x/time v0.14.0
-	golang.org/x/tools v0.38.0 // indirect
+	golang.org/x/tools v0.39.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
 	google.golang.org/protobuf v1.36.10 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect

+ 18 - 16
go.sum

@@ -130,6 +130,8 @@ github.com/DelineaXPM/dsv-sdk-go/v2 v2.2.0 h1:62E66sDf+Hs1TChuu3R7d+0U5s7yV84QIO
 github.com/DelineaXPM/dsv-sdk-go/v2 v2.2.0/go.mod h1:58Pflli0BtqeF0VgluDSSVE5QlIfLOJvat0JSvo/d70=
 github.com/DelineaXPM/tss-sdk-go/v3 v3.0.1 h1:4JBJukbaTjv2gJogF3MxZkrt7i+ayRhM//FgdJTKJ3Q=
 github.com/DelineaXPM/tss-sdk-go/v3 v3.0.1/go.mod h1:VmyoHQ25FhSVHTI3/ptQNOviNEMfCy2ALAf/3E4Eqxg=
+github.com/Devolutions/go-dvls v0.15.0 h1:T/uUK0sKli7i9yxcZb9/Lia/uUwmLi1phryNkYEp5t4=
+github.com/Devolutions/go-dvls v0.15.0/go.mod h1:4O3lb/RK1P1cDwU5auVi7CM4gRER7EuwyLwMVuEZjgg=
 github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
 github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
 github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
@@ -1274,8 +1276,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
-golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
-golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1322,8 +1324,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
-golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
+golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1384,8 +1386,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
-golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
-golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1418,8 +1420,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
-golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1509,8 +1511,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1525,8 +1527,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
-golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
-golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
+golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
+golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1544,8 +1546,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
-golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1615,8 +1617,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
-golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
+golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
+golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
 golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
 golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
 golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -161,6 +161,7 @@ nav:
       - OpenBao: provider/openbao.md
       - Volcengine: provider/volcengine.md
       - ngrok: provider/ngrok.md
+      - Devolutions Server: provider/devolutions-server.md
   - Examples:
       - FluxCD: examples/gitops-using-fluxcd.md
       - Anchore Engine: examples/anchore-engine-credentials.md

+ 30 - 0
pkg/register/dvls.go

@@ -0,0 +1,30 @@
+//go:build dvls || all_providers
+
+/*
+Copyright © 2025 ESO Maintainer Team
+
+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
+
+	https://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 register provides explicit registration of all providers and generators.
+package register
+
+import (
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	dvls "github.com/external-secrets/external-secrets/providers/v1/dvls"
+)
+
+func init() {
+	// Register DVLS provider
+	esv1.Register(dvls.NewProvider(), dvls.ProviderSpec(), dvls.MaintenanceStatus())
+}

+ 53 - 0
providers/v1/dvls/auth.go

@@ -0,0 +1,53 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+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
+
+    https://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 dvls implements the external-secrets provider for Devolutions Server (DVLS).
+package dvls
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/Devolutions/go-dvls"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
+)
+
+// NewDVLSClient creates a new authenticated DVLS client.
+func NewDVLSClient(ctx context.Context, kube kclient.Client, storeKind, namespace string, provider *esv1.DVLSProvider) (credentialClient, error) {
+	if provider == nil {
+		return nil, fmt.Errorf("missing provider configuration")
+	}
+
+	appID, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &provider.Auth.SecretRef.AppID)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get appId: %w", err)
+	}
+
+	appSecret, err := resolvers.SecretKeyRef(ctx, kube, storeKind, namespace, &provider.Auth.SecretRef.AppSecret)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get appSecret: %w", err)
+	}
+
+	client, err := dvls.NewClient(appID, appSecret, provider.ServerURL)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create DVLS client: %w", err)
+	}
+
+	return &realCredentialClient{cred: client.Entries.Credential}, nil
+}

+ 125 - 0
providers/v1/dvls/auth_test.go

@@ -0,0 +1,125 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+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
+
+    https://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 dvls
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+func TestNewDVLSClient_CrossNamespaceSecurityConstraint(t *testing.T) {
+	otherNamespace := "other-namespace"
+
+	tests := []struct {
+		name        string
+		storeKind   string
+		namespace   string
+		secretNS    *string
+		expectError bool
+		errorMsg    string
+	}{
+		{
+			name:        "ClusterSecretStore can access cross-namespace secrets",
+			storeKind:   esv1.ClusterSecretStoreKind,
+			namespace:   testNamespace,
+			secretNS:    &otherNamespace,
+			expectError: false,
+		},
+		{
+			name:        "SecretStore cannot access cross-namespace secrets",
+			storeKind:   esv1.SecretStoreKind,
+			namespace:   testNamespace,
+			secretNS:    &otherNamespace,
+			expectError: true,
+			errorMsg:    "cannot get Kubernetes secret",
+		},
+		{
+			name:        "SecretStore can access same-namespace secrets",
+			storeKind:   esv1.SecretStoreKind,
+			namespace:   testNamespace,
+			secretNS:    nil,
+			expectError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			targetNS := testNamespace
+			if tt.secretNS != nil {
+				targetNS = *tt.secretNS
+			}
+
+			secret := &corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      secretName,
+					Namespace: targetNS,
+				},
+				Data: map[string][]byte{
+					appIDKey:     []byte(testAppID),
+					appSecretKey: []byte(testAppSecret),
+				},
+			}
+
+			scheme := runtime.NewScheme()
+			require.NoError(t, corev1.AddToScheme(scheme))
+			kube := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build()
+
+			provider := &esv1.DVLSProvider{
+				ServerURL: testServerURL,
+				Auth: esv1.DVLSAuth{
+					SecretRef: esv1.DVLSAuthSecretRef{
+						AppID: esmeta.SecretKeySelector{
+							Name:      secretName,
+							Key:       appIDKey,
+							Namespace: tt.secretNS,
+						},
+						AppSecret: esmeta.SecretKeySelector{
+							Name:      secretName,
+							Key:       appSecretKey,
+							Namespace: tt.secretNS,
+						},
+					},
+				},
+			}
+
+			client, err := NewDVLSClient(context.Background(), kube, tt.storeKind, tt.namespace, provider)
+
+			if tt.expectError {
+				require.Error(t, err)
+				require.Nil(t, client)
+				if tt.errorMsg != "" {
+					assert.Contains(t, err.Error(), tt.errorMsg)
+				}
+			} else if err != nil {
+				// Verify Kubernetes secret access succeeded (DVLS connection will fail due to fake server).
+				assert.NotContains(t, err.Error(), "failed to get appId")
+				assert.NotContains(t, err.Error(), "failed to get appSecret")
+				assert.NotContains(t, err.Error(), "cannot get Kubernetes secret")
+			}
+		})
+	}
+}

+ 268 - 0
providers/v1/dvls/client.go

@@ -0,0 +1,268 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+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
+
+    https://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 dvls
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/Devolutions/go-dvls"
+	corev1 "k8s.io/api/core/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+const errFailedToGetEntry = "failed to get entry: %w"
+
+var errNotImplemented = errors.New("not implemented")
+
+var _ esv1.SecretsClient = &Client{}
+
+// Client implements the SecretsClient interface for DVLS.
+type Client struct {
+	dvls credentialClient
+}
+
+type credentialClient interface {
+	GetByID(ctx context.Context, vaultID, entryID string) (dvls.Entry, error)
+	Update(ctx context.Context, entry dvls.Entry) (dvls.Entry, error)
+	DeleteByID(ctx context.Context, vaultID, entryID string) error
+}
+
+type realCredentialClient struct {
+	cred *dvls.EntryCredentialService
+}
+
+func (r *realCredentialClient) GetByID(ctx context.Context, vaultID, entryID string) (dvls.Entry, error) {
+	return r.cred.GetByIdWithContext(ctx, vaultID, entryID)
+}
+
+func (r *realCredentialClient) Update(ctx context.Context, entry dvls.Entry) (dvls.Entry, error) {
+	return r.cred.UpdateWithContext(ctx, entry)
+}
+
+func (r *realCredentialClient) DeleteByID(ctx context.Context, vaultID, entryID string) error {
+	return r.cred.DeleteByIdWithContext(ctx, vaultID, entryID)
+}
+
+// NewClient creates a new DVLS secrets client.
+func NewClient(dvlsClient credentialClient) *Client {
+	return &Client{dvls: dvlsClient}
+}
+
+// GetSecret retrieves a secret from DVLS.
+func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	vaultID, entryID, err := c.parseSecretRef(ref.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	entry, err := c.dvls.GetByID(ctx, vaultID, entryID)
+	if isNotFoundError(err) {
+		return nil, esv1.NoSecretErr
+	}
+	if err != nil {
+		return nil, fmt.Errorf(errFailedToGetEntry, err)
+	}
+
+	secretMap, err := c.entryToSecretMap(entry)
+	if err != nil {
+		return nil, err
+	}
+
+	// Default to "password" when no property specified (consistent with 1Password provider).
+	property := ref.Property
+	if property == "" {
+		property = "password"
+	}
+
+	value, ok := secretMap[property]
+	if !ok {
+		return nil, fmt.Errorf("property %q not found in entry", property)
+	}
+	return value, nil
+}
+
+// GetSecretMap retrieves all fields from a DVLS entry.
+func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	vaultID, entryID, err := c.parseSecretRef(ref.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	entry, err := c.dvls.GetByID(ctx, vaultID, entryID)
+	if isNotFoundError(err) {
+		return nil, esv1.NoSecretErr
+	}
+	if err != nil {
+		return nil, fmt.Errorf(errFailedToGetEntry, err)
+	}
+
+	return c.entryToSecretMap(entry)
+}
+
+// GetAllSecrets is not implemented for DVLS.
+func (c *Client) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
+	return nil, errNotImplemented
+}
+
+// PushSecret updates an existing entry's password field.
+func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
+	if secret == nil {
+		return errors.New("secret is required for DVLS push")
+	}
+	vaultID, entryID, err := c.parseSecretRef(data.GetRemoteKey())
+	if err != nil {
+		return err
+	}
+
+	value, err := extractPushValue(secret, data)
+	if err != nil {
+		return err
+	}
+
+	existingEntry, err := c.dvls.GetByID(ctx, vaultID, entryID)
+	if isNotFoundError(err) {
+		return fmt.Errorf("entry %s not found in vault %s: entry must exist before pushing secrets", entryID, vaultID)
+	}
+	if err != nil {
+		return fmt.Errorf(errFailedToGetEntry, err)
+	}
+
+	// SetCredentialSecret only updates the password/secret field.
+	if err := existingEntry.SetCredentialSecret(string(value)); err != nil {
+		return err
+	}
+
+	_, err = c.dvls.Update(ctx, existingEntry)
+	if err != nil {
+		return fmt.Errorf("failed to update entry: %w", err)
+	}
+	return nil
+}
+
+// DeleteSecret deletes a secret from DVLS.
+func (c *Client) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
+	vaultID, entryID, err := c.parseSecretRef(ref.GetRemoteKey())
+	if err != nil {
+		return err
+	}
+	return c.dvls.DeleteByID(ctx, vaultID, entryID)
+}
+
+// SecretExists checks if a secret exists in DVLS.
+func (c *Client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
+	vaultID, entryID, err := c.parseSecretRef(ref.GetRemoteKey())
+	if err != nil {
+		return false, err
+	}
+
+	_, err = c.dvls.GetByID(ctx, vaultID, entryID)
+	if isNotFoundError(err) {
+		return false, nil
+	}
+	if err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
+// Validate checks if the client is properly configured.
+func (c *Client) Validate() (esv1.ValidationResult, error) {
+	if c.dvls == nil {
+		return esv1.ValidationResultError, errors.New("DVLS client is not initialized")
+	}
+	return esv1.ValidationResultReady, nil
+}
+
+// Close is a no-op for the DVLS client.
+func (c *Client) Close(_ context.Context) error {
+	return nil
+}
+
+// parseSecretRef parses the secret reference key.
+// Format: "<vault-id>/<entry-id>".
+func (c *Client) parseSecretRef(key string) (vaultID, entryID string, err error) {
+	parts := strings.SplitN(key, "/", 2)
+	if len(parts) != 2 {
+		return "", "", fmt.Errorf("invalid key format: expected '<vault-id>/<entry-id>', got %q", key)
+	}
+
+	vaultID = strings.TrimSpace(parts[0])
+	entryID = strings.TrimSpace(parts[1])
+
+	if vaultID == "" {
+		return "", "", errors.New("vault ID cannot be empty")
+	}
+	if entryID == "" {
+		return "", "", errors.New("entry ID cannot be empty")
+	}
+
+	return vaultID, entryID, nil
+}
+
+// entryToSecretMap converts a DVLS entry to a map of secret values.
+func (c *Client) entryToSecretMap(entry dvls.Entry) (map[string][]byte, error) {
+	secretMap, err := entry.ToCredentialMap()
+	if err != nil {
+		return nil, err
+	}
+
+	result := make(map[string][]byte, len(secretMap))
+	for k, v := range secretMap {
+		result[k] = []byte(v)
+	}
+
+	return result, nil
+}
+
+func extractPushValue(secret *corev1.Secret, data esv1.PushSecretData) ([]byte, error) {
+	if data.GetSecretKey() == "" {
+		return nil, fmt.Errorf("secretKey is required for DVLS push")
+	}
+
+	if secret.Data == nil {
+		return nil, fmt.Errorf("secret %q has no data", secret.Name)
+	}
+
+	value, ok := secret.Data[data.GetSecretKey()]
+	if !ok {
+		return nil, fmt.Errorf("key %q not found in secret %q", data.GetSecretKey(), secret.Name)
+	}
+
+	if len(value) == 0 {
+		return nil, fmt.Errorf("key %q in secret %q is empty", data.GetSecretKey(), secret.Name)
+	}
+
+	return value, nil
+}
+
+func isNotFoundError(err error) bool {
+	if err == nil {
+		return false
+	}
+
+	if dvls.IsNotFound(err) {
+		return true
+	}
+
+	var reqErr dvls.RequestError
+	return errors.As(err, &reqErr) && reqErr.StatusCode == http.StatusNotFound
+}

+ 454 - 0
providers/v1/dvls/client_test.go

@@ -0,0 +1,454 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+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
+
+    https://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 dvls
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/Devolutions/go-dvls"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+type mockCredentialClient struct {
+	entries     map[string]dvls.Entry
+	getErr      error
+	updateErr   error
+	deleteErr   error
+	lastUpdated dvls.Entry
+	lastDeleted string
+}
+
+func newMockCredentialClient(entries map[string]dvls.Entry) *mockCredentialClient {
+	if entries == nil {
+		entries = make(map[string]dvls.Entry)
+	}
+	return &mockCredentialClient{entries: entries}
+}
+
+func (m *mockCredentialClient) GetByID(_ context.Context, _, entryID string) (dvls.Entry, error) {
+	if m.getErr != nil {
+		return dvls.Entry{}, m.getErr
+	}
+
+	if entry, ok := m.entries[entryID]; ok {
+		return entry, nil
+	}
+
+	return dvls.Entry{}, &dvls.RequestError{Err: fmt.Errorf("unexpected status code %d", http.StatusNotFound), Url: entryID, StatusCode: http.StatusNotFound}
+}
+
+func (m *mockCredentialClient) Update(_ context.Context, entry dvls.Entry) (dvls.Entry, error) {
+	if m.updateErr != nil {
+		return entry, m.updateErr
+	}
+	m.entries[entry.Id] = entry
+	m.lastUpdated = entry
+	return entry, nil
+}
+
+func (m *mockCredentialClient) DeleteByID(_ context.Context, _, entryID string) error {
+	if m.deleteErr != nil {
+		return m.deleteErr
+	}
+
+	delete(m.entries, entryID)
+	m.lastDeleted = entryID
+	return nil
+}
+
+type pushSecretDataStub struct {
+	remoteKey string
+	secretKey string
+	property  string
+}
+
+func (p pushSecretDataStub) GetMetadata() *apiextensionsv1.JSON { return nil }
+func (p pushSecretDataStub) GetSecretKey() string               { return p.secretKey }
+func (p pushSecretDataStub) GetRemoteKey() string               { return p.remoteKey }
+func (p pushSecretDataStub) GetProperty() string                { return p.property }
+
+type pushSecretRemoteRefStub struct {
+	remoteKey string
+	property  string
+}
+
+func (p pushSecretRemoteRefStub) GetRemoteKey() string { return p.remoteKey }
+func (p pushSecretRemoteRefStub) GetProperty() string  { return p.property }
+
+func TestClient_parseSecretRef(t *testing.T) {
+	c := &Client{}
+
+	t.Run("case 1: valid key format", func(t *testing.T) {
+		vaultID, entryID, err := c.parseSecretRef("vault-123/entry-456")
+		assert.NoError(t, err)
+		assert.Equal(t, "vault-123", vaultID)
+		assert.Equal(t, "entry-456", entryID)
+	})
+
+	t.Run("case 2: invalid key format - no separator", func(t *testing.T) {
+		_, _, err := c.parseSecretRef("invalid-key")
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "invalid key format")
+	})
+
+	t.Run("case 3: invalid key format - empty vault ID", func(t *testing.T) {
+		_, _, err := c.parseSecretRef("/entry-456")
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "vault ID cannot be empty")
+	})
+
+	t.Run("case 4: invalid key format - empty entry ID", func(t *testing.T) {
+		_, _, err := c.parseSecretRef("vault-123/")
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "entry ID cannot be empty")
+	})
+
+	t.Run("case 5: key with spaces", func(t *testing.T) {
+		vaultID, entryID, err := c.parseSecretRef(" vault-123 / entry-456 ")
+		assert.NoError(t, err)
+		assert.Equal(t, "vault-123", vaultID)
+		assert.Equal(t, "entry-456", entryID)
+	})
+}
+
+func TestClient_Validate(t *testing.T) {
+	t.Run("case 1: nil client", func(t *testing.T) {
+		c := &Client{dvls: nil}
+		result, err := c.Validate()
+		assert.Error(t, err)
+		assert.Equal(t, esv1.ValidationResultError, result)
+	})
+
+	t.Run("case 2: initialized client", func(t *testing.T) {
+		c := &Client{dvls: newMockCredentialClient(nil)}
+		result, err := c.Validate()
+		assert.NoError(t, err)
+		assert.Equal(t, esv1.ValidationResultReady, result)
+	})
+}
+
+func TestNewClient(t *testing.T) {
+	// Test that NewClient returns a non-nil client
+	c := NewClient(nil)
+	assert.NotNil(t, c)
+	assert.Nil(t, c.dvls)
+}
+
+func TestClient_entryToSecretMap(t *testing.T) {
+	c := &Client{}
+
+	t.Run("Default credential type", func(t *testing.T) {
+		entry := dvls.Entry{
+			Id:      "entry-id-123",
+			Name:    "test-entry",
+			Type:    dvls.EntryCredentialType,
+			SubType: dvls.EntryCredentialSubTypeDefault,
+			Data: &dvls.EntryCredentialDefaultData{
+				Username: "testuser",
+				Password: "testpass",
+				Domain:   "testdomain",
+			},
+		}
+
+		secretMap, err := c.entryToSecretMap(entry)
+		assert.NoError(t, err)
+		assert.Equal(t, "entry-id-123", string(secretMap["entry-id"]))
+		assert.Equal(t, "test-entry", string(secretMap["entry-name"]))
+		assert.Equal(t, "testuser", string(secretMap["username"]))
+		assert.Equal(t, "testpass", string(secretMap["password"]))
+		assert.Equal(t, "testdomain", string(secretMap["domain"]))
+	})
+
+	t.Run("AccessCode credential type", func(t *testing.T) {
+		entry := dvls.Entry{
+			Id:      "entry-id-456",
+			Name:    "access-code-entry",
+			Type:    dvls.EntryCredentialType,
+			SubType: dvls.EntryCredentialSubTypeAccessCode,
+			Data: &dvls.EntryCredentialAccessCodeData{
+				Password: "access-code-123",
+			},
+		}
+
+		secretMap, err := c.entryToSecretMap(entry)
+		assert.NoError(t, err)
+		assert.Equal(t, "entry-id-456", string(secretMap["entry-id"]))
+		assert.Equal(t, "access-code-entry", string(secretMap["entry-name"]))
+		assert.Equal(t, "access-code-123", string(secretMap["password"]))
+	})
+
+	t.Run("ApiKey credential type", func(t *testing.T) {
+		entry := dvls.Entry{
+			Id:      "entry-id-789",
+			Name:    "api-key-entry",
+			Type:    dvls.EntryCredentialType,
+			SubType: dvls.EntryCredentialSubTypeApiKey,
+			Data: &dvls.EntryCredentialApiKeyData{
+				ApiId:    "api-id-123",
+				ApiKey:   "api-key-secret",
+				TenantId: "tenant-123",
+			},
+		}
+
+		secretMap, err := c.entryToSecretMap(entry)
+		assert.NoError(t, err)
+		assert.Equal(t, "entry-id-789", string(secretMap["entry-id"]))
+		assert.Equal(t, "api-key-entry", string(secretMap["entry-name"]))
+		assert.Equal(t, "api-id-123", string(secretMap["api-id"]))
+		assert.Equal(t, "api-key-secret", string(secretMap["api-key"]))
+		assert.Equal(t, "tenant-123", string(secretMap["tenant-id"]))
+	})
+
+	t.Run("AzureServicePrincipal credential type", func(t *testing.T) {
+		entry := dvls.Entry{
+			Id:      "entry-id-azure",
+			Name:    "azure-sp-entry",
+			Type:    dvls.EntryCredentialType,
+			SubType: dvls.EntryCredentialSubTypeAzureServicePrincipal,
+			Data: &dvls.EntryCredentialAzureServicePrincipalData{
+				ClientId:     "client-id-123",
+				ClientSecret: "client-secret-456",
+				TenantId:     "tenant-id-789",
+			},
+		}
+
+		secretMap, err := c.entryToSecretMap(entry)
+		assert.NoError(t, err)
+		assert.Equal(t, "entry-id-azure", string(secretMap["entry-id"]))
+		assert.Equal(t, "azure-sp-entry", string(secretMap["entry-name"]))
+		assert.Equal(t, "client-id-123", string(secretMap["client-id"]))
+		assert.Equal(t, "client-secret-456", string(secretMap["client-secret"]))
+		assert.Equal(t, "tenant-id-789", string(secretMap["tenant-id"]))
+	})
+
+	t.Run("ConnectionString credential type", func(t *testing.T) {
+		entry := dvls.Entry{
+			Id:      "entry-id-conn",
+			Name:    "connection-string-entry",
+			Type:    dvls.EntryCredentialType,
+			SubType: dvls.EntryCredentialSubTypeConnectionString,
+			Data: &dvls.EntryCredentialConnectionStringData{
+				ConnectionString: "Server=localhost;Database=mydb;User=admin;Password=secret",
+			},
+		}
+
+		secretMap, err := c.entryToSecretMap(entry)
+		assert.NoError(t, err)
+		assert.Equal(t, "entry-id-conn", string(secretMap["entry-id"]))
+		assert.Equal(t, "connection-string-entry", string(secretMap["entry-name"]))
+		assert.Equal(t, "Server=localhost;Database=mydb;User=admin;Password=secret", string(secretMap["connection-string"]))
+	})
+
+	t.Run("PrivateKey credential type", func(t *testing.T) {
+		entry := dvls.Entry{
+			Id:      "entry-id-pk",
+			Name:    "private-key-entry",
+			Type:    dvls.EntryCredentialType,
+			SubType: dvls.EntryCredentialSubTypePrivateKey,
+			Data: &dvls.EntryCredentialPrivateKeyData{
+				Username:   "ssh-user",
+				Password:   "key-password",
+				PrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIE...",
+				PublicKey:  "ssh-rsa AAAA...",
+				Passphrase: "my-passphrase",
+			},
+		}
+
+		secretMap, err := c.entryToSecretMap(entry)
+		assert.NoError(t, err)
+		assert.Equal(t, "entry-id-pk", string(secretMap["entry-id"]))
+		assert.Equal(t, "private-key-entry", string(secretMap["entry-name"]))
+		assert.Equal(t, "ssh-user", string(secretMap["username"]))
+		assert.Equal(t, "key-password", string(secretMap["password"]))
+		assert.Equal(t, "-----BEGIN RSA PRIVATE KEY-----\nMIIE...", string(secretMap["private-key"]))
+		assert.Equal(t, "ssh-rsa AAAA...", string(secretMap["public-key"]))
+		assert.Equal(t, "my-passphrase", string(secretMap["passphrase"]))
+	})
+
+	t.Run("Unsupported credential type", func(t *testing.T) {
+		entry := dvls.Entry{
+			Id:      "entry-id-unknown",
+			Name:    "unknown-entry",
+			Type:    dvls.EntryCredentialType,
+			SubType: "UnknownType",
+		}
+
+		_, err := c.entryToSecretMap(entry)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "unsupported credential subtype")
+	})
+
+	t.Run("Default credential with partial data", func(t *testing.T) {
+		entry := dvls.Entry{
+			Id:      "entry-id-partial",
+			Name:    "partial-entry",
+			Type:    dvls.EntryCredentialType,
+			SubType: dvls.EntryCredentialSubTypeDefault,
+			Data: &dvls.EntryCredentialDefaultData{
+				Username: "onlyuser",
+				// Password and Domain are empty
+			},
+		}
+
+		secretMap, err := c.entryToSecretMap(entry)
+		assert.NoError(t, err)
+		assert.Equal(t, "entry-id-partial", string(secretMap["entry-id"]))
+		assert.Equal(t, "partial-entry", string(secretMap["entry-name"]))
+		assert.Equal(t, "onlyuser", string(secretMap["username"]))
+		// Empty fields should not be included
+		_, hasPassword := secretMap["password"]
+		_, hasDomain := secretMap["domain"]
+		assert.False(t, hasPassword)
+		assert.False(t, hasDomain)
+	})
+}
+
+func TestClient_GetSecret_NotFound(t *testing.T) {
+	c := NewClient(newMockCredentialClient(nil))
+
+	_, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "vault/entry"})
+	assert.ErrorIs(t, err, esv1.NoSecretErr)
+}
+
+func TestClient_GetSecretAndMap_Success(t *testing.T) {
+	entry := dvls.Entry{
+		Id:      "entry-1",
+		Name:    "test-entry",
+		Type:    dvls.EntryCredentialType,
+		SubType: dvls.EntryCredentialSubTypeDefault,
+		Data: &dvls.EntryCredentialDefaultData{
+			Password: "super-secret",
+		},
+	}
+
+	mockClient := newMockCredentialClient(map[string]dvls.Entry{"entry-1": entry})
+	c := NewClient(mockClient)
+
+	val, err := c.GetSecret(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "vault-1/entry-1", Property: "password"})
+	assert.NoError(t, err)
+	assert.Equal(t, "super-secret", string(val))
+
+	secretMap, err := c.GetSecretMap(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "vault-1/entry-1"})
+	assert.NoError(t, err)
+	assert.Equal(t, "super-secret", string(secretMap["password"]))
+	assert.Equal(t, "test-entry", string(secretMap["entry-name"]))
+}
+
+func TestClient_SecretExists(t *testing.T) {
+	mockClient := newMockCredentialClient(nil)
+	c := NewClient(mockClient)
+
+	exists, err := c.SecretExists(context.Background(), pushSecretRemoteRefStub{remoteKey: "vault/entry"})
+	assert.NoError(t, err)
+	assert.False(t, exists)
+
+	mockClient.entries["entry"] = dvls.Entry{Id: "entry", Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault}
+
+	exists, err = c.SecretExists(context.Background(), pushSecretRemoteRefStub{remoteKey: "vault/entry"})
+	assert.NoError(t, err)
+	assert.True(t, exists)
+
+	mockClient.getErr = errors.New("boom")
+	_, err = c.SecretExists(context.Background(), pushSecretRemoteRefStub{remoteKey: "vault/entry"})
+	assert.Error(t, err)
+}
+
+func TestClient_DeleteSecret(t *testing.T) {
+	mockClient := newMockCredentialClient(map[string]dvls.Entry{"entry": {Id: "entry", Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeAccessCode}})
+	c := NewClient(mockClient)
+
+	err := c.DeleteSecret(context.Background(), pushSecretRemoteRefStub{remoteKey: "vault/entry"})
+	assert.NoError(t, err)
+	assert.Equal(t, "entry", mockClient.lastDeleted)
+}
+
+func TestClient_PushSecret_UpdateDefault(t *testing.T) {
+	mockClient := newMockCredentialClient(map[string]dvls.Entry{
+		"entry": {Id: "entry", Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeDefault},
+	})
+	c := NewClient(mockClient)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"password": []byte("new-value"),
+		},
+	}
+
+	data := pushSecretDataStub{remoteKey: "vault/entry", secretKey: "password"}
+
+	err := c.PushSecret(context.Background(), secret, data)
+	assert.NoError(t, err)
+
+	updatedEntry := mockClient.entries["entry"]
+	credData, ok := updatedEntry.Data.(*dvls.EntryCredentialDefaultData)
+	assert.True(t, ok)
+	assert.Equal(t, "new-value", credData.Password)
+}
+
+func TestClient_PushSecret_UpdateAccessCode(t *testing.T) {
+	mockClient := newMockCredentialClient(map[string]dvls.Entry{
+		"entry": {Id: "entry", Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeAccessCode},
+	})
+	c := NewClient(mockClient)
+
+	secret := &corev1.Secret{
+		Data: map[string][]byte{
+			"code": []byte("code-value"),
+		},
+	}
+
+	data := pushSecretDataStub{remoteKey: "vault/entry", secretKey: "code"}
+
+	err := c.PushSecret(context.Background(), secret, data)
+	assert.NoError(t, err)
+
+	updatedEntry := mockClient.entries["entry"]
+	credData, ok := updatedEntry.Data.(*dvls.EntryCredentialAccessCodeData)
+	assert.True(t, ok)
+	assert.Equal(t, "code-value", credData.Password)
+}
+
+func TestClient_PushSecret_NotFound(t *testing.T) {
+	c := NewClient(newMockCredentialClient(nil))
+	secret := &corev1.Secret{Data: map[string][]byte{"password": []byte("pw")}}
+	data := pushSecretDataStub{remoteKey: "vault/missing", secretKey: "password"}
+
+	err := c.PushSecret(context.Background(), secret, data)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "not found")
+}
+
+func TestClient_PushSecret_UnsupportedSubtype(t *testing.T) {
+	mockClient := newMockCredentialClient(map[string]dvls.Entry{
+		"entry": {Id: "entry", Type: dvls.EntryCredentialType, SubType: dvls.EntryCredentialSubTypeApiKey},
+	})
+	c := NewClient(mockClient)
+	secret := &corev1.Secret{Data: map[string][]byte{"password": []byte("pw")}}
+	data := pushSecretDataStub{remoteKey: "vault/entry", secretKey: "password"}
+
+	err := c.PushSecret(context.Background(), secret, data)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "cannot set secret for credential subtype")
+}

+ 94 - 0
providers/v1/dvls/go.mod

@@ -0,0 +1,94 @@
+module github.com/external-secrets/external-secrets/providers/v1/dvls
+
+go 1.25.5
+
+require (
+	github.com/Devolutions/go-dvls v0.15.0
+	github.com/external-secrets/external-secrets/apis v0.0.0
+	github.com/external-secrets/external-secrets/runtime v0.0.0
+	github.com/stretchr/testify v1.11.1
+	k8s.io/api v0.34.1
+	k8s.io/apiextensions-apiserver v0.34.1
+	k8s.io/apimachinery v0.34.1
+	sigs.k8s.io/controller-runtime v0.22.3
+)
+
+require (
+	dario.cat/mergo v1.0.1 // indirect
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.3.0 // indirect
+	github.com/Masterminds/sprig/v3 v3.3.0 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
+	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.20.2 // indirect
+	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/goccy/go-json v0.10.3 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/google/btree v1.1.3 // indirect
+	github.com/google/gnostic-models v0.7.0 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/huandu/xstrings v1.5.0 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/lestrrat-go/blackmagic v1.0.3 // indirect
+	github.com/lestrrat-go/httpcc v1.0.1 // indirect
+	github.com/lestrrat-go/httprc v1.0.6 // indirect
+	github.com/lestrrat-go/iter v1.0.2 // indirect
+	github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect
+	github.com/lestrrat-go/option v1.0.1 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/prometheus/client_golang v1.23.2 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.66.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
+	github.com/segmentio/asm v1.2.0 // indirect
+	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/spf13/cast v1.7.0 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.yaml.in/yaml/v2 v2.4.2 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	golang.org/x/crypto v0.46.0 // indirect
+	golang.org/x/net v0.47.0 // indirect
+	golang.org/x/oauth2 v0.30.0 // indirect
+	golang.org/x/sync v0.19.0 // indirect
+	golang.org/x/sys v0.39.0 // indirect
+	golang.org/x/term v0.38.0 // indirect
+	golang.org/x/text v0.32.0 // indirect
+	golang.org/x/time v0.9.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
+	google.golang.org/protobuf v1.36.8 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	k8s.io/client-go v0.34.1 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
+	k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
+	k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
+	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
+	software.sslmate.com/src/go-pkcs12 v0.6.0 // indirect
+)
+
+replace (
+	github.com/external-secrets/external-secrets/apis => ../../../apis
+	github.com/external-secrets/external-secrets/runtime => ../../../runtime
+)

+ 255 - 0
providers/v1/dvls/go.sum

@@ -0,0 +1,255 @@
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/Devolutions/go-dvls v0.15.0 h1:T/uUK0sKli7i9yxcZb9/Lia/uUwmLi1phryNkYEp5t4=
+github.com/Devolutions/go-dvls v0.15.0/go.mod h1:4O3lb/RK1P1cDwU5auVi7CM4gRER7EuwyLwMVuEZjgg=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
+github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
+github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
+github.com/aws/aws-sdk-go-v2 v1.39.3 h1:h7xSsanJ4EQJXG5iuW4UqgP7qBopLpj84mpkNx3wPjM=
+github.com/aws/aws-sdk-go-v2 v1.39.3/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
+github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
+github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
+github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
+github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k=
+github.com/gofrs/flock v0.10.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
+github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs=
+github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
+github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
+github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
+github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
+github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
+github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
+github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
+github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
+github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
+github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
+github.com/oracle/oci-go-sdk/v65 v65.102.1 h1:zLNLz5dVzZxOf5DK/f3WGZUjwrQ9m27fd4abOFwQRCQ=
+github.com/oracle/oci-go-sdk/v65 v65.102.1/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
+github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
+github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
+github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
+github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
+golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
+golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
+google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
+k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
+k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
+k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
+k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
+k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
+k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
+k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
+k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
+k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
+sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
+software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

+ 148 - 0
providers/v1/dvls/provider.go

@@ -0,0 +1,148 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+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
+
+    https://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 dvls
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+)
+
+var _ esv1.Provider = &Provider{}
+
+// Provider implements the external-secrets Provider interface for DVLS.
+type Provider struct{}
+
+// NewClient creates a new DVLS SecretsClient.
+func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
+	dvlsProvider, err := getDVLSProvider(store)
+	if err != nil {
+		return nil, err
+	}
+
+	storeKind := store.GetObjectKind().GroupVersionKind().Kind
+
+	dvlsClient, err := NewDVLSClient(ctx, kube, storeKind, namespace, dvlsProvider)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create DVLS client: %w", err)
+	}
+
+	return NewClient(dvlsClient), nil
+}
+
+// ValidateStore validates the SecretStore configuration.
+func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
+	dvlsProvider, err := getDVLSProvider(store)
+	if err != nil {
+		return nil, err
+	}
+
+	if dvlsProvider.ServerURL == "" {
+		return nil, fmt.Errorf("serverUrl is required")
+	}
+
+	parsedURL, err := url.Parse(dvlsProvider.ServerURL)
+	if err != nil {
+		return nil, fmt.Errorf("serverUrl must be a valid URL: %w", err)
+	}
+	if parsedURL.Scheme == "" || parsedURL.Host == "" {
+		return nil, fmt.Errorf("serverUrl must be a valid URL with scheme and host")
+	}
+
+	if parsedURL.Scheme != "https" && parsedURL.Scheme != "http" {
+		return nil, fmt.Errorf("serverUrl scheme must be http or https, got %q", parsedURL.Scheme)
+	}
+
+	if parsedURL.Scheme == "http" && !dvlsProvider.Insecure {
+		return nil, fmt.Errorf("http URLs require 'insecure: true' to be set explicitly")
+	}
+
+	// Validate auth configuration
+	if err := validateAuthSecretRef(store, &dvlsProvider.Auth.SecretRef); err != nil {
+		return nil, err
+	}
+
+	return nil, nil
+}
+
+// Capabilities returns the provider's capabilities.
+func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
+	return esv1.SecretStoreReadWrite
+}
+
+// validateAuthSecretRef validates the authentication secret references.
+func validateAuthSecretRef(store esv1.GenericStore, ref *esv1.DVLSAuthSecretRef) error {
+	if err := requireSecretSelector(ref.AppID, "appId"); err != nil {
+		return err
+	}
+	if err := esutils.ValidateSecretSelector(store, ref.AppID); err != nil {
+		return fmt.Errorf("invalid appId: %w", err)
+	}
+
+	if err := requireSecretSelector(ref.AppSecret, "appSecret"); err != nil {
+		return err
+	}
+	if err := esutils.ValidateSecretSelector(store, ref.AppSecret); err != nil {
+		return fmt.Errorf("invalid appSecret: %w", err)
+	}
+	return nil
+}
+
+func requireSecretSelector(sel esmeta.SecretKeySelector, field string) error {
+	if sel.Name == "" {
+		return fmt.Errorf("%s secret name is required", field)
+	}
+
+	if sel.Key == "" {
+		return fmt.Errorf("%s secret key is required", field)
+	}
+
+	return nil
+}
+
+// getDVLSProvider extracts the DVLS provider configuration from the store.
+func getDVLSProvider(store esv1.GenericStore) (*esv1.DVLSProvider, error) {
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider == nil || spec.Provider.DVLS == nil {
+		return nil, fmt.Errorf("DVLS provider configuration is missing")
+	}
+	return spec.Provider.DVLS, nil
+}
+
+// NewProvider creates a new DVLS Provider instance.
+func NewProvider() esv1.Provider {
+	return &Provider{}
+}
+
+// ProviderSpec returns the provider specification for registration.
+func ProviderSpec() *esv1.SecretStoreProvider {
+	return &esv1.SecretStoreProvider{
+		DVLS: &esv1.DVLSProvider{},
+	}
+}
+
+// MaintenanceStatus returns the maintenance status of the provider.
+func MaintenanceStatus() esv1.MaintenanceStatus {
+	return esv1.MaintenanceStatusMaintained
+}

+ 395 - 0
providers/v1/dvls/provider_test.go

@@ -0,0 +1,395 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+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
+
+    https://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 dvls
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+const (
+	testNamespace = "default"
+	testServerURL = "https://dvls.example.com"
+	secretName    = "dvls-secret"
+	appIDKey      = "app-id"
+	appSecretKey  = "app-secret"
+	otherNS       = "other"
+	testAppID     = "test-app-id"
+	testAppSecret = "test-app-secret"
+)
+
+func TestProvider_ValidateStore(t *testing.T) {
+	p := &Provider{}
+
+	t.Run("case 1: should return no error when valid", func(t *testing.T) {
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "dvls-store",
+				Namespace: testNamespace,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: testServerURL,
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appIDKey,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appSecretKey,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.NoError(t, err)
+	})
+
+	t.Run("case 1b: cluster store requires namespace", func(t *testing.T) {
+		store := &esv1.ClusterSecretStore{
+			TypeMeta: metav1.TypeMeta{Kind: "ClusterSecretStore"},
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "dvls-cluster-store",
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: testServerURL,
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appIDKey,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appSecretKey,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		_, err := p.ValidateStore(store)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "cluster scope requires namespace")
+	})
+
+	t.Run("case 1c: cluster store succeeds with namespace", func(t *testing.T) {
+		otherNamespace := otherNS
+		store := &esv1.ClusterSecretStore{
+			TypeMeta: metav1.TypeMeta{Kind: "ClusterSecretStore"},
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "dvls-cluster-store",
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: testServerURL,
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name:      secretName,
+									Key:       appIDKey,
+									Namespace: &otherNamespace,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name:      secretName,
+									Key:       appSecretKey,
+									Namespace: &otherNamespace,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		_, err := p.ValidateStore(store)
+		assert.NoError(t, err)
+	})
+
+	t.Run("case 2: should return error when provider is nil", func(t *testing.T) {
+		store := &esv1.SecretStore{
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: nil,
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "DVLS provider configuration is missing")
+	})
+
+	t.Run("case 2b: http URL without insecure flag should fail", func(t *testing.T) {
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "dvls-store",
+				Namespace: testNamespace,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: "http://dvls.example.com",
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appIDKey,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appSecretKey,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "http URLs require 'insecure: true'")
+	})
+
+	t.Run("case 2c: http URL with insecure flag should pass", func(t *testing.T) {
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "dvls-store",
+				Namespace: testNamespace,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: "http://dvls.example.com",
+						Insecure:  true,
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appIDKey,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appSecretKey,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.NoError(t, err)
+	})
+
+	t.Run("case 3: should return error when serverUrl is empty", func(t *testing.T) {
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "dvls-store",
+				Namespace: testNamespace,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: "",
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appIDKey,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appSecretKey,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "serverUrl is required")
+	})
+
+	t.Run("case 3b: should return error when appId key is missing", func(t *testing.T) {
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "dvls-store",
+				Namespace: testNamespace,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: testServerURL,
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  "",
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appSecretKey,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "appId secret key is required")
+	})
+
+	t.Run("case 3c: should return error when appSecret name is missing", func(t *testing.T) {
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "dvls-store",
+				Namespace: testNamespace,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: testServerURL,
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appIDKey,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name: "",
+									Key:  appSecretKey,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "appSecret secret name is required")
+	})
+
+	t.Run("case 4: should return error when AppID secret reference is invalid", func(t *testing.T) {
+		otherNamespace := otherNS
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "dvls-store",
+				Namespace: testNamespace,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: testServerURL,
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name:      secretName,
+									Key:       appIDKey,
+									Namespace: &otherNamespace,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appSecretKey,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "invalid appId")
+	})
+
+	t.Run("case 5: should return error when AppSecret secret reference is invalid", func(t *testing.T) {
+		otherNamespace := otherNS
+		store := &esv1.SecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "dvls-store",
+				Namespace: testNamespace,
+			},
+			Spec: esv1.SecretStoreSpec{
+				Provider: &esv1.SecretStoreProvider{
+					DVLS: &esv1.DVLSProvider{
+						ServerURL: testServerURL,
+						Auth: esv1.DVLSAuth{
+							SecretRef: esv1.DVLSAuthSecretRef{
+								AppID: esmeta.SecretKeySelector{
+									Name: secretName,
+									Key:  appIDKey,
+								},
+								AppSecret: esmeta.SecretKeySelector{
+									Name:      secretName,
+									Key:       appSecretKey,
+									Namespace: &otherNamespace,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, err := p.ValidateStore(store)
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), "invalid appSecret")
+	})
+}
+
+func TestProvider_Capabilities(t *testing.T) {
+	p := &Provider{}
+	assert.Equal(t, esv1.SecretStoreReadWrite, p.Capabilities())
+}
+
+func TestNewProvider(t *testing.T) {
+	p := NewProvider()
+	assert.NotNil(t, p)
+	_, ok := p.(*Provider)
+	assert.True(t, ok)
+}
+
+func TestProviderSpec(t *testing.T) {
+	spec := ProviderSpec()
+	assert.NotNil(t, spec)
+	assert.NotNil(t, spec.DVLS)
+}
+
+func TestMaintenanceStatus(t *testing.T) {
+	status := MaintenanceStatus()
+	assert.Equal(t, esv1.MaintenanceStatusMaintained, status)
+}

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

@@ -297,6 +297,19 @@ spec:
       format: "json" # "json", "dotnet-json", "env", "yaml", "docker"
       nameTransformer: "upper-camel" # "upper-camel", "camel", "lower-snake", "tf-var", "dotnet-env", "lower-kebab"
       project: string
+    dvls:
+      auth:
+        secretRef:
+          appId:
+            key: string
+            name: string
+            namespace: string
+          appSecret:
+            key: string
+            name: string
+            namespace: string
+      insecure: true
+      serverUrl: string
     fake:
       data:
       - key: string

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

@@ -297,6 +297,19 @@ spec:
       format: "json" # "json", "dotnet-json", "env", "yaml", "docker"
       nameTransformer: "upper-camel" # "upper-camel", "camel", "lower-snake", "tf-var", "dotnet-env", "lower-kebab"
       project: string
+    dvls:
+      auth:
+        secretRef:
+          appId:
+            key: string
+            name: string
+            namespace: string
+          appSecret:
+            key: string
+            name: string
+            namespace: string
+      insecure: true
+      serverUrl: string
     fake:
       data:
       - key: string