From a56818bcf859dbc91dca835a4ed84dbe62c3bb01 Mon Sep 17 00:00:00 2001 From: SergioMatias94 Date: Sun, 1 Jun 2025 22:49:25 -0600 Subject: [PATCH] Implement hashicorp vault --- .../Configuration/RegisterBlueprint.cs | 4 +- .../Configuration/VaultOptions.cs | 1 - .../Provider/KeyVaultProvider.cs | 128 ++++++++++++------ 3 files changed, 92 insertions(+), 41 deletions(-) diff --git a/Core.Blueprint.KeyVault/Configuration/RegisterBlueprint.cs b/Core.Blueprint.KeyVault/Configuration/RegisterBlueprint.cs index 0b2ce9a..599eaf0 100644 --- a/Core.Blueprint.KeyVault/Configuration/RegisterBlueprint.cs +++ b/Core.Blueprint.KeyVault/Configuration/RegisterBlueprint.cs @@ -23,8 +23,8 @@ namespace Core.Blueprint.KeyVault.Configuration { var vaultSettings = configuration.GetSection("Vault").Get(); - if (string.IsNullOrEmpty(vaultSettings?.Address) || string.IsNullOrEmpty(vaultSettings.Token) || - string.IsNullOrEmpty(vaultSettings?.SecretPath) || string.IsNullOrEmpty(vaultSettings.SecretMount)) + if (string.IsNullOrEmpty(vaultSettings?.Address) || string.IsNullOrEmpty(vaultSettings.Token) + || string.IsNullOrEmpty(vaultSettings.SecretMount)) { throw new ArgumentNullException("Vault options are not configured correctly."); } diff --git a/Core.Blueprint.KeyVault/Configuration/VaultOptions.cs b/Core.Blueprint.KeyVault/Configuration/VaultOptions.cs index 48aa990..dbc22f5 100644 --- a/Core.Blueprint.KeyVault/Configuration/VaultOptions.cs +++ b/Core.Blueprint.KeyVault/Configuration/VaultOptions.cs @@ -11,6 +11,5 @@ namespace Core.Blueprint.KeyVault.Configuration public string Address { get; set; } = string.Empty; public string Token { get; set; } = string.Empty; public string SecretMount { get; set; } = string.Empty; - public string SecretPath { get; set; } = string.Empty; } } diff --git a/Core.Blueprint.KeyVault/Provider/KeyVaultProvider.cs b/Core.Blueprint.KeyVault/Provider/KeyVaultProvider.cs index 74d495e..e20a9bc 100644 --- a/Core.Blueprint.KeyVault/Provider/KeyVaultProvider.cs +++ b/Core.Blueprint.KeyVault/Provider/KeyVaultProvider.cs @@ -3,6 +3,8 @@ using VaultSharp; using VaultSharp.V1.AuthMethods.Token; using Core.Blueprint.KeyVault.Configuration; using Microsoft.Extensions.Configuration; +using System.Net.Http.Json; +using VaultSharp.Core; namespace Core.Blueprint.KeyVault; @@ -28,22 +30,24 @@ public sealed class KeyVaultProvider : IKeyVaultProvider new TokenAuthMethodInfo(hashiOptions?.Token) )); } + else + { + var keyVaultUri = new Uri(configuration["ConnectionStrings:KeyVaultDAL"]!); + azureClient = new SecretClient(keyVaultUri, new Azure.Identity.DefaultAzureCredential()); + } } /// /// Creates a new secret in Azure Key Vault or HashiCorp Vault. /// - /// The request containing the name and value of the secret. - /// The cancellation token to cancel the operation. - /// A containing the details of the created secret. public async ValueTask CreateSecretAsync(KeyVaultRequest keyVaultRequest, CancellationToken cancellationToken) { if (environment == "Local") { await hashiClient!.V1.Secrets.KeyValue.V2.WriteSecretAsync( - path: hashiOptions!.SecretPath, - data: new Dictionary { { keyVaultRequest.Name, keyVaultRequest.Value } }, - mountPoint: hashiOptions.SecretMount + path: keyVaultRequest.Name, + data: new Dictionary { { "value", keyVaultRequest.Value } }, + mountPoint: hashiOptions!.SecretMount ); return new KeyVaultResponse { Name = keyVaultRequest.Name, Value = keyVaultRequest.Value }; } @@ -56,7 +60,7 @@ public sealed class KeyVaultProvider : IKeyVaultProvider } /// - /// Deletes a secret from Azure Key Vault or HashiCorp Vault if it exists. + /// Permanently deletes a secret from Azure Key Vault or HashiCorp Vault (hard delete for Vault). /// /// The name of the secret to delete. /// The cancellation token to cancel the operation. @@ -67,16 +71,11 @@ public sealed class KeyVaultProvider : IKeyVaultProvider { if (environment == "Local") { - await hashiClient!.V1.Secrets.KeyValue.V2.DeleteSecretAsync( - path: hashiOptions!.SecretPath, - mountPoint: hashiOptions.SecretMount - ); - - return new("Key Deleted", true); + await DestroyAllSecretVersionsAsync(secretName, cancellationToken); } var existingSecret = await this.GetSecretAsync(secretName, cancellationToken); - if (existingSecret != null) + if (existingSecret.Item2 == string.Empty) { await azureClient!.StartDeleteSecretAsync(secretName, cancellationToken); return new("Key Deleted", true); @@ -85,52 +84,105 @@ public sealed class KeyVaultProvider : IKeyVaultProvider return new("Key Not Found", false); } + /// /// Retrieves a secret from Azure Key Vault or HashiCorp Vault. /// - /// The name of the secret to retrieve. - /// The cancellation token to cancel the operation. - /// - /// A containing the with secret details - /// and an optional error message if the secret was not found. - /// public async ValueTask> GetSecretAsync(string secretName, CancellationToken cancellationToken) { if (environment == "Local") { - var secret = await hashiClient!.V1.Secrets.KeyValue.V2.ReadSecretAsync( - path: hashiOptions!.SecretPath, - mountPoint: hashiOptions.SecretMount - ); - - if (secret.Data.Data.TryGetValue(secretName, out var value)) + try { - return new(new KeyVaultResponse { Name = secretName, Value = value?.ToString() ?? "" }, string.Empty); - } + var secret = await hashiClient!.V1.Secrets.KeyValue.V2.ReadSecretAsync( + path: secretName, + mountPoint: hashiOptions!.SecretMount + ); - return new(new KeyVaultResponse(), "Key Not Found"); + if (secret.Data.Data.TryGetValue("value", out var value)) + { + return new(new KeyVaultResponse { Name = secretName, Value = value?.ToString() ?? "" }, string.Empty); + } + + return new(new KeyVaultResponse(), "Key Not Found"); + } + catch (VaultSharp.Core.VaultApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound) + { + return new(new KeyVaultResponse(), "Key Not Found"); + } } - KeyVaultSecret azureResponse = await azureClient!.GetSecretAsync(secretName, cancellationToken: cancellationToken); - return new(new KeyVaultResponse { Name = secretName, Value = azureResponse.Value }, string.Empty); + try + { + KeyVaultSecret azureResponse = await azureClient!.GetSecretAsync(secretName, cancellationToken: cancellationToken); + return new(new KeyVaultResponse { Name = secretName, Value = azureResponse.Value }, string.Empty); + } + catch (Azure.RequestFailedException ex) when (ex.Status == 404) + { + return new(new KeyVaultResponse(), "Key Not Found"); + } } /// /// Updates an existing secret in Azure Key Vault or HashiCorp Vault. If the secret does not exist, an error is returned. /// - /// The updated secret information. - /// The cancellation token to cancel the operation. - /// - /// A containing the updated and an optional error message if the secret was not found. - /// public async ValueTask> UpdateSecretAsync(KeyVaultRequest newSecret, CancellationToken cancellationToken) { var existingSecret = await this.GetSecretAsync(newSecret.Name, cancellationToken); - if (existingSecret == null) + if (!string.IsNullOrEmpty(existingSecret.Item2)) { return new(new KeyVaultResponse(), "Key Not Found"); } - return new(await CreateSecretAsync(newSecret, cancellationToken), string.Empty); + var updated = await CreateSecretAsync(newSecret, cancellationToken); + return new(updated, string.Empty); + } + + /// + /// Permanently deletes all versions of a given secret in HashiCorp Vault. + /// Returns a tuple indicating the result status and a message. + /// + /// The secret name/path. + /// A cancellation token. + /// + /// A tuple: + /// - bool?: true if deleted, false if no versions, null if not found. + /// - string: message explaining the result. + /// + private async Task<(bool? WasDeleted, string Message)> DestroyAllSecretVersionsAsync(string secretName, CancellationToken cancellationToken) + { + Dictionary versions; + + try + { + var metadata = await hashiClient!.V1.Secrets.KeyValue.V2.ReadSecretMetadataAsync( + path: secretName, + mountPoint: hashiOptions!.SecretMount + ); + + versions = metadata.Data.Versions.Keys.ToDictionary(k => k, _ => (object)0); + if (versions.Count == 0) + return (false, "Key exists but contains no versions."); + } + catch (VaultApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound) + { + return (null, "Key Not Found."); + } + + using var httpClient = new HttpClient { BaseAddress = new Uri(hashiOptions.Address) }; + var request = new HttpRequestMessage(HttpMethod.Post, $"/v1/{hashiOptions.SecretMount}/destroy/{secretName}") + { + Content = JsonContent.Create(new { versions = versions.Keys.ToArray() }) + }; + request.Headers.Add("X-Vault-Token", hashiOptions.Token); + var response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + await hashiClient.V1.Secrets.KeyValue.V2.DeleteMetadataAsync( + path: secretName, + mountPoint: hashiOptions.SecretMount + ); + + return (true, "Key Permanently Deleted."); } }