diff --git a/.gitignore b/.gitignore index 8932a7e..6be482b 100644 --- a/.gitignore +++ b/.gitignore @@ -352,3 +352,4 @@ healthchecksdb logs *.pem *.crt +.claude/settings.local.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b85e7..f16f424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +- 1.3.0 + - SaaS containerization changes with Google Credentials +- 1.2.2 + - Fixed Sync Issues at CA Level, was ignoring and always syncing at pool level +- 1.2.1 + - Doc Updates - 1.2.0 - Added Enable Flag - Dual Build Support diff --git a/GCPCAS/Client/GCPCASClient.cs b/GCPCAS/Client/GCPCASClient.cs index e3ed3e3..0197836 100644 --- a/GCPCAS/Client/GCPCASClient.cs +++ b/GCPCAS/Client/GCPCASClient.cs @@ -58,8 +58,9 @@ public class GCPCASClient : IGCPCASClient /// The GCP project ID where the target GCP CAS CA is located /// The CA Pool ID in GCP CAS to use for certificate operations. If the CA Pool has resource name projects/my-project/locations/us-central1/caPools/my-pool, this field should be set to my-pool /// The CA ID of a CA in the same CA Pool as CAPool. For example, to issue certificates from a CA with resource name projects/my-project/locations/us-central1/caPools/my-pool/certificateAuthorities/my-ca, this field should be set to my-ca. - public GCPCASClient(string locationId, string projectId, string caPool, string caId) - { + /// Optional JSON service account key. When provided, used instead of Application Default Credentials. + public GCPCASClient(string locationId, string projectId, string caPool, string caId, string serviceAccountKey = null) + { _logger = LogHandler.GetClassLogger(); _logger.MethodEntry(); _logger.LogDebug($"Creating GCP CA Services Client with Location: {locationId}, Project ID: {projectId}, CA Pool: {caPool}, CA ID: {caId}"); @@ -69,8 +70,18 @@ public GCPCASClient(string locationId, string projectId, string caPool, string c this._caPool = caPool; this._caId = caId; - _logger.LogTrace($"Setting up a {typeof(CertificateAuthorityServiceClient).ToString()} using the Default gRPC adapter"); - _client = new CertificateAuthorityServiceClientBuilder().Build(); + var builder = new CertificateAuthorityServiceClientBuilder(); + if (!string.IsNullOrEmpty(serviceAccountKey)) + { + _logger.LogTrace("Using provided service account key JSON for authentication"); + builder.JsonCredentials = serviceAccountKey; + } + else + { + _logger.LogTrace($"Setting up a {typeof(CertificateAuthorityServiceClient).ToString()} using Application Default Credentials"); + } + + _client = builder.Build(); _logger.MethodExit(); } @@ -84,13 +95,13 @@ public override string ToString() /// /// public Task Enable() - { + { _logger.MethodEntry(); if (!_clientIsEnabled) { _logger.LogDebug($"Enabling GCPCAS client {this.ToString()}"); _clientIsEnabled = true; - } + } _logger.MethodExit(); return Task.CompletedTask; } @@ -100,7 +111,7 @@ public Task Enable() /// /// public Task Disable() - { + { _logger.MethodEntry(); if (_clientIsEnabled) { @@ -118,12 +129,12 @@ public Task Disable() /// A indicating if the client is enabled. /// public bool IsEnabled() - { + { _logger.MethodEntry(); _logger.MethodExit(); return _clientIsEnabled; - } - + } + /// /// Attempts to connect to the GCP CAS service to verify connectivity. Verifies that the GCP Application Default Credentials are properly configured. /// @@ -131,53 +142,53 @@ public bool IsEnabled() /// Returns nothing if the connection is successful. /// /// Thrown if the GCP Application Default Credentials are not properly configured, if the GCP CAS CA Pool/CA is not found/is not compatible, or if the was not enabled via the method. - public async Task ValidateConnection() - { - _logger.MethodEntry(); - EnsureClientIsEnabled(); - - if (string.IsNullOrEmpty(_caId)) - { - _logger.LogTrace($"Validating CA Pool {_caPool} since no specific CA ID was provided"); - - CaPoolName poolName = new CaPoolName(_projectId, _locationId, _caPool); - CaPool pool = await _client.GetCaPoolAsync(poolName); - - if (pool.Tier != CaPool.Types.Tier.Enterprise) - { - string error = $"CA Pool {_caPool} is in Tier {pool.Tier}, expected {CaPool.Types.Tier.Enterprise}."; - _logger.LogError(error); - throw new Exception(error); - } - - _logger.LogDebug($"CA Pool {_caPool} is Enterprise tier and valid."); - _logger.MethodExit(); - return; - } - - _logger.LogTrace($"Searching for CA called {_caId} in CA Pool {_caPool}"); - CertificateAuthorityName caName = new CertificateAuthorityName(_projectId, _locationId, _caPool, _caId); - CertificateAuthority ca = await _client.GetCertificateAuthorityAsync(caName); - - _logger.LogDebug($"Found CA {ca.CertificateAuthorityName.CertificateAuthorityId} in CA Pool {ca.CertificateAuthorityName.CaPoolId}"); - - if (ca.State != CertificateAuthority.Types.State.Enabled) - { - string error = $"CA {_caId} is in state {ca.State}. Expected Enabled."; - _logger.LogError(error); - throw new Exception(error); - } - - if (ca.Tier != CaPool.Types.Tier.Enterprise) - { - string error = $"CA {_caId} is in tier {ca.Tier}. Only Enterprise tier is supported."; - _logger.LogError(error); - throw new Exception(error); - } - - _logger.LogDebug($"{nameof(GCPCASClient)} is compatible with CA {_caId} in Pool {_caPool}."); - _logger.MethodExit(); - } + public async Task ValidateConnection() + { + _logger.MethodEntry(); + EnsureClientIsEnabled(); + + if (string.IsNullOrEmpty(_caId)) + { + _logger.LogTrace($"Validating CA Pool {_caPool} since no specific CA ID was provided"); + + CaPoolName poolName = new CaPoolName(_projectId, _locationId, _caPool); + CaPool pool = await _client.GetCaPoolAsync(poolName); + + if (pool.Tier != CaPool.Types.Tier.Enterprise) + { + string error = $"CA Pool {_caPool} is in Tier {pool.Tier}, expected {CaPool.Types.Tier.Enterprise}."; + _logger.LogError(error); + throw new Exception(error); + } + + _logger.LogDebug($"CA Pool {_caPool} is Enterprise tier and valid."); + _logger.MethodExit(); + return; + } + + _logger.LogTrace($"Searching for CA called {_caId} in CA Pool {_caPool}"); + CertificateAuthorityName caName = new CertificateAuthorityName(_projectId, _locationId, _caPool, _caId); + CertificateAuthority ca = await _client.GetCertificateAuthorityAsync(caName); + + _logger.LogDebug($"Found CA {ca.CertificateAuthorityName.CertificateAuthorityId} in CA Pool {ca.CertificateAuthorityName.CaPoolId}"); + + if (ca.State != CertificateAuthority.Types.State.Enabled) + { + string error = $"CA {_caId} is in state {ca.State}. Expected Enabled."; + _logger.LogError(error); + throw new Exception(error); + } + + if (ca.Tier != CaPool.Types.Tier.Enterprise) + { + string error = $"CA {_caId} is in tier {ca.Tier}. Only Enterprise tier is supported."; + _logger.LogError(error); + throw new Exception(error); + } + + _logger.LogDebug($"{nameof(GCPCASClient)} is compatible with CA {_caId} in Pool {_caPool}."); + _logger.MethodExit(); + } /// @@ -195,84 +206,100 @@ public async Task ValidateConnection() /// /// Thrown if the is null or if the operation fails. /// - public async Task DownloadAllIssuedCertificates(BlockingCollection certificatesBuffer, CancellationToken cancelToken, DateTime? issuedAfter = null) - { - _logger.MethodEntry(); - EnsureClientIsEnabled(); - - if (certificatesBuffer == null) - { - string message = "Failed to download issued certificates - certificatesBuffer is null"; - _logger.LogError(message); - throw new ArgumentNullException(nameof(certificatesBuffer), message); - } - - _logger.LogTrace($"Setting up {typeof(ListCertificatesRequest).ToString()} with {this.ToString()}"); - - ListCertificatesRequest request = new ListCertificatesRequest - { - ParentAsCaPoolName = new CaPoolName(_projectId, _locationId, _caPool), - }; - - if (issuedAfter != null) - { - Timestamp ts = Timestamp.FromDateTime(issuedAfter.Value.ToUniversalTime()); - _logger.LogDebug($"Filtering issued certificates by update_time >= {ts}"); - request.Filter = $"update_time >= {ts}"; - } - - _logger.LogTrace($"Setting up {typeof(CallSettings).ToString()} with provided {typeof(CancellationToken).ToString()} {this.ToString()}"); - CallSettings settings = CallSettings.FromCancellationToken(cancelToken); - - _logger.LogDebug($"Downloading all issued certificates from GCP CAS {this.ToString()}"); - PagedAsyncEnumerable certificates = _client.ListCertificatesAsync(request, settings); - - int pageNumber = 0; - int numberOfCertificates = 0; - - try - { - await foreach (var response in certificates.AsRawResponses()) - { - if (response.Certificates == null) - { - _logger.LogWarning($"GCP returned null certificate list for page number {pageNumber} - continuing {this.ToString()}"); - continue; - } - - foreach (Certificate certificate in response.Certificates) - { - certificatesBuffer.Add(AnyCAPluginCertificateFromGCPCertificate(certificate)); - numberOfCertificates++; - _logger.LogDebug($"Found Certificate with name {certificate.CertificateName.CertificateId} {this.ToString()}"); - } - - _logger.LogTrace($"Fetched page {pageNumber} - Next Page Token: {response.NextPageToken}"); - pageNumber++; - } - } - catch (RpcException ex) when (ex.StatusCode == StatusCode.ResourceExhausted) - { - _logger.LogError($"Rate limit exceeded while fetching certificates: {ex.Message}"); - throw; - } - catch (OperationCanceledException) - { - _logger.LogWarning("Certificate download operation was canceled."); - throw; - } - catch (Exception ex) - { - _logger.LogError($"Unexpected error while fetching certificates: {ex.Message}"); - throw; - } - finally - { - certificatesBuffer.CompleteAdding(); - _logger.LogDebug($"Fetched {certificatesBuffer.Count} certificates from GCP over {pageNumber} pages."); - } - _logger.MethodExit(); - return numberOfCertificates; + public async Task DownloadAllIssuedCertificates(BlockingCollection certificatesBuffer, CancellationToken cancelToken, DateTime? issuedAfter = null) + { + _logger.MethodEntry(); + EnsureClientIsEnabled(); + + if (certificatesBuffer == null) + { + string message = "Failed to download issued certificates - certificatesBuffer is null"; + _logger.LogError(message); + throw new ArgumentNullException(nameof(certificatesBuffer), message); + } + + _logger.LogTrace($"Setting up {typeof(ListCertificatesRequest).ToString()} with {this.ToString()}"); + + ListCertificatesRequest request = new ListCertificatesRequest + { + ParentAsCaPoolName = new CaPoolName(_projectId, _locationId, _caPool), + }; + + string caFilter = null; + if (!string.IsNullOrEmpty(_caId)) + { + caFilter = _caId; + _logger.LogDebug($"Will filter certificates client-side by issuing CA ID: {caFilter}"); + } + + if (issuedAfter != null) + { + Timestamp ts = Timestamp.FromDateTime(issuedAfter.Value.ToUniversalTime()); + _logger.LogDebug($"Filtering issued certificates by update_time >= {ts}"); + request.Filter = $"update_time >= {ts}"; + } + + _logger.LogTrace($"Setting up {typeof(CallSettings).ToString()} with provided {typeof(CancellationToken).ToString()} {this.ToString()}"); + CallSettings settings = CallSettings.FromCancellationToken(cancelToken); + + _logger.LogDebug($"Downloading all issued certificates from GCP CAS {this.ToString()}"); + PagedAsyncEnumerable certificates = _client.ListCertificatesAsync(request, settings); + + int pageNumber = 0; + int numberOfCertificates = 0; + + try + { + await foreach (var response in certificates.AsRawResponses()) + { + if (response.Certificates == null) + { + _logger.LogWarning($"GCP returned null certificate list for page number {pageNumber} - continuing {this.ToString()}"); + continue; + } + + foreach (Certificate certificate in response.Certificates) + { + if (caFilter != null) + { + CertificateAuthorityName issuer = CertificateAuthorityName.Parse(certificate.IssuerCertificateAuthority); + if (issuer.CertificateAuthorityId != caFilter) + { + _logger.LogTrace($"Skipping certificate {certificate.CertificateName.CertificateId} - issued by {issuer.CertificateAuthorityId}, not {caFilter}"); + continue; + } + } + certificatesBuffer.Add(AnyCAPluginCertificateFromGCPCertificate(certificate)); + numberOfCertificates++; + _logger.LogDebug($"Found Certificate with name {certificate.CertificateName.CertificateId} {this.ToString()}"); + } + + _logger.LogTrace($"Fetched page {pageNumber} - Next Page Token: {response.NextPageToken}"); + pageNumber++; + } + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.ResourceExhausted) + { + _logger.LogError($"Rate limit exceeded while fetching certificates: {ex.Message}"); + throw; + } + catch (OperationCanceledException) + { + _logger.LogWarning("Certificate download operation was canceled."); + throw; + } + catch (Exception ex) + { + _logger.LogError($"Unexpected error while fetching certificates: {ex.Message}"); + throw; + } + finally + { + certificatesBuffer.CompleteAdding(); + _logger.LogDebug($"Fetched {certificatesBuffer.Count} certificates from GCP over {pageNumber} pages."); + } + _logger.MethodExit(); + return numberOfCertificates; } @@ -287,7 +314,7 @@ public async Task DownloadAllIssuedCertificates(BlockingCollection and task result as a containing the downloaded certificate. /// public async Task DownloadCertificate(string certificateId) - { + { _logger.MethodEntry(); EnsureClientIsEnabled(); @@ -300,13 +327,13 @@ public async Task DownloadCertificate(string certificate }; Certificate certificate = await _client.GetCertificateAsync(request); - _logger.LogTrace("GetCertificateAsync succeeded"); + _logger.LogTrace("GetCertificateAsync succeeded"); _logger.MethodExit(); return AnyCAPluginCertificateFromGCPCertificate(certificate); } private AnyCAPluginCertificate AnyCAPluginCertificateFromGCPCertificate(Certificate certificate) - { + { _logger.MethodEntry(); string productId = ""; if (certificate.CertificateTemplateAsCertificateTemplateName == null) @@ -328,7 +355,7 @@ private AnyCAPluginCertificate AnyCAPluginCertificateFromGCPCertificate(Certific revocationDate = certificate.RevocationDetails.RevocationTime.ToDateTime(); status = EndEntityStatus.REVOKED; revocationReason = (int)certificate.RevocationDetails.RevocationState; - } + } _logger.MethodExit(); return new AnyCAPluginCertificate { @@ -355,23 +382,23 @@ private AnyCAPluginCertificate AnyCAPluginCertificateFromGCPCertificate(Certific public async Task Enroll(ICreateCertificateRequestBuilder createCertificateRequestBuilder, CancellationToken cancelToken) { try - { + { _logger.MethodEntry(); EnsureClientIsEnabled(); CreateCertificateRequest request = createCertificateRequestBuilder.Build(_locationId, _projectId, _caPool, _caId); - if (request != null) - { - _logger.LogTrace($"Request Json {JsonConvert.SerializeObject(request)}"); + if (request != null) + { + _logger.LogTrace($"Request Json {JsonConvert.SerializeObject(request)}"); } Certificate certificate = await _client.CreateCertificateAsync(request, cancelToken); - if (certificate != null) - { - _logger.LogTrace($"Response Json {JsonConvert.SerializeObject(certificate)}"); - } + if (certificate != null) + { + _logger.LogTrace($"Response Json {JsonConvert.SerializeObject(certificate)}"); + } _logger.MethodExit(); return new EnrollmentResult { @@ -421,7 +448,7 @@ public async Task Enroll(ICreateCertificateRequestBuilder crea /// /// public Task RevokeCertificate(string certificateId, RevocationReason reason) - { + { _logger.MethodEntry(); EnsureClientIsEnabled(); @@ -444,7 +471,7 @@ public Task RevokeCertificate(string certificateId, RevocationReason reason) /// A of containing the available s. /// public List GetTemplates() - { + { _logger.MethodEntry(); EnsureClientIsEnabled(); diff --git a/GCPCAS/GCPCASCAPlugin.cs b/GCPCAS/GCPCASCAPlugin.cs index 7e94658..431720b 100644 --- a/GCPCAS/GCPCASCAPlugin.cs +++ b/GCPCAS/GCPCASCAPlugin.cs @@ -200,7 +200,7 @@ private void GCPCASClientFromCAConnectionData(Dictionary connect else { _logger.LogDebug("Creating new GCPCASClient instance."); - Client = new GCPCASClient(_config.LocationId, _config.ProjectId, _config.CAPool, _config.CAId); + Client = new GCPCASClient(_config.LocationId, _config.ProjectId, _config.CAPool, _config.CAId, _config.ServiceAccountKey); } if (_config.Enabled) diff --git a/GCPCAS/GCPCASCAPluginConfig.cs b/GCPCAS/GCPCASCAPluginConfig.cs index 6d5e424..4136db0 100644 --- a/GCPCAS/GCPCASCAPluginConfig.cs +++ b/GCPCAS/GCPCASCAPluginConfig.cs @@ -33,6 +33,7 @@ public class ConfigConstants public const string CAPool = "CAPool"; public const string CAId = "CAId"; public const string Enabled = "Enabled"; + public const string ServiceAccountKey = "ServiceAccountKey"; } public class Config @@ -42,6 +43,7 @@ public class Config public string CAPool { get; set; } public string CAId { get; set; } public bool Enabled { get; set; } + public string ServiceAccountKey { get; set; } } public static class EnrollmentParametersConstants @@ -88,6 +90,13 @@ public static Dictionary GetPluginAnnotations() DefaultValue = true, Type = "Boolean" }, + [ConfigConstants.ServiceAccountKey] = new PropertyConfigInfo() + { + Comments = "Optional JSON service account key for GCP authentication. When provided, this is used instead of Application Default Credentials (ADC). This is recommended for containerized environments where mounting a credentials file is not practical. Leave empty to use ADC.", + Hidden = true, + DefaultValue = "", + Type = "Secret" + }, }; } diff --git a/README.md b/README.md index 00f1a94..2c62011 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,19 @@ The GCP CAS AnyCA Gateway REST plugin is supported by Keyfactor for Keyfactor cu ## Requirements -### Application Default Credentials +### GCP Authentication -The GCP CAS AnyCA Gateway REST plugin connects to and authenticates with GCP CAS implicitly using [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). This means that all authentication-related configuration of the GCP CAS AnyCA Gateway REST plugin is implied by the environment where the AnyCA Gateway REST itself is running. +The GCP CAS AnyCA Gateway REST plugin supports two methods for authenticating with GCP CAS: + +#### Option 1: Service Account Key via CA Connection Configuration (Recommended for Containers) + +The plugin accepts an optional **ServiceAccountKey** field in the CA Connection configuration. When provided, the JSON service account key is used directly for authentication without requiring any credential files on the filesystem. This is the recommended approach for containerized deployments (e.g., Docker, Kubernetes) where mounting credential files is not practical. + +To use this method, paste the full JSON contents of a GCP service account key into the **ServiceAccountKey** field in the CA Connection tab. In Kubernetes, the service account key JSON can be stored as a Secret and injected via the Keyfactor configuration API. + +#### Option 2: Application Default Credentials (ADC) + +If the **ServiceAccountKey** field is left empty, the plugin falls back to [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). This means that all authentication-related configuration is implied by the environment where the AnyCA Gateway REST itself is running. Please refer to [Google's documentation](https://cloud.google.com/docs/authentication/provide-credentials-adc) to configure ADC on the server running the AnyCA Gateway REST. @@ -75,6 +85,8 @@ Please refer to [Google's documentation](https://cloud.google.com/docs/authentic > 1. The service account that the AnyCA Gateway REST runs under must have read permission to the GCP credential JSON file. > 2. You must set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable for the Windows Service running the AnyCA Gateway REST using the [Windows registry editor](https://learn.microsoft.com/en-us/troubleshoot/windows-server/performance/windows-registry-advanced-users). > * Refer to the [HKLM\SYSTEM\CurrentControlSet\Services Registry Tree](https://learn.microsoft.com/en-us/windows-hardware/drivers/install/hklm-system-currentcontrolset-services-registry-tree) docs +> +> For containerized environments running on GCP (e.g., GKE), [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) can be used instead, which requires no credential files or environment variables. If the selected ADC mechanism is [Service Account Key](https://cloud.google.com/docs/authentication/provide-credentials-adc#wlif-key), it's recommended that a [custom role is created](https://cloud.google.com/iam/docs/creating-custom-roles) that has the following minimum permissions: @@ -140,6 +152,7 @@ Both the Keyfactor Command and AnyCA Gateway REST servers must trust the root CA * **CAPool** - The CA Pool ID in GCP CAS to use for certificate operations. If the CA Pool has resource name `projects/my-project/locations/us-central1/caPools/my-pool`, this field should be set to `my-pool` * **CAId** - The CA ID of a CA in the same CA Pool as CAPool. For example, to issue certificates from a CA with resource name `projects/my-project/locations/us-central1/caPools/my-pool/certificateAuthorities/my-ca`, this field should be set to `my-ca`. * **Enabled** - Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available. + * **ServiceAccountKey** - Optional JSON service account key for GCP authentication. When provided, this is used instead of Application Default Credentials (ADC). This is recommended for containerized environments where mounting a credentials file is not practical. Leave empty to use ADC. 2. Define [Certificate Profiles](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCP-Gateway.htm) and [Certificate Templates](https://software.keyfactor.com/Guides/AnyCAGatewayREST/Content/AnyCAGatewayREST/AddCA-Gateway.htm) for the Certificate Authority as required. One Certificate Profile must be defined per Certificate Template. It's recommended that each Certificate Profile be named after the Product ID. diff --git a/docsource/configuration.md b/docsource/configuration.md index fe02049..576c39c 100644 --- a/docsource/configuration.md +++ b/docsource/configuration.md @@ -19,9 +19,19 @@ The [Google Cloud Platform (GCP) CA Services (CAS)](https://cloud.google.com/sec ## Requirements -### Application Default Credentials +### GCP Authentication -The GCP CAS AnyCA Gateway REST plugin connects to and authenticates with GCP CAS implicitly using [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). This means that all authentication-related configuration of the GCP CAS AnyCA Gateway REST plugin is implied by the environment where the AnyCA Gateway REST itself is running. +The GCP CAS AnyCA Gateway REST plugin supports two methods for authenticating with GCP CAS: + +#### Option 1: Service Account Key via CA Connection Configuration (Recommended for Containers) + +The plugin accepts an optional **ServiceAccountKey** field in the CA Connection configuration. When provided, the JSON service account key is used directly for authentication without requiring any credential files on the filesystem. This is the recommended approach for containerized deployments (e.g., Docker, Kubernetes) where mounting credential files is not practical. + +To use this method, paste the full JSON contents of a GCP service account key into the **ServiceAccountKey** field in the CA Connection tab. In Kubernetes, the service account key JSON can be stored as a Secret and injected via the Keyfactor configuration API. + +#### Option 2: Application Default Credentials (ADC) + +If the **ServiceAccountKey** field is left empty, the plugin falls back to [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). This means that all authentication-related configuration is implied by the environment where the AnyCA Gateway REST itself is running. Please refer to [Google's documentation](https://cloud.google.com/docs/authentication/provide-credentials-adc) to configure ADC on the server running the AnyCA Gateway REST. @@ -32,6 +42,8 @@ Please refer to [Google's documentation](https://cloud.google.com/docs/authentic > 1. The service account that the AnyCA Gateway REST runs under must have read permission to the GCP credential JSON file. > 2. You must set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable for the Windows Service running the AnyCA Gateway REST using the [Windows registry editor](https://learn.microsoft.com/en-us/troubleshoot/windows-server/performance/windows-registry-advanced-users). > * Refer to the [HKLM\SYSTEM\CurrentControlSet\Services Registry Tree](https://learn.microsoft.com/en-us/windows-hardware/drivers/install/hklm-system-currentcontrolset-services-registry-tree) docs +> +> For containerized environments running on GCP (e.g., GKE), [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) can be used instead, which requires no credential files or environment variables. If the selected ADC mechanism is [Service Account Key](https://cloud.google.com/docs/authentication/provide-credentials-adc#wlif-key), it's recommended that a [custom role is created](https://cloud.google.com/iam/docs/creating-custom-roles) that has the following minimum permissions: diff --git a/integration-manifest.json b/integration-manifest.json index 10c9afd..baaf854 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -32,6 +32,10 @@ { "name": "Enabled", "description": "Flag to Enable or Disable gateway functionality. Disabling is primarily used to allow creation of the CA prior to configuration information being available." + }, + { + "name": "ServiceAccountKey", + "description": "Optional JSON service account key for GCP authentication. When provided, this is used instead of Application Default Credentials (ADC). This is recommended for containerized environments where mounting a credentials file is not practical. Leave empty to use ADC." } ], "enrollment_config": [