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

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
 	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
 	// if the ServerURL URL is using HTTPS protocol. If not set the system root certificates
 	// 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)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.SiteID != nil {
+		in, out := &in.SiteID, &out.SiteID
+		*out = new(int)
+		**out = **in
+	}
 	if in.CABundle != nil {
 		in, out := &in.CABundle, &out.CABundle
 		*out = make([]byte, len(*in))

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

@@ -5824,6 +5824,11 @@ spec:
                         - name
                         - type
                         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:
                         description: Domain is the secret server domain.
                         type: string
@@ -5874,6 +5879,13 @@ spec:
                           ServerURL
                           URL to your secret server installation
                         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:
                         description: |-
                           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
                         - type
                         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:
                         description: Domain is the secret server domain.
                         type: string
@@ -5874,6 +5879,13 @@ spec:
                           ServerURL
                           URL to your secret server installation
                         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:
                         description: |-
                           Token is an access token used to authenticate to the secret server,

+ 24 - 0
deploy/crds/bundle.yaml

@@ -7750,6 +7750,11 @@ spec:
                             - name
                             - type
                           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:
                           description: Domain is the secret server domain.
                           type: string
@@ -7797,6 +7802,13 @@ spec:
                             ServerURL
                             URL to your secret server installation
                           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:
                           description: |-
                             Token is an access token used to authenticate to the secret server,
@@ -21043,6 +21055,11 @@ spec:
                             - name
                             - type
                           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:
                           description: Domain is the secret server domain.
                           type: string
@@ -21090,6 +21107,13 @@ spec:
                             ServerURL
                             URL to your secret server installation
                           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:
                           description: |-
                             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>
 <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>
 <em>
 []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.
 
 **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
 
-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
 apiVersion: external-secrets.io/v1alpha1
@@ -475,6 +475,7 @@ spec:
         spec:
           folderId: 73 # Required for new secrets: folder to create the secret in
           secretTemplateId: 6098 # Required for new secrets: template to use
+          siteId: 1 # Required for new secrets: site where the secret is located
     - match:
         secretKey: password
         remoteRef:
@@ -486,15 +487,16 @@ spec:
         spec:
           folderId: 73
           secretTemplateId: 6098
+          siteId: 1
 ```
 
 > **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
 
-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
 `folderId:<id>/<name>` format, a path-based key, or a numeric ID to ensure the correct secret is
@@ -544,4 +546,22 @@ spec:
         spec:
           folderId: 73
           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 {
 	FolderID         int `json:"folderId"`
 	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 {
-	api secretAPI
+	api                     secretAPI
+	siteID                  int
+	disableSiteIDValidation bool
 }
 
 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 {
-		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.
 	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.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.
@@ -287,7 +313,7 @@ func (c *client) updateSecret(secret *server.Secret, property, value string) err
 // createSecret creates a new secret in Delinea Secret Server.
 // Only the targeted field is populated; other required template fields
 // 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)
 	if err != nil {
 		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{
 		Name:             normalizedName,
 		FolderID:         meta.FolderID,
+		SiteID:           siteID,
 		SecretTemplateID: meta.SecretTemplateID,
 		Fields:           make([]server.SecretField, 0),
 	}

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

@@ -42,6 +42,18 @@ type fakeAPI struct {
 	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 (
 	usernameSlug = "username"
 	passwordSlug = "password"
@@ -289,6 +301,7 @@ func newTestClient(t *testing.T) esv1.SecretsClient {
 		api: &fakeAPI{
 			secrets: secrets,
 		},
+		siteID: defaultSiteID,
 	}
 }
 
@@ -575,7 +588,7 @@ func TestPushSecret(t *testing.T) {
 	}
 
 	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
@@ -631,11 +644,27 @@ func TestPushSecret(t *testing.T) {
 	}
 	err = c.PushSecret(ctx, secret, dataMissingMeta)
 	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
 	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{
 		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)
 	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{
 		remoteKey: "FolderSecretname",
@@ -735,6 +764,74 @@ func TestPushSecret(t *testing.T) {
 	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.
 func TestDeleteSecret(t *testing.T) {
 	ctx := context.Background()
@@ -1170,7 +1267,7 @@ func TestPushSecretInvalidPathKeys(t *testing.T) {
 	}
 
 	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 {
@@ -1314,7 +1411,7 @@ func TestPushSecretWithFolderPrefix(t *testing.T) {
 	}
 
 	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)
@@ -1346,7 +1443,7 @@ func TestPushSecretWithFolderPrefix(t *testing.T) {
 
 	// Create a new secret using folderId prefix
 	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{
 		remoteKey: "folderId:42/brand-new-secret",
@@ -1366,7 +1463,7 @@ func TestPushSecretWithFolderPrefix(t *testing.T) {
 	// Test precedence: remoteKey folderId overrides metadata folderId for lookups.
 	// Metadata says folderId:4, but remoteKey says folderId:5 — should target folder 5.
 	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{
 		remoteKey: "folderId:5/FolderSecretname",
@@ -1581,7 +1678,7 @@ func TestPushSecretEmptyProperty(t *testing.T) {
 	}
 
 	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
@@ -1628,7 +1725,7 @@ func TestPushSecretConflictingFolderIDs(t *testing.T) {
 	// Metadata says folderId:99, but prefix says folderId:42.
 	// The prefix should win for creation.
 	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{
@@ -1704,7 +1801,7 @@ func TestCreateSecretFolderPrefixWithSlashes(t *testing.T) {
 	}
 
 	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{
@@ -1729,7 +1826,7 @@ func TestCreateSecretEmptyTemplateFields(t *testing.T) {
 	err := c.createSecret("test-secret", "", "value", PushSecretMetadataSpec{
 		FolderID:         1,
 		SecretTemplateID: 888,
-	})
+	}, defaultSiteID)
 	assert.Error(t, err)
 	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
 	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{
@@ -1887,7 +1984,7 @@ func TestPushSecretMetadataNoFolderID(t *testing.T) {
 	}
 	err := c.PushSecret(ctx, secret, data)
 	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
@@ -1905,7 +2002,7 @@ func TestPushSecretCreateWithFolderPrefixNoMetadataFolder(t *testing.T) {
 
 	// Metadata has secretTemplateId but folderId is 0; prefix provides the folder.
 	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{
@@ -1987,7 +2084,7 @@ func TestPushSecretNonExistentTemplateField(t *testing.T) {
 	}
 
 	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{

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

@@ -36,6 +36,7 @@ var (
 	errEmptyUserName                 = errors.New("username must not be empty")
 	errEmptyPassword                 = errors.New("password 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")
 	errInvalidSpec                   = errors.New("invalid specification for secret server provider")
 	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")
 )
 
+const defaultSiteID = 1
+
 // Provider struct that implements the ESO esv1.Provider.
 type Provider struct{}
 
@@ -102,8 +105,17 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 		return nil, err
 	}
 
+	siteID := defaultSiteID
+	if cfg.SiteID != nil {
+		siteID = *cfg.SiteID
+	} else if cfg.DisableSiteIDValidation {
+		siteID = 0
+	}
+
 	return &client{
-		api: secretServer,
+		api:                     secretServer,
+		siteID:                  siteID,
+		disableSiteIDValidation: cfg.DisableSiteIDValidation,
 	}, nil
 }
 
@@ -204,6 +216,9 @@ func getConfig(store esv1.GenericStore) (*esv1.SecretServerProvider, error) {
 	if cfg.ServerURL == "" {
 		return nil, errEmptyServerURL
 	}
+	if cfg.SiteID != nil && *cfg.SiteID <= 0 {
+		return nil, errInvalidSiteID
+	}
 
 	// Token authentication takes precedence over username/password.
 	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/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	kubeErrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -214,6 +215,14 @@ func TestValidateStore(t *testing.T) {
 			},
 			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 {
 		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) {
 	userNameKey := "username"
 	userNameValue := "foo"

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

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

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

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