Browse Source

feat: support vault provider check and set for push secrets (#5197)

* Add CheckAndSet to vault api

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

* generate crds

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

* Add documentation for check and set funcitonality

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

* Add validation to VaultProvider to ensure CAS is not enabled for unsupported version.

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

* tidy: break out core logic of helper functions in fake vault.

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

* Add store helper that supports tests that need to read both metadata and data path requests.

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

* Add failing tests for CAS behavior.

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

* Add support for check and set for push secrets pushing to vault kv v2 (makes tests pass)

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

* fix: formatting on unrelated files (make fmt).

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>

---------

Signed-off-by: Erik Westra <e.s.westra.95@gmail.com>
Co-authored-by: Gergely Brautigam <skarlso777@gmail.com>
Erik Westra 7 months ago
parent
commit
cc13d31698

+ 14 - 0
apis/externalsecrets/v1/secretstore_vault_types.go

@@ -90,6 +90,12 @@ type VaultProvider struct {
 	// Headers to be added in Vault request
 	// +optional
 	Headers map[string]string `json:"headers,omitempty"`
+
+	// CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+	// Only applies to Vault KV v2 stores. When enabled, write operations must include
+	// the current version of the secret to prevent unintentional overwrites.
+	// +optional
+	CheckAndSet *VaultCheckAndSet `json:"checkAndSet,omitempty"`
 }
 
 // VaultClientTLS is the configuration used for client side related TLS communication,
@@ -371,3 +377,11 @@ type VaultUserPassAuth struct {
 	// +optional
 	SecretRef esmeta.SecretKeySelector `json:"secretRef,omitempty"`
 }
+
+// VaultCheckAndSet defines the Check-And-Set (CAS) settings for Vault KV v2 PushSecret operations.
+type VaultCheckAndSet struct {
+	// Required when true, all write operations must include a check-and-set parameter.
+	// This helps prevent unintentional overwrites of secrets.
+	// +optional
+	Required bool `json:"required,omitempty"`
+}

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

@@ -3627,6 +3627,21 @@ func (in *VaultCertAuth) DeepCopy() *VaultCertAuth {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VaultCheckAndSet) DeepCopyInto(out *VaultCheckAndSet) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultCheckAndSet.
+func (in *VaultCheckAndSet) DeepCopy() *VaultCheckAndSet {
+	if in == nil {
+		return nil
+	}
+	out := new(VaultCheckAndSet)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VaultClientTLS) DeepCopyInto(out *VaultClientTLS) {
 	*out = *in
 	if in.CertSecretRef != nil {
@@ -3808,6 +3823,11 @@ func (in *VaultProvider) DeepCopyInto(out *VaultProvider) {
 			(*out)[key] = val
 		}
 	}
+	if in.CheckAndSet != nil {
+		in, out := &in.CheckAndSet, &out.CheckAndSet
+		*out = new(VaultCheckAndSet)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultProvider.

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

@@ -4716,6 +4716,18 @@ spec:
                         - name
                         - type
                         type: object
+                      checkAndSet:
+                        description: |-
+                          CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+                          Only applies to Vault KV v2 stores. When enabled, write operations must include
+                          the current version of the secret to prevent unintentional overwrites.
+                        properties:
+                          required:
+                            description: |-
+                              Required when true, all write operations must include a check-and-set parameter.
+                              This helps prevent unintentional overwrites of secrets.
+                            type: boolean
+                        type: object
                       forwardInconsistent:
                         description: |-
                           ForwardInconsistent tells Vault to forward read-after-write requests to the Vault

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

@@ -4716,6 +4716,18 @@ spec:
                         - name
                         - type
                         type: object
+                      checkAndSet:
+                        description: |-
+                          CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+                          Only applies to Vault KV v2 stores. When enabled, write operations must include
+                          the current version of the secret to prevent unintentional overwrites.
+                        properties:
+                          required:
+                            description: |-
+                              Required when true, all write operations must include a check-and-set parameter.
+                              This helps prevent unintentional overwrites of secrets.
+                            type: boolean
+                        type: object
                       forwardInconsistent:
                         description: |-
                           ForwardInconsistent tells Vault to forward read-after-write requests to the Vault

+ 12 - 0
config/crds/bases/generators.external-secrets.io_clustergenerators.yaml

@@ -1641,6 +1641,18 @@ spec:
                             - name
                             - type
                             type: object
+                          checkAndSet:
+                            description: |-
+                              CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+                              Only applies to Vault KV v2 stores. When enabled, write operations must include
+                              the current version of the secret to prevent unintentional overwrites.
+                            properties:
+                              required:
+                                description: |-
+                                  Required when true, all write operations must include a check-and-set parameter.
+                                  This helps prevent unintentional overwrites of secrets.
+                                type: boolean
+                            type: object
                           forwardInconsistent:
                             description: |-
                               ForwardInconsistent tells Vault to forward read-after-write requests to the Vault

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

@@ -736,6 +736,18 @@ spec:
                     - name
                     - type
                     type: object
+                  checkAndSet:
+                    description: |-
+                      CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+                      Only applies to Vault KV v2 stores. When enabled, write operations must include
+                      the current version of the secret to prevent unintentional overwrites.
+                    properties:
+                      required:
+                        description: |-
+                          Required when true, all write operations must include a check-and-set parameter.
+                          This helps prevent unintentional overwrites of secrets.
+                        type: boolean
+                    type: object
                   forwardInconsistent:
                     description: |-
                       ForwardInconsistent tells Vault to forward read-after-write requests to the Vault

+ 48 - 0
deploy/crds/bundle.yaml

@@ -6425,6 +6425,18 @@ spec:
                             - name
                             - type
                           type: object
+                        checkAndSet:
+                          description: |-
+                            CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+                            Only applies to Vault KV v2 stores. When enabled, write operations must include
+                            the current version of the secret to prevent unintentional overwrites.
+                          properties:
+                            required:
+                              description: |-
+                                Required when true, all write operations must include a check-and-set parameter.
+                                This helps prevent unintentional overwrites of secrets.
+                              type: boolean
+                          type: object
                         forwardInconsistent:
                           description: |-
                             ForwardInconsistent tells Vault to forward read-after-write requests to the Vault
@@ -17228,6 +17240,18 @@ spec:
                             - name
                             - type
                           type: object
+                        checkAndSet:
+                          description: |-
+                            CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+                            Only applies to Vault KV v2 stores. When enabled, write operations must include
+                            the current version of the secret to prevent unintentional overwrites.
+                          properties:
+                            required:
+                              description: |-
+                                Required when true, all write operations must include a check-and-set parameter.
+                                This helps prevent unintentional overwrites of secrets.
+                              type: boolean
+                          type: object
                         forwardInconsistent:
                           description: |-
                             ForwardInconsistent tells Vault to forward read-after-write requests to the Vault
@@ -23655,6 +23679,18 @@ spec:
                                 - name
                                 - type
                               type: object
+                            checkAndSet:
+                              description: |-
+                                CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+                                Only applies to Vault KV v2 stores. When enabled, write operations must include
+                                the current version of the secret to prevent unintentional overwrites.
+                              properties:
+                                required:
+                                  description: |-
+                                    Required when true, all write operations must include a check-and-set parameter.
+                                    This helps prevent unintentional overwrites of secrets.
+                                  type: boolean
+                              type: object
                             forwardInconsistent:
                               description: |-
                                 ForwardInconsistent tells Vault to forward read-after-write requests to the Vault
@@ -26037,6 +26073,18 @@ spec:
                         - name
                         - type
                       type: object
+                    checkAndSet:
+                      description: |-
+                        CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+                        Only applies to Vault KV v2 stores. When enabled, write operations must include
+                        the current version of the secret to prevent unintentional overwrites.
+                      properties:
+                        required:
+                          description: |-
+                            Required when true, all write operations must include a check-and-set parameter.
+                            This helps prevent unintentional overwrites of secrets.
+                          type: boolean
+                      type: object
                     forwardInconsistent:
                       description: |-
                         ForwardInconsistent tells Vault to forward read-after-write requests to the Vault

+ 48 - 0
docs/api/spec.md

@@ -9782,6 +9782,38 @@ authenticate with Vault using the Cert authentication method</p>
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1.VaultCheckAndSet">VaultCheckAndSet
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.VaultProvider">VaultProvider</a>)
+</p>
+<p>
+<p>VaultCheckAndSet defines the Check-And-Set (CAS) settings for Vault KV v2 PushSecret operations.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>required</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Required when true, all write operations must include a check-and-set parameter.
+This helps prevent unintentional overwrites of secrets.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1.VaultClientTLS">VaultClientTLS
 </h3>
 <p>
@@ -10424,6 +10456,22 @@ map[string]string
 <p>Headers to be added in Vault request</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>checkAndSet</code></br>
+<em>
+<a href="#external-secrets.io/v1.VaultCheckAndSet">
+VaultCheckAndSet
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>CheckAndSet defines the Check-And-Set (CAS) settings for PushSecret operations.
+Only applies to Vault KV v2 stores. When enabled, write operations must include
+the current version of the secret to prevent unintentional overwrites.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1.VaultUserPassAuth">VaultUserPassAuth

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

@@ -430,6 +430,36 @@ Here is an example of how to set up `PushSecret`:
 
 Note that in this example, we are generating two secrets in the target vault with the same structure but using different input formats.
 
+#### Check-And-Set (CAS) for PushSecret
+
+Vault KV v2 supports Check-And-Set operations to prevent unintentional overwrites when multiple clients modify the same secret. When CAS is enabled in your Vault configuration, External Secrets Operator can be configured to include the required version parameter in write operations.
+
+To enable CAS support, add the `checkAndSet` configuration to your Vault provider:
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: vault-backend
+spec:
+  provider:
+    vault:
+      server: "http://my.vault.server:8200"
+      path: "secret"
+      version: "v2"  # CAS only works with KV v2
+      checkAndSet:
+        required: true  # Enable CAS for all write operations
+      auth:
+        # ... authentication config
+```
+
+!!! note "CAS Requirements"
+    - CAS is only supported with Vault KV v2 stores
+    - When `checkAndSet.required` is true, all PushSecret operations will include version information
+    - For new secrets, External Secrets Operator uses CAS version 0
+    - For existing secrets, it automatically retrieves the current version before updating
+    - CAS helps prevent conflicts when multiple External Secrets instances manage the same secrets
+
 ### Vault Enterprise
 
 #### Eventual Consistency and Performance Standby Nodes

+ 71 - 2
pkg/provider/vault/client_push.go

@@ -67,8 +67,10 @@ func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv
 	if err != nil && !errors.Is(err, esv1.NoSecretError{}) {
 		return err
 	}
-	// If the secret exists (err == nil), we should check if it is managed by external-secrets
-	if err == nil {
+
+	secretExists := err == nil
+	// If the secret exists, we should check if it is managed by external-secrets
+	if secretExists {
 		metadata, err := c.readSecretMetadata(ctx, data.GetRemoteKey())
 		if err != nil {
 			return err
@@ -123,6 +125,18 @@ func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv
 		secretToPush = map[string]any{
 			"data": secretVal,
 		}
+
+		// Add CAS options if required
+		if c.store.CheckAndSet != nil && c.store.CheckAndSet.Required {
+			casVersion, casErr := c.getCASVersion(ctx, data.GetRemoteKey(), secretExists)
+			if casErr != nil {
+				return fmt.Errorf("failed to get CAS version: %w", casErr)
+			}
+
+			secretToPush["options"] = map[string]any{
+				"cas": casVersion,
+			}
+		}
 	}
 	if err != nil {
 		return fmt.Errorf("failed to convert value to a valid JSON: %w", err)
@@ -197,3 +211,58 @@ func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemo
 	}
 	return nil
 }
+
+// getCASVersion retrieves the current version of the secret for check-and-set operations.
+// Returns:
+//   - 0 for new secrets (CAS version 0 means "create only if doesn't exist")
+//   - N for existing secrets (CAS version N means "update only if current version is N")
+func (c *client) getCASVersion(ctx context.Context, remoteKey string, secretExists bool) (int, error) {
+	// For new secrets, use CAS version 0 (create only if doesn't exist)
+	if !secretExists {
+		return 0, nil
+	}
+
+	// For existing secrets, read the full metadata to get current version
+	metaPath, err := c.buildMetadataPath(remoteKey)
+	if err != nil {
+		return 0, fmt.Errorf("failed to build metadata path: %w", err)
+	}
+
+	secret, err := c.logical.ReadWithDataWithContext(ctx, metaPath, nil)
+	if err != nil {
+		return 0, fmt.Errorf("failed to read metadata: %w", err)
+	}
+
+	if secret == nil || secret.Data == nil {
+		// If no metadata found for an existing secret, assume this is version 1.
+		// This can happen with older secrets that were created before version tracking.
+		// Vault KV v2 secrets start at version 1 (not 0) when first created.
+		return 1, nil
+	}
+
+	return getCurrentVersionFromMetadata(secret.Data)
+}
+
+func getCurrentVersionFromMetadata(data map[string]any) (int, error) {
+	var err error
+	if currentVersion, ok := data["current_version"]; ok {
+		switch v := currentVersion.(type) {
+		case int:
+			return v, nil
+		case float64:
+			return int(v), nil
+		case json.Number:
+			if intVal, err := v.Int64(); err == nil {
+				return int(intVal), nil
+			}
+			return 0, fmt.Errorf("failed to convert json.Number to int: %w", err)
+		default:
+			return 0, fmt.Errorf("unexpected type for current_version: %T", currentVersion)
+		}
+	}
+
+	// If metadata exists but no current_version found, assume this is version 1.
+	// This handles edge cases with legacy secrets or incomplete metadata.
+	// Vault KV v2 secrets start at version 1, so this is the safest assumption.
+	return 1, nil
+}

+ 126 - 0
pkg/provider/vault/client_push_test.go

@@ -664,6 +664,124 @@ func TestPushSecret(t *testing.T) {
 				err: nil,
 			},
 		},
+		"CASRequiredNewSecretKV2": {
+			reason: "CAS required: new secret should be created with cas=0",
+			args: args{
+				store: makeValidSecretStoreWithCASRequired(esv1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]any{
+						"options": map[string]any{
+							"cas": 0,
+						},
+						"data": map[string]any{fakeKey: fakeValue},
+					}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"CASRequiredExistingSecretKV2": {
+			reason: "CAS required: existing secret should be updated with current version",
+			args: args{
+				store: makeValidSecretStoreWithCASRequired(esv1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithDataAndMetadataFn(
+						map[string]any{
+							"data": map[string]any{
+								"existing": "value",
+							},
+						},
+						map[string]any{
+							"custom_metadata": map[string]any{
+								managedBy: managedByESO,
+							},
+							"current_version": 3,
+						},
+						nil, nil,
+					),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]any{
+						"options": map[string]any{
+							"cas": 3,
+						},
+						"data": map[string]any{fakeKey: fakeValue},
+					}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"CASRequiredPropertyUpdateKV2": {
+			reason: "CAS required: property update should use current version",
+			value:  []byte("property-value"),
+			data:   &testingfake.PushSecretData{SecretKey: "secret-key", RemoteKey: "secret", Property: "new-prop"},
+			args: args{
+				store: makeValidSecretStoreWithCASRequired(esv1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithDataAndMetadataFn(
+						map[string]any{
+							"data": map[string]any{
+								"existing": "value",
+							},
+						},
+						map[string]any{
+							"custom_metadata": map[string]any{
+								managedBy: managedByESO,
+							},
+							"current_version": 2,
+						},
+						nil, nil,
+					),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]any{
+						"options": map[string]any{
+							"cas": 2,
+						},
+						"data": map[string]any{
+							"existing": "value",
+							"new-prop": "property-value",
+						},
+					}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"CASNotRequiredKV2": {
+			reason: "CAS not required: should work without CAS options",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1.VaultKVStoreV2).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]any{
+						"data": map[string]any{fakeKey: fakeValue},
+					}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"CASIgnoredKV1": {
+			reason: "CAS ignored for KV v1: should work without CAS options even when required",
+			args: args{
+				store: makeValidSecretStoreWithCASRequired(esv1.VaultKVStoreV1).Spec.Provider.Vault,
+				vLogical: &fake.Logical{
+					ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, nil),
+					WriteWithContextFn: fake.ExpectWriteWithContextValue(map[string]any{
+						fakeKey: fakeValue,
+						"custom_metadata": map[string]string{
+							managedBy: managedByESO,
+						},
+					}),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
 	}
 
 	for name, tc := range tests {
@@ -700,3 +818,11 @@ func TestPushSecret(t *testing.T) {
 		})
 	}
 }
+
+func makeValidSecretStoreWithCASRequired(version esv1.VaultKVStoreVersion) *esv1.SecretStore {
+	store := makeValidSecretStoreWithVersion(version)
+	store.Spec.Provider.Vault.CheckAndSet = &esv1.VaultCheckAndSet{
+		Required: true,
+	}
+	return store
+}

+ 34 - 15
pkg/provider/vault/fake/vault.go

@@ -59,29 +59,48 @@ func NewDeleteWithContextFn(secret map[string]any, err error) DeleteWithContextF
 	}
 }
 
+func buildDataResponse(secret map[string]any, err error) (*vault.Secret, error) {
+	if secret == nil {
+		return nil, err
+	}
+	return &vault.Secret{Data: secret}, err
+}
+
+func buildMetadataResponse(secret map[string]any, err error) (*vault.Secret, error) {
+	if secret == nil {
+		return nil, err
+	}
+	// If the secret already has the expected metadata structure, return as-is
+	if _, hasCustomMetadata := secret["custom_metadata"]; hasCustomMetadata {
+		return &vault.Secret{Data: secret}, err
+	}
+	// Otherwise, wrap in custom_metadata for backwards compatibility
+	metadata := make(map[string]any)
+	metadata["custom_metadata"] = secret
+	return &vault.Secret{Data: metadata}, err
+}
+
 func NewReadWithContextFn(secret map[string]any, err error) ReadWithDataWithContextFn {
 	return func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
-		if secret == nil {
-			return nil, err
-		}
-		vault := &vault.Secret{
-			Data: secret,
-		}
-		return vault, err
+		return buildDataResponse(secret, err)
 	}
 }
 
 func NewReadMetadataWithContextFn(secret map[string]any, err error) ReadWithDataWithContextFn {
 	return func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
-		if secret == nil {
-			return nil, err
-		}
-		metadata := make(map[string]any)
-		metadata["custom_metadata"] = secret
-		vault := &vault.Secret{
-			Data: metadata,
+		return buildMetadataResponse(secret, err)
+	}
+}
+
+func NewReadWithDataAndMetadataFn(dataSecret, metadataSecret map[string]any, dataErr, metadataErr error) ReadWithDataWithContextFn {
+	return func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
+		// Check if this is a metadata path request
+		if strings.Contains(path, "/metadata/") {
+			return buildMetadataResponse(metadataSecret, metadataErr)
 		}
-		return vault, err
+
+		// This is a data path request
+		return buildDataResponse(dataSecret, dataErr)
 	}
 }
 

+ 9 - 0
pkg/provider/vault/validate.go

@@ -45,6 +45,7 @@ const (
 	errInvalidClientTLSCert   = "invalid ClientTLS.ClientCert: %w"
 	errInvalidClientTLSSecret = "invalid ClientTLS.SecretRef: %w"
 	errInvalidClientTLS       = "when provided, both ClientTLS.ClientCert and ClientTLS.SecretRef should be provided"
+	errCASNotSupportedInKVv1  = "checkAndSet is not supported with Vault KV version v1"
 )
 
 func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
@@ -162,6 +163,14 @@ func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, e
 	} else if vaultProvider.ClientTLS.CertSecretRef != nil || vaultProvider.ClientTLS.KeySecretRef != nil {
 		return nil, errors.New(errInvalidClientTLS)
 	}
+
+	// Validate CAS configuration
+	if vaultProvider.CheckAndSet != nil && vaultProvider.CheckAndSet.Required {
+		if vaultProvider.Version == esv1.VaultKVStoreV1 {
+			return nil, errors.New(errCASNotSupportedInKVv1)
+		}
+	}
+
 	return nil, nil
 }
 

+ 59 - 4
pkg/provider/vault/validate_test.go

@@ -26,8 +26,10 @@ const fakeValidationValue = "fake-value"
 
 func TestValidateStore(t *testing.T) {
 	type args struct {
-		auth      esv1.VaultAuth
-		clientTLS esv1.VaultClientTLS
+		auth        esv1.VaultAuth
+		clientTLS   esv1.VaultClientTLS
+		version     esv1.VaultKVStoreVersion
+		checkAndSet *esv1.VaultCheckAndSet
 	}
 
 	tests := []struct {
@@ -248,6 +250,57 @@ func TestValidateStore(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "valid CAS config with KV v2",
+			args: args{
+				auth: esv1.VaultAuth{
+					AppRole: &esv1.VaultAppRole{
+						RoleRef: &esmeta.SecretKeySelector{
+							Name: fakeValidationValue,
+						},
+					},
+				},
+				version: esv1.VaultKVStoreV2,
+				checkAndSet: &esv1.VaultCheckAndSet{
+					Required: true,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "invalid CAS config with KV v1",
+			args: args{
+				auth: esv1.VaultAuth{
+					AppRole: &esv1.VaultAppRole{
+						RoleRef: &esmeta.SecretKeySelector{
+							Name: fakeValidationValue,
+						},
+					},
+				},
+				version: esv1.VaultKVStoreV1,
+				checkAndSet: &esv1.VaultCheckAndSet{
+					Required: true,
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "CAS config not required is valid with any version",
+			args: args{
+				auth: esv1.VaultAuth{
+					AppRole: &esv1.VaultAppRole{
+						RoleRef: &esmeta.SecretKeySelector{
+							Name: fakeValidationValue,
+						},
+					},
+				},
+				version: esv1.VaultKVStoreV1,
+				checkAndSet: &esv1.VaultCheckAndSet{
+					Required: false,
+				},
+			},
+			wantErr: false,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -259,8 +312,10 @@ func TestValidateStore(t *testing.T) {
 				Spec: esv1.SecretStoreSpec{
 					Provider: &esv1.SecretStoreProvider{
 						Vault: &esv1.VaultProvider{
-							Auth:      &auth,
-							ClientTLS: tt.args.clientTLS,
+							Auth:        &auth,
+							ClientTLS:   tt.args.clientTLS,
+							Version:     tt.args.version,
+							CheckAndSet: tt.args.checkAndSet,
 						},
 					},
 				},