diff --git a/.github/workflows/keyfactor-merge-store-types.yml b/.github/workflows/keyfactor-merge-store-types.yml new file mode 100644 index 0000000..c70659f --- /dev/null +++ b/.github/workflows/keyfactor-merge-store-types.yml @@ -0,0 +1,27 @@ +name: Keyfactor Merge Cert Store Types +on: [workflow_dispatch] + +jobs: + get-manifest-properties: + runs-on: windows-latest + outputs: + update_catalog: ${{ steps.read-json.outputs.update_catalog }} + integration_type: ${{ steps.read-json.outputs.integration_type }} + steps: + - uses: actions/checkout@v3 + - name: Store json + id: read-json + shell: pwsh + run: | + $json = Get-Content integration-manifest.json | ConvertFrom-Json + $myvar = $json.update_catalog + echo "update_catalog=$myvar" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + $myvar = $json.integration_type + echo "integration_type=$myvar" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + + call-update-store-types-workflow: + needs: get-manifest-properties + if: needs.get-manifest-properties.outputs.integration_type == 'orchestrator' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + uses: Keyfactor/actions/.github/workflows/update-store-types.yml@main + secrets: + token: ${{ secrets.UPDATE_STORE_TYPES }} diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml new file mode 100644 index 0000000..a4649f2 --- /dev/null +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -0,0 +1,20 @@ +name: Keyfactor Bootstrap Workflow + +on: + workflow_dispatch: + pull_request: + types: [opened, closed, synchronize, edited, reopened] + push: + create: + branches: + - 'release-*.*' + +jobs: + call-starter-workflow: + uses: keyfactor/actions/.github/workflows/starter.yml@3.1.2 + secrets: + token: ${{ secrets.V2BUILDTOKEN}} + APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} + gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} + gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} + scan_token: ${{ secrets.SAST_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e577aae --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +v1.0.0 +- Initial Version diff --git a/SampleOrchestratorExtension.sln b/GCPSecretManager.sln similarity index 83% rename from SampleOrchestratorExtension.sln rename to GCPSecretManager.sln index 27a5c1f..a4d7ee9 100644 --- a/SampleOrchestratorExtension.sln +++ b/GCPSecretManager.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31702.278 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleOrchestratorExtension", "SampleOrchestratorExtension\SampleOrchestratorExtension.csproj", "{ECFD4531-6959-431C-8D5A-8CD62301A82A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GCPSecretManagereOrchestratorExtension", "GCPSecretManager\GCPSecretManager.csproj", "{ECFD4531-6959-431C-8D5A-8CD62301A82A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/GCPSecretManager/CertificateFormatter/BaseCertificateFormatter.cs b/GCPSecretManager/CertificateFormatter/BaseCertificateFormatter.cs new file mode 100644 index 0000000..56ec3b6 --- /dev/null +++ b/GCPSecretManager/CertificateFormatter/BaseCertificateFormatter.cs @@ -0,0 +1,24 @@ +using Keyfactor.Logging; +using Microsoft.Extensions.Logging; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager + +{ + abstract class BaseCertificateFormatter : ICertificateFormatter + { + internal ILogger Logger { get; set; } + + internal BaseCertificateFormatter() + { + Logger = LogHandler.GetClassLogger(this.GetType()); + } + + public abstract bool HasPrivateKey(string entry); + + public abstract bool IsValid(string entry); + + public abstract string[] ConvertSecretToCertificateChain(string entry); + + public abstract string ConvertCertificateEntryToSecret(string certificateContents, string privateKeyPassword, bool includeChain, string newPassword); + } +} diff --git a/GCPSecretManager/CertificateFormatter/ICertificateFormatter.cs b/GCPSecretManager/CertificateFormatter/ICertificateFormatter.cs new file mode 100644 index 0000000..fbf8148 --- /dev/null +++ b/GCPSecretManager/CertificateFormatter/ICertificateFormatter.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager + +{ + interface ICertificateFormatter + { + bool HasPrivateKey(string entry); + + bool IsValid(string entry); + + string[] ConvertSecretToCertificateChain(string entry); + + string ConvertCertificateEntryToSecret(string certificateContents, string privateKeyPassword, bool includeChain, string newPassword); + } +} diff --git a/GCPSecretManager/CertificateFormatter/PEMCertificateFormatter.cs b/GCPSecretManager/CertificateFormatter/PEMCertificateFormatter.cs new file mode 100644 index 0000000..6e45d07 --- /dev/null +++ b/GCPSecretManager/CertificateFormatter/PEMCertificateFormatter.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Microsoft.Extensions.Logging; + +using Keyfactor.Logging; +using Keyfactor.PKI.PEM; +using Keyfactor.PKI.PrivateKeys; +using Keyfactor.PKI.X509; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Pkcs; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager + +{ + class PEMCertificateFormatter : BaseCertificateFormatter + { + private string BEGIN_DELIMITER = "-----BEGIN CERTIFICATE-----"; + private string END_DELIMITER = "-----END CERTIFICATE-----"; + private string[] PRIVATE_KEY_DELIMITERS = new string[] { "-----BEGIN PRIVATE KEY-----", "-----BEGIN ENCRYPTED PRIVATE KEY-----", "-----BEGIN RSA PRIVATE KEY-----" }; + + public override bool HasPrivateKey(string entry) + { + Logger.MethodEntry(LogLevel.Debug); + + bool rtnValue = false; + + foreach (string privateKeyDelimiter in PRIVATE_KEY_DELIMITERS) + { + if (entry.Contains(privateKeyDelimiter, StringComparison.OrdinalIgnoreCase)) + { + rtnValue = true; + break; + } + } + + Logger.MethodExit(LogLevel.Debug); + + return rtnValue; + } + + public override bool IsValid(string entry) + { + Logger.MethodEntry(LogLevel.Debug); + + Logger.MethodExit(LogLevel.Debug); + + return entry.Contains(BEGIN_DELIMITER, StringComparison.OrdinalIgnoreCase) && entry.Contains(END_DELIMITER, StringComparison.OrdinalIgnoreCase); + } + + public override string[] ConvertSecretToCertificateChain(string entry) + { + Logger.MethodEntry(LogLevel.Debug); + + entry = entry.Replace(System.Environment.NewLine, string.Empty); + List rtnCertificates = new List(); + int currStart = 0; + int currEnd = 0; + + do + { + currStart = entry.IndexOf(BEGIN_DELIMITER, currEnd) + BEGIN_DELIMITER.Length; + currEnd = entry.IndexOf(END_DELIMITER, currStart); + rtnCertificates.Add(entry.Substring(currStart, currEnd - currStart)); + currEnd++; + } + while (entry.IndexOf(END_DELIMITER, currEnd) > -1); + + Logger.MethodExit(LogLevel.Debug); + + return rtnCertificates.ToArray(); + } + + public override string ConvertCertificateEntryToSecret(string certificateContents, string privateKeyPassword, bool includeChain, string newPassword) + { + Logger.MethodEntry(LogLevel.Debug); + + if (string.IsNullOrEmpty(privateKeyPassword)) + return PemUtilities.DERToPEM(Convert.FromBase64String(certificateContents), PemUtilities.PemObjectType.Certificate); + + Pkcs12StoreBuilder builder = new Pkcs12StoreBuilder(); + Pkcs12Store pkcs12Store = builder.Build(); + using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(certificateContents))) + { + pkcs12Store.Load(ms, privateKeyPassword.ToCharArray()); + } + + string alias = pkcs12Store.Aliases.First(); + + X509CertificateEntry[] certChainEntries = pkcs12Store.GetCertificateChain(alias); + CertificateConverter certConverter = CertificateConverterFactory.FromBouncyCastleCertificate(certChainEntries[0].Certificate); + + AsymmetricKeyParameter privateKey = pkcs12Store.GetKey(alias).Key; + AsymmetricKeyParameter publicKey = certChainEntries[0].Certificate.GetPublicKey(); + + PrivateKeyConverter keyConverter = PrivateKeyConverterFactory.FromBCKeyPair(privateKey, publicKey, false); + + byte[] privateKeyBytes = string.IsNullOrEmpty(newPassword) ? keyConverter.ToPkcs8BlobUnencrypted() : keyConverter.ToPkcs8Blob(newPassword); + string keyString = PemUtilities.DERToPEM(privateKeyBytes, string.IsNullOrEmpty(newPassword) ? PemUtilities.PemObjectType.PrivateKey : PemUtilities.PemObjectType.EncryptedPrivateKey); + + string pemString = certConverter.ToPEM(true); + pemString += keyString; + + if (includeChain) + { + for (int i = 1; i < certChainEntries.Length; i++) + { + CertificateConverter chainConverter = CertificateConverterFactory.FromBouncyCastleCertificate(certChainEntries[i].Certificate); + pemString += chainConverter.ToPEM(true); + } + } + + Logger.MethodExit(LogLevel.Debug); + + return pemString; + } + } +} diff --git a/GCPSecretManager/ExceptionHandler.cs b/GCPSecretManager/ExceptionHandler.cs new file mode 100644 index 0000000..376b8f5 --- /dev/null +++ b/GCPSecretManager/ExceptionHandler.cs @@ -0,0 +1,29 @@ +// Copyright 2021 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using System; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager +{ + class GCPException : ApplicationException + { + public GCPException(string message) : base(message) + { } + + public GCPException(string message, Exception ex) : base(message, ex) + { } + + public static string FlattenExceptionMessages(Exception ex, string message) + { + message += ex.Message + Environment.NewLine; + if (ex.InnerException != null) + message = FlattenExceptionMessages(ex.InnerException, message); + + return message; + } + } +} diff --git a/GCPSecretManager/GCPClient.cs b/GCPSecretManager/GCPClient.cs new file mode 100644 index 0000000..1dca852 --- /dev/null +++ b/GCPSecretManager/GCPClient.cs @@ -0,0 +1,184 @@ +using Google.Api.Gax.ResourceNames; +using Google.Cloud.SecretManager.V1; + +using Microsoft.Extensions.Logging; + +using Keyfactor.Logging; + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager +{ + internal class GCPClient + { + ILogger _logger; + string ProjectId { get; set; } + SecretManagerServiceClient Client { get; set; } + + public GCPClient(string projectId) + { + _logger = LogHandler.GetClassLogger(this.GetType()); + ProjectId = projectId; + Client = SecretManagerServiceClient.Create(); + } + + public List GetSecretNames() + { + _logger.MethodEntry(LogLevel.Debug); + + List rtnSecrets = new List(); + + ListSecretsRequest request = new ListSecretsRequest(); + request.PageSize = 50; + request.ParentAsProjectName = ProjectName.FromProject(ProjectId); + + ListSecretsResponse response; + + try + { + do + { + response = Client.ListSecrets(request).AsRawResponses().FirstOrDefault(); + if (response == null) + break; + else + { + foreach (var item in response.Secrets) + { + rtnSecrets.Add(item.Name); + } + } + + request.PageToken = response.NextPageToken; + } while (!string.IsNullOrEmpty(response.NextPageToken)); + } + catch (Exception ex) + { + _logger.LogError(GCPException.FlattenExceptionMessages(ex, "Error retrieving certificates: ")); + throw; + } + finally + { + _logger.MethodExit(LogLevel.Debug); + } + + return rtnSecrets; + } + + public string GetCertificateEntry(string name) + { + _logger.MethodEntry(LogLevel.Debug); + + string rtnValue; + + try + { + AccessSecretVersionResponse version = Client.AccessSecretVersion(new AccessSecretVersionRequest() { Name = name + "/versions/latest" }); + rtnValue = version.Payload.Data.ToStringUtf8(); + } + catch (Exception ex) + { + _logger.LogError(GCPException.FlattenExceptionMessages(ex, $"Error retrieving certificate {name}: ")); + throw; + } + finally + { + _logger.MethodExit(LogLevel.Debug); + } + + return rtnValue; + } + + public void AddSecret(string alias, string secretContent, bool entryExists) + { + _logger.MethodEntry(LogLevel.Debug); + + try + { + SecretName secretName = new SecretName(ProjectId, alias); + + if (!entryExists) + { + AccessSecretVersionRequest request = new AccessSecretVersionRequest(); + + //create secret + CreateSecretRequest secretRequest = new CreateSecretRequest(); + secretRequest.ParentAsProjectName = new ProjectName(ProjectId); + secretRequest.SecretId = alias; + secretRequest.Secret = new Secret { Replication = new Replication { Automatic = new Replication.Types.Automatic() } }; + + Secret secret = Client.CreateSecret(secretRequest); + } + + //create new version + AddSecretVersionRequest secretVersionRequest = new AddSecretVersionRequest(); + secretVersionRequest.ParentAsSecretName = secretName; + secretVersionRequest.Payload = new SecretPayload { Data = Google.Protobuf.ByteString.CopyFromUtf8(secretContent) }; + + SecretVersion secretVersion = Client.AddSecretVersion(secretVersionRequest); + } + catch (Exception ex) + { + _logger.LogError(GCPException.FlattenExceptionMessages(ex, "Error adding/replacing certificate. ")); + throw; + } + finally + { + _logger.MethodExit(LogLevel.Debug); + } + } + + public void DeleteCertificate(string name) + { + _logger.MethodEntry(LogLevel.Debug); + + DeleteSecretRequest request = new DeleteSecretRequest() + { + SecretName = new SecretName(ProjectId, name) + }; + + try + { + Client.DeleteSecret(request); + } + catch (Exception ex) + { + _logger.LogError(GCPException.FlattenExceptionMessages(ex, $"Error deleting certificate {name}: ")); + throw; + } + finally + { + _logger.MethodExit(LogLevel.Debug); + } + } + + public bool Exists(string name) + { + _logger.MethodEntry(LogLevel.Debug); + + bool rtnValue = true; + + GetSecretRequest request = new GetSecretRequest() + { + SecretName = new SecretName(ProjectId, name) + }; + + try + { + Client.GetSecret(request); + } + catch (Grpc.Core.RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound) + { + rtnValue = false; + } + finally + { + _logger.MethodExit(LogLevel.Debug); + } + + return rtnValue; + } + } +} diff --git a/GCPSecretManager/GCPSecretManager.csproj b/GCPSecretManager/GCPSecretManager.csproj new file mode 100644 index 0000000..21cb578 --- /dev/null +++ b/GCPSecretManager/GCPSecretManager.csproj @@ -0,0 +1,22 @@ + + + + true + net6.0;net8.0 + true + disable + + + + + + + + + + + Always + + + + diff --git a/GCPSecretManager/Inventory.cs b/GCPSecretManager/Inventory.cs new file mode 100644 index 0000000..6299d2e --- /dev/null +++ b/GCPSecretManager/Inventory.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; + +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions; +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager +{ + public class Inventory : JobBase, IInventoryJobExtension + { + public string ExtensionName => "Keyfactor.Extensions.Orchestrator.GCPSecretManager.Inventory"; + + public Inventory(IPAMSecretResolver resolver) + { + Resolver = resolver; + } + + //Job Entry Point + public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate submitInventory) + { + Logger = LogHandler.GetClassLogger(this.GetType()); + Logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}..."); + Logger.LogDebug($"Server: {config.CertificateStoreDetails.ClientMachine}"); + Logger.LogDebug($"Store Path: {config.CertificateStoreDetails.StorePath}"); + Logger.LogDebug($"Job Properties:"); + foreach (KeyValuePair keyValue in config.JobProperties ?? new Dictionary()) + { + Logger.LogDebug($" {keyValue.Key}: {keyValue.Value}"); + } + + List inventoryItems = new List(); + + bool hasWarnings = false; + + try + { + Initialize(config.CertificateStoreDetails); + + GCPClient client = new GCPClient(ProjectId); + List secretNames = client.GetSecretNames(); + foreach(string secretName in secretNames) + { + string certificateEntry = string.Empty; + try + { + certificateEntry = client.GetCertificateEntry(secretName); + } + catch (Exception) + { + hasWarnings = true; + continue; + } + + if (!CertificateFormatter.IsValid(certificateEntry)) + continue; + string[] certificateChain = CertificateFormatter.ConvertSecretToCertificateChain(certificateEntry); + + inventoryItems.Add(new CurrentInventoryItem() + { + ItemStatus = OrchestratorInventoryItemStatus.Unknown, + Alias = secretName.Substring(secretName.LastIndexOf("/") + 1), + PrivateKeyEntry = CertificateFormatter.HasPrivateKey(certificateEntry), + UseChainLevel = certificateChain.Length > 1, + Certificates = certificateChain + }); + } + } + catch (Exception ex) + { + return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = GCPException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}:") }; + } + + try + { + submitInventory.Invoke(inventoryItems); + + if (hasWarnings) + return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Warning, JobHistoryId = config.JobHistoryId, FailureMessage = $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Inventory completed successfully, but one or more secrets were not able to be retrieved. The secrets that had issues may or may not be valid certificates. Please check the orchestrator log for more details." }; + else + return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; + } + catch (Exception ex) + { + return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = GCPException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Error performing Inventory. ") }; + } + } + } +} \ No newline at end of file diff --git a/GCPSecretManager/JobBase.cs b/GCPSecretManager/JobBase.cs new file mode 100644 index 0000000..ef07846 --- /dev/null +++ b/GCPSecretManager/JobBase.cs @@ -0,0 +1,42 @@ +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Extensions; +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager +{ + public class JobBase + { + internal ICertificateFormatter CertificateFormatter { get; set; } + internal ILogger Logger { get; set; } + internal IPAMSecretResolver Resolver { get; set; } + internal string StorePassword { get; set; } + internal string ProjectId { get; set; } + internal string PasswordSecretSuffix { get; set; } + internal bool IncludeChain { get; set; } + + internal void Initialize(CertificateStore certificateStoreDetails) + { + Logger.MethodEntry(LogLevel.Debug); + + StorePassword = PAMUtilities.ResolvePAMField(Resolver, Logger, "Store Password", certificateStoreDetails.StorePassword); + ProjectId = certificateStoreDetails.StorePath; + + string errMessage = string.Empty; + dynamic properties = JsonConvert.DeserializeObject(certificateStoreDetails.Properties.ToString()); + + if (properties.PasswordSecretSuffix != null) + PasswordSecretSuffix = properties.PasswordSecretSuffix.Value; + + IncludeChain = properties.IncludeChain == null || string.IsNullOrEmpty(properties.IncludeChain.Value) ? true : bool.Parse(properties.IncludeChain.Value); + + CertificateFormatter = GetCertificateFormatter(); + } + + internal ICertificateFormatter GetCertificateFormatter() + { + return new PEMCertificateFormatter(); + } + } +} diff --git a/GCPSecretManager/Management.cs b/GCPSecretManager/Management.cs new file mode 100644 index 0000000..d1b823c --- /dev/null +++ b/GCPSecretManager/Management.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; + +using Keyfactor.Logging; +using Keyfactor.Orchestrators.Extensions; +using Keyfactor.Orchestrators.Common.Enums; +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Keyfactor.Extensions.Orchestrator.GCPSecretManager; + +using Microsoft.Extensions.Logging; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager +{ + public class Management : JobBase, IManagementJobExtension + { + public string ExtensionName => "Keyfactor.Extensions.Orchestrator.GCPSecretManager.Management"; + + public Management(IPAMSecretResolver resolver) + { + Resolver = resolver; + } + + public JobResult ProcessJob(ManagementJobConfiguration config) + { + Logger = LogHandler.GetClassLogger(this.GetType()); + Logger.LogDebug($"Begin {config.Capability} for job id {config.JobId}..."); + Logger.LogDebug($"Server: {config.CertificateStoreDetails.ClientMachine}"); + Logger.LogDebug($"Store Path: {config.CertificateStoreDetails.StorePath}"); + Logger.LogDebug($"Job Properties:"); + foreach (KeyValuePair keyValue in config.JobProperties ?? new Dictionary()) + { + Logger.LogDebug($" {keyValue.Key}: {keyValue.Value}"); + } + + try + { + Initialize(config.CertificateStoreDetails); + + GCPClient client = new GCPClient(ProjectId); + + switch (config.OperationType) + { + case CertStoreOperationType.Add: + PerformAdd(config, client); + break; + case CertStoreOperationType.Remove: + client.DeleteCertificate(config.JobCertificate.Alias); + break; + default: + return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Unsupported operation: {config.OperationType.ToString()}" }; + } + } + catch (Exception ex) + { + return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = GCPException.FlattenExceptionMessages(ex, $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Error adding certificate for {config.JobCertificate.Alias}. ") }; + } + + return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; + } + + private void PerformAdd(ManagementJobConfiguration config, GCPClient client) + { + Logger.MethodEntry(LogLevel.Debug); + + string alias = config.JobCertificate.Alias; + bool entryExists = client.Exists(alias); + string newPassword = string.Empty; + + if (!config.Overwrite && entryExists) + { + string errMsg = $"Secret {alias} already exists but Overwrite set to False. Set Overwrite to True to replace the certificate."; + Logger.LogError(errMsg); + Logger.MethodExit(LogLevel.Debug); + throw new GCPException(errMsg); + } + + if (string.IsNullOrEmpty(StorePassword)) + { + if (!string.IsNullOrEmpty(PasswordSecretSuffix)) + newPassword = config.JobCertificate.PrivateKeyPassword; + } + else + newPassword = StorePassword; + + try + { + string secret = CertificateFormatter.ConvertCertificateEntryToSecret(config.JobCertificate.Contents, config.JobCertificate.PrivateKeyPassword, IncludeChain, newPassword); + client.AddSecret(alias, secret, entryExists); + if (!string.IsNullOrEmpty(newPassword) && string.IsNullOrEmpty(StorePassword)) + { + bool passwordEntryExists = client.Exists(alias + PasswordSecretSuffix); + client.AddSecret(alias + PasswordSecretSuffix, newPassword, passwordEntryExists); + } + } + catch { throw; } + finally + { + Logger.MethodExit(LogLevel.Debug); + } + } + } +} \ No newline at end of file diff --git a/GCPSecretManager/PAMUtilities.cs b/GCPSecretManager/PAMUtilities.cs new file mode 100644 index 0000000..86675cd --- /dev/null +++ b/GCPSecretManager/PAMUtilities.cs @@ -0,0 +1,21 @@ +// Copyright 2024 Keyfactor +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions +// and limitations under the License. + +using Keyfactor.Orchestrators.Extensions.Interfaces; +using Microsoft.Extensions.Logging; + +namespace Keyfactor.Extensions.Orchestrator.GCPSecretManager +{ + internal class PAMUtilities + { + internal static string ResolvePAMField(IPAMSecretResolver resolver, ILogger logger, string name, string key) + { + logger.LogDebug($"Attempting to resolve PAM eligible field {name}"); + return string.IsNullOrEmpty(key) ? key : resolver.Resolve(key); + } + } +} diff --git a/GCPSecretManager/manifest.json b/GCPSecretManager/manifest.json new file mode 100644 index 0000000..fe70420 --- /dev/null +++ b/GCPSecretManager/manifest.json @@ -0,0 +1,14 @@ +{ + "extensions": { + "Keyfactor.Orchestrators.Extensions.IOrchestratorJobExtension": { + "CertStores.GCPScrtMgr.Inventory": { + "assemblypath": "GCPSecretManager.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.GCPSecretManager.Inventory" + }, + "CertStores.GCPScrtMgr.Management": { + "assemblypath": "GCPSecretManager.dll", + "TypeFullName": "Keyfactor.Extensions.Orchestrator.GCPSecretManager.Management" + } + } + } +} diff --git a/README.md b/README.md index 1843da0..3e8f266 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,282 @@ -# cpr-orchestrator-template - -## Template for new (Universal) Orchestrator integrations - -### Use this repository to create new integrations for new universal orchestrator integration types. - - -1. [Use this repository](#using-the-repository) -1. [Update the integration-manifest.json](#updating-the-integration-manifest.json) -1. [Add Keyfactor Bootstrap Workflow (keyfactor-bootstrap-workflow.yml)](#add-bootstrap) -1. [Create required branches](#create-required-branches) -1. [Replace template files/folders](#replace-template-files-and-folders) -1. [Create initial prerelease](#create-initial-prerelease) ---- - -#### Using the repository -1. Select the ```Use this template``` button at the top of this page -1. Update the repository name following [these guidelines](https://keyfactorinc.sharepoint.com/sites/IntegrationWiki/SitePages/GitHub-Processes.aspx#repository-naming-conventions) - 1. All repositories must be in lower-case - 1. General pattern: company-product-type - 1. e.g. hashicorp-vault-orchestator -1. Click the ```Create repository``` button - ---- - -#### Updating the integration-manifest.json - -*The following properties must be updated in the [integration-manifest.json](./integration-manifest.json)* - -Clone the repository locally, use vsdev.io, or the GitHub online editor to update the file. - -* "name": "Friendly name for the integration" - * This will be used in the readme file generation and catalog entries -* "description": "Brief description of the integration." - * This will be used in the readme file generation - * If the repository description is empty this value will be used for the repository description upon creating a release branch -* "release_dir": "PATH\\\TO\\\BINARY\\\RELEASE\\\OUTPUT\\\FOLDER" - * Path separators can be "\\\\" or "/" - * Be sure to specify the release folder name. This can be found by running a Release build and noting the output folder - * Example: "AzureAppGatewayOrchestrator\\bin\\Release" -* "about.orchestrator.platform_UOFramework": "10.1" - * Universal Orchestrator Framework version required -* "about.orchestrator.keyfactor_platform_support": "9.10" - * Command platform version required -* "about.orchestrator.pam_support": false - * If the orchestrator supports PAM change this value to true - -For each platform (win and linux) define which capabilities are present for this orchestrator extension. You must update the boolean properties for both win and linux platforms. - -* "supportsCreateStore" -* "supportsDiscovery" -* "supportsManagementAdd" -* "supportsManagementRemove" -* "supportsReenrollment" -* "supportsInventory" - -### Cert Store Definitions - -The integration-manifest.json contains cert-store definitions for use with [kfutil](https://github.com/keyfactor/kfutil). - -Instructions for creating the store type entries can be found on the [kfutil Orchestrator Store Type Integration page on Confluence](https://keyfactor.atlassian.net/wiki/x/SoBVBQ) - ---- - -#### Add Bootstrap -Add Keyfactor Bootstrap Workflow (keyfactor-bootstrap-workflow.yml). This can be copied directly from the workflow templates or through the Actions tab -* Directly: - 1. Create a file named ```.github\workflows\keyfactor-bootstrap-workflow.yml``` - 1. Copy the contents of [keyfactor/.github/workflow-templates/keyfactor-bootstrap-workflow.yml](https://raw.githubusercontent.com/Keyfactor/.github/main/workflow-templates/keyfactor-bootstrap-workflow.yml) into the file created in the previous step -* Actions tab: - 1. Navigate to the [Actions tab](./actions) in the new repository - 1. Click the ```New workflow``` button - 1. Find the ```Keyfactor Bootstrap Workflow``` and click the ```Configure``` button - 1. Click the ```Commit changes...``` button on this screen and the next to add the bootstrap workflow to the main branch - -A new build will run the tasks of a *Push* trigger on the main branch - -*Ensure there are no errors during the workflow run in the Actions tab.* - ---- - -#### Create required branches -1. Create a release branch from main: release-1.0 -1. Create a dev branch from the starting with the devops id in the format ab#\, e.g. ab#53535. - 1. For the cleanest pull request merge, create the dev branch from the release branch. - 1. Optionally, add a suffix to the branch name indicating initial release. e.g. ab#53535-initial-release - ---- - - -#### Replace template files and folders -1. Replace the contents of readme_source.md -1. Create a CHANGELOG.md file in the root of the repository indicating ```1.0: Initial release``` -1. Replace the SampleOrchestratorExtension.sln solution file and SampleOrchestratorExtension folder with your new orchestrator dotnet solution -1. Push your updates to the dev branch (ab#xxxxx) - ---- - - -#### Create initial prerelease -1. Create a pull request from the dev branch to the release-1.0 branch - - ----- - -When the repository is ready for SE Demo, change the following property: -* "status": "pilot" - -When the integration has been approved by Support and Delivery teams, change the following property: -* "status": "production" - -If the repository is ready to be published in the public catalog, the following properties must be updated: -* "update_catalog": true -* "link_github": true +

+ GCP Secret Manager Universal Orchestrator Extension +

+ +

+ +Integration Status: production +Release +Issues +GitHub Downloads (all assets, all releases) +

+ +

+ + + Support + + · + + Installation + + · + + License + + · + + Related Integrations + +

+ +## Overview + +The Google Cloud Platform (GCP) Secret Manager Orchestrator Extension remotely manages certificates stored as secrets in Google Cloud's Secret Manager. Each certificate store set up in Keyfactor Command represents a Google Cloud project. This orchestrator extension supports the inventory and management of certificates in PEM format stored as secrets and supports the following use cases: + +* PEM encoded certificate and unencrypted or encrypted private key +* PEM encoded certificate and unencrypted or encrypted private key with full certificate chain +* PEM encoded certificate only + +For use cases including an encrypted private key, please refer to [Certificate Encryption Details](#certificate-encryption-details) for more information on handling/storing the encryption password for the private key. + + + +## Compatibility + +This integration is compatible with Keyfactor Universal Orchestrator version 10.4 and later. + +## Support +The GCP Secret Manager Universal Orchestrator extension is supported by Keyfactor for Keyfactor customers. If you have a support issue, please open a support ticket with your Keyfactor representative. If you have a support issue, please open a support ticket via the Keyfactor Support Portal at https://support.keyfactor.com. + +> To report a problem or suggest a new feature, use the **[Issues](../../issues)** tab. If you want to contribute actual bug fixes or proposed enhancements, use the **[Pull requests](../../pulls)** tab. + +## Requirements & Prerequisites + +Before installing the GCP Secret Manager Universal Orchestrator extension, we recommend that you install [kfutil](https://github.com/Keyfactor/kfutil). Kfutil is a command-line tool that simplifies the process of creating store types, installing extensions, and instantiating certificate stores in Keyfactor Command. + + +The GCP Secret Manager Orchestrator Extension uses Google Application Default Credentials (ADC) for authentication. Testing of this orchestrator extension was performed using a service account, but please review [Google Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) for more information on the various ways authentication can be set up. + +The GCP project and account being used to access Secret Manager must have access to and enabled the Secret Manger API and also must have assigned to it the Secret Manager Admin role. + + +## Create the GCPScrtMgr Certificate Store Type + +To use the GCP Secret Manager Universal Orchestrator extension, you **must** create the GCPScrtMgr Certificate Store Type. This only needs to happen _once_ per Keyfactor Command instance. + + + +* **Create GCPScrtMgr using kfutil**: + + ```shell + # GCPScrtMgr + kfutil store-types create GCPScrtMgr + ``` + +* **Create GCPScrtMgr manually in the Command UI**: +
Create GCPScrtMgr manually in the Command UI + + Create a store type called `GCPScrtMgr` with the attributes in the tables below: + + #### Basic Tab + | Attribute | Value | Description | + | --------- | ----- | ----- | + | Name | GCPScrtMgr | Display name for the store type (may be customized) | + | Short Name | GCPScrtMgr | Short display name for the store type | + | Capability | GCPScrtMgr | Store type name orchestrator will register with. Check the box to allow entry of value | + | Supports Add | ✅ Checked | Check the box. Indicates that the Store Type supports Management Add | + | Supports Remove | ✅ Checked | Check the box. Indicates that the Store Type supports Management Remove | + | Supports Discovery | 🔲 Unchecked | Indicates that the Store Type supports Discovery | + | Supports Reenrollment | 🔲 Unchecked | Indicates that the Store Type supports Reenrollment | + | Supports Create | 🔲 Unchecked | Indicates that the Store Type supports store creation | + | Needs Server | 🔲 Unchecked | Determines if a target server name is required when creating store | + | Blueprint Allowed | ✅ Checked | Determines if store type may be included in an Orchestrator blueprint | + | Uses PowerShell | 🔲 Unchecked | Determines if underlying implementation is PowerShell | + | Requires Store Password | ✅ Checked | Enables users to optionally specify a store password when defining a Certificate Store. | + | Supports Entry Password | 🔲 Unchecked | Determines if an individual entry within a store can have a password. | + + The Basic tab should look like this: + + ![GCPScrtMgr Basic Tab](docsource/images/GCPScrtMgr-basic-store-type-dialog.png) + + #### Advanced Tab + | Attribute | Value | Description | + | --------- | ----- | ----- | + | Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. | + | Private Key Handling | Optional | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. | + | PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) | + + The Advanced tab should look like this: + + ![GCPScrtMgr Advanced Tab](docsource/images/GCPScrtMgr-advanced-store-type-dialog.png) + + #### Custom Fields Tab + Custom fields operate at the certificate store level and are used to control how the orchestrator connects to the remote target server containing the certificate store to be managed. The following custom fields should be added to the store type: + + | Name | Display Name | Description | Type | Default Value/Options | Required | + | ---- | ------------ | ---- | --------------------- | -------- | ----------- | + | PasswordSecretSuffix | Password Secret Location Suffix | If storing a certificate with an encrypted private key, this is the suffix to add to the certificate (secret) alias name where the encrypted private key password will be stored. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information | String | | 🔲 Unchecked | + | IncludeChain | Include Chain | Determines whether to include the certificate chain when adding a certificate as a secret. | Bool | True | 🔲 Unchecked | + + The Custom Fields tab should look like this: + + ![GCPScrtMgr Custom Fields Tab](docsource/images/GCPScrtMgr-custom-fields-store-type-dialog.png) + + + +
+ +## Installation + +1. **Download the latest GCP Secret Manager Universal Orchestrator extension from GitHub.** + + Navigate to the [GCP Secret Manager Universal Orchestrator extension GitHub version page](https://github.com/Keyfactor/gcp-secretmanager-orchestrator/releases/latest). Refer to the compatibility matrix below to determine whether the `net6.0` or `net8.0` asset should be downloaded. Then, click the corresponding asset to download the zip archive. + | Universal Orchestrator Version | Latest .NET version installed on the Universal Orchestrator server | `rollForward` condition in `Orchestrator.runtimeconfig.json` | `gcp-secretmanager-orchestrator` .NET version to download | + | --------- | ----------- | ----------- | ----------- | + | Older than `11.0.0` | | | `net6.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net6.0` | | `net6.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `Disable` | `net6.0` | + | Between `11.0.0` and `11.5.1` (inclusive) | `net8.0` | `LatestMajor` | `net8.0` | + | `11.6` _and_ newer | `net8.0` | | `net8.0` | + + Unzip the archive containing extension assemblies to a known location. + + > **Note** If you don't see an asset with a corresponding .NET version, you should always assume that it was compiled for `net6.0`. + +2. **Locate the Universal Orchestrator extensions directory.** + + * **Default on Windows** - `C:\Program Files\Keyfactor\Keyfactor Orchestrator\extensions` + * **Default on Linux** - `/opt/keyfactor/orchestrator/extensions` + +3. **Create a new directory for the GCP Secret Manager Universal Orchestrator extension inside the extensions directory.** + + Create a new directory called `gcp-secretmanager-orchestrator`. + > The directory name does not need to match any names used elsewhere; it just has to be unique within the extensions directory. + +4. **Copy the contents of the downloaded and unzipped assemblies from __step 2__ to the `gcp-secretmanager-orchestrator` directory.** + +5. **Restart the Universal Orchestrator service.** + + Refer to [Starting/Restarting the Universal Orchestrator service](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/StarttheService.htm). + + +6. **(optional) PAM Integration** + + The GCP Secret Manager Universal Orchestrator extension is compatible with all supported Keyfactor PAM extensions to resolve PAM-eligible secrets. PAM extensions running on Universal Orchestrators enable secure retrieval of secrets from a connected PAM provider. + + To configure a PAM provider, [reference the Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam) to select an extension, and follow the associated instructions to install it on the Universal Orchestrator (remote). + + +> The above installation steps can be supplimented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/InstallingAgents/NetCoreOrchestrator/CustomExtensions.htm?Highlight=extensions). + + + +## Defining Certificate Stores + + + +* **Manually with the Command UI** + +
Create Certificate Stores manually in the UI + + 1. **Navigate to the _Certificate Stores_ page in Keyfactor Command.** + + Log into Keyfactor Command, toggle the _Locations_ dropdown, and click _Certificate Stores_. + + 2. **Add a Certificate Store.** + + Click the Add button to add a new Certificate Store. Use the table below to populate the **Attributes** in the **Add** form. + | Attribute | Description | + | --------- | ----------- | + | Category | Select "GCPScrtMgr" or the customized certificate store name from the previous step. | + | Container | Optional container to associate certificate store with. | + | Client Machine | Not used | + | Store Path | The Project ID of the Google Secret Manager being managed. | + | Orchestrator | Select an approved orchestrator capable of managing `GCPScrtMgr` certificates. Specifically, one with the `GCPScrtMgr` capability. | + | PasswordSecretSuffix | If storing a certificate with an encrypted private key, this is the suffix to add to the certificate (secret) alias name where the encrypted private key password will be stored. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information | + | IncludeChain | Determines whether to include the certificate chain when adding a certificate as a secret. | + | Store Password | Password used to encrypt the private key of ALL certificate secrets. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information | + + + +
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator + + If a PAM provider was installed _on the Universal Orchestrator_ in the [Installation](#Installation) section, the following parameters can be configured for retrieval _on the Universal Orchestrator_. + | Attribute | Description | + | --------- | ----------- | + | Store Password | Password used to encrypt the private key of ALL certificate secrets. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information | + + Please refer to the **Universal Orchestrator (remote)** usage section ([PAM providers on the Keyfactor Integration Catalog](https://keyfactor.github.io/integrations-catalog/content/pam)) for your selected PAM provider for instructions on how to load attributes orchestrator-side. + + > Any secret can be rendered by a PAM provider _installed on the Keyfactor Command server_. The above parameters are specific to attributes that can be fetched by an installed PAM provider running on the Universal Orchestrator server itself. +
+ + +
+ +* **Using kfutil** + +
Create Certificate Stores with kfutil + + 1. **Generate a CSV template for the GCPScrtMgr certificate store** + + ```shell + kfutil stores import generate-template --store-type-name GCPScrtMgr --outpath GCPScrtMgr.csv + ``` + 2. **Populate the generated CSV file** + + Open the CSV file, and reference the table below to populate parameters for each **Attribute**. + | Attribute | Description | + | --------- | ----------- | + | Category | Select "GCPScrtMgr" or the customized certificate store name from the previous step. | + | Container | Optional container to associate certificate store with. | + | Client Machine | Not used | + | Store Path | The Project ID of the Google Secret Manager being managed. | + | Orchestrator | Select an approved orchestrator capable of managing `GCPScrtMgr` certificates. Specifically, one with the `GCPScrtMgr` capability. | + | PasswordSecretSuffix | If storing a certificate with an encrypted private key, this is the suffix to add to the certificate (secret) alias name where the encrypted private key password will be stored. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information | + | IncludeChain | Determines whether to include the certificate chain when adding a certificate as a secret. | + | Store Password | Password used to encrypt the private key of ALL certificate secrets. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information | + + + +
Attributes eligible for retrieval by a PAM Provider on the Universal Orchestrator + + If a PAM provider was installed _on the Universal Orchestrator_ in the [Installation](#Installation) section, the following parameters can be configured for retrieval _on the Universal Orchestrator_. + | Attribute | Description | + | --------- | ----------- | + | Store Password | Password used to encrypt the private key of ALL certificate secrets. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information | + + > Any secret can be rendered by a PAM provider _installed on the Keyfactor Command server_. The above parameters are specific to attributes that can be fetched by an installed PAM provider running on the Universal Orchestrator server itself. +
+ + + 3. **Import the CSV file to create the certificate stores** + + ```shell + kfutil stores import csv --store-type-name GCPScrtMgr --file GCPScrtMgr.csv + ``` +
+ +> The content in this section can be supplimented by the [official Command documentation](https://software.keyfactor.com/Core-OnPrem/Current/Content/ReferenceGuide/Certificate%20Stores.htm?Highlight=certificate%20store). + + + + +## Certificate Encryption Details + +For GCP Secret Manager secrets containing private keys, the GCP Secret Manager Orchestrator Extension provides three ways to manage the certificate private key: + +1. Using the Keyfactor Command Store Password on the certificate store definition to store the password that will be used to encrypt ALL private keys for the GCP Secret Manager project. +2. Using the Password Secret Location Suffix field on the certificate store definition to store a "suffix" that will be used in conjunction with the secret alias (name) to create a second secret in Secret Manager to store the encryption password. +3. If no Store Password is set and the Password Secret Location Suffix is either missing or blank, the private key will not be encrypted. + +If the Store Password has a value, this will be used to encrypt the private key during a Management Add job. If no value is set for the Store Password, the one time password that Keyfactor Command generates when triggering a Management-Add job will be used to encrypt the private key and this password will be stored as a secret in GCP Secret Manager with a name of Alias + Password Secret Location Suffix. For example, if the certificate alias is set as "Alias1" and the Password Secret Location Suffix is set as "_Key", the certificate and encrypted private key will be stored in a secret named "Alias1" and the password for the key encryption will be stored in a secret named "Alias1_Key". Please note that if using the generated password Keyfactor Command provides and storing the password in Secret Manager, each renewal/replacement of a certificate will encrypt the private key with a new generated password, which will then be stored as a new version of the password secret. + + +## License + +Apache License 2.0, see [LICENSE](LICENSE). + +## Related Integrations + +See all [Keyfactor Universal Orchestrator extensions](https://github.com/orgs/Keyfactor/repositories?q=orchestrator). \ No newline at end of file diff --git a/SampleOrchestratorExtension/Discovery.cs b/SampleOrchestratorExtension/Discovery.cs deleted file mode 100644 index 7350632..0000000 --- a/SampleOrchestratorExtension/Discovery.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; - -using Keyfactor.Logging; -using Keyfactor.Orchestrators.Extensions; - -using Microsoft.Extensions.Logging; - -namespace Keyfactor.Extensions.Orchestrator.SampleOrchestratorExtension -{ - // The Discovery class implementes IAgentJobExtension and is meant to find all certificate stores based on the information passed when creating the job in KF Command - public class Discovery : IDiscoveryJobExtension - { - //Necessary to implement IDiscoveryJobExtension but not used. Leave as empty string. - public string ExtensionName => ""; - - //Job Entry Point - public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate submitDiscovery) - { - //METHOD ARGUMENTS... - //config - contains context information passed from KF Command to this job run: - // - // config.ServerUsername, config.ServerPassword - credentials for orchestrated server - use to authenticate to certificate store server. - // config.ClientMachine - server name or IP address of orchestrated server - // - // config.JobProperties["dirs"] - Directories to search - // config.JobProperties["extensions"] - Extensions to search - // config.JobProperties["ignoreddirs"] - Directories to ignore - // config.JobProperties["patterns"] - File name patterns to match - - - //NLog Logging to c:\CMS\Logs\CMS_Agent_Log.txt - ILogger logger = LogHandler.GetClassLogger(this.GetType()); - logger.LogDebug($"Begin Discovery..."); - - //Instantiate collection of certificate store locations to pass back - List locations = new List(); - - try - { - //Code logic to: - // 1) Connect to the orchestrated server if necessary (config.CertificateStoreDetails.ClientMachine) - // 2) Custom logic to search for valid certificate stores based on passed in: - // a) Directories to search - // b) Extensions - // c) Directories to ignore - // d) File name patterns to match - // 3) Place found and validated store locations (path and file name) in "locations" collection instantiated above - } - catch (Exception ex) - { - //Status: 2=Success, 3=Warning, 4=Error - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = "Custom message you want to show to show up as the error message in Job History in KF Command" }; - } - - try - { - //Sends store locations back to KF command where they can be approved or rejected - submitDiscovery.Invoke(locations); - //Status: 2=Success, 3=Warning, 4=Error - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; - } - catch (Exception ex) - { - // NOTE: if the cause of the submitInventory.Invoke exception is a communication issue between the Orchestrator server and the Command server, the job status returned here - // may not be reflected in Keyfactor Command. - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = "Custom message you want to show to show up as the error message in Job History in KF Command" }; - } - } - } -} \ No newline at end of file diff --git a/SampleOrchestratorExtension/Inventory.cs b/SampleOrchestratorExtension/Inventory.cs deleted file mode 100644 index 22c7252..0000000 --- a/SampleOrchestratorExtension/Inventory.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; - -using Keyfactor.Logging; -using Keyfactor.Orchestrators.Extensions; - -using Microsoft.Extensions.Logging; - -namespace Keyfactor.Extensions.Orchestrator.SampleOrchestratorExtension -{ - // The Inventory class implementes IAgentJobExtension and is meant to find all of the certificates in a given certificate store on a given server - // and return those certificates back to Keyfactor for storing in its database. Private keys will NOT be passed back to Keyfactor Command - public class Inventory : IInventoryJobExtension - { - //Necessary to implement IInventoryJobExtension but not used. Leave as empty string. - public string ExtensionName => ""; - - //Job Entry Point - public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate submitInventory) - { - //METHOD ARGUMENTS... - //config - contains context information passed from KF Command to this job run: - // - // config.Server.Username, config.Server.Password - credentials for orchestrated server - use to authenticate to certificate store server. - // - // config.ServerUsername, config.ServerPassword - credentials for orchestrated server - use to authenticate to certificate store server. - // config.CertificateStoreDetails.ClientMachine - server name or IP address of orchestrated server - // config.CertificateStoreDetails.StorePath - location path of certificate store on orchestrated server - // config.CertificateStoreDetails.StorePassword - if the certificate store has a password, it would be passed here - // config.CertificateStoreDetails.Properties - JSON string containing custom store properties for this specific store type - - //NLog Logging to c:\CMS\Logs\CMS_Agent_Log.txt - ILogger logger = LogHandler.GetClassLogger(this.GetType()); - logger.LogDebug($"Begin Inventory..."); - - //List is the collection that the interface expects to return from this job. It will contain a collection of certificates found in the store along with other information about those certificates - List inventoryItems = new List(); - - try - { - //Code logic to: - // 1) Connect to the orchestrated server (config.CertificateStoreDetails.ClientMachine) containing the certificate store to be inventoried (config.CertificateStoreDetails.StorePath) - // 2) Custom logic to retrieve certificates from certificate store. - // 3) Add certificates (no private keys) to the collection below. If multiple certs in a store comprise a chain, the Certificates array will house multiple certs per InventoryItem. If multiple certs - // in a store comprise separate unrelated certs, there will be one InventoryItem object created per certificate. - - //**** Will need to uncomment the block below and code to the extension's specific needs. This builds the collection of certificates and related information that will be passed back to the KF Orchestrator service and then Command. - //inventoryItems.Add(new AgentCertStoreInventoryItem() - //{ - // ItemStatus = OrchestratorInventoryItemStatus.Unknown, //There are other statuses, but Command can determine how to handle new vs modified certificates - // Alias = {valueRepresentingChainIdentifier} - // PrivateKeyEntry = true|false //You will not pass the private key back, but you can identify if the main certificate of the chain contains a private key in the store - // UseChainLevel = true|false, //true if Certificates will contain > 1 certificate, main cert => intermediate CA cert => root CA cert. false if Certificates will contain an array of 1 certificate - // Certificates = //Array of single X509 certificates in Base64 string format (certificates if chain, single cert if not), something like: - // **************************** - // foreach(X509Certificate2 certificate in certificates) - // certList.Add(Convert.ToBase64String(certificate.Export(X509ContentType.Cert))); - // certList.ToArray(); - // **************************** - //}); - - } - catch (Exception ex) - { - //Status: 2=Success, 3=Warning, 4=Error - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = "Custom message you want to show to show up as the error message in Job History in KF Command" }; - } - - try - { - //Sends inventoried certificates back to KF Command - submitInventory.Invoke(inventoryItems); - //Status: 2=Success, 3=Warning, 4=Error - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; - } - catch (Exception ex) - { - // NOTE: if the cause of the submitInventory.Invoke exception is a communication issue between the Orchestrator server and the Command server, the job status returned here - // may not be reflected in Keyfactor Command. - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = "Custom message you want to show to show up as the error message in Job History in KF Command" }; - } - } - } -} \ No newline at end of file diff --git a/SampleOrchestratorExtension/Management.cs b/SampleOrchestratorExtension/Management.cs deleted file mode 100644 index a91e14b..0000000 --- a/SampleOrchestratorExtension/Management.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; - -using Keyfactor.Logging; -using Keyfactor.Orchestrators.Extensions; -using Keyfactor.Orchestrators.Common.Enums; - -using Microsoft.Extensions.Logging; - -namespace Keyfactor.Extensions.Orchestrator.SampleOrchestratorExtension -{ - public class Management : IManagementJobExtension - { - //Necessary to implement IManagementJobExtension but not used. Leave as empty string. - public string ExtensionName => ""; - - //Job Entry Point - public JobResult ProcessJob(ManagementJobConfiguration config) - { - //METHOD ARGUMENTS... - //config - contains context information passed from KF Command to this job run: - // - // config.Server.Username, config.Server.Password - credentials for orchestrated server - use to authenticate to certificate store server. - // - // config.ServerUsername, config.ServerPassword - credentials for orchestrated server - use to authenticate to certificate store server. - // config.CertificateStoreDetails.ClientMachine - server name or IP address of orchestrated server - // config.CertificateStoreDetails.StorePath - location path of certificate store on orchestrated server - // config.CertificateStoreDetails.StorePassword - if the certificate store has a password, it would be passed here - // config.CertificateStoreDetails.Properties - JSON string containing custom store properties for this specific store type - // - // config.JobCertificate.EntryContents - Base64 encoded string representation (PKCS12 if private key is included, DER if not) of the certificate to add for Management-Add jobs. - // config.JobCertificate.Alias - optional string value of certificate alias (used in java keystores and some other store types) - // config.OpeerationType - enumeration representing function with job type. Used only with Management jobs where this value determines whether the Management job is a CREATE/ADD/REMOVE job. - // config.Overwrite - Boolean value telling the Orchestrator Extension whether to overwrite an existing certificate in a store. How you determine whether a certificate is "the same" as the one provided is AnyAgent implementation dependent - // config.JobCertificate.PrivateKeyPassword - For a Management Add job, if the certificate being added includes the private key (therefore, a pfx is passed in config.JobCertificate.EntryContents), this will be the password for the pfx. - - - //NLog Logging to c:\CMS\Logs\CMS_Agent_Log.txt - ILogger logger = LogHandler.GetClassLogger(this.GetType()); - logger.LogDebug($"Begin Management..."); - - try - { - //Management jobs, unlike Discovery, Inventory, and Reenrollment jobs can have 3 different purposes: - switch (config.OperationType) - { - case CertStoreOperationType.Add: - //OperationType == Add - Add a certificate to the certificate store passed in the config object - //Code logic to: - // 1) Connect to the orchestrated server (config.CertificateStoreDetails.ClientMachine) containing the certificate store - // 2) Custom logic to add certificate to certificate store (config.CertificateStoreDetails.StorePath) possibly using alias as an identifier if applicable (config.JobCertificate.Alias). Use alias and overwrite flag (config.Overwrite) - // to determine if job should overwrite an existing certificate in the store, for example a renewal. - break; - case CertStoreOperationType.Remove: - //OperationType == Remove - Delete a certificate from the certificate store passed in the config object - //Code logic to: - // 1) Connect to the orchestrated server (config.CertificateStoreDetails.ClientMachine) containing the certificate store - // 2) Custom logic to remove the certificate in a certificate store (config.CertificateStoreDetails.StorePath), possibly using alias (config.JobCertificate.Alias) or certificate thumbprint to identify the certificate (implementation dependent) - break; - case CertStoreOperationType.Create: - //OperationType == Create - Create an empty certificate store in the provided location - //Code logic to: - // 1) Connect to the orchestrated server (config.CertificateStoreDetails.ClientMachine) where the certificate store (config.CertificateStoreDetails.StorePath) will be located - // 2) Custom logic to first check if the store already exists and add it if not. If it already exists, implementation dependent as to how to handle - error, warning, success - break; - default: - //Invalid OperationType. Return error. Should never happen though - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = $"Site {config.CertificateStoreDetails.StorePath} on server {config.CertificateStoreDetails.ClientMachine}: Unsupported operation: {config.OperationType.ToString()}" }; - } - } - catch (Exception ex) - { - //Status: 2=Success, 3=Warning, 4=Error - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = "Custom message you want to show to show up as the error message in Job History in KF Command" }; - } - - //Status: 2=Success, 3=Warning, 4=Error - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; - } - } -} \ No newline at end of file diff --git a/SampleOrchestratorExtension/Reenrollment.cs b/SampleOrchestratorExtension/Reenrollment.cs deleted file mode 100644 index e66903b..0000000 --- a/SampleOrchestratorExtension/Reenrollment.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; - -using Keyfactor.Logging; -using Keyfactor.Orchestrators.Extensions; - -using Microsoft.Extensions.Logging; - -namespace Keyfactor.Extensions.Orchestrator.SampleOrchestratorExtension -{ - // The Reenrollment class implementes IAgentJobExtension and is meant to: - // 1) Generate a new public/private keypair locally - // 2) Generate a CSR from the keypair, - // 3) Submit the CSR to KF Command to enroll the certificate and retrieve the certificate back - // 4) Deploy the newly re-enrolled certificate to a certificate store - public class Reenrollment : IReenrollmentJobExtension - { - //Necessary to implement IReenrollmentJobExtension but not used. Leave as empty string. - public string ExtensionName => ""; - - //Job Entry Point - public JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReenrollment) - { - //METHOD ARGUMENTS... - //config - contains context information passed from KF Command to this job run: - // - // config.Server.Username, config.Server.Password - credentials for orchestrated server - use to authenticate to certificate store server. - // - // config.ServerUsername, config.ServerPassword - credentials for orchestrated server - use to authenticate to certificate store server. - // config.CertificateStoreDetails.ClientMachine - server name or IP address of orchestrated server - // config.CertificateStoreDetails.StorePath - location path of certificate store on orchestrated server - // config.CertificateStoreDetails.StorePassword - if the certificate store has a password, it would be passed here - // config.CertificateStoreDetails.Properties - JSON string containing custom store properties for this specific store type - // - // config.JobProperties = Dictionary of custom parameters to use in building CSR and placing enrolled certiciate in a the proper certificate store - - //NLog Logging to c:\CMS\Logs\CMS_Agent_Log.txt - ILogger logger = LogHandler.GetClassLogger(this.GetType()); - logger.LogDebug($"Begin Reenrollment..."); - - try - { - //Code logic to: - // 1) Generate a new public/private keypair locally from any config.JobProperties passed - // 2) Generate a CSR from the keypair (PKCS10), - // 3) Submit the CSR to KF Command to enroll the certificate using: - // string resp = (string)submitEnrollmentRequest.Invoke(Convert.ToBase64String(PKCS10_bytes); - // X509Certificate2 cert = new X509Certificate2(Convert.FromBase64String(resp)); - // 4) Deploy the newly re-enrolled certificate (cert in #3) to a certificate store - } - catch (Exception ex) - { - //Status: 2=Success, 3=Warning, 4=Error - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Failure, JobHistoryId = config.JobHistoryId, FailureMessage = "Custom message you want to show to show up as the error message in Job History in KF Command" }; - } - - //Status: 2=Success, 3=Warning, 4=Error - return new JobResult() { Result = Keyfactor.Orchestrators.Common.Enums.OrchestratorJobStatusJobResult.Success, JobHistoryId = config.JobHistoryId }; - } - } -} \ No newline at end of file diff --git a/SampleOrchestratorExtension/SampleOrchestratorExtension.csproj b/SampleOrchestratorExtension/SampleOrchestratorExtension.csproj deleted file mode 100644 index 9f84aa9..0000000 --- a/SampleOrchestratorExtension/SampleOrchestratorExtension.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - netcoreapp3.1 - - - - - - - - diff --git a/docsource/content.md b/docsource/content.md new file mode 100644 index 0000000..c93966d --- /dev/null +++ b/docsource/content.md @@ -0,0 +1,26 @@ +## Overview + +The Google Cloud Platform (GCP) Secret Manager Orchestrator Extension remotely manages certificates stored as secrets in Google Cloud's Secret Manager. Each certificate store set up in Keyfactor Command represents a Google Cloud project. This orchestrator extension supports the inventory and management of certificates in PEM format stored as secrets and supports the following use cases: + +* PEM encoded certificate and unencrypted or encrypted private key +* PEM encoded certificate and unencrypted or encrypted private key with full certificate chain +* PEM encoded certificate only + +For use cases including an encrypted private key, please refer to [Certificate Encryption Details](#certificate-encryption-details) for more information on handling/storing the encryption password for the private key. + +## Requirements + +The GCP Secret Manager Orchestrator Extension uses Google Application Default Credentials (ADC) for authentication. Testing of this orchestrator extension was performed using a service account, but please review [Google Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) for more information on the various ways authentication can be set up. + +The GCP project and account being used to access Secret Manager must have access to and enabled the Secret Manger API and also must have assigned to it the Secret Manager Admin role. + + +## Certificate Encryption Details + +For GCP Secret Manager secrets containing private keys, the GCP Secret Manager Orchestrator Extension provides three ways to manage the certificate private key: + +1. Using the Keyfactor Command Store Password on the certificate store definition to store the password that will be used to encrypt ALL private keys for the GCP Secret Manager project. +2. Using the Password Secret Location Suffix field on the certificate store definition to store a "suffix" that will be used in conjunction with the secret alias (name) to create a second secret in Secret Manager to store the encryption password. +3. If no Store Password is set and the Password Secret Location Suffix is either missing or blank, the private key will not be encrypted. + +If the Store Password has a value, this will be used to encrypt the private key during a Management Add job. If no value is set for the Store Password, the one time password that Keyfactor Command generates when triggering a Management-Add job will be used to encrypt the private key and this password will be stored as a secret in GCP Secret Manager with a name of Alias + Password Secret Location Suffix. For example, if the certificate alias is set as "Alias1" and the Password Secret Location Suffix is set as "_Key", the certificate and encrypted private key will be stored in a secret named "Alias1" and the password for the key encryption will be stored in a secret named "Alias1_Key". Please note that if using the generated password Keyfactor Command provides and storing the password in Secret Manager, each renewal/replacement of a certificate will encrypt the private key with a new generated password, which will then be stored as a new version of the password secret. diff --git a/docsource/gcpscrtmgr.md b/docsource/gcpscrtmgr.md new file mode 100644 index 0000000..ed37e8e --- /dev/null +++ b/docsource/gcpscrtmgr.md @@ -0,0 +1 @@ +## Overview diff --git a/docsource/images/GCPScrtMgr-advanced-store-type-dialog.png b/docsource/images/GCPScrtMgr-advanced-store-type-dialog.png new file mode 100644 index 0000000..06bc133 Binary files /dev/null and b/docsource/images/GCPScrtMgr-advanced-store-type-dialog.png differ diff --git a/docsource/images/GCPScrtMgr-basic-store-type-dialog.png b/docsource/images/GCPScrtMgr-basic-store-type-dialog.png new file mode 100644 index 0000000..79beaa5 Binary files /dev/null and b/docsource/images/GCPScrtMgr-basic-store-type-dialog.png differ diff --git a/docsource/images/GCPScrtMgr-custom-fields-store-type-dialog.png b/docsource/images/GCPScrtMgr-custom-fields-store-type-dialog.png new file mode 100644 index 0000000..a961f51 Binary files /dev/null and b/docsource/images/GCPScrtMgr-custom-fields-store-type-dialog.png differ diff --git a/integration-manifest.json b/integration-manifest.json index e72dc0f..cb4ba1c 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -1,81 +1,72 @@ { - "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", - "integration_type": "orchestrator", - "name": "Integration Template", - "status": "prototype", - "support_level": "community", - "description": "This project is meant to be a template to quickly build a basic integration product build. Currently in dev, a work in progress,", - "link_github": false, - "update_catalog": false, - "release_dir": "UPDATE-THIS-WITH-PATH-TO-BINARY-BUILD-FOLDER", - "about": { - "orchestrator": { - "UOFramework": "10.1", - "keyfactor_platform_version": "9.10", - "pam_support": false, - "win": { - "supportsCreateStore": false, - "supportsDiscovery": false, - "supportsManagementAdd": false, - "supportsManagementRemove": false, - "supportsReenrollment": false, - "supportsInventory": false - }, - "linux": { - "supportsCreateStore": false, - "supportsDiscovery": false, - "supportsManagementAdd": false, - "supportsManagementRemove": false, - "supportsReenrollment": false, - "supportsInventory": false - }, - "store_types": [ - { - "Name": "MyOrchestratorStoreType", - "ShortName": "MOST", - "Capability": "MOST", - "LocalStore": false, - "SupportedOperations": { - "Add": false, - "Create": false, - "Discovery": true, - "Enrollment": false, - "Remove": false - }, - "Properties": [ - { - "Name": "CustomField1", - "DisplayName": "CustomField1", - "Type": "String", - "DependsOn": "", - "DefaultValue": "default", - "Required": true - }, - { - "Name": "CustomField2", - "DisplayName": "CustomField2", - "Type": "String", - "DependsOn": "", - "DefaultValue": null, - "Required": true - } - ], - "EntryParameters": [], - "PasswordOptions": { - "EntrySupported": false, - "StoreRequired": false, - "Style": "Default" - }, - "StorePathType": "", - "StorePathValue": "", - "PrivateKeyAllowed": "Forbidden", - "JobProperties": [], - "ServerRequired": true, - "PowerShell": false, - "BlueprintAllowed": false, - "CustomAliasAllowed": "Forbidden" + "$schema": "https://keyfactor.github.io/v2/integration-manifest-schema.json", + "integration_type": "orchestrator", + "name": "GCP Secret Manager Universal Orchestrator", + "status": "production", + "link_github": false, + "update_catalog": false, + "support_level": "kf-supported", + "release_dir": "GCPSecretManager/bin/Release", + "release_project": "GCPSecretManager/GCPSecretManager.csproj", + "description": "This orchestrator extension manages certificates stored as secrets in Google Secret Manager.", + "about": { + "orchestrator": { + "UOFramework": "10.4", + "pam_support": true, + "keyfactor_platform_version": "10.4", + "store_types": [ + { + "Name": "GCPScrtMgr", + "ShortName": "GCPScrtMgr", + "Capability": "GCPScrtMgr", + "ServerRequired": false, + "BlueprintAllowed": true, + "CustomAliasAllowed": "Required", + "PowerShell": false, + "PrivateKeyAllowed": "Optional", + "SupportedOperations": { + "Add": true, + "Create": false, + "Discovery": false, + "Enrollment": false, + "Remove": true + }, + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": true, + "Style": "Default", + "StorePassword": { + "Description": "Password used to encrypt the private key of ALL certificate secrets. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information", + "IsPAMEligible": true + } + }, + "Properties": [ + { + "Name": "PasswordSecretSuffix", + "DisplayName": "Password Secret Location Suffix", + "Type": "String", + "DependsOn": "", + "DefaultValue": "", + "Required": false, + "IsPAMEligible": false, + "Description": "If storing a certificate with an encrypted private key, this is the suffix to add to the certificate (secret) alias name where the encrypted private key password will be stored. Please see [Certificate Encryption Details](#certificate-encryption-details) for more information" + }, + { + "Name": "IncludeChain", + "DisplayName": "Include Chain", + "Type": "Bool", + "DependsOn": "", + "DefaultValue": "True", + "Required": false, + "IsPAMEligible": false, + "Description": "Determines whether to include the certificate chain when adding a certificate as a secret." + } + ], + "EntryParameters": [], + "ClientMachineDescription": "Not used", + "StorePathDescription": "The Project ID of the Google Secret Manager being managed." + } + ] } - ] } - } }