From fc89e019e47318a7aefdd6a790a3409a2eb56eb9 Mon Sep 17 00:00:00 2001 From: idata Date: Fri, 20 Mar 2026 10:40:46 +0100 Subject: [PATCH 1/2] feat(gcpkms): add SOPS_GCP_KMS_ENDPOINT and SOPS_GCP_KMS_UNIVERSE_DOMAIN env vars Allow overriding the GCP KMS endpoint and universe domain via environment variables. This enables use of sovereign cloud environments that expose a GCP-compatible KMS API at a non-standard endpoint (e.g. S3NS/Thales TPC with cloudkms.s3nsapis.fr). Signed-off-by: David RIBEIRO --- README.rst | 15 +++++++++++++++ gcpkms/keysource.go | 15 +++++++++++++++ gcpkms/keysource_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/README.rst b/README.rst index d3cef70b72..e430006127 100644 --- a/README.rst +++ b/README.rst @@ -325,6 +325,21 @@ switch to the REST client by setting the ``SOPS_GCP_KMS_CLIENT_TYPE`` environmen $ export SOPS_GCP_KMS_CLIENT_TYPE=rest # Use REST client $ export SOPS_GCP_KMS_CLIENT_TYPE=grpc # Use gRPC client (default) +For sovereign cloud environments that expose a GCP-compatible KMS API at a +non-standard endpoint (e.g. S3NS/Thales TPC: ``cloudkms.s3nsapis.fr``), +you can override the endpoint or the universe domain: + +.. code:: sh + + # Override the KMS endpoint directly + $ export SOPS_GCP_KMS_ENDPOINT=cloudkms.example.com:443 + + # Or derive the endpoint from the universe domain (cloudkms.:443) + $ export SOPS_GCP_KMS_UNIVERSE_DOMAIN=example.com + +Note: ``SOPS_GCP_KMS_ENDPOINT`` takes precedence over +``SOPS_GCP_KMS_UNIVERSE_DOMAIN`` if both are set. + Encrypting/decrypting with GCP KMS requires a KMS ResourceID. You can use the cloud console the get the ResourceID or you can create one using the gcloud sdk: diff --git a/gcpkms/keysource.go b/gcpkms/keysource.go index 75820d430d..3462ee2b1d 100644 --- a/gcpkms/keysource.go +++ b/gcpkms/keysource.go @@ -30,6 +30,14 @@ const ( // SopsGCPKMSClientTypeEnv is the environment variable used to specify the // GCP KMS client type. Valid values are "grpc" (default) and "rest". SopsGCPKMSClientTypeEnv = "SOPS_GCP_KMS_CLIENT_TYPE" + // SopsGCPKMSEndpointEnv overrides the GCP KMS endpoint URL. Useful for + // sovereign cloud environments that expose a GCP-compatible KMS API at a + // non-standard endpoint (e.g. S3NS/Thales TPC: cloudkms.s3nsapis.fr). + SopsGCPKMSEndpointEnv = "SOPS_GCP_KMS_ENDPOINT" + // SopsGCPKMSUniverseDomainEnv sets the universe domain for the GCP KMS + // client, which derives the endpoint as cloudkms.{UNIVERSE_DOMAIN}:443. + // Example: "s3nsapis.fr" for S3NS/Thales TPC. + SopsGCPKMSUniverseDomainEnv = "SOPS_GCP_KMS_UNIVERSE_DOMAIN" // KeyTypeIdentifier is the string used to identify a GCP KMS MasterKey. KeyTypeIdentifier = "gcp_kms" ) @@ -321,6 +329,13 @@ func (key *MasterKey) newKMSClient(ctx context.Context) (*kms.KeyManagementClien // Add extra options. opts = append(opts, key.clientOpts...) + if endpoint := os.Getenv(SopsGCPKMSEndpointEnv); endpoint != "" { + opts = append(opts, option.WithEndpoint(endpoint)) + } + if ud := os.Getenv(SopsGCPKMSUniverseDomainEnv); ud != "" { + opts = append(opts, option.WithUniverseDomain(ud)) + } + // Select client type based on inputs. clientType := strings.ToLower(os.Getenv(SopsGCPKMSClientTypeEnv)) var client *kms.KeyManagementClient diff --git a/gcpkms/keysource_test.go b/gcpkms/keysource_test.go index e0365de34f..631c0fde2a 100644 --- a/gcpkms/keysource_test.go +++ b/gcpkms/keysource_test.go @@ -189,6 +189,32 @@ func TestMasterKey_createCloudKMSService_withoutCredentials(t *testing.T) { assert.ErrorContains(t, err, "credentials: could not find default credentials") } +func TestMasterKey_createCloudKMSService_withEndpointEnv(t *testing.T) { + t.Setenv(SopsGCPKMSEndpointEnv, "cloudkms.example.com:443") + t.Setenv(SopsGoogleCredentialsOAuthTokenEnv, "token") + + masterKey := MasterKey{ + ResourceID: testResourceID, + } + + _, err := masterKey.newKMSClient(context.Background()) + + assert.NoError(t, err) +} + +func TestMasterKey_createCloudKMSService_withUniverseDomainEnv(t *testing.T) { + t.Setenv(SopsGCPKMSUniverseDomainEnv, "example.com") + t.Setenv(SopsGoogleCredentialsOAuthTokenEnv, "token") + + masterKey := MasterKey{ + ResourceID: testResourceID, + } + + _, err := masterKey.newKMSClient(context.Background()) + + assert.NoError(t, err) +} + func newGRPCServer(port string) *grpc.ClientConn { serv := grpc.NewServer() kmspb.RegisterKeyManagementServiceServer(serv, &mockKeyManagement) From fd62dc33d976b9b34a0d9dc30a3bfe260a935728 Mon Sep 17 00:00:00 2001 From: David RIBEIRO Date: Tue, 31 Mar 2026 10:22:24 +0200 Subject: [PATCH 2/2] fix(gcpkms): address review comments on sovereign cloud KMS support - Use else if to make SOPS_GCP_KMS_ENDPOINT precedence over SOPS_GCP_KMS_UNIVERSE_DOMAIN explicit in code - Use RST note directive in README.rst - Verify env var options are applied by asserting on client.Connection().Target() in tests Signed-off-by: David RIBEIRO --- README.rst | 3 +-- gcpkms/keysource.go | 3 +-- gcpkms/keysource_test.go | 8 ++++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index e430006127..c80989d38f 100644 --- a/README.rst +++ b/README.rst @@ -337,8 +337,7 @@ you can override the endpoint or the universe domain: # Or derive the endpoint from the universe domain (cloudkms.:443) $ export SOPS_GCP_KMS_UNIVERSE_DOMAIN=example.com -Note: ``SOPS_GCP_KMS_ENDPOINT`` takes precedence over -``SOPS_GCP_KMS_UNIVERSE_DOMAIN`` if both are set. +.. note:: ``SOPS_GCP_KMS_ENDPOINT`` takes precedence over ``SOPS_GCP_KMS_UNIVERSE_DOMAIN`` if both are set. Encrypting/decrypting with GCP KMS requires a KMS ResourceID. You can use the cloud console the get the ResourceID or you can create one using the gcloud diff --git a/gcpkms/keysource.go b/gcpkms/keysource.go index 8a2b35aaf5..2acbdd9c0c 100644 --- a/gcpkms/keysource.go +++ b/gcpkms/keysource.go @@ -330,8 +330,7 @@ func (key *MasterKey) newKMSClient(ctx context.Context) (*kms.KeyManagementClien if endpoint := os.Getenv(SopsGCPKMSEndpointEnv); endpoint != "" { opts = append(opts, option.WithEndpoint(endpoint)) - } - if ud := os.Getenv(SopsGCPKMSUniverseDomainEnv); ud != "" { + } else if ud := os.Getenv(SopsGCPKMSUniverseDomainEnv); ud != "" { opts = append(opts, option.WithUniverseDomain(ud)) } diff --git a/gcpkms/keysource_test.go b/gcpkms/keysource_test.go index 631c0fde2a..fa50b6bb87 100644 --- a/gcpkms/keysource_test.go +++ b/gcpkms/keysource_test.go @@ -197,9 +197,9 @@ func TestMasterKey_createCloudKMSService_withEndpointEnv(t *testing.T) { ResourceID: testResourceID, } - _, err := masterKey.newKMSClient(context.Background()) - + client, err := masterKey.newKMSClient(context.Background()) assert.NoError(t, err) + assert.Contains(t, client.Connection().Target(), "cloudkms.example.com") } func TestMasterKey_createCloudKMSService_withUniverseDomainEnv(t *testing.T) { @@ -210,9 +210,9 @@ func TestMasterKey_createCloudKMSService_withUniverseDomainEnv(t *testing.T) { ResourceID: testResourceID, } - _, err := masterKey.newKMSClient(context.Background()) - + client, err := masterKey.newKMSClient(context.Background()) assert.NoError(t, err) + assert.Contains(t, client.Connection().Target(), "cloudkms.example.com") } func newGRPCServer(port string) *grpc.ClientConn {