Browse Source

feat: AWS SecretsManager Config (allow ForceDeleteWithoutRecovery for PushSecret) (#2854)

* Add secretsmanager config.

Signed-off-by: Yonatan Koren <10080107+korenyoni@users.noreply.github.com>

* Fix unit tests.

Signed-off-by: Yonatan Koren <10080107+korenyoni@users.noreply.github.com>

* Update docs, fix validation, tests.

Signed-off-by: Yonatan Koren <10080107+korenyoni@users.noreply.github.com>

* Fix grammatical error in attribute descriptions.

Signed-off-by: Yonatan Koren <10080107+korenyoni@users.noreply.github.com>

* Improve API docs for SecretsManager.

Signed-off-by: Yonatan Koren <10080107+korenyoni@users.noreply.github.com>

---------

Signed-off-by: Yonatan Koren <10080107+korenyoni@users.noreply.github.com>
Yonatan Koren 2 years ago
parent
commit
d42e19dc70

+ 30 - 5
apis/externalsecrets/v1beta1/secretstore_aws_types.go

@@ -54,14 +54,35 @@ type AWSJWTAuth struct {
 type AWSServiceType string
 
 const (
-	// AWSServiceSecretsManager is the AWS SecretsManager.
+	// AWSServiceSecretsManager is the AWS SecretsManager service.
 	// see: https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html
 	AWSServiceSecretsManager AWSServiceType = "SecretsManager"
-	// AWSServiceParameterStore is the AWS SystemsManager ParameterStore.
+	// AWSServiceParameterStore is the AWS SystemsManager ParameterStore service.
 	// see: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html
 	AWSServiceParameterStore AWSServiceType = "ParameterStore"
 )
 
+// SecretsManager defines how the provider behaves when interacting with AWS
+// SecretsManager. Some of these settings are only applicable to controlling how
+// secrets are deleted, and hence only apply to PushSecret (and only when
+// deletionPolicy is set to Delete).
+type SecretsManager struct {
+	// Specifies whether to delete the secret without any recovery window. You
+	// can't use both this parameter and RecoveryWindowInDays in the same call.
+	// If you don't use either, then by default Secrets Manager uses a 30 day
+	// recovery window.
+	// see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-ForceDeleteWithoutRecovery
+	// +optional
+	ForceDeleteWithoutRecovery bool `json:"forceDeleteWithoutRecovery,omitempty"`
+	// The number of days from 7 to 30 that Secrets Manager waits before
+	// permanently deleting the secret. You can't use both this parameter and
+	// ForceDeleteWithoutRecovery in the same call. If you don't use either,
+	// then by default Secrets Manager uses a 30 day recovery window.
+	// see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-RecoveryWindowInDays
+	// +optional
+	RecoveryWindowInDays int64 `json:"recoveryWindowInDays,omitempty"`
+}
+
 type Tag struct {
 	Key   string `json:"key"`
 	Value string `json:"value"`
@@ -78,14 +99,14 @@ type AWSProvider struct {
 	// +optional
 	Auth AWSAuth `json:"auth,omitempty"`
 
-	// Role is a Role ARN which the SecretManager provider will assume
+	// Role is a Role ARN which the provider will assume
 	// +optional
 	Role string `json:"role,omitempty"`
 
 	// AWS Region to be used for the provider
 	Region string `json:"region"`
 
-	// AdditionalRoles is a chained list of Role ARNs which the SecretManager provider will sequentially assume before assuming Role
+	// AdditionalRoles is a chained list of Role ARNs which the provider will sequentially assume before assuming the Role
 	// +optional
 	AdditionalRoles []string `json:"additionalRoles,omitempty"`
 
@@ -96,7 +117,11 @@ type AWSProvider struct {
 	// +optional
 	SessionTags []*Tag `json:"sessionTags,omitempty"`
 
-	// AWS STS assume role transitive session tags. Required when multiple rules are used with SecretStore
+	// SecretsManager defines how the provider behaves when interacting with AWS SecretsManager
+	// +optional
+	SecretsManager *SecretsManager `json:"secretsManager,omitempty"`
+
+	// AWS STS assume role transitive session tags. Required when multiple rules are used with the provider
 	// +optional
 	TransitiveTagKeys []*string `json:"transitiveTagKeys,omitempty"`
 }

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

@@ -111,6 +111,11 @@ func (in *AWSProvider) DeepCopyInto(out *AWSProvider) {
 			}
 		}
 	}
+	if in.SecretsManager != nil {
+		in, out := &in.SecretsManager, &out.SecretsManager
+		*out = new(SecretsManager)
+		**out = **in
+	}
 	if in.TransitiveTagKeys != nil {
 		in, out := &in.TransitiveTagKeys, &out.TransitiveTagKeys
 		*out = make([]*string, len(*in))
@@ -2110,6 +2115,21 @@ func (in *SecretStoreStatusCondition) DeepCopy() *SecretStoreStatusCondition {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SecretsManager) DeepCopyInto(out *SecretsManager) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretsManager.
+func (in *SecretsManager) DeepCopy() *SecretsManager {
+	if in == nil {
+		return nil
+	}
+	out := new(SecretsManager)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SenhaseguraAuth) DeepCopyInto(out *SenhaseguraAuth) {
 	*out = *in
 	in.ClientSecret.DeepCopyInto(&out.ClientSecret)

+ 25 - 5
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -1972,8 +1972,8 @@ spec:
                     properties:
                       additionalRoles:
                         description: AdditionalRoles is a chained list of Role ARNs
-                          which the SecretManager provider will sequentially assume
-                          before assuming Role
+                          which the provider will sequentially assume before assuming
+                          the Role
                         items:
                           type: string
                         type: array
@@ -2088,9 +2088,29 @@ spec:
                         description: AWS Region to be used for the provider
                         type: string
                       role:
-                        description: Role is a Role ARN which the SecretManager provider
-                          will assume
+                        description: Role is a Role ARN which the provider will assume
                         type: string
+                      secretsManager:
+                        description: SecretsManager defines how the provider behaves
+                          when interacting with AWS SecretsManager
+                        properties:
+                          forceDeleteWithoutRecovery:
+                            description: 'Specifies whether to delete the secret without
+                              any recovery window. You can''t use both this parameter
+                              and RecoveryWindowInDays in the same call. If you don''t
+                              use either, then by default Secrets Manager uses a 30
+                              day recovery window. see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-ForceDeleteWithoutRecovery'
+                            type: boolean
+                          recoveryWindowInDays:
+                            description: 'The number of days from 7 to 30 that Secrets
+                              Manager waits before permanently deleting the secret.
+                              You can''t use both this parameter and ForceDeleteWithoutRecovery
+                              in the same call. If you don''t use either, then by
+                              default Secrets Manager uses a 30 day recovery window.
+                              see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-RecoveryWindowInDays'
+                            format: int64
+                            type: integer
+                        type: object
                       service:
                         description: Service defines which service should be used
                           to fetch the secrets
@@ -2113,7 +2133,7 @@ spec:
                         type: array
                       transitiveTagKeys:
                         description: AWS STS assume role transitive session tags.
-                          Required when multiple rules are used with SecretStore
+                          Required when multiple rules are used with the provider
                         items:
                           type: string
                         type: array

+ 25 - 5
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -1972,8 +1972,8 @@ spec:
                     properties:
                       additionalRoles:
                         description: AdditionalRoles is a chained list of Role ARNs
-                          which the SecretManager provider will sequentially assume
-                          before assuming Role
+                          which the provider will sequentially assume before assuming
+                          the Role
                         items:
                           type: string
                         type: array
@@ -2088,9 +2088,29 @@ spec:
                         description: AWS Region to be used for the provider
                         type: string
                       role:
-                        description: Role is a Role ARN which the SecretManager provider
-                          will assume
+                        description: Role is a Role ARN which the provider will assume
                         type: string
+                      secretsManager:
+                        description: SecretsManager defines how the provider behaves
+                          when interacting with AWS SecretsManager
+                        properties:
+                          forceDeleteWithoutRecovery:
+                            description: 'Specifies whether to delete the secret without
+                              any recovery window. You can''t use both this parameter
+                              and RecoveryWindowInDays in the same call. If you don''t
+                              use either, then by default Secrets Manager uses a 30
+                              day recovery window. see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-ForceDeleteWithoutRecovery'
+                            type: boolean
+                          recoveryWindowInDays:
+                            description: 'The number of days from 7 to 30 that Secrets
+                              Manager waits before permanently deleting the secret.
+                              You can''t use both this parameter and ForceDeleteWithoutRecovery
+                              in the same call. If you don''t use either, then by
+                              default Secrets Manager uses a 30 day recovery window.
+                              see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-RecoveryWindowInDays'
+                            format: int64
+                            type: integer
+                        type: object
                       service:
                         description: Service defines which service should be used
                           to fetch the secrets
@@ -2113,7 +2133,7 @@ spec:
                         type: array
                       transitiveTagKeys:
                         description: AWS STS assume role transitive session tags.
-                          Required when multiple rules are used with SecretStore
+                          Required when multiple rules are used with the provider
                         items:
                           type: string
                         type: array

+ 28 - 6
deploy/crds/bundle.yaml

@@ -1961,7 +1961,7 @@ spec:
                       description: AWS configures this store to sync secrets using AWS Secret Manager provider
                       properties:
                         additionalRoles:
-                          description: AdditionalRoles is a chained list of Role ARNs which the SecretManager provider will sequentially assume before assuming Role
+                          description: AdditionalRoles is a chained list of Role ARNs which the provider will sequentially assume before assuming the Role
                           items:
                             type: string
                           type: array
@@ -2040,8 +2040,19 @@ spec:
                           description: AWS Region to be used for the provider
                           type: string
                         role:
-                          description: Role is a Role ARN which the SecretManager provider will assume
+                          description: Role is a Role ARN which the provider will assume
                           type: string
+                        secretsManager:
+                          description: SecretsManager defines how the provider behaves when interacting with AWS SecretsManager
+                          properties:
+                            forceDeleteWithoutRecovery:
+                              description: 'Specifies whether to delete the secret without any recovery window. You can''t use both this parameter and RecoveryWindowInDays in the same call. If you don''t use either, then by default Secrets Manager uses a 30 day recovery window. see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-ForceDeleteWithoutRecovery'
+                              type: boolean
+                            recoveryWindowInDays:
+                              description: 'The number of days from 7 to 30 that Secrets Manager waits before permanently deleting the secret. You can''t use both this parameter and ForceDeleteWithoutRecovery in the same call. If you don''t use either, then by default Secrets Manager uses a 30 day recovery window. see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-RecoveryWindowInDays'
+                              format: int64
+                              type: integer
+                          type: object
                         service:
                           description: Service defines which service should be used to fetch the secrets
                           enum:
@@ -2062,7 +2073,7 @@ spec:
                             type: object
                           type: array
                         transitiveTagKeys:
-                          description: AWS STS assume role transitive session tags. Required when multiple rules are used with SecretStore
+                          description: AWS STS assume role transitive session tags. Required when multiple rules are used with the provider
                           items:
                             type: string
                           type: array
@@ -5884,7 +5895,7 @@ spec:
                       description: AWS configures this store to sync secrets using AWS Secret Manager provider
                       properties:
                         additionalRoles:
-                          description: AdditionalRoles is a chained list of Role ARNs which the SecretManager provider will sequentially assume before assuming Role
+                          description: AdditionalRoles is a chained list of Role ARNs which the provider will sequentially assume before assuming the Role
                           items:
                             type: string
                           type: array
@@ -5963,8 +5974,19 @@ spec:
                           description: AWS Region to be used for the provider
                           type: string
                         role:
-                          description: Role is a Role ARN which the SecretManager provider will assume
+                          description: Role is a Role ARN which the provider will assume
                           type: string
+                        secretsManager:
+                          description: SecretsManager defines how the provider behaves when interacting with AWS SecretsManager
+                          properties:
+                            forceDeleteWithoutRecovery:
+                              description: 'Specifies whether to delete the secret without any recovery window. You can''t use both this parameter and RecoveryWindowInDays in the same call. If you don''t use either, then by default Secrets Manager uses a 30 day recovery window. see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-ForceDeleteWithoutRecovery'
+                              type: boolean
+                            recoveryWindowInDays:
+                              description: 'The number of days from 7 to 30 that Secrets Manager waits before permanently deleting the secret. You can''t use both this parameter and ForceDeleteWithoutRecovery in the same call. If you don''t use either, then by default Secrets Manager uses a 30 day recovery window. see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-RecoveryWindowInDays'
+                              format: int64
+                              type: integer
+                          type: object
                         service:
                           description: Service defines which service should be used to fetch the secrets
                           enum:
@@ -5985,7 +6007,7 @@ spec:
                             type: object
                           type: array
                         transitiveTagKeys:
-                          description: AWS STS assume role transitive session tags. Required when multiple rules are used with SecretStore
+                          description: AWS STS assume role transitive session tags. Required when multiple rules are used with the provider
                           items:
                             type: string
                           type: array

+ 73 - 5
docs/api/spec.md

@@ -204,7 +204,7 @@ string
 </td>
 <td>
 <em>(Optional)</em>
-<p>Role is a Role ARN which the SecretManager provider will assume</p>
+<p>Role is a Role ARN which the provider will assume</p>
 </td>
 </tr>
 <tr>
@@ -227,7 +227,7 @@ string
 </td>
 <td>
 <em>(Optional)</em>
-<p>AdditionalRoles is a chained list of Role ARNs which the SecretManager provider will sequentially assume before assuming Role</p>
+<p>AdditionalRoles is a chained list of Role ARNs which the provider will sequentially assume before assuming the Role</p>
 </td>
 </tr>
 <tr>
@@ -257,6 +257,20 @@ string
 </tr>
 <tr>
 <td>
+<code>secretsManager</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.SecretsManager">
+SecretsManager
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>SecretsManager defines how the provider behaves when interacting with AWS SecretsManager</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>transitiveTagKeys</code></br>
 <em>
 []*string
@@ -264,7 +278,7 @@ string
 </td>
 <td>
 <em>(Optional)</em>
-<p>AWS STS assume role transitive session tags. Required when multiple rules are used with SecretStore</p>
+<p>AWS STS assume role transitive session tags. Required when multiple rules are used with the provider</p>
 </td>
 </tr>
 </tbody>
@@ -286,11 +300,11 @@ string
 </tr>
 </thead>
 <tbody><tr><td><p>&#34;ParameterStore&#34;</p></td>
-<td><p>AWSServiceParameterStore is the AWS SystemsManager ParameterStore.
+<td><p>AWSServiceParameterStore is the AWS SystemsManager ParameterStore service.
 see: <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html">https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html</a></p>
 </td>
 </tr><tr><td><p>&#34;SecretsManager&#34;</p></td>
-<td><p>AWSServiceSecretsManager is the AWS SecretsManager.
+<td><p>AWSServiceSecretsManager is the AWS SecretsManager service.
 see: <a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html">https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html</a></p>
 </td>
 </tr></tbody>
@@ -5573,6 +5587,60 @@ Kubernetes meta/v1.Time
 <p>
 <p>SecretsClient provides access to secrets.</p>
 </p>
+<h3 id="external-secrets.io/v1beta1.SecretsManager">SecretsManager
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.AWSProvider">AWSProvider</a>)
+</p>
+<p>
+<p>SecretsManager defines how the provider behaves when interacting with AWS
+SecretsManager. Some of these settings are only applicable to controlling how
+secrets are deleted, and hence only apply to PushSecret (and only when
+deletionPolicy is set to Delete).</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>forceDeleteWithoutRecovery</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Specifies whether to delete the secret without any recovery window. You
+can&rsquo;t use both this parameter and RecoveryWindowInDays in the same call.
+If you don&rsquo;t use either, then by default Secrets Manager uses a 30 day
+recovery window.
+see: <a href="https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-ForceDeleteWithoutRecovery">https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-ForceDeleteWithoutRecovery</a></p>
+</td>
+</tr>
+<tr>
+<td>
+<code>recoveryWindowInDays</code></br>
+<em>
+int64
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The number of days from 7 to 30 that Secrets Manager waits before
+permanently deleting the secret. You can&rsquo;t use both this parameter and
+ForceDeleteWithoutRecovery in the same call. If you don&rsquo;t use either,
+then by default Secrets Manager uses a 30 day recovery window.
+see: <a href="https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-RecoveryWindowInDays">https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#SecretsManager-DeleteSecret-request-RecoveryWindowInDays</a></p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.SenhaseguraAuth">SenhaseguraAuth
 </h3>
 <p>

+ 8 - 0
docs/provider/aws-secrets-manager.md

@@ -92,6 +92,14 @@ Here's a more restrictive version of the IAM policy:
 
 In this policy, the DeleteSecret action is restricted to secrets that have the specified tag, ensuring that deletion operations are more controlled and in line with the intended management of the secrets.
 
+#### Additional Settings for PushSecret
+
+Additional settings can be set at the `SecretStore` level to control the behavior of `PushSecret` when interacting with AWS Secrets Manager.
+
+```yaml
+{% include 'aws-sm-store-secretsmanager-config.yaml' %}
+```
+
 ### JSON Secret Values
 
 SecretsManager supports *simple* key/value pairs that are stored as json. If you use the API you can store more complex JSON objects. You can access nested values or arrays using [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md):

+ 16 - 0
docs/snippets/aws-sm-store-secretsmanager-config.yaml

@@ -0,0 +1,16 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: aws-secretsmanager
+spec:
+  provider:
+    aws:
+      service: SecretsManager
+      role: arn:aws:iam::123456789012:role/external-secrets
+      region: eu-central-1
+      secretsManager:
+        # Additional parameters can be added to the AWS Secrets Manager DeleteSecret API call.
+        # These parameters are only relevant when the deletionPolicy is set to Delete.
+        # See: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html#API_DeleteSecret_RequestSyntax
+        forceDeleteWithoutRecovery: true
+        # recoveryWindowInDays: 9 (conflicts with forceDeleteWithoutRecovery)

+ 18 - 2
pkg/provider/aws/provider.go

@@ -24,6 +24,7 @@ import (
 	"github.com/aws/aws-sdk-go/aws/endpoints"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/session"
+	awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@@ -45,6 +46,7 @@ const (
 	errUnknownProviderService = "unknown AWS Provider Service: %s"
 	errRegionNotFound         = "region not found: %s"
 	errInitAWSProvider        = "unable to initialize aws provider: %s"
+	errInvalidSecretsManager  = "invalid SecretsManager settings: %s"
 )
 
 // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
@@ -66,6 +68,10 @@ func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
 	if err != nil {
 		return err
 	}
+	err = validateSecretsManagerConfig(prov)
+	if err != nil {
+		return err
+	}
 
 	// case: static credentials
 	if prov.Auth.SecretRef != nil {
@@ -118,6 +124,16 @@ func validateRegion(prov *esv1beta1.AWSProvider) error {
 	return nil
 }
 
+func validateSecretsManagerConfig(prov *esv1beta1.AWSProvider) error {
+	if prov.SecretsManager == nil {
+		return nil
+	}
+	return util.ValidateDeleteSecretInput(awssm.DeleteSecretInput{
+		ForceDeleteWithoutRecovery: &prov.SecretsManager.ForceDeleteWithoutRecovery,
+		RecoveryWindowInDays:       &prov.SecretsManager.RecoveryWindowInDays,
+	})
+}
+
 func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, assumeRoler awsauth.STSProvider) (esv1beta1.SecretsClient, error) {
 	prov, err := util.GetAWSProvider(store)
 	if err != nil {
@@ -137,7 +153,7 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
 		sess := &session.Session{Config: cfg}
 		switch prov.Service {
 		case esv1beta1.AWSServiceSecretsManager:
-			return secretsmanager.New(sess, cfg, true)
+			return secretsmanager.New(sess, cfg, prov.SecretsManager, true)
 		case esv1beta1.AWSServiceParameterStore:
 			return parameterstore.New(sess, cfg, true)
 		}
@@ -176,7 +192,7 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
 
 	switch prov.Service {
 	case esv1beta1.AWSServiceSecretsManager:
-		return secretsmanager.New(sess, cfg, false)
+		return secretsmanager.New(sess, cfg, prov.SecretsManager, false)
 	case esv1beta1.AWSServiceParameterStore:
 		return parameterstore.New(sess, cfg, false)
 	}

+ 86 - 7
pkg/provider/aws/provider_test.go

@@ -197,13 +197,13 @@ func TestValidateStore(t *testing.T) {
 			},
 		},
 		{
-			name: "valid region secrets manager",
+			name: "valid fips region secrets manager",
 			args: args{
 				store: &esv1beta1.SecretStore{
 					Spec: esv1beta1.SecretStoreSpec{
 						Provider: &esv1beta1.SecretStoreProvider{
 							AWS: &esv1beta1.AWSProvider{
-								Region:  validRegion,
+								Region:  validFipsSecretManagerRegion,
 								Service: esv1beta1.AWSServiceSecretsManager,
 							},
 						},
@@ -212,14 +212,32 @@ func TestValidateStore(t *testing.T) {
 			},
 		},
 		{
-			name: "valid fips region secrets manager",
+			name: "valid fips region parameter store",
 			args: args{
 				store: &esv1beta1.SecretStore{
 					Spec: esv1beta1.SecretStoreSpec{
 						Provider: &esv1beta1.SecretStoreProvider{
 							AWS: &esv1beta1.AWSProvider{
-								Region:  validFipsSecretManagerRegion,
+								Region:  validFipsSsmRegion,
+								Service: esv1beta1.AWSServiceParameterStore,
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "valid secretsmanager config: force delete without recovery",
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region:  validRegion,
 								Service: esv1beta1.AWSServiceSecretsManager,
+								SecretsManager: &esv1beta1.SecretsManager{
+									ForceDeleteWithoutRecovery: true,
+								},
 							},
 						},
 					},
@@ -227,14 +245,17 @@ func TestValidateStore(t *testing.T) {
 			},
 		},
 		{
-			name: "valid fips region parameter store",
+			name: "valid secretsmanager config: recovery window",
 			args: args{
 				store: &esv1beta1.SecretStore{
 					Spec: esv1beta1.SecretStoreSpec{
 						Provider: &esv1beta1.SecretStoreProvider{
 							AWS: &esv1beta1.AWSProvider{
-								Region:  validFipsSsmRegion,
-								Service: esv1beta1.AWSServiceParameterStore,
+								Region:  validRegion,
+								Service: esv1beta1.AWSServiceSecretsManager,
+								SecretsManager: &esv1beta1.SecretsManager{
+									RecoveryWindowInDays: 30,
+								},
 							},
 						},
 					},
@@ -391,6 +412,64 @@ func TestValidateStore(t *testing.T) {
 				},
 			},
 		},
+		{
+			name:    "invalid SecretsManager config: conflicting settings",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1beta1.AWSServiceSecretsManager,
+								SecretsManager: &esv1beta1.SecretsManager{
+									ForceDeleteWithoutRecovery: true,
+									RecoveryWindowInDays:       7,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid SecretsManager config: recovery window too small",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1beta1.AWSServiceSecretsManager,
+								SecretsManager: &esv1beta1.SecretsManager{
+									RecoveryWindowInDays: 6,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:    "invalid SecretsManager config: recovery window too big",
+			wantErr: true,
+			args: args{
+				store: &esv1beta1.SecretStore{
+					Spec: esv1beta1.SecretStoreSpec{
+						Provider: &esv1beta1.SecretStoreProvider{
+							AWS: &esv1beta1.AWSProvider{
+								Region:  validRegion,
+								Service: esv1beta1.AWSServiceSecretsManager,
+								SecretsManager: &esv1beta1.SecretsManager{
+									RecoveryWindowInDays: 31,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {

+ 4 - 0
pkg/provider/aws/secretsmanager/fake/fake.go

@@ -17,6 +17,7 @@ package fake
 import (
 	"bytes"
 	"fmt"
+	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/request"
@@ -65,6 +66,9 @@ func (sm Client) DeleteSecretWithContext(ctx aws.Context, input *awssm.DeleteSec
 
 func NewDeleteSecretWithContextFn(output *awssm.DeleteSecretOutput, err error) DeleteSecretWithContextFn {
 	return func(ctx aws.Context, input *awssm.DeleteSecretInput, opts ...request.Option) (*awssm.DeleteSecretOutput, error) {
+		if input.ForceDeleteWithoutRecovery != nil && *input.ForceDeleteWithoutRecovery {
+			output.SetDeletionDate(time.Now())
+		}
 		return output, err
 	}
 }

+ 13 - 1
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -51,6 +51,7 @@ type SecretsManager struct {
 	client       SMInterface
 	referentAuth bool
 	cache        map[string]*awssm.GetSecretValueOutput
+	config       *esv1beta1.SecretsManager
 }
 
 // SMInterface is a subset of the smiface api.
@@ -75,12 +76,13 @@ const (
 var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager")
 
 // New creates a new SecretsManager client.
-func New(sess *session.Session, cfg *aws.Config, referentAuth bool) (*SecretsManager, error) {
+func New(sess *session.Session, cfg *aws.Config, secretsManagerCfg *esv1beta1.SecretsManager, referentAuth bool) (*SecretsManager, error) {
 	return &SecretsManager{
 		sess:         sess,
 		client:       awssm.New(sess, cfg),
 		referentAuth: referentAuth,
 		cache:        make(map[string]*awssm.GetSecretValueOutput),
+		config:       secretsManagerCfg,
 	}, nil
 }
 
@@ -187,6 +189,16 @@ func (sm *SecretsManager) DeleteSecret(ctx context.Context, remoteRef esv1beta1.
 	deleteInput := &awssm.DeleteSecretInput{
 		SecretId: awsSecret.ARN,
 	}
+	if sm.config != nil && sm.config.ForceDeleteWithoutRecovery {
+		deleteInput.ForceDeleteWithoutRecovery = &sm.config.ForceDeleteWithoutRecovery
+	}
+	if sm.config != nil && sm.config.RecoveryWindowInDays > 0 {
+		deleteInput.RecoveryWindowInDays = &sm.config.RecoveryWindowInDays
+	}
+	err = util.ValidateDeleteSecretInput(*deleteInput)
+	if err != nil {
+		return err
+	}
 	_, err = sm.client.DeleteSecretWithContext(ctx, deleteInput)
 	metrics.ObserveAPICall(constants.ProviderAWSSM, constants.CallAWSSMDeleteSecret, err)
 	return err

+ 78 - 2
pkg/provider/aws/secretsmanager/secretsmanager_test.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
@@ -779,6 +780,7 @@ func TestDeleteSecret(t *testing.T) {
 	}
 	type args struct {
 		client               fakesm.Client
+		config               esv1beta1.SecretsManager
 		getSecretOutput      *awssm.GetSecretValueOutput
 		describeSecretOutput *awssm.DescribeSecretOutput
 		deleteSecretOutput   *awssm.DeleteSecretOutput
@@ -799,6 +801,7 @@ func TestDeleteSecret(t *testing.T) {
 			args: args{
 
 				client:          fakeClient,
+				config:          esv1beta1.SecretsManager{},
 				getSecretOutput: &awssm.GetSecretValueOutput{},
 				describeSecretOutput: &awssm.DescribeSecretOutput{
 					Tags: []*awssm.Tag{&secretTag},
@@ -813,10 +816,34 @@ func TestDeleteSecret(t *testing.T) {
 			},
 			reason: "",
 		},
+		"Deletes Successfully with ForceDeleteWithoutRecovery": {
+			args: args{
+
+				client: fakeClient,
+				config: esv1beta1.SecretsManager{
+					ForceDeleteWithoutRecovery: true,
+				},
+				getSecretOutput: &awssm.GetSecretValueOutput{},
+				describeSecretOutput: &awssm.DescribeSecretOutput{
+					Tags: []*awssm.Tag{&secretTag},
+				},
+				deleteSecretOutput: &awssm.DeleteSecretOutput{
+					DeletionDate: aws.Time(time.Now()),
+				},
+				getSecretErr:      nil,
+				describeSecretErr: nil,
+				deleteSecretErr:   nil,
+			},
+			want: want{
+				err: nil,
+			},
+			reason: "",
+		},
 		"Not Managed by ESO": {
 			args: args{
 
 				client:          fakeClient,
+				config:          esv1beta1.SecretsManager{},
 				getSecretOutput: &awssm.GetSecretValueOutput{},
 				describeSecretOutput: &awssm.DescribeSecretOutput{
 					Tags: []*awssm.Tag{},
@@ -831,10 +858,54 @@ func TestDeleteSecret(t *testing.T) {
 			},
 			reason: "",
 		},
+		"Invalid Recovery Window": {
+			args: args{
+
+				client: fakesm.Client{},
+				config: esv1beta1.SecretsManager{
+					RecoveryWindowInDays: 1,
+				},
+				getSecretOutput: &awssm.GetSecretValueOutput{},
+				describeSecretOutput: &awssm.DescribeSecretOutput{
+					Tags: []*awssm.Tag{&secretTag},
+				},
+				deleteSecretOutput: &awssm.DeleteSecretOutput{},
+				getSecretErr:       nil,
+				describeSecretErr:  nil,
+				deleteSecretErr:    nil,
+			},
+			want: want{
+				err: errors.New("invalid DeleteSecretInput: RecoveryWindowInDays must be between 7 and 30 days"),
+			},
+			reason: "",
+		},
+		"RecoveryWindowInDays is supplied with ForceDeleteWithoutRecovery": {
+			args: args{
+
+				client: fakesm.Client{},
+				config: esv1beta1.SecretsManager{
+					RecoveryWindowInDays:       7,
+					ForceDeleteWithoutRecovery: true,
+				},
+				getSecretOutput: &awssm.GetSecretValueOutput{},
+				describeSecretOutput: &awssm.DescribeSecretOutput{
+					Tags: []*awssm.Tag{&secretTag},
+				},
+				deleteSecretOutput: &awssm.DeleteSecretOutput{},
+				getSecretErr:       nil,
+				describeSecretErr:  nil,
+				deleteSecretErr:    nil,
+			},
+			want: want{
+				err: errors.New("invalid DeleteSecretInput: ForceDeleteWithoutRecovery conflicts with RecoveryWindowInDays"),
+			},
+			reason: "",
+		},
 		"Failed to get Tags": {
 			args: args{
 
 				client:               fakeClient,
+				config:               esv1beta1.SecretsManager{},
 				getSecretOutput:      &awssm.GetSecretValueOutput{},
 				describeSecretOutput: nil,
 				deleteSecretOutput:   nil,
@@ -850,6 +921,7 @@ func TestDeleteSecret(t *testing.T) {
 		"Secret Not Found": {
 			args: args{
 				client:               fakeClient,
+				config:               esv1beta1.SecretsManager{},
 				getSecretOutput:      nil,
 				describeSecretOutput: nil,
 				deleteSecretOutput:   nil,
@@ -864,6 +936,7 @@ func TestDeleteSecret(t *testing.T) {
 		"Not expected AWS error": {
 			args: args{
 				client:               fakeClient,
+				config:               esv1beta1.SecretsManager{},
 				getSecretOutput:      nil,
 				describeSecretOutput: nil,
 				deleteSecretOutput:   nil,
@@ -878,6 +951,7 @@ func TestDeleteSecret(t *testing.T) {
 		"unexpected error": {
 			args: args{
 				client:               fakeClient,
+				config:               esv1beta1.SecretsManager{},
 				getSecretOutput:      nil,
 				describeSecretOutput: nil,
 				deleteSecretOutput:   nil,
@@ -895,21 +969,23 @@ func TestDeleteSecret(t *testing.T) {
 			ref := fake.PushSecretData{RemoteKey: "fake-key"}
 			sm := SecretsManager{
 				client: &tc.args.client,
+				config: &tc.args.config,
 			}
 			tc.args.client.GetSecretValueWithContextFn = fakesm.NewGetSecretValueWithContextFn(tc.args.getSecretOutput, tc.args.getSecretErr)
 			tc.args.client.DescribeSecretWithContextFn = fakesm.NewDescribeSecretWithContextFn(tc.args.describeSecretOutput, tc.args.describeSecretErr)
 			tc.args.client.DeleteSecretWithContextFn = fakesm.NewDeleteSecretWithContextFn(tc.args.deleteSecretOutput, tc.args.deleteSecretErr)
 			err := sm.DeleteSecret(context.TODO(), ref)
+			t.Logf("DeleteSecret error: %v", err)
 
 			// Error nil XOR tc.want.err nil
 			if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
-				t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
+				t.Errorf("\nTesting DeleteSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
 			}
 
 			// if errors are the same type but their contents do not match
 			if err != nil && tc.want.err != nil {
 				if !strings.Contains(err.Error(), tc.want.err.Error()) {
-					t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
+					t.Errorf("\nTesting DeleteSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
 				}
 			}
 		})

+ 41 - 0
pkg/provider/aws/util/validation.go

@@ -0,0 +1,41 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package util
+
+import (
+	"fmt"
+
+	awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
+)
+
+const (
+	errInvalidDeleteSecretInput = "invalid DeleteSecretInput: %s"
+)
+
+// ValidateDeleteSecretInput validates the DeleteSecretInput.
+// The AWS sdk v2 does not validate the input before making the API call, leaving it to the API to return the error.
+// This function allows one to validate the input before any such call is made.
+// See: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/secretsmanager#DeleteSecretInput
+func ValidateDeleteSecretInput(input awssm.DeleteSecretInput) error {
+	// Validate range for RecoveryWindowInDays
+	// See: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/secretsmanager#DeleteSecretInput
+	if input.RecoveryWindowInDays != nil && *input.RecoveryWindowInDays != 0 && (*input.RecoveryWindowInDays < 7 || *input.RecoveryWindowInDays > 30) {
+		return fmt.Errorf(errInvalidDeleteSecretInput, "RecoveryWindowInDays must be between 7 and 30 days")
+	}
+	// Validate that ForceDeleteWithoutRecovery is not set when RecoveryWindowInDays is set
+	if input.RecoveryWindowInDays != nil && *input.RecoveryWindowInDays != 0 && input.ForceDeleteWithoutRecovery != nil && *input.ForceDeleteWithoutRecovery {
+		return fmt.Errorf(errInvalidDeleteSecretInput, "ForceDeleteWithoutRecovery conflicts with RecoveryWindowInDays")
+	}
+	return nil
+}