diff --git a/README.md b/README.md index ceece95..f2f7fe0 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,10 @@ This integration was built on the .NET Core 3.1 target framework and are compati 1. It is not necessary to use the Vault root token when creating a Certificate Store for HashicorpVault. We recommend creating a token with policies that reflect the minimum permissions necessary to perform the intended operations. -1. For the Key-Value secrets engine, the certificates are stored as an entry with 3 fields. +1. For the Key-Value secrets engine, the certificates are stored as an entry with 2 fields. - `PUBLIC_KEY` - The certificate public key - `PRIVATE_KEY` - The certificate private key -- `KEY_SECRET` - The certificate private key password ## Extension Configuration @@ -121,6 +120,7 @@ This integration was built on the .NET Core 3.1 target framework and are compati - **MountPoint** - type: *string* - **VaultServerUrl** - type: *string*, *required* - **VaultToken** - type: *secret*, *required* + - **SubfolderInventory** - type: *bool* (By default, this is set to false. Not a required field) ![](images/store_type_fields.png) @@ -144,6 +144,7 @@ In Keyfactor Command create a new Certificate Store that resembles the one below - If left blank, will default to "kv-v2". - **Vault Token** - This is the access token that will be used by the orchestrator for requests to Vault. - **Vault Server Url** - the full url and port of the Vault server instance +- **Subfolder Inventory** - Set to 'True' if it is a requirement to inventory secrets at the subfolder/component level. The default, 'False' will inventory secrets stored at the root of the "Store Path", but will not look at secrets in subfolders. ### For the Keyfactor and PKI plugins @@ -237,5 +238,4 @@ At this point you should be able to enroll a certificate and store it in Vault u ## Notes / Future Enhancements -- For the Key-Value stores we operate on a single version of the Key Value secret (no versioning capabilities through the Orchesterator Extension / Keyfactor). - +- For the Key-Value stores we operate on a single version of the Key Value secret (no versioning capabilities through the Orchesterator Extension / Keyfactor). \ No newline at end of file diff --git a/hashicorp-vault-orchestrator/HcvKeyValueClient.cs b/hashicorp-vault-orchestrator/HcvKeyValueClient.cs index 3fb1dbf..75d8b61 100644 --- a/hashicorp-vault-orchestrator/HcvKeyValueClient.cs +++ b/hashicorp-vault-orchestrator/HcvKeyValueClient.cs @@ -35,10 +35,11 @@ public class HcvKeyValueClient : IHashiClient private string _storePath { get; set; } private string _mountPoint { get; set; } + private bool _subfolderInventory { get; set; } //private VaultClientSettings clientSettings { get; set; } - public HcvKeyValueClient(string vaultToken, string serverUrl, string mountPoint, string storePath) + public HcvKeyValueClient(string vaultToken, string serverUrl, string mountPoint, string storePath,bool SubfolderInventory = false) { // Initialize one of the several auth methods. IAuthMethodInfo authMethod = new TokenAuthMethodInfo(vaultToken); @@ -48,18 +49,47 @@ public HcvKeyValueClient(string vaultToken, string serverUrl, string mountPoint, _mountPoint = mountPoint; _storePath = !string.IsNullOrEmpty(storePath) ? "/" + storePath : storePath; _vaultClient = new VaultClient(clientSettings); + _subfolderInventory = SubfolderInventory; } + public async Task> ListComponentPathsAsync(string storagePath) + { + VaultClient.V1.Auth.ResetVaultToken(); + List componentPaths = new List {}; + try + { + Secret listInfo = (await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretPathsAsync(storagePath, _mountPoint)); + + foreach (var path in listInfo.Data.Keys) + { + if (!path.EndsWith("/")) + { + continue; + } + string fullPath = $"{storagePath}{path}"; + componentPaths.Add(fullPath); + + List subPaths = await ListComponentPathsAsync(fullPath); + componentPaths.AddRange(subPaths); + } + } + catch (Exception ex) + { + logger.LogWarning($"Error while listing component paths: {ex}"); + } + return componentPaths; + } public async Task GetCertificate(string key) { VaultClient.V1.Auth.ResetVaultToken(); Dictionary certData; Secret res; - + var fullPath = _storePath + key; + var relativePath = fullPath.Substring(_storePath.Length); try { - var fullPath = _storePath + key; + try { @@ -74,7 +104,7 @@ public async Task GetCertificate(string key) } catch (Exception ex) { - logger.LogWarning("Error getting certificate (deleted?)", ex); + logger.LogWarning($"Error getting certificate (deleted?) {fullPath}", ex); return null; } @@ -99,7 +129,7 @@ public async Task GetCertificate(string key) return new CurrentInventoryItem() { - Alias = key, + Alias = relativePath, PrivateKeyEntry = hasPrivateKey, ItemStatus = OrchestratorInventoryItemStatus.Unknown, UseChainLevel = true, @@ -247,31 +277,48 @@ public async Task> GetCertificates() { VaultClient.V1.Auth.ResetVaultToken(); _storePath = _storePath.TrimStart('/'); + List subPaths = new List(); + //Grabs the list of subpaths to get certificates from, if SubFolder Inventory is turned on. + //Otherwise just define the single path _storePath + if (_subfolderInventory == true) + { + subPaths = (await ListComponentPathsAsync(_storePath)); + subPaths.Add(_storePath); + } + else + { + subPaths.Add(_storePath); + } var certs = new List(); var certNames = new List(); - try + logger.LogDebug($"SubInventoryEnabled: {_subfolderInventory}"); + foreach (var path in subPaths) { - if (string.IsNullOrEmpty(_mountPoint)) + var relative_path = path.Substring(_storePath.Length); + try { - certNames = (await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretPathsAsync(_storePath)).Data.Keys.ToList(); + + if (string.IsNullOrEmpty(_mountPoint)) + { + certNames = (await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretPathsAsync(path)).Data.Keys.ToList(); + } + else + { + certNames = (await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretPathsAsync(path, mountPoint: _mountPoint)).Data.Keys.ToList(); + } + + certNames.ForEach(k => + { + var cert = GetCertificate($"{relative_path}{k}").Result; + if (cert != null) certs.Add(cert); + }); } - else + catch (Exception ex) { - certNames = (await VaultClient.V1.Secrets.KeyValue.V2.ReadSecretPathsAsync(_storePath, mountPoint: _mountPoint)).Data.Keys.ToList(); + logger.LogError(ex.Message); + throw ex; } - - certNames.ForEach(k => - { - var cert = GetCertificate(k).Result; - if (cert != null) certs.Add(cert); - }); } - catch (Exception ex) - { - logger.LogError(ex.Message); - throw ex; - } - return certs; } private static Func Pemify = base64Cert => @@ -285,4 +332,4 @@ public async Task> GetCertificates() return header + FormatBase64(base64Cert) + footer; }; } -} +} \ No newline at end of file diff --git a/hashicorp-vault-orchestrator/Jobs/JobBase.cs b/hashicorp-vault-orchestrator/Jobs/JobBase.cs index 76652cc..a3345b8 100644 --- a/hashicorp-vault-orchestrator/Jobs/JobBase.cs +++ b/hashicorp-vault-orchestrator/Jobs/JobBase.cs @@ -21,6 +21,8 @@ public abstract class JobBase public string SecretsEngine { get; set; } // "PKI", "Keyfactor", "Key Value" public string VaultServerUrl { get; set; } + + public bool SubfolderInventory { get; set; } public string MountPoint { get; set; } // the mount point of the KV secrets engine. defaults to KV @@ -69,12 +71,20 @@ private void InitProps(dynamic props, string capability) VaultServerUrl = props["VaultServerUrl"]; SecretsEngine = props["SecretsEngine"]; MountPoint = props["MountPoint"] ?? null; + if (props["SubfolderInventory"] == null) + { + SubfolderInventory = false; + } + else + { + SubfolderInventory = props["SubfolderInventory"]; + } var isPki = capability.Contains("HCVPKI"); if (!isPki) { - VaultClient = new HcvKeyValueClient(VaultToken, VaultServerUrl, MountPoint, StorePath); + VaultClient = new HcvKeyValueClient(VaultToken, VaultServerUrl, MountPoint, StorePath, SubfolderInventory); } else { @@ -83,4 +93,4 @@ private void InitProps(dynamic props, string capability) } } -} +} \ No newline at end of file