From 4f053812453de171462d75931f7e6bba00b47108 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Mon, 10 Mar 2025 11:26:58 -0400 Subject: [PATCH 01/14] Removed check for client cert as client cert auth has not been implemented yet and client cert is not available as a parameter. --- .../HashicorpVaultCAConnector.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index e020e9e..011da5d 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -352,18 +352,22 @@ public async Task ValidateCAConnectionInfo(Dictionary connection // make sure an authentication mechanism is defined (either certificate or token) var token = connectionInfo[Constants.CAConfig.TOKEN] as string; - var cert = connectionInfo[Constants.CAConfig.CLIENTCERT] as string; - - if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(cert)) - { - errors.Add("Either an authentication token or client certificate must be defined for authentication into Vault."); - } - if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(cert)) - { - logger.LogWarning("Both an authentication token and client certificate are defined. Using the token for authentication."); - } + + /// REMOVING CERT VALIDATION UNTIL CLIENT CERT AUTH IS IMPLEMENTED + + //var cert = connectionInfo[Constants.CAConfig.CLIENTCERT] as string; + + //if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(cert)) + //{ + // errors.Add("Either an authentication token or client certificate must be defined for authentication into Vault."); + //} + //if (!string.IsNullOrEmpty(token) && !string.IsNullOrEmpty(cert)) + //{ + // logger.LogWarning("Both an authentication token and client certificate are defined. Using the token for authentication."); + //} // if any errors, throw + if (errors.Any()) { var allErrors = string.Join("\n", errors); From 3987b0f5b1c505c0c60b4810d4c08308dfa14588 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Tue, 11 Mar 2025 15:59:16 -0400 Subject: [PATCH 02/14] Removing validation for RoleName to be defined in the template; unnecessary since productID == roleName --- .../HashicorpVaultCAConnector.cs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 011da5d..6e3f5ef 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -352,9 +352,9 @@ public async Task ValidateCAConnectionInfo(Dictionary connection // make sure an authentication mechanism is defined (either certificate or token) var token = connectionInfo[Constants.CAConfig.TOKEN] as string; - + /// REMOVING CERT VALIDATION UNTIL CLIENT CERT AUTH IS IMPLEMENTED - + //var cert = connectionInfo[Constants.CAConfig.CLIENTCERT] as string; //if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(cert)) @@ -443,10 +443,15 @@ public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary ProductIdIsValid(string productID, HashicorpVaultCAConfig config) + { + + _client = new HashicorpVaultClient(config); + + // attempt an authenticated request to retreive role names + try + { + logger.LogTrace("making an authenticated request to the Vault server to verify credentials (listing role names).."); + var roleNames = await _client.GetRoleNamesAsync(); + logger.LogTrace($"successfule request: received a response containing {roleNames.Count} role names"); + return roleNames.Any(rn => rn == productID); + } + catch (Exception ex) + { + logger.LogError($"Authenticated request failed. {ex.Message}"); + throw; + } + finally { logger.MethodExit(); } + } + /// /// Gets annotations for the CA connector properties. /// From b17a29d199fb16f5e633d71cfe156fffaee22abc Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Thu, 13 Mar 2025 11:35:35 -0400 Subject: [PATCH 03/14] Updated to initialize a new instance of the vault client on each request. --- .../HashicorpVaultCAConnector.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 6e3f5ef..124285b 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -70,6 +70,9 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) { logger.MethodEntry(LogLevel.Trace); + + _client = new HashicorpVaultClient(_caConfig); + logger.LogInformation($"Begin {enrollmentType} enrollment for {subject}"); string statusMessage; SignResponse signResponse; @@ -142,6 +145,9 @@ public async Task Enroll(string csr, string subject, Dictionar public async Task GetSingleRecord(string caRequestID) { logger.MethodEntry(); + + _client = new HashicorpVaultClient(_caConfig); + logger.LogTrace($"preparing to send request to retrieve certificate with id {caRequestID}"); try { @@ -176,6 +182,7 @@ public async Task GetSingleRecord(string caRequestID) public async Task Ping() { logger.MethodEntry(); + _client = new HashicorpVaultClient(_caConfig); logger.LogTrace("Attempting ping of Vault endpoint"); try { @@ -198,6 +205,7 @@ public async Task Ping() /// The status of the request as an int representing EndEntityStatus public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) { + _client = new HashicorpVaultClient(_caConfig); logger.MethodEntry(); logger.LogTrace($"Sending request to revoke certificate with id: {caRequestID}"); try @@ -223,6 +231,7 @@ public async Task Revoke(string caRequestID, string hexSerialNumber, uint r /// The cancellation token. public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) { + _client = new HashicorpVaultClient(_caConfig); // !! Any certificates issued outside of this CA Gateway will not necessarily be associated with the role name / (product ID) that was used to generate it // !! since that value is not retreivable after the initial generation. @@ -247,6 +256,7 @@ public async Task Synchronize(BlockingCollection blockin foreach (var certSerial in certSerials) { + cancelToken.ThrowIfCancellationRequested(); CertResponse certFromVault = null; var dbStatus = -1; @@ -467,7 +477,6 @@ public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary ProductIdIsValid(string productID, HashicorpVaultCAConfig config) { - _client = new HashicorpVaultClient(config); // attempt an authenticated request to retreive role names From e05f8a8275c41f0dd5d9c40ef702ac8341fab74a Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Mon, 17 Mar 2025 12:04:54 -0400 Subject: [PATCH 04/14] setting certificatedatareader during initialization --- hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 124285b..a18b4fa 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -54,12 +54,12 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa _caConfig = JsonSerializer.Deserialize(rawConfig); logger.MethodExit(LogLevel.Trace); _client = new HashicorpVaultClient(_caConfig); + _certificateDataReader = certificateDataReader; } /// /// Enrolls for a certificate through the API. /// - /// Reads certificate data from the database. /// The certificate request CSR in PEM format. /// The subject of the certificate request. /// Any SANs added to the request. From 5d674a63e4d1d7dc6db8587e193c75d211ad2991 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Mon, 17 Mar 2025 12:23:53 -0400 Subject: [PATCH 05/14] Updated logging --- .../HashicorpVaultCAConnector.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index a18b4fa..47a4e96 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -240,6 +240,7 @@ public async Task Synchronize(BlockingCollection blockin var certSerials = new List(); var count = 0; + var changedCount = 0; try { @@ -281,15 +282,16 @@ public async Task Synchronize(BlockingCollection blockin logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); dbStatus = await _certificateDataReader.GetStatusByRequestID(trackingId); } - catch + catch(Exception ex) { + logger.LogTrace($"exception when retrieving cert from DB. It could simply be missing, but logging for analysis: {ex.Message}"); logger.LogTrace($"tracking id {trackingId} was not found in the database. it will be added."); } if (dbStatus == -1 || fullSync) // it's missing and needs added, or a full sync is requested { logger.LogTrace($"adding cert with serial {trackingId} to the database. fullsync is {fullSync}, and the certificate {(dbStatus == -1 ? "does not yet exist" : "already exists")} in the database."); - + changedCount++; var newCert = new AnyCAPluginCertificate { CARequestID = trackingId, @@ -312,10 +314,13 @@ public async Task Synchronize(BlockingCollection blockin } else // the cert exists in the database; just update the status if necessary { + logger.LogTrace($"certificate with id {trackingId} was found in the database, comparing status."); var revoked = certFromVault.RevocationTime != null; var vaultStatus = revoked ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED; if (vaultStatus != dbStatus) // if there is a mismatch, we need to update { + changedCount++; + logger.LogTrace($"status in vault is {vaultStatus}, status in db is {dbStatus}; updating db."); var newCert = new AnyCAPluginCertificate { CARequestID = trackingId, @@ -325,10 +330,13 @@ public async Task Synchronize(BlockingCollection blockin // ProductID is not available via the API after the initial issuance. we do not want to overwrite }; } + else { + logger.LogTrace($"The status is unchanged and we are doing an incremental scan; no need to update db."); + } } count++; } - logger.LogTrace($"Completed sync of {count} certificates"); + logger.LogTrace($"Completed sync of {count} certificates. It was {(fullSync ? "a full" : "an incremental")} sync and {changedCount} records were updated."); logger.MethodExit(); } From 91507f061a3e6491437dc9dee281e86e2389b215 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Tue, 18 Mar 2025 17:23:52 -0400 Subject: [PATCH 06/14] now creating new httpclient for each request. additional logging. --- .../Client/HashicorpVaultClient.cs | 40 ++++++------ hashicorp-vault-cagateway/Client/VaultHttp.cs | 62 ++++++++++++++----- .../HashicorpVaultCAConnector.cs | 51 +++++++-------- 3 files changed, 91 insertions(+), 62 deletions(-) diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index 3ab133d..3ec5ea1 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -25,15 +25,16 @@ namespace Keyfactor.Extensions.CAPlugin.HashicorpVault /// public class HashicorpVaultClient { - private VaultHttp _vaultHttp { get; set; } + private HashicorpVaultCAConfig _caConfig { get; set; } + private HashicorpVaultCATemplateConfig _templateConfig { get; set; } + private static readonly ILogger logger = LogHandler.GetClassLogger(); public HashicorpVaultClient(HashicorpVaultCAConfig caConfig, HashicorpVaultCATemplateConfig templateConfig = null) { logger.MethodEntry(); - - SetClientValuesFromConfigs(caConfig, templateConfig); - + _caConfig = caConfig; + _templateConfig = templateConfig; logger.MethodExit(); } @@ -101,6 +102,7 @@ public async Task SignCSR(string csr, string subject, Dictionary GetCertificate(string certSerial) try { + var _vaultHttp = ConfigureNewVaultClient(); var response = await _vaultHttp.GetAsync($"cert/{certSerial}"); logger.LogTrace($"successfully received a response for certificate with serial number: {certSerial}"); return response; @@ -151,7 +154,8 @@ public async Task RevokeCertificate(string serial) logger.MethodEntry(); logger.LogTrace($"making request to revoke cert with serial: {serial}"); try - { + { + var _vaultHttp = ConfigureNewVaultClient(); var response = await _vaultHttp.PostAsync("revoke", new RevokeRequest(serial)); logger.LogTrace($"successfully revoked cert with serial {serial}, revocation time: {response.RevocationTime}"); return response; @@ -170,6 +174,7 @@ public async Task PingServer() logger.LogTrace($"performing a system health check request to Vault"); try { + var _vaultHttp = ConfigureNewVaultClient(); var res = await _vaultHttp.HealthCheckAsync(); logger.LogTrace($"-- Vault health check response --"); logger.LogTrace($"Vault version : {res.VaultVersion}"); @@ -198,6 +203,7 @@ public async Task> GetAllCertSerialNumbers() var keys = new List(); try { + var _vaultHttp = ConfigureNewVaultClient(); var res = await _vaultHttp.GetAsync>("certs/?list=true"); return res.Data.Entries; } @@ -215,6 +221,7 @@ private async Task> GetRevokedSerialNumbers() var keys = new List(); try { + var _vaultHttp = ConfigureNewVaultClient(); var res = await _vaultHttp.GetAsync("certs/revoked"); keys = res.Entries; } @@ -233,6 +240,7 @@ public async Task> GetRoleNamesAsync() var roleNames = new List(); try { + var _vaultHttp = ConfigureNewVaultClient(); logger.LogTrace("getting the role names as a wrapped keyed-list response.."); var response = await _vaultHttp.GetAsync>("roles/?list=true"); logger.LogTrace($"received {response.Data?.Entries?.Count} role names (or product IDs)"); @@ -246,14 +254,14 @@ public async Task> GetRoleNamesAsync() finally { logger.MethodExit(); } } - private void SetClientValuesFromConfigs(HashicorpVaultCAConfig caConfig, HashicorpVaultCATemplateConfig templateConfig) + private VaultHttp ConfigureNewVaultClient() { logger.MethodEntry(); - var hostUrl = caConfig.Host; // host url and authentication details come from the CA config - var token = caConfig.Token; - var nameSpace = string.IsNullOrEmpty(templateConfig?.Namespace) ? caConfig.Namespace : templateConfig.Namespace; // Namespace comes from templateconfig if available, otherwise defaults to caConfig; can be null - var mountPoint = string.IsNullOrEmpty(templateConfig?.MountPoint) ? caConfig.MountPoint : templateConfig.MountPoint; // Mountpoint comes from templateconfig if available, otherwise defaults to caConfig; if null, uses "pki" (Vault Default) + var hostUrl = _caConfig.Host; // host url and authentication details come from the CA config + var token = _caConfig.Token; + var nameSpace = string.IsNullOrEmpty(_templateConfig?.Namespace) ? _caConfig.Namespace : _templateConfig.Namespace; // Namespace comes from templateconfig if available, otherwise defaults to caConfig; can be null + var mountPoint = string.IsNullOrEmpty(_templateConfig?.MountPoint) ? _caConfig.MountPoint : _templateConfig.MountPoint; // Mountpoint comes from templateconfig if available, otherwise defaults to caConfig; if null, uses "pki" (Vault Default) mountPoint = mountPoint ?? "pki"; // using the vault default PKI secrets engine mount point if not present in config logger.LogTrace($"set value for Host url: {hostUrl}"); @@ -268,19 +276,9 @@ private void SetClientValuesFromConfigs(HashicorpVaultCAConfig caConfig, Hashico //{ // throw new MissingFieldException("Either an authentication token or certificate to use for authentication into Vault must be provided."); //} - - _vaultHttp = new VaultHttp(hostUrl, mountPoint, token, nameSpace); - logger.MethodExit(); - } - - private static string ConvertSerialToTrackingId(string serialNumber) - { - // vault returns certificate serial formatted thusly: 17:67:16:b0:b9:45:58:c0:3a:29:e3:cb:d6:98:33:7a:a6:3b:66:c1 - // we cannot use the ':' character as part of our internal tracking id, but Vault requests can work with either ':' or '-' - // so we convert from colon-separated pairs to hyphen separated pairs. - return serialNumber.Replace(":", "-"); + return new VaultHttp(hostUrl, mountPoint, token, nameSpace); } } } \ No newline at end of file diff --git a/hashicorp-vault-cagateway/Client/VaultHttp.cs b/hashicorp-vault-cagateway/Client/VaultHttp.cs index c1c5f99..6fc2a9a 100644 --- a/hashicorp-vault-cagateway/Client/VaultHttp.cs +++ b/hashicorp-vault-cagateway/Client/VaultHttp.cs @@ -23,8 +23,28 @@ namespace Keyfactor.Extensions.CAPlugin.HashicorpVault.Client /// public class VaultHttp { + private string _vaultApiPath { get; set; } private string _mountPoint { get; set; } // not all requests are the the secrets engine, so can't append this permanently to the base path - private RestClient _restClient { get; set; } + private string _authToken { get; set; } + private string _nameSpace { get; set; } + private RestClient _restClient; + protected RestClient restClient + { + get + { + if (_restClient != null) { return _restClient; } + var restClientOptions = new RestClientOptions(_vaultApiPath) { ThrowOnAnyError = true }; + _restClient = new RestClient(restClientOptions, configureSerialization: s => s.UseSystemTextJson(_serializerOptions)); + _restClient.AddDefaultHeader("X-Vault-Request", "true"); + _restClient.AddDefaultHeader("X-Vault-Token", _authToken); + if (_nameSpace != null) _restClient.AddDefaultHeader("X-Vault-Namespace", _nameSpace); + logger.LogTrace($"configured a new instance of our Vault http client with the provided values:"); + logger.LogTrace($"vault api path: {_vaultApiPath}"); + logger.LogTrace($"mount point: {_mountPoint}"); + logger.LogTrace($"namespace: {_nameSpace}"); + return _restClient; + } + } private JsonSerializerOptions _serializerOptions { get; set; } private static readonly ILogger logger = LogHandler.GetClassLogger(); @@ -40,20 +60,16 @@ public VaultHttp(string host, string mountPoint, string authToken, string nameSp PropertyNameCaseInsensitive = true, PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace, }; + _vaultApiPath = $"{host.TrimEnd('/')}/v1"; + _mountPoint = mountPoint.TrimStart('/').TrimEnd('/'); + _authToken = authToken; - var restClientOptions = new RestClientOptions($"{host.TrimEnd('/')}/v1") { ThrowOnAnyError = true }; - _restClient = new RestClient(restClientOptions, configureSerialization: s => s.UseSystemTextJson(_serializerOptions)); - - _mountPoint = mountPoint.TrimStart('/').TrimEnd('/'); // remove leading and trailing slashes - _restClient.AddDefaultHeader("X-Vault-Request", "true"); - _restClient.AddDefaultHeader("X-Vault-Token", authToken); - - if (nameSpace != null) _restClient.AddDefaultHeader("X-Vault-Namespace", nameSpace); + if (!string.IsNullOrEmpty(nameSpace)) + { + _nameSpace = nameSpace; + } - logger.LogTrace($"configured an instance of our restsharp client with the provided values:"); - logger.LogTrace($"host url: {host}"); - logger.LogTrace($"mount point: {_mountPoint}"); - logger.LogTrace($"namespace: {nameSpace}"); + logger.LogTrace($"configured our httpclient wrapper with the following values:\nbase path: {_vaultApiPath}\nnamespace: {(!string.IsNullOrEmpty(_nameSpace) ? _nameSpace : "(no namespace configured)")}\nmount point: {_mountPoint}\nauth token: {_authToken.Substring(0, 8)}..."); logger.MethodExit(); } @@ -89,6 +105,8 @@ public async Task GetAsync(string path, Dictionary paramet } finally { + _restClient.Dispose(); + _restClient = null; logger.MethodExit(); } } @@ -119,7 +137,7 @@ public async Task PostAsync(string path, dynamic parameters = default) logger.LogTrace($"request completed. response returned:"); logger.LogTrace($"response.StatusCode: {response!.StatusCode}"); logger.LogTrace($"response.contentType: {response!.ContentType}"); - + if (response.ErrorMessage != null) logger.LogTrace($"response.ErrorMessage: {response!.ErrorMessage}"); ErrorResponse errorResponse = null; @@ -144,6 +162,8 @@ public async Task PostAsync(string path, dynamic parameters = default) } finally { + _restClient.Dispose(); + _restClient = null; logger.MethodExit(); } } @@ -161,7 +181,12 @@ public async Task HealthCheckAsync() logger.LogError($"there was an error making the request: {LogHandler.FlattenException(ex)}"); throw; } - finally { logger.MethodExit(); } + finally + { + _restClient.Dispose(); + _restClient = null; + logger.MethodExit(); + } } /// @@ -183,7 +208,12 @@ public async Task> GetCapabilitiesForThisTokenAndNamespace() logger.LogError($"request to get capabilities for token failed: {LogHandler.FlattenException(ex)}"); throw; } - finally { logger.MethodExit(); } + finally + { + _restClient.Dispose(); + _restClient = null; + logger.MethodExit(); + } } } } diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 47a4e96..6ed5e63 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -25,7 +25,7 @@ public class HashicorpVaultCAConnector : IAnyCAPlugin { private readonly ILogger logger; private HashicorpVaultCAConfig _caConfig { get; set; } - private HashicorpVaultClient _client { get; set; } + //private HashicorpVaultClient _client { get; set; } private ICertificateDataReader _certificateDataReader; private JsonSerializerOptions _serializerOptions; @@ -53,7 +53,7 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa logger.LogTrace($"serialized config: {rawConfig}"); _caConfig = JsonSerializer.Deserialize(rawConfig); logger.MethodExit(LogLevel.Trace); - _client = new HashicorpVaultClient(_caConfig); + //_client = new HashicorpVaultClient(_caConfig); _certificateDataReader = certificateDataReader; } @@ -69,9 +69,7 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa /// public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) { - logger.MethodEntry(LogLevel.Trace); - - _client = new HashicorpVaultClient(_caConfig); + logger.MethodEntry(LogLevel.Trace); logger.LogInformation($"Begin {enrollmentType} enrollment for {subject}"); string statusMessage; @@ -87,7 +85,7 @@ public async Task Enroll(string csr, string subject, Dictionar // create the client logger.LogTrace("instantiating the client.."); - _client = new HashicorpVaultClient(_caConfig, templateConfig); + var hashiClient = new HashicorpVaultClient(_caConfig, templateConfig); logger.LogDebug("Parse subject for Common Name"); string commonName = ParseSubject(subject, "CN="); @@ -97,7 +95,7 @@ public async Task Enroll(string csr, string subject, Dictionar logger.LogTrace($"using vault role name {vaultRole}"); - signResponse = await _client.SignCSR(csr, subject, san, vaultRole); + signResponse = await hashiClient.SignCSR(csr, subject, san, vaultRole); // trace logs logger.LogTrace($"back to calling method"); @@ -146,12 +144,12 @@ public async Task GetSingleRecord(string caRequestID) { logger.MethodEntry(); - _client = new HashicorpVaultClient(_caConfig); + var hashiClient = new HashicorpVaultClient(_caConfig); logger.LogTrace($"preparing to send request to retrieve certificate with id {caRequestID}"); try { - var cert = await _client.GetCertificate(caRequestID); + var cert = await hashiClient.GetCertificate(caRequestID); logger.LogTrace($"got a response from the request.."); logger.LogTrace($"revocation time: {cert.RevocationTime}"); @@ -182,11 +180,11 @@ public async Task GetSingleRecord(string caRequestID) public async Task Ping() { logger.MethodEntry(); - _client = new HashicorpVaultClient(_caConfig); + var hashiClient = new HashicorpVaultClient(_caConfig); logger.LogTrace("Attempting ping of Vault endpoint"); try { - var result = await _client.PingServer(); + var result = await hashiClient.PingServer(); } catch (Exception ex) { @@ -205,12 +203,12 @@ public async Task Ping() /// The status of the request as an int representing EndEntityStatus public async Task Revoke(string caRequestID, string hexSerialNumber, uint revocationReason) { - _client = new HashicorpVaultClient(_caConfig); + var hashiClient = new HashicorpVaultClient(_caConfig); logger.MethodEntry(); logger.LogTrace($"Sending request to revoke certificate with id: {caRequestID}"); try { - var response = await _client.RevokeCertificate(caRequestID); + var response = await hashiClient.RevokeCertificate(caRequestID); logger.LogTrace($"returning 'REVOKED' EndEntityStatus ({(int)EndEntityStatus.REVOKED})"); return (int)EndEntityStatus.REVOKED; } @@ -231,7 +229,7 @@ public async Task Revoke(string caRequestID, string hexSerialNumber, uint r /// The cancellation token. public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) { - _client = new HashicorpVaultClient(_caConfig); + var hashiClient = new HashicorpVaultClient(_caConfig); // !! Any certificates issued outside of this CA Gateway will not necessarily be associated with the role name / (product ID) that was used to generate it // !! since that value is not retreivable after the initial generation. @@ -245,7 +243,7 @@ public async Task Synchronize(BlockingCollection blockin try { logger.LogTrace("getting all certificate serial numbers from vault"); - certSerials = await _client.GetAllCertSerialNumbers(); + certSerials = await hashiClient.GetAllCertSerialNumbers(); } catch (Exception ex) { @@ -265,7 +263,7 @@ public async Task Synchronize(BlockingCollection blockin try { logger.LogTrace($"Calling GetCertificate on our client, passing serial number: {certSerial}"); - certFromVault = await _client.GetCertificate(certSerial); + certFromVault = await hashiClient.GetCertificate(certSerial); } catch (Exception ex) { @@ -282,9 +280,12 @@ public async Task Synchronize(BlockingCollection blockin logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); dbStatus = await _certificateDataReader.GetStatusByRequestID(trackingId); } - catch(Exception ex) - { - logger.LogTrace($"exception when retrieving cert from DB. It could simply be missing, but logging for analysis: {ex.Message}"); + catch(Exception ex) // an exception is thrown if cert doesn't exist + { + if (!ex.Message.Contains("No matching")) { // if the exception message doesn't contain this; something else happened. + logger.LogTrace($"exception when retrieving cert from database: {ex.Message}"); + throw; + } logger.LogTrace($"tracking id {trackingId} was not found in the database. it will be added."); } @@ -419,13 +420,13 @@ public async Task ValidateCAConnectionInfo(Dictionary connection // create an instance of our client with those values - _client = new HashicorpVaultClient(config); + var hashiClient = new HashicorpVaultClient(config); // attempt an authenticated request to retreive role names try { logger.LogTrace("making an authenticated request to the Vault server to verify credentials (listing role names).."); - var roleNames = await _client.GetRoleNamesAsync(); + var roleNames = await hashiClient.GetRoleNamesAsync(); logger.LogTrace($"successfule request: received a response containing {roleNames.Count} role names"); } catch (Exception ex) @@ -485,13 +486,13 @@ public Task ValidateProductInfo(EnrollmentProductInfo productInfo, Dictionary ProductIdIsValid(string productID, HashicorpVaultCAConfig config) { - _client = new HashicorpVaultClient(config); + var hashiClient = new HashicorpVaultClient(config); // attempt an authenticated request to retreive role names try { logger.LogTrace("making an authenticated request to the Vault server to verify credentials (listing role names).."); - var roleNames = await _client.GetRoleNamesAsync(); + var roleNames = await hashiClient.GetRoleNamesAsync(); logger.LogTrace($"successfule request: received a response containing {roleNames.Count} role names"); return roleNames.Any(rn => rn == productID); } @@ -592,11 +593,11 @@ public Dictionary GetTemplateParameterAnnotations() public List GetProductIds() { logger.MethodEntry(); - // Initialize should have been called in order to populate the caConfig and create the client. + var hashiClient = new HashicorpVaultClient(_caConfig); try { logger.LogTrace("requesting role names from vault.."); - var roleNames = _client.GetRoleNamesAsync().Result; + var roleNames = hashiClient.GetRoleNamesAsync().Result; logger.LogTrace($"got {roleNames.Count} role names from vault:"); foreach (var name in roleNames) { From 9df7571a55b3365c913b50d5427a224a106cb479 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Tue, 18 Mar 2025 17:31:19 -0400 Subject: [PATCH 07/14] cleaned up vaulthttp methods --- hashicorp-vault-cagateway/Client/VaultHttp.cs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/hashicorp-vault-cagateway/Client/VaultHttp.cs b/hashicorp-vault-cagateway/Client/VaultHttp.cs index 6fc2a9a..3b779ab 100644 --- a/hashicorp-vault-cagateway/Client/VaultHttp.cs +++ b/hashicorp-vault-cagateway/Client/VaultHttp.cs @@ -27,6 +27,8 @@ public class VaultHttp private string _mountPoint { get; set; } // not all requests are the the secrets engine, so can't append this permanently to the base path private string _authToken { get; set; } private string _nameSpace { get; set; } + private JsonSerializerOptions _serializerOptions { get; set; } + private RestClient _restClient; protected RestClient restClient { @@ -38,14 +40,13 @@ protected RestClient restClient _restClient.AddDefaultHeader("X-Vault-Request", "true"); _restClient.AddDefaultHeader("X-Vault-Token", _authToken); if (_nameSpace != null) _restClient.AddDefaultHeader("X-Vault-Namespace", _nameSpace); - logger.LogTrace($"configured a new instance of our Vault http client with the provided values:"); + logger.LogTrace($"configured a new instance of our Vault restsharp client with the configured values:"); logger.LogTrace($"vault api path: {_vaultApiPath}"); logger.LogTrace($"mount point: {_mountPoint}"); logger.LogTrace($"namespace: {_nameSpace}"); return _restClient; } - } - private JsonSerializerOptions _serializerOptions { get; set; } + } private static readonly ILogger logger = LogHandler.GetClassLogger(); @@ -60,7 +61,7 @@ public VaultHttp(string host, string mountPoint, string authToken, string nameSp PropertyNameCaseInsensitive = true, PreferredObjectCreationHandling = JsonObjectCreationHandling.Replace, }; - _vaultApiPath = $"{host.TrimEnd('/')}/v1"; + _vaultApiPath = $"{host.TrimEnd('/')}/v1"; // api path ends in /v1 _mountPoint = mountPoint.TrimStart('/').TrimEnd('/'); _authToken = authToken; @@ -92,7 +93,7 @@ public async Task GetAsync(string path, Dictionary paramet var request = new RestRequest($"{_mountPoint}/{path}", Method.Get); if (parameters != null) { request.AddJsonBody(parameters); } - var response = await _restClient.ExecuteGetAsync(request); + var response = await restClient.ExecuteGetAsync(request); response.ThrowIfError(); @@ -105,8 +106,7 @@ public async Task GetAsync(string path, Dictionary paramet } finally { - _restClient.Dispose(); - _restClient = null; + DisposeRestClient(); logger.MethodExit(); } } @@ -117,7 +117,7 @@ public async Task PostAsync(string path, dynamic parameters = default) var resourcePath = $"{_mountPoint}/{path}"; - logger.LogTrace($"preparing to send POST request to {_restClient.Options.BaseUrl}{resourcePath}"); + logger.LogTrace($"preparing to send POST request to {restClient.Options.BaseUrl}{resourcePath}"); logger.LogTrace($"will attempt to deserialize the response into a {typeof(T)}"); try @@ -130,7 +130,7 @@ public async Task PostAsync(string path, dynamic parameters = default) request.AddJsonBody(serializedParams); } - logger.LogTrace($"full url for the request: {_restClient.Options.BaseUrl}/{request.Resource}"); + logger.LogTrace($"full url for the request: {restClient.Options.BaseUrl}/{request.Resource}"); var response = await _restClient.ExecutePostAsync(request); @@ -162,8 +162,7 @@ public async Task PostAsync(string path, dynamic parameters = default) } finally { - _restClient.Dispose(); - _restClient = null; + DisposeRestClient(); logger.MethodExit(); } } @@ -174,7 +173,7 @@ public async Task HealthCheckAsync() try { - return await _restClient.GetAsync("sys/seal-status"); + return await restClient.GetAsync("sys/seal-status"); } catch (Exception ex) { @@ -183,8 +182,7 @@ public async Task HealthCheckAsync() } finally { - _restClient.Dispose(); - _restClient = null; + DisposeRestClient(); logger.MethodExit(); } } @@ -210,10 +208,14 @@ public async Task> GetCapabilitiesForThisTokenAndNamespace() } finally { - _restClient.Dispose(); - _restClient = null; + DisposeRestClient(); logger.MethodExit(); } } + + private void DisposeRestClient() { + _restClient.Dispose(); + _restClient = null; + } } } From 2f645ba4494f908134b0ddf6a96f25da27730253 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Tue, 18 Mar 2025 17:49:38 -0400 Subject: [PATCH 08/14] adding explicit completion call to blockingbuffer --- .../HashicorpVaultCAConnector.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 6ed5e63..4cc97c7 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -248,6 +248,7 @@ public async Task Synchronize(BlockingCollection blockin catch (Exception ex) { logger.LogError($"failed to retreive serial numbers: {LogHandler.FlattenException(ex)}"); + blockingBuffer.CompleteAdding(); throw; } @@ -268,6 +269,7 @@ public async Task Synchronize(BlockingCollection blockin catch (Exception ex) { logger.LogError($"Failed to retreive details for certificate with serial number {certSerial} from Vault. Errors: {LogHandler.FlattenException(ex)}"); + blockingBuffer.CompleteAdding(); throw; } logger.LogTrace($"converting {certSerial} to database trackingId"); @@ -284,6 +286,7 @@ public async Task Synchronize(BlockingCollection blockin { if (!ex.Message.Contains("No matching")) { // if the exception message doesn't contain this; something else happened. logger.LogTrace($"exception when retrieving cert from database: {ex.Message}"); + blockingBuffer.CompleteAdding(); throw; } logger.LogTrace($"tracking id {trackingId} was not found in the database. it will be added."); @@ -304,13 +307,19 @@ public async Task Synchronize(BlockingCollection blockin try { logger.LogTrace($"writing the result."); - blockingBuffer.Add(newCert); - logger.LogTrace($"successfully added certificate to the database."); + if (blockingBuffer.TryAdd(newCert, 50, cancelToken)) + { + logger.LogTrace($"successfully added certificate to the database."); + } + else { + logger.LogTrace($"adding to queue for writing was blocked."); + } } catch (Exception ex) { logger.LogError($"Failed to add the cert to the database: {LogHandler.FlattenException(ex)}"); - throw; + blockingBuffer.CompleteAdding(); + break; } } else // the cert exists in the database; just update the status if necessary @@ -330,6 +339,14 @@ public async Task Synchronize(BlockingCollection blockin RevocationDate = certFromVault.RevocationTime // ProductID is not available via the API after the initial issuance. we do not want to overwrite }; + if (blockingBuffer.TryAdd(newCert, 50, cancelToken)) + { + logger.LogTrace($"successfully updated certificate {trackingId} in the database."); + } + else + { + logger.LogTrace($"adding to queue for writing was blocked."); + } } else { logger.LogTrace($"The status is unchanged and we are doing an incremental scan; no need to update db."); @@ -337,6 +354,7 @@ public async Task Synchronize(BlockingCollection blockin } count++; } + blockingBuffer.CompleteAdding(); logger.LogTrace($"Completed sync of {count} certificates. It was {(fullSync ? "a full" : "an incremental")} sync and {changedCount} records were updated."); logger.MethodExit(); } From cf99f42e430a0a9f54fd64a648ad7b64be59b946 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Wed, 19 Mar 2025 12:23:39 -0400 Subject: [PATCH 09/14] made logger a non-static property --- .../Client/HashicorpVaultClient.cs | 20 ++++---- hashicorp-vault-cagateway/Client/VaultHttp.cs | 48 ++++--------------- .../HashicorpVaultCAConnector.cs | 12 +++-- 3 files changed, 28 insertions(+), 52 deletions(-) diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index 3ec5ea1..5a84861 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -27,11 +27,11 @@ public class HashicorpVaultClient { private HashicorpVaultCAConfig _caConfig { get; set; } private HashicorpVaultCATemplateConfig _templateConfig { get; set; } - - private static readonly ILogger logger = LogHandler.GetClassLogger(); + private readonly ILogger logger; public HashicorpVaultClient(HashicorpVaultCAConfig caConfig, HashicorpVaultCATemplateConfig templateConfig = null) { + logger = LogHandler.GetClassLogger(); logger.MethodEntry(); _caConfig = caConfig; _templateConfig = templateConfig; @@ -91,7 +91,7 @@ public async Task SignCSR(string csr, string subject, Dictionary(); + } public VaultHttp(string host, string mountPoint, string authToken, string nameSpace = null) { + logger = LogHandler.GetClassLogger(); logger.MethodEntry(); - _serializerOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, @@ -71,7 +70,6 @@ public VaultHttp(string host, string mountPoint, string authToken, string nameSp } logger.LogTrace($"configured our httpclient wrapper with the following values:\nbase path: {_vaultApiPath}\nnamespace: {(!string.IsNullOrEmpty(_nameSpace) ? _nameSpace : "(no namespace configured)")}\nmount point: {_mountPoint}\nauth token: {_authToken.Substring(0, 8)}..."); - logger.MethodExit(); } @@ -87,7 +85,7 @@ public async Task GetAsync(string path, Dictionary paramet { logger.MethodEntry(); logger.LogTrace($"preparing to send GET request to {path} with parameters {JsonSerializer.Serialize(parameters)}"); - logger.LogTrace($"will attempt to deserialize the response into a {typeof(T)}"); + logger.LogTrace($"will attempt to deserialize the response into a {typeof(T).Name}"); try { var request = new RestRequest($"{_mountPoint}/{path}", Method.Get); @@ -101,7 +99,7 @@ public async Task GetAsync(string path, Dictionary paramet } catch (Exception ex) { - logger.LogError($"there was an error making the request: {LogHandler.FlattenException(ex)}"); + logger.LogError($"there was an error making the GET request: {LogHandler.FlattenException(ex)}"); throw; } finally @@ -132,7 +130,7 @@ public async Task PostAsync(string path, dynamic parameters = default) logger.LogTrace($"full url for the request: {restClient.Options.BaseUrl}/{request.Resource}"); - var response = await _restClient.ExecutePostAsync(request); + var response = await restClient.ExecutePostAsync(request); logger.LogTrace($"request completed. response returned:"); logger.LogTrace($"response.StatusCode: {response!.StatusCode}"); @@ -157,7 +155,7 @@ public async Task PostAsync(string path, dynamic parameters = default) } catch (Exception ex) { - logger.LogError($"there was an error making the request: {LogHandler.FlattenException(ex)}"); + logger.LogError($"there was an error making the POST request: {LogHandler.FlattenException(ex)}"); throw; } finally @@ -177,7 +175,7 @@ public async Task HealthCheckAsync() } catch (Exception ex) { - logger.LogError($"there was an error making the request: {LogHandler.FlattenException(ex)}"); + logger.LogError($"there was an error making the health-check request: {LogHandler.FlattenException(ex)}"); throw; } finally @@ -185,36 +183,10 @@ public async Task HealthCheckAsync() DisposeRestClient(); logger.MethodExit(); } - } - - /// - /// gets the capabilities for the current token in the given namespace. - /// using this method to verify connectivity - /// - /// - public async Task> GetCapabilitiesForThisTokenAndNamespace() - { - logger.MethodEntry(); - try - { - var response = await _restClient.GetAsync("sys/capabilities/self"); - response!.ThrowIfError(); - return response.Content?.data?.capabilities as List; - } - catch (Exception ex) - { - logger.LogError($"request to get capabilities for token failed: {LogHandler.FlattenException(ex)}"); - throw; - } - finally - { - DisposeRestClient(); - logger.MethodExit(); - } - } + } private void DisposeRestClient() { - _restClient.Dispose(); + _restClient?.Dispose(); _restClient = null; } } diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 4cc97c7..0c9dfe3 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -51,10 +51,10 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa logger.MethodEntry(LogLevel.Trace); string rawConfig = JsonSerializer.Serialize(configProvider.CAConnectionData); logger.LogTrace($"serialized config: {rawConfig}"); - _caConfig = JsonSerializer.Deserialize(rawConfig); - logger.MethodExit(LogLevel.Trace); + _caConfig = JsonSerializer.Deserialize(rawConfig); //_client = new HashicorpVaultClient(_caConfig); _certificateDataReader = certificateDataReader; + logger.MethodExit(LogLevel.Trace); } /// @@ -228,8 +228,7 @@ public async Task Revoke(string caRequestID, string hexSerialNumber, uint r /// Information about the last CA sync. /// The cancellation token. public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) - { - var hashiClient = new HashicorpVaultClient(_caConfig); + { // !! Any certificates issued outside of this CA Gateway will not necessarily be associated with the role name / (product ID) that was used to generate it // !! since that value is not retreivable after the initial generation. @@ -240,8 +239,11 @@ public async Task Synchronize(BlockingCollection blockin var count = 0; var changedCount = 0; + var hashiClient = new HashicorpVaultClient(_caConfig); + try { + logger.LogTrace("getting all certificate serial numbers from vault"); certSerials = await hashiClient.GetAllCertSerialNumbers(); } @@ -264,7 +266,7 @@ public async Task Synchronize(BlockingCollection blockin try { logger.LogTrace($"Calling GetCertificate on our client, passing serial number: {certSerial}"); - certFromVault = await hashiClient.GetCertificate(certSerial); + certFromVault = hashiClient.GetCertificate(certSerial).Result; } catch (Exception ex) { From f9588c1891f6a650a287d61e9eae1bfebef95c03 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Wed, 19 Mar 2025 15:30:52 -0400 Subject: [PATCH 10/14] using a blocking collection on the cert serial numbers for thread safety --- .../Client/HashicorpVaultClient.cs | 65 +++++++++++++------ .../HashicorpVaultCAConnector.cs | 34 ++++++---- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index 5a84861..73b87cb 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -11,9 +11,11 @@ using Microsoft.Extensions.Logging; using Org.BouncyCastle.Asn1.X509; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; namespace Keyfactor.Extensions.CAPlugin.HashicorpVault @@ -197,15 +199,28 @@ public async Task PingServer() /// Retreives all serial numbers for issued certificates /// /// a list of the certificate serial number strings - public async Task> GetAllCertSerialNumbers() + public async Task GetAllCertSerialNumbers(BlockingCollection serialNumberCollection, CancellationToken token) { + if (token.IsCancellationRequested) { + logger.LogWarning($"cancelation was requested. Stopping task"); + serialNumberCollection.CompleteAdding(); + return; + } logger.MethodEntry(); - var keys = new List(); try { var _vaultHttp = ConfigureNewVaultClient(); var res = await _vaultHttp.GetAsync>("certs/?list=true"); - return res.Data.Entries; + var serials = res.Data?.Entries; + if (serials == null || serials.Count == 0) { + return; + } + foreach (var serial in serials) { + if (!serialNumberCollection.TryAdd(serial, 50, token)) { + logger.LogWarning($"unable to add serial number {serial} to the collection"); + } + } + serialNumberCollection.CompleteAdding(); } catch (Exception ex) { @@ -257,30 +272,40 @@ public async Task> GetRoleNamesAsync() private VaultHttp ConfigureNewVaultClient() { logger.MethodEntry(); + try + { + var hostUrl = _caConfig.Host; // host url and authentication details come from the CA config + logger.LogTrace($"set value for Host url: {hostUrl}"); - var hostUrl = _caConfig.Host; // host url and authentication details come from the CA config - logger.LogTrace($"set value for Host url: {hostUrl}"); + var token = _caConfig.Token; + logger.LogTrace($"set value for authentication token: {token ?? "(not defined)"}"); - var token = _caConfig.Token; - logger.LogTrace($"set value for authentication token: {token ?? "(not defined)"}"); + var nameSpace = string.IsNullOrEmpty(_templateConfig?.Namespace) ? _caConfig.Namespace : _templateConfig.Namespace; // Namespace comes from templateconfig if available, otherwise defaults to caConfig; can be null + logger.LogTrace($"set value for Namespace: {nameSpace ?? "(not defined)"}"); - var nameSpace = string.IsNullOrEmpty(_templateConfig?.Namespace) ? _caConfig.Namespace : _templateConfig.Namespace; // Namespace comes from templateconfig if available, otherwise defaults to caConfig; can be null - logger.LogTrace($"set value for Namespace: {nameSpace ?? "(not defined)"}"); + var mountPoint = string.IsNullOrEmpty(_templateConfig?.MountPoint) ? _caConfig.MountPoint : _templateConfig.MountPoint; // Mountpoint comes from templateconfig if available, otherwise defaults to caConfig; if null, uses "pki" (Vault Default) + mountPoint = mountPoint ?? "pki"; // using the vault default PKI secrets engine mount point if not present in config + logger.LogTrace($"set value for Mountpoint: {mountPoint}"); - var mountPoint = string.IsNullOrEmpty(_templateConfig?.MountPoint) ? _caConfig.MountPoint : _templateConfig.MountPoint; // Mountpoint comes from templateconfig if available, otherwise defaults to caConfig; if null, uses "pki" (Vault Default) - mountPoint = mountPoint ?? "pki"; // using the vault default PKI secrets engine mount point if not present in config - logger.LogTrace($"set value for Mountpoint: {mountPoint}"); + // _certAuthInfo = caConfig?.ClientCertificate; + // logger.LogTrace($"set value for Certificate authentication; thumbprint: {_certAuthInfo?.Thumbprint ?? "(missing) - using token authentication"}"); - // _certAuthInfo = caConfig?.ClientCertificate; - // logger.LogTrace($"set value for Certificate authentication; thumbprint: {_certAuthInfo?.Thumbprint ?? "(missing) - using token authentication"}"); + //if (_token == null && _certAuthInfo == null) + //{ + // throw new MissingFieldException("Either an authentication token or certificate to use for authentication into Vault must be provided."); + //} - //if (_token == null && _certAuthInfo == null) - //{ - // throw new MissingFieldException("Either an authentication token or certificate to use for authentication into Vault must be provided."); - //} - - return new VaultHttp(hostUrl, mountPoint, token, nameSpace); + return new VaultHttp(hostUrl, mountPoint, token, nameSpace); + } + catch (Exception ex) + { + logger.LogError($"error when creating new vault client: {LogHandler.FlattenException(ex)}"); + throw; + } + finally { + logger.MethodExit(); + } } } } \ No newline at end of file diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 0c9dfe3..452754e 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -49,11 +49,10 @@ public HashicorpVaultCAConnector() public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) { logger.MethodEntry(LogLevel.Trace); - string rawConfig = JsonSerializer.Serialize(configProvider.CAConnectionData); - logger.LogTrace($"serialized config: {rawConfig}"); - _caConfig = JsonSerializer.Deserialize(rawConfig); - //_client = new HashicorpVaultClient(_caConfig); _certificateDataReader = certificateDataReader; + string rawConfig = JsonSerializer.Serialize(configProvider.CAConnectionData); + logger.LogTrace($"serialized config: {rawConfig}"); + _caConfig = JsonSerializer.Deserialize(rawConfig); logger.MethodExit(LogLevel.Trace); } @@ -225,7 +224,6 @@ public async Task Revoke(string caRequestID, string hexSerialNumber, uint r /// /// Provides information about the gateway's certificate database. /// Buffer into which certificates are places from the CA. - /// Information about the last CA sync. /// The cancellation token. public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) { @@ -235,17 +233,16 @@ public async Task Synchronize(BlockingCollection blockin logger.MethodEntry(); logger.LogTrace("Beginning Synchronization Task.."); - var certSerials = new List(); + var certSerials = new BlockingCollection(); var count = 0; var changedCount = 0; var hashiClient = new HashicorpVaultClient(_caConfig); try - { - + { logger.LogTrace("getting all certificate serial numbers from vault"); - certSerials = await hashiClient.GetAllCertSerialNumbers(); + await hashiClient.GetAllCertSerialNumbers(certSerials, cancelToken); } catch (Exception ex) { @@ -254,11 +251,21 @@ public async Task Synchronize(BlockingCollection blockin throw; } - logger.LogTrace($"got {certSerials.Count()} serial numbers. Begin checking status for each..."); + logger.LogTrace($"got {certSerials?.Count() ?? 0} serial numbers. Begin checking status for each..."); - foreach (var certSerial in certSerials) + if (certSerials == null || certSerials.Count == 0) { // exit if no certs were found in vault + blockingBuffer.CompleteAdding(); + logger.LogTrace($"no certificates found at path {_caConfig.Host} using namespace {_caConfig.Namespace} and mount point {_caConfig.MountPoint}"); + logger.MethodExit(); + return; + } + + foreach (var certSerial in certSerials.GetConsumingEnumerable()) { - cancelToken.ThrowIfCancellationRequested(); + if (cancelToken.IsCancellationRequested) + { + break; + } CertResponse certFromVault = null; var dbStatus = -1; @@ -281,7 +288,7 @@ public async Task Synchronize(BlockingCollection blockin // then, check for an existing local entry try { - logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); + logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); dbStatus = await _certificateDataReader.GetStatusByRequestID(trackingId); } catch(Exception ex) // an exception is thrown if cert doesn't exist @@ -320,6 +327,7 @@ public async Task Synchronize(BlockingCollection blockin catch (Exception ex) { logger.LogError($"Failed to add the cert to the database: {LogHandler.FlattenException(ex)}"); + logger.LogTrace($"closing buffer and aborting sync"); blockingBuffer.CompleteAdding(); break; } From 1d66e55e1912379e6f0d0c4690c86d6dd5eccf85 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Fri, 21 Mar 2025 10:53:19 -0400 Subject: [PATCH 11/14] adding additional logging --- hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs | 5 +++-- hashicorp-vault-cagateway/Client/VaultHttp.cs | 2 +- hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index 73b87cb..d7a5140 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -209,12 +209,13 @@ public async Task GetAllCertSerialNumbers(BlockingCollection serialNumbe logger.MethodEntry(); try { - var _vaultHttp = ConfigureNewVaultClient(); + var _vaultHttp = ConfigureNewVaultClient(); var res = await _vaultHttp.GetAsync>("certs/?list=true"); var serials = res.Data?.Entries; if (serials == null || serials.Count == 0) { return; } + logger.LogTrace($"got {res.Data?.Entries?.Count} serial numbers from {_caConfig.Host} namespace: {_caConfig.Namespace}, mount-point: {_caConfig.MountPoint}"); foreach (var serial in serials) { if (!serialNumberCollection.TryAdd(serial, 50, token)) { logger.LogWarning($"unable to add serial number {serial} to the collection"); @@ -224,7 +225,7 @@ public async Task GetAllCertSerialNumbers(BlockingCollection serialNumbe } catch (Exception ex) { - logger.LogError($"there was an error retreiving the certificate keys: {ex.Message}"); + logger.LogError($"there was an error retreiving the certificate keys from {_caConfig.Host} using namespace: {_templateConfig.Namespace ?? _caConfig.Namespace} and mount-point: {_caConfig.MountPoint}. Error: {ex.Message}"); throw; } finally { logger.MethodExit(); } diff --git a/hashicorp-vault-cagateway/Client/VaultHttp.cs b/hashicorp-vault-cagateway/Client/VaultHttp.cs index 7a6d016..6396874 100644 --- a/hashicorp-vault-cagateway/Client/VaultHttp.cs +++ b/hashicorp-vault-cagateway/Client/VaultHttp.cs @@ -92,7 +92,7 @@ public async Task GetAsync(string path, Dictionary paramet if (parameters != null) { request.AddJsonBody(parameters); } var response = await restClient.ExecuteGetAsync(request); - + logger.LogTrace($"response headers: {response.Headers}\nresponse content: {response.Content}\nresponse data: {response.Data}"); response.ThrowIfError(); return response.Data; diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 452754e..4476674 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -241,7 +241,7 @@ public async Task Synchronize(BlockingCollection blockin try { - logger.LogTrace("getting all certificate serial numbers from vault"); + logger.LogTrace($"getting all certificate serial numbers from vault from {_caConfig.Host} using namespace {_caConfig.Namespace} and mount-point {_caConfig.MountPoint}"); await hashiClient.GetAllCertSerialNumbers(certSerials, cancelToken); } catch (Exception ex) @@ -329,7 +329,7 @@ public async Task Synchronize(BlockingCollection blockin logger.LogError($"Failed to add the cert to the database: {LogHandler.FlattenException(ex)}"); logger.LogTrace($"closing buffer and aborting sync"); blockingBuffer.CompleteAdding(); - break; + throw; } } else // the cert exists in the database; just update the status if necessary From c583992619c94288c037a988c67817cf56e6341f Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Mon, 24 Mar 2025 14:56:44 -0400 Subject: [PATCH 12/14] Adding additional thread safety mechanisms. --- .../Client/HashicorpVaultClient.cs | 45 ++++++------ .../HashicorpVaultCAConnector.cs | 69 ++++++++++++------- 2 files changed, 68 insertions(+), 46 deletions(-) diff --git a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs index d7a5140..790443c 100644 --- a/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs +++ b/hashicorp-vault-cagateway/Client/HashicorpVaultClient.cs @@ -29,7 +29,7 @@ public class HashicorpVaultClient { private HashicorpVaultCAConfig _caConfig { get; set; } private HashicorpVaultCATemplateConfig _templateConfig { get; set; } - private readonly ILogger logger; + private readonly ILogger logger; public HashicorpVaultClient(HashicorpVaultCAConfig caConfig, HashicorpVaultCATemplateConfig templateConfig = null) { @@ -51,8 +51,9 @@ public async Task SignCSR(string csr, string subject, Dictionary SignCSR(string csr, string subject, Dictionary 0) { logger.LogTrace($"the response contained warnings: {string.Join(", ", response.Warnings)}"); } - - logger.LogTrace($"serialized SignResponse: {JsonSerializer.Serialize(response.Data)}"); - + + logger.LogTrace($"serialized SignResponse: {JsonSerializer.Serialize(response.Data)}"); + return response.Data; } catch (Exception ex) @@ -181,7 +182,7 @@ public async Task PingServer() logger.LogTrace($"-- Vault health check response --"); logger.LogTrace($"Vault version : {res.VaultVersion}"); logger.LogTrace($"sealed? : {res.Sealed}"); - logger.LogTrace($"initialized? : {res.Initialized}"); + logger.LogTrace($"initialized? : {res.Initialized}"); return true; } catch (Exception ex) @@ -199,29 +200,26 @@ public async Task PingServer() /// Retreives all serial numbers for issued certificates /// /// a list of the certificate serial number strings - public async Task GetAllCertSerialNumbers(BlockingCollection serialNumberCollection, CancellationToken token) + public async Task> GetAllCertSerialNumbers(CancellationToken token) { - if (token.IsCancellationRequested) { - logger.LogWarning($"cancelation was requested. Stopping task"); - serialNumberCollection.CompleteAdding(); - return; + var certSerials = new List(); + if (token.IsCancellationRequested) + { + logger.LogWarning($"cancelation was requested; stopping task"); + return certSerials; } logger.MethodEntry(); try { - var _vaultHttp = ConfigureNewVaultClient(); + var _vaultHttp = ConfigureNewVaultClient(); var res = await _vaultHttp.GetAsync>("certs/?list=true"); var serials = res.Data?.Entries; - if (serials == null || serials.Count == 0) { - return; + if (serials == null || serials.Count == 0) + { + return certSerials; } logger.LogTrace($"got {res.Data?.Entries?.Count} serial numbers from {_caConfig.Host} namespace: {_caConfig.Namespace}, mount-point: {_caConfig.MountPoint}"); - foreach (var serial in serials) { - if (!serialNumberCollection.TryAdd(serial, 50, token)) { - logger.LogWarning($"unable to add serial number {serial} to the collection"); - } - } - serialNumberCollection.CompleteAdding(); + return serials; } catch (Exception ex) { @@ -267,7 +265,7 @@ public async Task> GetRoleNamesAsync() logger.LogError($"There was a problem retreiving the PKI role names: {LogHandler.FlattenException(ex)}"); throw; } - finally { logger.MethodExit(); } + finally { logger.MethodExit(); } } private VaultHttp ConfigureNewVaultClient() @@ -304,7 +302,8 @@ private VaultHttp ConfigureNewVaultClient() logger.LogError($"error when creating new vault client: {LogHandler.FlattenException(ex)}"); throw; } - finally { + finally + { logger.MethodExit(); } } diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 4476674..fd69ceb 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -18,13 +18,16 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Runtime.CompilerServices; namespace Keyfactor.Extensions.CAPlugin.HashicorpVault { public class HashicorpVaultCAConnector : IAnyCAPlugin { private readonly ILogger logger; + private static int _threadLock = 0; private HashicorpVaultCAConfig _caConfig { get; set; } + //private HashicorpVaultClient _client { get; set; } private ICertificateDataReader _certificateDataReader; private JsonSerializerOptions _serializerOptions; @@ -46,11 +49,12 @@ public HashicorpVaultCAConnector() /// Initialize the /// /// The config provider contains information required to connect to the CA. + [MethodImpl(MethodImplOptions.Synchronized)] public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) { logger.MethodEntry(LogLevel.Trace); _certificateDataReader = certificateDataReader; - string rawConfig = JsonSerializer.Serialize(configProvider.CAConnectionData); + string rawConfig = JsonSerializer.Serialize(configProvider.CAConnectionData); logger.LogTrace($"serialized config: {rawConfig}"); _caConfig = JsonSerializer.Deserialize(rawConfig); logger.MethodExit(LogLevel.Trace); @@ -68,7 +72,7 @@ public void Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDa /// public async Task Enroll(string csr, string subject, Dictionary san, EnrollmentProductInfo productInfo, RequestFormat requestFormat, EnrollmentType enrollmentType) { - logger.MethodEntry(LogLevel.Trace); + logger.MethodEntry(LogLevel.Trace); logger.LogInformation($"Begin {enrollmentType} enrollment for {subject}"); string statusMessage; @@ -143,7 +147,7 @@ public async Task GetSingleRecord(string caRequestID) { logger.MethodEntry(); - var hashiClient = new HashicorpVaultClient(_caConfig); + var hashiClient = new HashicorpVaultClient(_caConfig); logger.LogTrace($"preparing to send request to retrieve certificate with id {caRequestID}"); try @@ -224,25 +228,32 @@ public async Task Revoke(string caRequestID, string hexSerialNumber, uint r /// /// Provides information about the gateway's certificate database. /// Buffer into which certificates are places from the CA. - /// The cancellation token. + /// The cancellation token. public async Task Synchronize(BlockingCollection blockingBuffer, DateTime? lastSync, bool fullSync, CancellationToken cancelToken) - { + { // !! Any certificates issued outside of this CA Gateway will not necessarily be associated with the role name / (product ID) that was used to generate it // !! since that value is not retreivable after the initial generation. logger.MethodEntry(); logger.LogTrace("Beginning Synchronization Task.."); - var certSerials = new BlockingCollection(); + var certSerials = new List(); var count = 0; var changedCount = 0; - var hashiClient = new HashicorpVaultClient(_caConfig); + HashicorpVaultClient hashiClient; + + logger.LogTrace("attempt locking of _caConfig."); + Monitor.Enter(_caConfig); + try - { + { + + hashiClient = new HashicorpVaultClient(_caConfig); logger.LogTrace($"getting all certificate serial numbers from vault from {_caConfig.Host} using namespace {_caConfig.Namespace} and mount-point {_caConfig.MountPoint}"); - await hashiClient.GetAllCertSerialNumbers(certSerials, cancelToken); + var serials = hashiClient.GetAllCertSerialNumbers(cancelToken).Result; + Interlocked.Exchange(ref certSerials, serials); } catch (Exception ex) { @@ -250,17 +261,22 @@ public async Task Synchronize(BlockingCollection blockin blockingBuffer.CompleteAdding(); throw; } + finally + { + Monitor.Exit(_caConfig); + } logger.LogTrace($"got {certSerials?.Count() ?? 0} serial numbers. Begin checking status for each..."); - if (certSerials == null || certSerials.Count == 0) { // exit if no certs were found in vault + if (certSerials == null || certSerials.Count == 0) + { // exit if no certs were found in vault blockingBuffer.CompleteAdding(); logger.LogTrace($"no certificates found at path {_caConfig.Host} using namespace {_caConfig.Namespace} and mount point {_caConfig.MountPoint}"); logger.MethodExit(); - return; + return; } - foreach (var certSerial in certSerials.GetConsumingEnumerable()) + foreach (var certSerial in certSerials) { if (cancelToken.IsCancellationRequested) { @@ -281,6 +297,7 @@ public async Task Synchronize(BlockingCollection blockin blockingBuffer.CompleteAdding(); throw; } + finally { Monitor.Exit(_caConfig); } logger.LogTrace($"converting {certSerial} to database trackingId"); var trackingId = certSerial.Replace(":", "-"); // we store with '-'; hashi stores with ':' @@ -288,23 +305,25 @@ public async Task Synchronize(BlockingCollection blockin // then, check for an existing local entry try { - logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); + logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); dbStatus = await _certificateDataReader.GetStatusByRequestID(trackingId); } - catch(Exception ex) // an exception is thrown if cert doesn't exist - { - if (!ex.Message.Contains("No matching")) { // if the exception message doesn't contain this; something else happened. + catch (Exception ex) // an exception is thrown if cert doesn't exist + { + if (!ex.Message.Contains("No matching")) + { // if the exception message doesn't contain this; something else happened. logger.LogTrace($"exception when retrieving cert from database: {ex.Message}"); blockingBuffer.CompleteAdding(); throw; } logger.LogTrace($"tracking id {trackingId} was not found in the database. it will be added."); } + finally { Monitor.Exit(_caConfig); } if (dbStatus == -1 || fullSync) // it's missing and needs added, or a full sync is requested { logger.LogTrace($"adding cert with serial {trackingId} to the database. fullsync is {fullSync}, and the certificate {(dbStatus == -1 ? "does not yet exist" : "already exists")} in the database."); - changedCount++; + Interlocked.Increment(ref changedCount); var newCert = new AnyCAPluginCertificate { CARequestID = trackingId, @@ -320,9 +339,10 @@ public async Task Synchronize(BlockingCollection blockin { logger.LogTrace($"successfully added certificate to the database."); } - else { + else + { logger.LogTrace($"adding to queue for writing was blocked."); - } + } } catch (Exception ex) { @@ -331,6 +351,7 @@ public async Task Synchronize(BlockingCollection blockin blockingBuffer.CompleteAdding(); throw; } + finally { Monitor.Exit(_caConfig); } } else // the cert exists in the database; just update the status if necessary { @@ -339,7 +360,7 @@ public async Task Synchronize(BlockingCollection blockin var vaultStatus = revoked ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED; if (vaultStatus != dbStatus) // if there is a mismatch, we need to update { - changedCount++; + Interlocked.Increment(ref changedCount); logger.LogTrace($"status in vault is {vaultStatus}, status in db is {dbStatus}; updating db."); var newCert = new AnyCAPluginCertificate { @@ -358,15 +379,17 @@ public async Task Synchronize(BlockingCollection blockin logger.LogTrace($"adding to queue for writing was blocked."); } } - else { + else + { logger.LogTrace($"The status is unchanged and we are doing an incremental scan; no need to update db."); } } - count++; + Interlocked.Increment(ref count); } blockingBuffer.CompleteAdding(); logger.LogTrace($"Completed sync of {count} certificates. It was {(fullSync ? "a full" : "an incremental")} sync and {changedCount} records were updated."); logger.MethodExit(); + Monitor.Exit(_caConfig); } /// @@ -621,7 +644,7 @@ public Dictionary GetTemplateParameterAnnotations() public List GetProductIds() { logger.MethodEntry(); - var hashiClient = new HashicorpVaultClient(_caConfig); + var hashiClient = new HashicorpVaultClient(_caConfig); try { logger.LogTrace("requesting role names from vault.."); From 9ae23f64301d85edc5b38916555345d4239e0000 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Tue, 25 Mar 2025 18:39:27 -0400 Subject: [PATCH 13/14] implemented semaphore --- .../HashicorpVaultCAConnector.cs | 224 +++++++++--------- 1 file changed, 114 insertions(+), 110 deletions(-) diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index fd69ceb..404301a 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -25,8 +25,8 @@ namespace Keyfactor.Extensions.CAPlugin.HashicorpVault public class HashicorpVaultCAConnector : IAnyCAPlugin { private readonly ILogger logger; - private static int _threadLock = 0; - private HashicorpVaultCAConfig _caConfig { get; set; } + private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // Binary semaphore (1 indicates only one thread/task can enter) + private HashicorpVaultCAConfig _caConfig; //private HashicorpVaultClient _client { get; set; } private ICertificateDataReader _certificateDataReader; @@ -233,8 +233,12 @@ public async Task Synchronize(BlockingCollection blockin { // !! Any certificates issued outside of this CA Gateway will not necessarily be associated with the role name / (product ID) that was used to generate it // !! since that value is not retreivable after the initial generation. - logger.MethodEntry(); + + await _semaphore.WaitAsync(cancelToken); + + logger.LogTrace($"got semaphore lock, current count: {_semaphore.CurrentCount}"); + logger.LogTrace("Beginning Synchronization Task.."); var certSerials = new List(); @@ -243,153 +247,153 @@ public async Task Synchronize(BlockingCollection blockin HashicorpVaultClient hashiClient; - logger.LogTrace("attempt locking of _caConfig."); - Monitor.Enter(_caConfig); - - try - { - - hashiClient = new HashicorpVaultClient(_caConfig); - logger.LogTrace($"getting all certificate serial numbers from vault from {_caConfig.Host} using namespace {_caConfig.Namespace} and mount-point {_caConfig.MountPoint}"); - var serials = hashiClient.GetAllCertSerialNumbers(cancelToken).Result; - Interlocked.Exchange(ref certSerials, serials); - } - catch (Exception ex) - { - logger.LogError($"failed to retreive serial numbers: {LogHandler.FlattenException(ex)}"); - blockingBuffer.CompleteAdding(); - throw; - } - finally - { - Monitor.Exit(_caConfig); - } + { // wrapper for single thread block - logger.LogTrace($"got {certSerials?.Count() ?? 0} serial numbers. Begin checking status for each..."); - - if (certSerials == null || certSerials.Count == 0) - { // exit if no certs were found in vault - blockingBuffer.CompleteAdding(); - logger.LogTrace($"no certificates found at path {_caConfig.Host} using namespace {_caConfig.Namespace} and mount point {_caConfig.MountPoint}"); - logger.MethodExit(); - return; - } - - foreach (var certSerial in certSerials) - { - if (cancelToken.IsCancellationRequested) - { - break; - } - CertResponse certFromVault = null; - var dbStatus = -1; - - // first, retreive the details from Vault try { - logger.LogTrace($"Calling GetCertificate on our client, passing serial number: {certSerial}"); - certFromVault = hashiClient.GetCertificate(certSerial).Result; + + hashiClient = new HashicorpVaultClient(_caConfig); + logger.LogTrace($"getting all certificate serial numbers from vault from {_caConfig.Host} using namespace {_caConfig.Namespace} and mount-point {_caConfig.MountPoint}"); + certSerials = await hashiClient.GetAllCertSerialNumbers(cancelToken); } catch (Exception ex) { - logger.LogError($"Failed to retreive details for certificate with serial number {certSerial} from Vault. Errors: {LogHandler.FlattenException(ex)}"); + logger.LogError($"failed to retreive serial numbers: {LogHandler.FlattenException(ex)}"); blockingBuffer.CompleteAdding(); + _semaphore.Release(); throw; } - finally { Monitor.Exit(_caConfig); } - logger.LogTrace($"converting {certSerial} to database trackingId"); - var trackingId = certSerial.Replace(":", "-"); // we store with '-'; hashi stores with ':' + logger.LogTrace($"got {certSerials?.Count() ?? 0} serial numbers. Begin checking status for each..."); - // then, check for an existing local entry - try - { - logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); - dbStatus = await _certificateDataReader.GetStatusByRequestID(trackingId); - } - catch (Exception ex) // an exception is thrown if cert doesn't exist - { - if (!ex.Message.Contains("No matching")) - { // if the exception message doesn't contain this; something else happened. - logger.LogTrace($"exception when retrieving cert from database: {ex.Message}"); - blockingBuffer.CompleteAdding(); - throw; - } - logger.LogTrace($"tracking id {trackingId} was not found in the database. it will be added."); + if (certSerials == null || certSerials.Count == 0) + { // exit if no certs were found in vault + blockingBuffer.CompleteAdding(); + logger.LogTrace($"no certificates found at path {_caConfig.Host} using namespace {_caConfig.Namespace} and mount point {_caConfig.MountPoint}"); + _semaphore.Release(); + logger.MethodExit(); + return; } - finally { Monitor.Exit(_caConfig); } - if (dbStatus == -1 || fullSync) // it's missing and needs added, or a full sync is requested + foreach (var certSerial in certSerials) { - logger.LogTrace($"adding cert with serial {trackingId} to the database. fullsync is {fullSync}, and the certificate {(dbStatus == -1 ? "does not yet exist" : "already exists")} in the database."); - Interlocked.Increment(ref changedCount); - var newCert = new AnyCAPluginCertificate + if (cancelToken.IsCancellationRequested) { - CARequestID = trackingId, - Certificate = certFromVault.Certificate, - Status = certFromVault.RevocationTime != null ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED, - RevocationDate = certFromVault.RevocationTime, - }; + break; + } + CertResponse certFromVault = null; + var dbStatus = -1; + // first, retreive the details from Vault try { - logger.LogTrace($"writing the result."); - if (blockingBuffer.TryAdd(newCert, 50, cancelToken)) - { - logger.LogTrace($"successfully added certificate to the database."); - } - else - { - logger.LogTrace($"adding to queue for writing was blocked."); - } + logger.LogTrace($"Calling GetCertificate on our client, passing serial number: {certSerial}"); + certFromVault = hashiClient.GetCertificate(certSerial).Result; } catch (Exception ex) { - logger.LogError($"Failed to add the cert to the database: {LogHandler.FlattenException(ex)}"); - logger.LogTrace($"closing buffer and aborting sync"); + logger.LogError($"Failed to retreive details for certificate with serial number {certSerial} from Vault. Errors: {LogHandler.FlattenException(ex)}"); blockingBuffer.CompleteAdding(); + _semaphore.Release(); throw; } - finally { Monitor.Exit(_caConfig); } - } - else // the cert exists in the database; just update the status if necessary - { - logger.LogTrace($"certificate with id {trackingId} was found in the database, comparing status."); - var revoked = certFromVault.RevocationTime != null; - var vaultStatus = revoked ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED; - if (vaultStatus != dbStatus) // if there is a mismatch, we need to update + logger.LogTrace($"converting {certSerial} to database trackingId"); + + var trackingId = certSerial.Replace(":", "-"); // we store with '-'; hashi stores with ':' + + // then, check for an existing local entry + try + { + logger.LogTrace($"attempting to retreive status of cert with tracking id {trackingId} from the database"); + dbStatus = await _certificateDataReader.GetStatusByRequestID(trackingId); + } + catch (Exception ex) // an exception is thrown if cert doesn't exist { - Interlocked.Increment(ref changedCount); - logger.LogTrace($"status in vault is {vaultStatus}, status in db is {dbStatus}; updating db."); + if (!ex.Message.Contains("No matching")) + { // if the exception message doesn't contain this; something else happened. + logger.LogTrace($"exception when retrieving cert from database: {ex.Message}"); + blockingBuffer.CompleteAdding(); + _semaphore.Release(); + throw; + } + logger.LogTrace($"tracking id {trackingId} was not found in the database. it will be added."); + } + + if (dbStatus == -1 || fullSync) // it's missing and needs added, or a full sync is requested + { + logger.LogTrace($"adding cert with serial {trackingId} to the database. fullsync is {fullSync}, and the certificate {(dbStatus == -1 ? "does not yet exist" : "already exists")} in the database."); + changedCount++; var newCert = new AnyCAPluginCertificate { CARequestID = trackingId, Certificate = certFromVault.Certificate, - Status = vaultStatus, - RevocationDate = certFromVault.RevocationTime - // ProductID is not available via the API after the initial issuance. we do not want to overwrite + Status = certFromVault.RevocationTime != null ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED, + RevocationDate = certFromVault.RevocationTime, }; - if (blockingBuffer.TryAdd(newCert, 50, cancelToken)) + + try { - logger.LogTrace($"successfully updated certificate {trackingId} in the database."); + logger.LogTrace($"writing the result."); + if (blockingBuffer.TryAdd(newCert, 50, cancelToken)) + { + logger.LogTrace($"successfully added certificate to the database."); + } + else + { + logger.LogTrace($"adding to queue for writing was blocked."); + } } - else + catch (Exception ex) { - logger.LogTrace($"adding to queue for writing was blocked."); + logger.LogError($"Failed to add the cert to the database: {LogHandler.FlattenException(ex)}"); + logger.LogTrace($"closing buffer and aborting sync"); + blockingBuffer.CompleteAdding(); + _semaphore.Release(); + throw; } } - else + else // the cert exists in the database; just update the status if necessary { - logger.LogTrace($"The status is unchanged and we are doing an incremental scan; no need to update db."); + logger.LogTrace($"certificate with id {trackingId} was found in the database, comparing status."); + var revoked = certFromVault.RevocationTime != null; + var vaultStatus = revoked ? (int)EndEntityStatus.REVOKED : (int)EndEntityStatus.GENERATED; + if (vaultStatus != dbStatus) // if there is a mismatch, we need to update + { + changedCount++; + logger.LogTrace($"status in vault is {vaultStatus}, status in db is {dbStatus}; updating db."); + var newCert = new AnyCAPluginCertificate + { + CARequestID = trackingId, + Certificate = certFromVault.Certificate, + Status = vaultStatus, + RevocationDate = certFromVault.RevocationTime + // ProductID is not available via the API after the initial issuance. we do not want to overwrite + }; + if (blockingBuffer.TryAdd(newCert, 50, cancelToken)) + { + logger.LogTrace($"successfully updated certificate {trackingId} in the database."); + } + else + { + logger.LogTrace($"adding to queue for writing was blocked."); + } + } + else + { + logger.LogTrace($"The status is unchanged and we are doing an incremental scan; no need to update db."); + } } + count++; } - Interlocked.Increment(ref count); + blockingBuffer.CompleteAdding(); + logger.LogTrace($"Completed sync of {count} certificates. It was {(fullSync ? "a full" : "an incremental")} sync and {changedCount} records were updated."); } - blockingBuffer.CompleteAdding(); - logger.LogTrace($"Completed sync of {count} certificates. It was {(fullSync ? "a full" : "an incremental")} sync and {changedCount} records were updated."); - logger.MethodExit(); - Monitor.Exit(_caConfig); + finally + { + _semaphore.Release(); + logger.MethodExit(); + } } /// From 952f5b018f351623065a64c620353cfb589d3596 Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele Date: Mon, 31 Mar 2025 11:43:35 -0400 Subject: [PATCH 14/14] removed inneffective semaphore --- .../HashicorpVaultCAConnector.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs index 404301a..358ee4f 100644 --- a/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs +++ b/hashicorp-vault-cagateway/HashicorpVaultCAConnector.cs @@ -25,7 +25,6 @@ namespace Keyfactor.Extensions.CAPlugin.HashicorpVault public class HashicorpVaultCAConnector : IAnyCAPlugin { private readonly ILogger logger; - private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // Binary semaphore (1 indicates only one thread/task can enter) private HashicorpVaultCAConfig _caConfig; //private HashicorpVaultClient _client { get; set; } @@ -235,10 +234,6 @@ public async Task Synchronize(BlockingCollection blockin // !! since that value is not retreivable after the initial generation. logger.MethodEntry(); - await _semaphore.WaitAsync(cancelToken); - - logger.LogTrace($"got semaphore lock, current count: {_semaphore.CurrentCount}"); - logger.LogTrace("Beginning Synchronization Task.."); var certSerials = new List(); @@ -260,8 +255,7 @@ public async Task Synchronize(BlockingCollection blockin catch (Exception ex) { logger.LogError($"failed to retreive serial numbers: {LogHandler.FlattenException(ex)}"); - blockingBuffer.CompleteAdding(); - _semaphore.Release(); + blockingBuffer.CompleteAdding(); throw; } @@ -270,8 +264,7 @@ public async Task Synchronize(BlockingCollection blockin if (certSerials == null || certSerials.Count == 0) { // exit if no certs were found in vault blockingBuffer.CompleteAdding(); - logger.LogTrace($"no certificates found at path {_caConfig.Host} using namespace {_caConfig.Namespace} and mount point {_caConfig.MountPoint}"); - _semaphore.Release(); + logger.LogTrace($"no certificates found at path {_caConfig.Host} using namespace {_caConfig.Namespace} and mount point {_caConfig.MountPoint}"); logger.MethodExit(); return; } @@ -294,8 +287,7 @@ public async Task Synchronize(BlockingCollection blockin catch (Exception ex) { logger.LogError($"Failed to retreive details for certificate with serial number {certSerial} from Vault. Errors: {LogHandler.FlattenException(ex)}"); - blockingBuffer.CompleteAdding(); - _semaphore.Release(); + blockingBuffer.CompleteAdding(); throw; } logger.LogTrace($"converting {certSerial} to database trackingId"); @@ -313,8 +305,7 @@ public async Task Synchronize(BlockingCollection blockin if (!ex.Message.Contains("No matching")) { // if the exception message doesn't contain this; something else happened. logger.LogTrace($"exception when retrieving cert from database: {ex.Message}"); - blockingBuffer.CompleteAdding(); - _semaphore.Release(); + blockingBuffer.CompleteAdding(); throw; } logger.LogTrace($"tracking id {trackingId} was not found in the database. it will be added."); @@ -348,8 +339,7 @@ public async Task Synchronize(BlockingCollection blockin { logger.LogError($"Failed to add the cert to the database: {LogHandler.FlattenException(ex)}"); logger.LogTrace($"closing buffer and aborting sync"); - blockingBuffer.CompleteAdding(); - _semaphore.Release(); + blockingBuffer.CompleteAdding(); throw; } } @@ -391,7 +381,6 @@ public async Task Synchronize(BlockingCollection blockin } finally { - _semaphore.Release(); logger.MethodExit(); } }