Просмотр исходного кода

fix(secretserver): missing field siteId in secret creation (#6649)

* fix(secretserver): missing field siteId in secret creation

Signed-off-by: Marc Singer <marc@singer.gg>

* fix: added missing kubebuilder markers

Signed-off-by: Marc Singer <marc@singer.gg>

* fix: review comment verbosity

Signed-off-by: Marc Singer <marc@singer.gg>

* fix(secretserver): configure push site ID at store level

Signed-off-by: Marc Singer <marc@singer.gg>

---------

Signed-off-by: Marc Singer <marc@singer.gg>
Co-authored-by: Jean-Philippe Evrard <jean-philippe.evrard+rochepub@external.roche.com>
Marc Singer 2 недель назад
Родитель
Сommit
5ca82af3fa

+ 12 - 0
apis/externalsecrets/v1/secretsstore_secretserver_types.go

@@ -64,6 +64,18 @@ type SecretServerProvider struct {
 	// +required
 	// +required
 	ServerURL string `json:"serverURL"`
 	ServerURL string `json:"serverURL"`
 
 
+	// SiteID is the ID of the Secret Server site for new secrets.
+	// PushSecret metadata can override this value for one secret.
+	// The provider uses 1 if this field is not set.
+	// +optional
+	// +kubebuilder:validation:Minimum=1
+	SiteID *int `json:"siteId,omitempty"`
+
+	// DisableSiteIDValidation permits a missing site ID for new secrets.
+	// The provider sends 0 if no site ID is set.
+	// +optional
+	DisableSiteIDValidation bool `json:"disableSiteIDValidation,omitempty"`
+
 	// PEM/base64 encoded CA bundle used to validate Secret ServerURL. Only used
 	// PEM/base64 encoded CA bundle used to validate Secret ServerURL. Only used
 	// if the ServerURL URL is using HTTPS protocol. If not set the system root certificates
 	// if the ServerURL URL is using HTTPS protocol. If not set the system root certificates
 	// are used to validate the TLS connection.
 	// are used to validate the TLS connection.

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

@@ -3815,6 +3815,11 @@ func (in *SecretServerProvider) DeepCopyInto(out *SecretServerProvider) {
 		*out = new(SecretServerProviderRef)
 		*out = new(SecretServerProviderRef)
 		(*in).DeepCopyInto(*out)
 		(*in).DeepCopyInto(*out)
 	}
 	}
+	if in.SiteID != nil {
+		in, out := &in.SiteID, &out.SiteID
+		*out = new(int)
+		**out = **in
+	}
 	if in.CABundle != nil {
 	if in.CABundle != nil {
 		in, out := &in.CABundle, &out.CABundle
 		in, out := &in.CABundle, &out.CABundle
 		*out = make([]byte, len(*in))
 		*out = make([]byte, len(*in))

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

@@ -5824,6 +5824,11 @@ spec:
                         - name
                         - name
                         - type
                         - type
                         type: object
                         type: object
+                      disableSiteIDValidation:
+                        description: |-
+                          DisableSiteIDValidation permits a missing site ID for new secrets.
+                          The provider sends 0 if no site ID is set.
+                        type: boolean
                       domain:
                       domain:
                         description: Domain is the secret server domain.
                         description: Domain is the secret server domain.
                         type: string
                         type: string
@@ -5874,6 +5879,13 @@ spec:
                           ServerURL
                           ServerURL
                           URL to your secret server installation
                           URL to your secret server installation
                         type: string
                         type: string
+                      siteId:
+                        description: |-
+                          SiteID is the ID of the Secret Server site for new secrets.
+                          PushSecret metadata can override this value for one secret.
+                          The provider uses 1 if this field is not set.
+                        minimum: 1
+                        type: integer
                       token:
                       token:
                         description: |-
                         description: |-
                           Token is an access token used to authenticate to the secret server,
                           Token is an access token used to authenticate to the secret server,

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

@@ -5824,6 +5824,11 @@ spec:
                         - name
                         - name
                         - type
                         - type
                         type: object
                         type: object
+                      disableSiteIDValidation:
+                        description: |-
+                          DisableSiteIDValidation permits a missing site ID for new secrets.
+                          The provider sends 0 if no site ID is set.
+                        type: boolean
                       domain:
                       domain:
                         description: Domain is the secret server domain.
                         description: Domain is the secret server domain.
                         type: string
                         type: string
@@ -5874,6 +5879,13 @@ spec:
                           ServerURL
                           ServerURL
                           URL to your secret server installation
                           URL to your secret server installation
                         type: string
                         type: string
+                      siteId:
+                        description: |-
+                          SiteID is the ID of the Secret Server site for new secrets.
+                          PushSecret metadata can override this value for one secret.
+                          The provider uses 1 if this field is not set.
+                        minimum: 1
+                        type: integer
                       token:
                       token:
                         description: |-
                         description: |-
                           Token is an access token used to authenticate to the secret server,
                           Token is an access token used to authenticate to the secret server,

+ 24 - 0
deploy/crds/bundle.yaml

@@ -7750,6 +7750,11 @@ spec:
                             - name
                             - name
                             - type
                             - type
                           type: object
                           type: object
+                        disableSiteIDValidation:
+                          description: |-
+                            DisableSiteIDValidation permits a missing site ID for new secrets.
+                            The provider sends 0 if no site ID is set.
+                          type: boolean
                         domain:
                         domain:
                           description: Domain is the secret server domain.
                           description: Domain is the secret server domain.
                           type: string
                           type: string
@@ -7797,6 +7802,13 @@ spec:
                             ServerURL
                             ServerURL
                             URL to your secret server installation
                             URL to your secret server installation
                           type: string
                           type: string
+                        siteId:
+                          description: |-
+                            SiteID is the ID of the Secret Server site for new secrets.
+                            PushSecret metadata can override this value for one secret.
+                            The provider uses 1 if this field is not set.
+                          minimum: 1
+                          type: integer
                         token:
                         token:
                           description: |-
                           description: |-
                             Token is an access token used to authenticate to the secret server,
                             Token is an access token used to authenticate to the secret server,
@@ -21043,6 +21055,11 @@ spec:
                             - name
                             - name
                             - type
                             - type
                           type: object
                           type: object
+                        disableSiteIDValidation:
+                          description: |-
+                            DisableSiteIDValidation permits a missing site ID for new secrets.
+                            The provider sends 0 if no site ID is set.
+                          type: boolean
                         domain:
                         domain:
                           description: Domain is the secret server domain.
                           description: Domain is the secret server domain.
                           type: string
                           type: string
@@ -21090,6 +21107,13 @@ spec:
                             ServerURL
                             ServerURL
                             URL to your secret server installation
                             URL to your secret server installation
                           type: string
                           type: string
+                        siteId:
+                          description: |-
+                            SiteID is the ID of the Secret Server site for new secrets.
+                            PushSecret metadata can override this value for one secret.
+                            The provider uses 1 if this field is not set.
+                          minimum: 1
+                          type: integer
                         token:
                         token:
                           description: |-
                           description: |-
                             Token is an access token used to authenticate to the secret server,
                             Token is an access token used to authenticate to the secret server,

+ 27 - 0
docs/api/spec.md

@@ -10648,6 +10648,33 @@ URL to your secret server installation</p>
 </tr>
 </tr>
 <tr>
 <tr>
 <td>
 <td>
+<code>siteId</code></br>
+<em>
+int
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>SiteID is the ID of the Secret Server site for new secrets.
+PushSecret metadata can override this value for one secret.
+The provider uses 1 if this field is not set.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>disableSiteIDValidation</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>DisableSiteIDValidation permits a missing site ID for new secrets.
+The provider sends 0 if no site ID is set.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>caBundle</code></br>
 <code>caBundle</code></br>
 <em>
 <em>
 []byte
 []byte

+ 27 - 7
docs/provider/secretserver.md

@@ -442,13 +442,13 @@ By using the `folderId:<id>/<name>` format (e.g., `folderId:73/my-secret`), the
 encoded directly in the key and is available to **all** operations, ensuring consistent behavior.
 encoded directly in the key and is available to **all** operations, ensuring consistent behavior.
 
 
 **Precedence rule:** If both a `folderId` in the `remoteKey` and a `folderId` in the metadata are
 **Precedence rule:** If both a `folderId` in the `remoteKey` and a `folderId` in the metadata are
-specified, the value from the `remoteKey` takes precedence for lookups. The metadata `folderId` and
-`secretTemplateId` are still required when **creating** a new secret (they tell the API which folder
-and template to use for the new secret).
+specified, the value from the `remoteKey` takes precedence for lookups. The metadata `folderId`,
+`secretTemplateId`, and `siteId` are still required when **creating** a new secret (they tell the API
+which folder, template, and site to use for the new secret).
 
 
 ### Requirements for Creating New Secrets
 ### Requirements for Creating New Secrets
 
 
-When creating a **new** secret in Secret Server, you must provide a `folderId` and a `secretTemplateId`. These are passed as `metadata` in the `PushSecret` spec:
+When creating a **new** secret in Secret Server, you must provide a `folderId`, `secretTemplateId`, and `siteId`. These are passed as `metadata` in the `PushSecret` spec:
 
 
 ```yaml
 ```yaml
 apiVersion: external-secrets.io/v1alpha1
 apiVersion: external-secrets.io/v1alpha1
@@ -475,6 +475,7 @@ spec:
         spec:
         spec:
           folderId: 73 # Required for new secrets: folder to create the secret in
           folderId: 73 # Required for new secrets: folder to create the secret in
           secretTemplateId: 6098 # Required for new secrets: template to use
           secretTemplateId: 6098 # Required for new secrets: template to use
+          siteId: 1 # Required for new secrets: site where the secret is located
     - match:
     - match:
         secretKey: password
         secretKey: password
         remoteRef:
         remoteRef:
@@ -486,15 +487,16 @@ spec:
         spec:
         spec:
           folderId: 73
           folderId: 73
           secretTemplateId: 6098
           secretTemplateId: 6098
+          siteId: 1
 ```
 ```
 
 
 > **Note:** The `folderId` in the `remoteKey` (`folderId:73/...`) is used when **looking up** the
 > **Note:** The `folderId` in the `remoteKey` (`folderId:73/...`) is used when **looking up** the
-> secret (for push, delete, and existence checks). The `folderId` and `secretTemplateId` in
-> `metadata` are used when **creating** a new secret via the Secret Server API.
+> secret (for push, delete, and existence checks). The `folderId`, `secretTemplateId`, and `siteId`
+> in `metadata` are used when **creating** a new secret via the Secret Server API.
 
 
 ### Updating Existing Secrets
 ### Updating Existing Secrets
 
 
-When updating an existing secret, you do not strictly need the `folderId` or `secretTemplateId` metadata, as the provider will fetch the existing secret by its name or ID to update the corresponding fields.
+When updating an existing secret, you do not strictly need the `folderId`, `secretTemplateId`, or `siteId` metadata, as the provider will fetch the existing secret by its name or ID to update the corresponding fields.
 
 
 However, if multiple secrets share the same name across different folders, you should use either the
 However, if multiple secrets share the same name across different folders, you should use either the
 `folderId:<id>/<name>` format, a path-based key, or a numeric ID to ensure the correct secret is
 `folderId:<id>/<name>` format, a path-based key, or a numeric ID to ensure the correct secret is
@@ -544,4 +546,22 @@ spec:
         spec:
         spec:
           folderId: 73
           folderId: 73
           secretTemplateId: 6098
           secretTemplateId: 6098
+          siteId: 1
 ```
 ```
+
+### Site Configuration
+
+Secret Server uses a site ID when it creates a secret. The provider uses site ID `1` by default.
+
+Set `spec.provider.secretserver.siteId` if your installation uses a different ID. This setting applies to `SecretStore` and `ClusterSecretStore`.
+
+`PushSecret` metadata can set `siteId` for one secret. The metadata value overrides the store value.
+
+To find the site ID:
+
+1. Open **Admin > Distributed Engine > Manage Sites**.
+2. Select the site.
+3. Read the value after `SiteId=` in the `SiteView.aspx?SiteId=<id>` URL.
+
+If your server accepts a missing site ID, set `disableSiteIDValidation: true` in the store configuration.
+When the store and metadata omit `siteId`, the provider sends `0`.

+ 32 - 5
providers/v1/secretserver/client.go

@@ -136,10 +136,16 @@ func parseFolderPrefix(key string) (folderID int, name string, hasFolderPrefix b
 type PushSecretMetadataSpec struct {
 type PushSecretMetadataSpec struct {
 	FolderID         int `json:"folderId"`
 	FolderID         int `json:"folderId"`
 	SecretTemplateID int `json:"secretTemplateId"`
 	SecretTemplateID int `json:"secretTemplateId"`
+	// SiteID overrides the site ID from the SecretStore for one secret.
+	// +optional
+	// +kubebuilder:validation:Minimum=1
+	SiteID *int `json:"siteId,omitempty"`
 }
 }
 
 
 type client struct {
 type client struct {
-	api secretAPI
+	api                     secretAPI
+	siteID                  int
+	disableSiteIDValidation bool
 }
 }
 
 
 var _ esv1.SecretsClient = &client{}
 var _ esv1.SecretsClient = &client{}
@@ -241,17 +247,37 @@ func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv
 	}
 	}
 
 
 	if meta == nil || meta.Spec.SecretTemplateID <= 0 {
 	if meta == nil || meta.Spec.SecretTemplateID <= 0 {
-		return errors.New("folderId and secretTemplateId must be provided in metadata to create a new secret")
+		return errors.New("folderId and secretTemplateId must be set in metadata to create a secret")
 	}
 	}
 
 
 	// Use the effective folderID (prefix-overridden or metadata-supplied) for creation.
 	// Use the effective folderID (prefix-overridden or metadata-supplied) for creation.
 	if folderID <= 0 {
 	if folderID <= 0 {
-		return errors.New("folderId and secretTemplateId must be provided in metadata to create a new secret")
+		return errors.New("folderId and secretTemplateId must be set in metadata to create a secret")
+	}
+
+	siteID, err := c.resolveSiteID(meta.Spec.SiteID)
+	if err != nil {
+		return err
 	}
 	}
 
 
 	createSpec := meta.Spec
 	createSpec := meta.Spec
 	createSpec.FolderID = folderID
 	createSpec.FolderID = folderID
-	return c.createSecret(data.GetRemoteKey(), data.GetProperty(), string(value), createSpec)
+	return c.createSecret(data.GetRemoteKey(), data.GetProperty(), string(value), createSpec, siteID)
+}
+
+func (c *client) resolveSiteID(metadataSiteID *int) (int, error) {
+	if metadataSiteID != nil {
+		if *metadataSiteID <= 0 {
+			return 0, errors.New("siteId must be greater than zero")
+		}
+		return *metadataSiteID, nil
+	}
+
+	if c.siteID > 0 || c.disableSiteIDValidation {
+		return c.siteID, nil
+	}
+
+	return 0, errors.New("siteId must be set in PushSecret metadata or SecretStore configuration")
 }
 }
 
 
 // updateSecret updates an existing secret in Delinea Secret Server.
 // updateSecret updates an existing secret in Delinea Secret Server.
@@ -287,7 +313,7 @@ func (c *client) updateSecret(secret *server.Secret, property, value string) err
 // createSecret creates a new secret in Delinea Secret Server.
 // createSecret creates a new secret in Delinea Secret Server.
 // Only the targeted field is populated; other required template fields
 // Only the targeted field is populated; other required template fields
 // may cause an API error.
 // may cause an API error.
-func (c *client) createSecret(name, property, value string, meta PushSecretMetadataSpec) error {
+func (c *client) createSecret(name, property, value string, meta PushSecretMetadataSpec, siteID int) error {
 	template, err := c.api.SecretTemplate(meta.SecretTemplateID)
 	template, err := c.api.SecretTemplate(meta.SecretTemplateID)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("failed to get secret template: %w", err)
 		return fmt.Errorf("failed to get secret template: %w", err)
@@ -319,6 +345,7 @@ func (c *client) createSecret(name, property, value string, meta PushSecretMetad
 	newSecret := server.Secret{
 	newSecret := server.Secret{
 		Name:             normalizedName,
 		Name:             normalizedName,
 		FolderID:         meta.FolderID,
 		FolderID:         meta.FolderID,
+		SiteID:           siteID,
 		SecretTemplateID: meta.SecretTemplateID,
 		SecretTemplateID: meta.SecretTemplateID,
 		Fields:           make([]server.SecretField, 0),
 		Fields:           make([]server.SecretField, 0),
 	}
 	}

+ 113 - 16
providers/v1/secretserver/client_test.go

@@ -42,6 +42,18 @@ type fakeAPI struct {
 	secretsErr error
 	secretsErr error
 }
 }
 
 
+type siteValidatingAPI struct {
+	*fakeAPI
+}
+
+func (f *siteValidatingAPI) CreateSecret(secret server.Secret) (*server.Secret, error) {
+	if secret.SiteID < 1 {
+		return nil, errors.New(`400 Bad Request: {"message":"The request is invalid.","modelState":{"secretCreateArgs.SiteId":["The field SiteId must be between 1 and 2147483647."]}}`)
+	}
+
+	return f.fakeAPI.CreateSecret(secret)
+}
+
 const (
 const (
 	usernameSlug = "username"
 	usernameSlug = "username"
 	passwordSlug = "password"
 	passwordSlug = "password"
@@ -289,6 +301,7 @@ func newTestClient(t *testing.T) esv1.SecretsClient {
 		api: &fakeAPI{
 		api: &fakeAPI{
 			secrets: secrets,
 			secrets: secrets,
 		},
 		},
+		siteID: defaultSiteID,
 	}
 	}
 }
 }
 
 
@@ -575,7 +588,7 @@ func TestPushSecret(t *testing.T) {
 	}
 	}
 
 
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	// Create a new secret
 	// Create a new secret
@@ -631,11 +644,27 @@ func TestPushSecret(t *testing.T) {
 	}
 	}
 	err = c.PushSecret(ctx, secret, dataMissingMeta)
 	err = c.PushSecret(ctx, secret, dataMissingMeta)
 	assert.Error(t, err)
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "folderId and secretTemplateId must be provided in metadata to create a new secret")
+	assert.Contains(t, err.Error(), "folderId and secretTemplateId must be set in metadata to create a secret")
+
+	// Missing siteId for new secret
+	metadataWithoutSiteID := apiextensionsv1.JSON{
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId":1,"secretTemplateId":1}}`),
+	}
+	dataWithoutSiteID := fakePushSecretData{
+		remoteKey: "new-secret-no-site-id",
+		property:  "username",
+		secretKey: "my-key",
+		metadata:  &metadataWithoutSiteID,
+	}
+	err = c.PushSecret(ctx, secret, dataWithoutSiteID)
+	assert.NoError(t, err)
+	createdWithoutSiteID, err := c.(*client).api.SecretByPath("new-secret-no-site-id")
+	assert.NoError(t, err)
+	assert.Equal(t, defaultSiteID, createdWithoutSiteID.SiteID)
 
 
 	// Invalid secretTemplateId in metadata
 	// Invalid secretTemplateId in metadata
 	invalidMetadataJSON := apiextensionsv1.JSON{
 	invalidMetadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 999}}`), // non-existent template
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 999, "siteId": 1}}`), // non-existent template
 	}
 	}
 	dataInvalidMeta := fakePushSecretData{
 	dataInvalidMeta := fakePushSecretData{
 		remoteKey: "new-secret-invalid-meta",
 		remoteKey: "new-secret-invalid-meta",
@@ -671,7 +700,7 @@ func TestPushSecret(t *testing.T) {
 
 
 	// Update duplicate-named secret in specific folder (ID 9001 in FolderID 5)
 	// Update duplicate-named secret in specific folder (ID 9001 in FolderID 5)
 	metadataFolder5 := apiextensionsv1.JSON{
 	metadataFolder5 := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 	dataFolderUpdate := fakePushSecretData{
 	dataFolderUpdate := fakePushSecretData{
 		remoteKey: "FolderSecretname",
 		remoteKey: "FolderSecretname",
@@ -735,6 +764,74 @@ func TestPushSecret(t *testing.T) {
 	assert.Contains(t, err.Error(), "secret value is not valid UTF-8")
 	assert.Contains(t, err.Error(), "secret value is not valid UTF-8")
 }
 }
 
 
+func TestPushSecretSiteIDResolution(t *testing.T) {
+	tests := map[string]struct {
+		clientSiteID            int
+		disableSiteIDValidation bool
+		metadataSiteID          string
+		wantSiteID              int
+		wantErr                 string
+	}{
+		"metadata overrides store": {
+			clientSiteID:   2,
+			metadataSiteID: `,"siteId":3`,
+			wantSiteID:     3,
+		},
+		"store supplies site ID": {
+			clientSiteID: 2,
+			wantSiteID:   2,
+		},
+		"provider default supplies site ID": {
+			clientSiteID: defaultSiteID,
+			wantSiteID:   defaultSiteID,
+		},
+		"disabled validation permits missing site ID": {
+			disableSiteIDValidation: true,
+			wantSiteID:              0,
+		},
+		"enabled validation rejects missing site ID": {
+			wantErr: "siteId must be set in PushSecret metadata or SecretStore configuration",
+		},
+		"explicit zero is invalid": {
+			clientSiteID:   2,
+			metadataSiteID: `,"siteId":0`,
+			wantErr:        "siteId must be greater than zero",
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			testClient := newTestClient(t).(*client)
+			testClient.siteID = tc.clientSiteID
+			testClient.disableSiteIDValidation = tc.disableSiteIDValidation
+			if tc.wantSiteID > 0 {
+				testClient.api = &siteValidatingAPI{fakeAPI: testClient.api.(*fakeAPI)}
+			}
+
+			metadataJSON := apiextensionsv1.JSON{
+				Raw: fmt.Appendf(nil, `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId":1,"secretTemplateId":1%s}}`, tc.metadataSiteID),
+			}
+			secret := &corev1.Secret{Data: map[string][]byte{"my-key": []byte("my-value")}}
+			err := testClient.PushSecret(t.Context(), secret, fakePushSecretData{
+				remoteKey: "new-secret-with-site",
+				property:  "username",
+				secretKey: "my-key",
+				metadata:  &metadataJSON,
+			})
+
+			if tc.wantErr != "" {
+				require.ErrorContains(t, err, tc.wantErr)
+				return
+			}
+
+			require.NoError(t, err)
+			created, err := testClient.api.SecretByPath("new-secret-with-site")
+			require.NoError(t, err)
+			assert.Equal(t, tc.wantSiteID, created.SiteID)
+		})
+	}
+}
+
 // TestDeleteSecret tests the DeleteSecret functionality.
 // TestDeleteSecret tests the DeleteSecret functionality.
 func TestDeleteSecret(t *testing.T) {
 func TestDeleteSecret(t *testing.T) {
 	ctx := context.Background()
 	ctx := context.Background()
@@ -1170,7 +1267,7 @@ func TestPushSecretInvalidPathKeys(t *testing.T) {
 	}
 	}
 
 
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	testCases := map[string]struct {
 	testCases := map[string]struct {
@@ -1314,7 +1411,7 @@ func TestPushSecretWithFolderPrefix(t *testing.T) {
 	}
 	}
 
 
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 5, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	// Update an existing secret using folderId prefix — should target folder 5 (ID 9001)
 	// Update an existing secret using folderId prefix — should target folder 5 (ID 9001)
@@ -1346,7 +1443,7 @@ func TestPushSecretWithFolderPrefix(t *testing.T) {
 
 
 	// Create a new secret using folderId prefix
 	// Create a new secret using folderId prefix
 	metadataCreate := apiextensionsv1.JSON{
 	metadataCreate := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 42, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 42, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 	dataCreate := fakePushSecretData{
 	dataCreate := fakePushSecretData{
 		remoteKey: "folderId:42/brand-new-secret",
 		remoteKey: "folderId:42/brand-new-secret",
@@ -1366,7 +1463,7 @@ func TestPushSecretWithFolderPrefix(t *testing.T) {
 	// Test precedence: remoteKey folderId overrides metadata folderId for lookups.
 	// Test precedence: remoteKey folderId overrides metadata folderId for lookups.
 	// Metadata says folderId:4, but remoteKey says folderId:5 — should target folder 5.
 	// Metadata says folderId:4, but remoteKey says folderId:5 — should target folder 5.
 	metadataFolder4 := apiextensionsv1.JSON{
 	metadataFolder4 := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 4, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 4, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 	dataPrecedence := fakePushSecretData{
 	dataPrecedence := fakePushSecretData{
 		remoteKey: "folderId:5/FolderSecretname",
 		remoteKey: "folderId:5/FolderSecretname",
@@ -1581,7 +1678,7 @@ func TestPushSecretEmptyProperty(t *testing.T) {
 	}
 	}
 
 
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	// Create new secret with empty property → uses first template field
 	// Create new secret with empty property → uses first template field
@@ -1628,7 +1725,7 @@ func TestPushSecretConflictingFolderIDs(t *testing.T) {
 	// Metadata says folderId:99, but prefix says folderId:42.
 	// Metadata says folderId:99, but prefix says folderId:42.
 	// The prefix should win for creation.
 	// The prefix should win for creation.
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 99, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 99, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	data := fakePushSecretData{
 	data := fakePushSecretData{
@@ -1704,7 +1801,7 @@ func TestCreateSecretFolderPrefixWithSlashes(t *testing.T) {
 	}
 	}
 
 
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 73, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 73, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	data := fakePushSecretData{
 	data := fakePushSecretData{
@@ -1729,7 +1826,7 @@ func TestCreateSecretEmptyTemplateFields(t *testing.T) {
 	err := c.createSecret("test-secret", "", "value", PushSecretMetadataSpec{
 	err := c.createSecret("test-secret", "", "value", PushSecretMetadataSpec{
 		FolderID:         1,
 		FolderID:         1,
 		SecretTemplateID: 888,
 		SecretTemplateID: 888,
-	})
+	}, defaultSiteID)
 	assert.Error(t, err)
 	assert.Error(t, err)
 	assert.Contains(t, err.Error(), "secret template has no fields")
 	assert.Contains(t, err.Error(), "secret template has no fields")
 }
 }
@@ -1876,7 +1973,7 @@ func TestPushSecretMetadataNoFolderID(t *testing.T) {
 
 
 	// Metadata has secretTemplateId but no folderId
 	// Metadata has secretTemplateId but no folderId
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	data := fakePushSecretData{
 	data := fakePushSecretData{
@@ -1887,7 +1984,7 @@ func TestPushSecretMetadataNoFolderID(t *testing.T) {
 	}
 	}
 	err := c.PushSecret(ctx, secret, data)
 	err := c.PushSecret(ctx, secret, data)
 	assert.Error(t, err)
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "folderId and secretTemplateId must be provided")
+	assert.Contains(t, err.Error(), "folderId and secretTemplateId must be set")
 }
 }
 
 
 // TestPushSecretCreateWithFolderPrefixNoMetadataFolder tests that PushSecret can
 // TestPushSecretCreateWithFolderPrefixNoMetadataFolder tests that PushSecret can
@@ -1905,7 +2002,7 @@ func TestPushSecretCreateWithFolderPrefixNoMetadataFolder(t *testing.T) {
 
 
 	// Metadata has secretTemplateId but folderId is 0; prefix provides the folder.
 	// Metadata has secretTemplateId but folderId is 0; prefix provides the folder.
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 0, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	data := fakePushSecretData{
 	data := fakePushSecretData{
@@ -1987,7 +2084,7 @@ func TestPushSecretNonExistentTemplateField(t *testing.T) {
 	}
 	}
 
 
 	metadataJSON := apiextensionsv1.JSON{
 	metadataJSON := apiextensionsv1.JSON{
-		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1}}`),
+		Raw: []byte(`{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"folderId": 1, "secretTemplateId": 1, "siteId": 1}}`),
 	}
 	}
 
 
 	data := fakePushSecretData{
 	data := fakePushSecretData{

+ 16 - 1
providers/v1/secretserver/provider.go

@@ -36,6 +36,7 @@ var (
 	errEmptyUserName                 = errors.New("username must not be empty")
 	errEmptyUserName                 = errors.New("username must not be empty")
 	errEmptyPassword                 = errors.New("password must be set")
 	errEmptyPassword                 = errors.New("password must be set")
 	errEmptyServerURL                = errors.New("serverURL must be set")
 	errEmptyServerURL                = errors.New("serverURL must be set")
+	errInvalidSiteID                 = errors.New("siteId must be greater than zero")
 	errMissingStore                  = errors.New("missing store specification")
 	errMissingStore                  = errors.New("missing store specification")
 	errInvalidSpec                   = errors.New("invalid specification for secret server provider")
 	errInvalidSpec                   = errors.New("invalid specification for secret server provider")
 	errClusterStoreRequiresNamespace = errors.New("when using a ClusterSecretStore, namespaces must be explicitly set")
 	errClusterStoreRequiresNamespace = errors.New("when using a ClusterSecretStore, namespaces must be explicitly set")
@@ -43,6 +44,8 @@ var (
 	errMissingSecretKey              = errors.New("must specify a secret key")
 	errMissingSecretKey              = errors.New("must specify a secret key")
 )
 )
 
 
+const defaultSiteID = 1
+
 // Provider struct that implements the ESO esv1.Provider.
 // Provider struct that implements the ESO esv1.Provider.
 type Provider struct{}
 type Provider struct{}
 
 
@@ -102,8 +105,17 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	siteID := defaultSiteID
+	if cfg.SiteID != nil {
+		siteID = *cfg.SiteID
+	} else if cfg.DisableSiteIDValidation {
+		siteID = 0
+	}
+
 	return &client{
 	return &client{
-		api: secretServer,
+		api:                     secretServer,
+		siteID:                  siteID,
+		disableSiteIDValidation: cfg.DisableSiteIDValidation,
 	}, nil
 	}, nil
 }
 }
 
 
@@ -204,6 +216,9 @@ func getConfig(store esv1.GenericStore) (*esv1.SecretServerProvider, error) {
 	if cfg.ServerURL == "" {
 	if cfg.ServerURL == "" {
 		return nil, errEmptyServerURL
 		return nil, errEmptyServerURL
 	}
 	}
+	if cfg.SiteID != nil && *cfg.SiteID <= 0 {
+		return nil, errInvalidSiteID
+	}
 
 
 	// Token authentication takes precedence over username/password.
 	// Token authentication takes precedence over username/password.
 	if cfg.Token != nil {
 	if cfg.Token != nil {

+ 76 - 0
providers/v1/secretserver/provider_test.go

@@ -23,6 +23,7 @@ import (
 
 
 	"github.com/DelineaXPM/tss-sdk-go/v3/server"
 	"github.com/DelineaXPM/tss-sdk-go/v3/server"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
 	kubeErrors "k8s.io/apimachinery/pkg/api/errors"
 	kubeErrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -214,6 +215,14 @@ func TestValidateStore(t *testing.T) {
 			},
 			},
 			want: esutils.ErrValueOrRefMissing,
 			want: esutils.ErrValueOrRefMissing,
 		},
 		},
+		"invalid with zero site ID": {
+			cfg: esv1.SecretServerProvider{
+				Token:     validSecretRefUsingValue,
+				ServerURL: testURL,
+				SiteID:    new(0),
+			},
+			want: errInvalidSiteID,
+		},
 	}
 	}
 	for name, tc := range tests {
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
@@ -231,6 +240,73 @@ func TestValidateStore(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestNewClientSiteIDConfiguration(t *testing.T) {
+	newProvider := func(siteID *int, disableValidation bool) *esv1.SecretServerProvider {
+		return &esv1.SecretServerProvider{
+			Token:                   makeSecretRefUsingValue("token"),
+			ServerURL:               "https://example.com",
+			SiteID:                  siteID,
+			DisableSiteIDValidation: disableValidation,
+		}
+	}
+
+	tests := map[string]struct {
+		store        esv1.GenericStore
+		wantSiteID   int
+		wantDisabled bool
+	}{
+		"SecretStore uses default": {
+			store: &esv1.SecretStore{
+				TypeMeta: metav1.TypeMeta{Kind: esv1.SecretStoreKind},
+				Spec: esv1.SecretStoreSpec{Provider: &esv1.SecretStoreProvider{
+					SecretServer: newProvider(nil, false),
+				}},
+			},
+			wantSiteID: defaultSiteID,
+		},
+		"SecretStore uses configured value": {
+			store: &esv1.SecretStore{
+				TypeMeta: metav1.TypeMeta{Kind: esv1.SecretStoreKind},
+				Spec: esv1.SecretStoreSpec{Provider: &esv1.SecretStoreProvider{
+					SecretServer: newProvider(new(7), false),
+				}},
+			},
+			wantSiteID: 7,
+		},
+		"ClusterSecretStore uses configured value": {
+			store: &esv1.ClusterSecretStore{
+				TypeMeta: metav1.TypeMeta{Kind: esv1.ClusterSecretStoreKind},
+				Spec: esv1.SecretStoreSpec{Provider: &esv1.SecretStoreProvider{
+					SecretServer: newProvider(new(9), false),
+				}},
+			},
+			wantSiteID: 9,
+		},
+		"disabled validation preserves missing value": {
+			store: &esv1.SecretStore{
+				TypeMeta: metav1.TypeMeta{Kind: esv1.SecretStoreKind},
+				Spec: esv1.SecretStoreSpec{Provider: &esv1.SecretStoreProvider{
+					SecretServer: newProvider(nil, true),
+				}},
+			},
+			wantDisabled: true,
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			provider := &Provider{}
+			secretsClient, err := provider.NewClient(t.Context(), tc.store, clientfake.NewClientBuilder().Build(), "default")
+			require.NoError(t, err)
+
+			secretServerClient, ok := secretsClient.(*client)
+			require.True(t, ok)
+			assert.Equal(t, tc.wantSiteID, secretServerClient.siteID)
+			assert.Equal(t, tc.wantDisabled, secretServerClient.disableSiteIDValidation)
+		})
+	}
+}
+
 func TestNewClient(t *testing.T) {
 func TestNewClient(t *testing.T) {
 	userNameKey := "username"
 	userNameKey := "username"
 	userNameValue := "foo"
 	userNameValue := "foo"

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

@@ -851,6 +851,7 @@ spec:
         name: string
         name: string
         namespace: string
         namespace: string
         type: "Secret" # "Secret", "ConfigMap"
         type: "Secret" # "Secret", "ConfigMap"
+      disableSiteIDValidation: true
       domain: string
       domain: string
       password:
       password:
         secretRef:
         secretRef:
@@ -859,6 +860,7 @@ spec:
           namespace: string
           namespace: string
         value: string
         value: string
       serverURL: string
       serverURL: string
+      siteId: 1
       token:
       token:
         secretRef:
         secretRef:
           key: string
           key: string

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

@@ -851,6 +851,7 @@ spec:
         name: string
         name: string
         namespace: string
         namespace: string
         type: "Secret" # "Secret", "ConfigMap"
         type: "Secret" # "Secret", "ConfigMap"
+      disableSiteIDValidation: true
       domain: string
       domain: string
       password:
       password:
         secretRef:
         secretRef:
@@ -859,6 +860,7 @@ spec:
           namespace: string
           namespace: string
         value: string
         value: string
       serverURL: string
       serverURL: string
+      siteId: 1
       token:
       token:
         secretRef:
         secretRef:
           key: string
           key: string