From 294a225c997099ba2b342bec707c02042255b019 Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Wed, 15 Apr 2026 12:34:32 -0700
Subject: [PATCH 01/28] refactor: extract service layer from monolithic JobBase
Break domain logic out of JobBase into focused, testable services:
- StoreConfigurationParser: parses CertificateStoreDetails.Properties JSON
into a typed StoreConfiguration, eliminating dynamic dispatch
- StorePathResolver: resolves StorePath strings (namespace/secret-name)
into structured PathResolutionResult for all store type patterns
- JobCertificateParser: extracts certificate/key/chain from
ManagementJobConfiguration with explicit format detection
- PasswordResolver: resolves passwords from inline values or K8S secret
references, centralising the "buddy password" pattern
- CertificateChainExtractor: parses PEM chains into leaf + intermediates,
handling both bundled and pre-separated chain formats
- KeystoreOperations: JKS/PKCS12 read/write operations moved out of
handlers into a standalone service
None of these services require a Kubernetes client, making them fully
unit-testable without network access.
---
.../Services/CertificateChainExtractor.cs | 206 ++++++++
.../Services/JobCertificateParser.cs | 291 ++++++++++++
.../Services/KeystoreOperations.cs | 121 +++++
.../Services/PasswordResolver.cs | 163 +++++++
.../Services/StoreConfigurationParser.cs | 292 ++++++++++++
.../Services/StorePathResolver.cs | 441 ++++++++++++++++++
6 files changed, 1514 insertions(+)
create mode 100644 kubernetes-orchestrator-extension/Services/CertificateChainExtractor.cs
create mode 100644 kubernetes-orchestrator-extension/Services/JobCertificateParser.cs
create mode 100644 kubernetes-orchestrator-extension/Services/KeystoreOperations.cs
create mode 100644 kubernetes-orchestrator-extension/Services/PasswordResolver.cs
create mode 100644 kubernetes-orchestrator-extension/Services/StoreConfigurationParser.cs
create mode 100644 kubernetes-orchestrator-extension/Services/StorePathResolver.cs
diff --git a/kubernetes-orchestrator-extension/Services/CertificateChainExtractor.cs b/kubernetes-orchestrator-extension/Services/CertificateChainExtractor.cs
new file mode 100644
index 0000000..0c44c88
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Services/CertificateChainExtractor.cs
@@ -0,0 +1,206 @@
+// 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 System.Collections.Generic;
+using System.Text;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Services;
+
+///
+/// Extracts certificate chains from Kubernetes secret data.
+/// Handles both PEM chains and single DER certificates, with fallback logic.
+///
+public class CertificateChainExtractor
+{
+ private readonly KubeCertificateManagerClient _kubeClient;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of CertificateChainExtractor.
+ ///
+ /// Kubernetes client for certificate operations.
+ /// Logger instance for diagnostic output.
+ public CertificateChainExtractor(KubeCertificateManagerClient kubeClient, ILogger logger = null)
+ {
+ _kubeClient = kubeClient;
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ /// Extracts certificates from PEM or DER data.
+ /// First tries to parse as a PEM chain, then falls back to single DER certificate.
+ ///
+ /// Certificate data (PEM string or base64 DER).
+ /// Description of the source for logging (e.g., "key 'tls.crt'").
+ /// List of PEM-formatted certificates, or empty list if parsing fails.
+ public List ExtractCertificates(string certData, string sourceDescription = "certificate data")
+ {
+ var result = new List();
+
+ if (string.IsNullOrWhiteSpace(certData))
+ {
+ _logger.LogDebug("Certificate data from {Source} is empty or whitespace", sourceDescription);
+ return result;
+ }
+
+ // First, try to parse as a PEM chain (handles multiple certs in one field)
+ var certChain = _kubeClient.LoadCertificateChain(certData);
+ if (certChain != null && certChain.Count > 0)
+ {
+ _logger.LogDebug("Found {Count} certificate(s) in {Source}", certChain.Count, sourceDescription);
+ foreach (var cert in certChain)
+ {
+ var certPem = _kubeClient.ConvertToPem(cert);
+ _logger.LogTrace("Adding certificate from {Source}: {Subject}", sourceDescription, cert.SubjectDN);
+ result.Add(certPem);
+ }
+ return result;
+ }
+
+ // Fallback: try to parse as a single DER certificate
+ _logger.LogDebug("Failed to parse {Source} as PEM chain, trying DER format", sourceDescription);
+ var certObj = _kubeClient.ReadDerCertificate(certData);
+ if (certObj != null)
+ {
+ var certPem = _kubeClient.ConvertToPem(certObj);
+ _logger.LogTrace("Adding DER certificate from {Source}: {Subject}", sourceDescription, certObj.SubjectDN);
+ result.Add(certPem);
+ }
+ else
+ {
+ _logger.LogWarning("Failed to parse certificate from {Source} as PEM or DER format", sourceDescription);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Extracts certificates from byte array data (converts to UTF-8 string first).
+ ///
+ /// Certificate data as bytes.
+ /// Description of the source for logging.
+ /// List of PEM-formatted certificates, or empty list if parsing fails.
+ public List ExtractCertificates(byte[] certBytes, string sourceDescription = "certificate data")
+ {
+ if (certBytes == null || certBytes.Length == 0)
+ {
+ _logger.LogDebug("Certificate bytes from {Source} is null or empty", sourceDescription);
+ return new List();
+ }
+
+ var certData = Encoding.UTF8.GetString(certBytes);
+ return ExtractCertificates(certData, sourceDescription);
+ }
+
+ ///
+ /// Extracts certificates and adds them to an existing list, avoiding duplicates.
+ /// Useful for adding CA chain certificates to an existing certificate list.
+ ///
+ /// Certificate data (PEM string or base64 DER).
+ /// Existing list of PEM certificates to append to.
+ /// Description of the source for logging.
+ /// Number of new certificates added.
+ public int ExtractAndAppendUnique(string certData, List existingCerts, string sourceDescription = "certificate data")
+ {
+ var newCerts = ExtractCertificates(certData, sourceDescription);
+ var addedCount = 0;
+
+ foreach (var cert in newCerts)
+ {
+ if (!existingCerts.Contains(cert))
+ {
+ existingCerts.Add(cert);
+ addedCount++;
+ }
+ else
+ {
+ _logger.LogTrace("Skipping duplicate certificate from {Source}", sourceDescription);
+ }
+ }
+
+ return addedCount;
+ }
+
+ ///
+ /// Extracts certificates from byte array and adds them to an existing list, avoiding duplicates.
+ ///
+ /// Certificate data as bytes.
+ /// Existing list of PEM certificates to append to.
+ /// Description of the source for logging.
+ /// Number of new certificates added.
+ public int ExtractAndAppendUnique(byte[] certBytes, List existingCerts, string sourceDescription = "certificate data")
+ {
+ if (certBytes == null || certBytes.Length == 0)
+ {
+ return 0;
+ }
+
+ var certData = Encoding.UTF8.GetString(certBytes);
+ return ExtractAndAppendUnique(certData, existingCerts, sourceDescription);
+ }
+
+ ///
+ /// Extracts certificates from a secret's data dictionary using the specified allowed keys.
+ /// Tries each key in order until certificates are found.
+ ///
+ /// Dictionary of secret data (key -> byte array).
+ /// Keys to try, in priority order.
+ /// Name of the secret for logging.
+ /// Namespace of the secret for logging.
+ /// List of PEM-formatted certificates.
+ public List ExtractFromSecretData(
+ IDictionary secretData,
+ string[] allowedKeys,
+ string secretName,
+ string namespaceName)
+ {
+ var certsList = new List();
+
+ if (secretData == null)
+ {
+ _logger.LogWarning("Secret data is null for {SecretName} in {Namespace}", secretName, namespaceName);
+ return certsList;
+ }
+
+ // Try primary keys first (excludes ca.crt which is handled separately)
+ foreach (var key in allowedKeys)
+ {
+ if (key == "ca.crt") continue; // CA chain is processed separately
+
+ if (!secretData.TryGetValue(key, out var certBytes) || certBytes == null || certBytes.Length == 0)
+ {
+ continue;
+ }
+
+ var sourceDesc = $"secret '{secretName}' key '{key}' in namespace '{namespaceName}'";
+ var certs = ExtractCertificates(certBytes, sourceDesc);
+
+ if (certs.Count > 0)
+ {
+ certsList.AddRange(certs);
+ _logger.LogDebug("Found {Count} certificate(s) in {Source}", certs.Count, sourceDesc);
+ break; // Found certificates, stop trying other primary keys
+ }
+ }
+
+ // Process ca.crt separately to add chain certificates (avoiding duplicates)
+ if (secretData.TryGetValue("ca.crt", out var caBytes) && caBytes != null && caBytes.Length > 0)
+ {
+ var sourceDesc = $"secret '{secretName}' key 'ca.crt' in namespace '{namespaceName}'";
+ var addedCount = ExtractAndAppendUnique(caBytes, certsList, sourceDesc);
+ if (addedCount > 0)
+ {
+ _logger.LogDebug("Added {Count} CA certificate(s) from ca.crt", addedCount);
+ }
+ }
+
+ return certsList;
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Services/JobCertificateParser.cs b/kubernetes-orchestrator-extension/Services/JobCertificateParser.cs
new file mode 100644
index 0000000..845524d
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Services/JobCertificateParser.cs
@@ -0,0 +1,291 @@
+// 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 System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Keyfactor.Extensions.Orchestrator.K8S.Enums;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Extensions.Orchestrator.K8S.Utilities;
+using Keyfactor.Orchestrators.Extensions;
+using Keyfactor.Logging;
+using Keyfactor.PKI.Extensions;
+using Keyfactor.PKI.PEM;
+using Microsoft.Extensions.Logging;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.X509;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Services;
+
+///
+/// Parses certificate data from job configuration into a K8SJobCertificate.
+/// Handles PKCS12, DER, and PEM format detection and extraction.
+///
+public class JobCertificateParser
+{
+ private readonly ILogger _logger;
+
+ public JobCertificateParser(ILogger logger)
+ {
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ /// Parses certificate data from a management job configuration.
+ ///
+ /// The management job configuration.
+ /// Whether to include the certificate chain.
+ /// A populated K8SJobCertificate.
+ public K8SJobCertificate Parse(ManagementJobConfiguration config, bool includeCertChain)
+ {
+ _logger.LogDebug("Parsing job certificate data");
+
+ var jobCert = new K8SJobCertificate();
+
+ if (config.JobCertificate == null ||
+ string.IsNullOrEmpty(config.JobCertificate.Contents))
+ {
+ _logger.LogWarning("Job certificate contents are null or empty");
+ return jobCert;
+ }
+
+ string password = config.JobCertificate.PrivateKeyPassword ?? "";
+ jobCert.Password = password;
+
+ byte[] certBytes = Convert.FromBase64String(config.JobCertificate.Contents);
+ _logger.LogDebug("Certificate data length: {Length} bytes", certBytes.Length);
+
+ if (certBytes.Length == 0)
+ {
+ _logger.LogError("Certificate data is empty");
+ return jobCert;
+ }
+
+ return DetectAndRoute(certBytes, password, jobCert, includeCertChain, config);
+ }
+
+ ///
+ /// Detects certificate format and routes to the appropriate parser.
+ /// Order: PKCS12 → PEM → DER → error.
+ /// PEM is checked before DER because X509CertificateParser (used by IsDerFormat)
+ /// can also parse PEM data, which would cause multi-cert PEM chains to be truncated.
+ ///
+ private K8SJobCertificate DetectAndRoute(byte[] certBytes, string password,
+ K8SJobCertificate jobCert, bool includeCertChain, ManagementJobConfiguration config)
+ {
+ // Try PKCS12 first (most common format for certs with keys)
+ var pkcs12Result = TryParsePkcs12(certBytes, password);
+ if (pkcs12Result.HasValue)
+ {
+ return ParseFromPkcs12(pkcs12Result.Value.Store, pkcs12Result.Value.Alias,
+ certBytes, password, jobCert, config);
+ }
+
+ // Check PEM format before DER — X509CertificateParser (used by IsDerFormat) can also
+ // parse PEM data, so PEM must be detected first to handle multi-cert chains correctly.
+ var dataStr = Encoding.UTF8.GetString(certBytes);
+ if (dataStr.Contains("-----BEGIN CERTIFICATE-----"))
+ {
+ _logger.LogDebug("Certificate data is in PEM format");
+ return ParsePemCertificate(dataStr, jobCert);
+ }
+
+ // Check DER format
+ if (CertificateUtilities.IsDerFormat(certBytes))
+ {
+ _logger.LogDebug("Certificate data is in DER format (no private key)");
+ return ParseDerCertificate(certBytes, jobCert, includeCertChain);
+ }
+
+ _logger.LogError("Failed to parse certificate data as PKCS12, DER, or PEM format");
+ throw new InvalidOperationException(
+ "Failed to parse certificate data. The data does not appear to be a valid PKCS12, DER, or PEM certificate.");
+ }
+
+ ///
+ /// Attempts to parse data as PKCS12. Returns the store and alias if successful.
+ ///
+ private (Pkcs12Store Store, string Alias)? TryParsePkcs12(byte[] certBytes, string password)
+ {
+ try
+ {
+ var store = CertificateUtilities.LoadPkcs12Store(certBytes, password);
+ var alias = store.Aliases.FirstOrDefault(store.IsKeyEntry);
+ if (alias != null)
+ {
+ _logger.LogDebug("Successfully parsed as PKCS12 format, alias: {Alias}", alias);
+ return (store, alias);
+ }
+
+ _logger.LogDebug("PKCS12 parsed but no key entry found");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug("Not PKCS12 format: {Error}", ex.Message);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Extracts certificate, key, and chain from a PKCS12 store.
+ ///
+ private K8SJobCertificate ParseFromPkcs12(Pkcs12Store store, string alias,
+ byte[] rawBytes, string password, K8SJobCertificate jobCert, ManagementJobConfiguration config)
+ {
+ _logger.LogDebug("Extracting certificate data from PKCS12 store");
+
+ var x509Obj = store.GetCertificate(alias);
+ if (x509Obj?.Certificate == null)
+ {
+ _logger.LogError("Unable to retrieve certificate from PKCS12 store");
+ return jobCert;
+ }
+
+ var bcCert = x509Obj.Certificate;
+ _logger.LogDebug("Certificate loaded: {Summary}", LoggingUtilities.GetCertificateSummary(bcCert));
+
+ jobCert.CertPem = PemUtilities.DERToPEM(bcCert.GetEncoded(), PemUtilities.PemObjectType.Certificate);
+ jobCert.CertBytes = bcCert.GetEncoded();
+ jobCert.CertThumbprint = bcCert.Thumbprint();
+ jobCert.Pkcs12 = rawBytes;
+ jobCert.CertificateEntry = x509Obj;
+
+ // Extract chain
+ var chain = store.GetCertificateChain(alias);
+ if (chain != null && chain.Length > 0)
+ {
+ _logger.LogDebug("Certificate chain: {Count} certificates", chain.Length);
+ jobCert.CertificateEntryChain = chain;
+ jobCert.ChainPem = chain.Select(c => PemUtilities.DERToPEM(c.Certificate.GetEncoded(), PemUtilities.PemObjectType.Certificate)).ToList();
+ }
+
+ // Extract private key
+ ExtractPrivateKeyFromStore(store, alias, password, jobCert);
+
+ jobCert.StorePassword = config.CertificateStoreDetails?.StorePassword;
+ return jobCert;
+ }
+
+ ///
+ /// Extracts the private key from a PKCS12 store and sets it on the job certificate.
+ ///
+ private void ExtractPrivateKeyFromStore(Pkcs12Store store, string alias,
+ string password, K8SJobCertificate jobCert)
+ {
+ try
+ {
+ var keyEntry = store.GetKey(alias);
+ if (keyEntry?.Key == null)
+ {
+ _logger.LogDebug("No private key found for alias '{Alias}'", alias);
+ return;
+ }
+
+ var privateKey = keyEntry.Key;
+ jobCert.PrivateKeyParameter = privateKey;
+ jobCert.PrivateKeyPem = PrivateKeyFormatUtilities.ExportPrivateKeyAsPem(privateKey, PrivateKeyFormat.Pkcs8);
+ jobCert.PrivateKeyBytes = CertificateUtilities.ExportPrivateKeyPkcs8(privateKey);
+ jobCert.HasPrivateKey = true;
+
+ _logger.LogDebug("Private key extracted for certificate: {Thumbprint}", jobCert.CertThumbprint);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Private key extraction failed for certificate: {Thumbprint}", jobCert.CertThumbprint);
+ }
+ }
+
+ ///
+ /// Parses a DER-encoded certificate (no private key).
+ ///
+ private K8SJobCertificate ParseDerCertificate(byte[] derBytes, K8SJobCertificate jobCert, bool includeCertChain)
+ {
+ if (includeCertChain)
+ {
+ _logger.LogWarning(
+ "IncludeCertChain is enabled but certificate is DER format (no private key). " +
+ "Chain cannot be included.");
+ }
+
+ var parser = new X509CertificateParser();
+ var bcCert = parser.ReadCertificate(derBytes);
+ if (bcCert == null)
+ {
+ _logger.LogError("Failed to parse DER certificate - parser returned null");
+ return jobCert;
+ }
+
+ _logger.LogDebug("DER certificate loaded: {Summary}", LoggingUtilities.GetCertificateSummary(bcCert));
+
+ jobCert.CertPem = PemUtilities.DERToPEM(bcCert.GetEncoded(), PemUtilities.PemObjectType.Certificate);
+ jobCert.CertBytes = bcCert.GetEncoded();
+ jobCert.CertThumbprint = bcCert.Thumbprint();
+ jobCert.CertificateEntry = new X509CertificateEntry(bcCert);
+ jobCert.HasPrivateKey = false;
+ jobCert.CertificateEntryChain = new[] { jobCert.CertificateEntry };
+ jobCert.ChainPem = new List { jobCert.CertPem };
+
+ return jobCert;
+ }
+
+ ///
+ /// Parses PEM-encoded certificate(s) (no private key).
+ ///
+ private K8SJobCertificate ParsePemCertificate(string pemData, K8SJobCertificate jobCert)
+ {
+ var certificates = new List();
+ using var stringReader = new StringReader(pemData);
+ var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(stringReader);
+
+ object pemObject;
+ while ((pemObject = pemReader.ReadObject()) != null)
+ {
+ if (pemObject is X509Certificate cert)
+ {
+ certificates.Add(cert);
+ }
+ }
+
+ if (certificates.Count == 0)
+ {
+ // Fallback: try parsing as raw certificate data
+ var parser = new X509CertificateParser();
+ var bcCert = parser.ReadCertificate(Encoding.UTF8.GetBytes(pemData));
+ if (bcCert != null)
+ certificates.Add(bcCert);
+ }
+
+ if (certificates.Count == 0)
+ {
+ _logger.LogError("Failed to parse PEM certificate - no certificates found");
+ return jobCert;
+ }
+
+ var leafCert = certificates[0];
+ _logger.LogDebug("Leaf certificate: {Summary}", LoggingUtilities.GetCertificateSummary(leafCert));
+
+ jobCert.CertPem = PemUtilities.DERToPEM(leafCert.GetEncoded(), PemUtilities.PemObjectType.Certificate);
+ jobCert.CertBytes = leafCert.GetEncoded();
+ jobCert.CertThumbprint = leafCert.Thumbprint();
+ jobCert.CertificateEntry = new X509CertificateEntry(leafCert);
+ jobCert.HasPrivateKey = false;
+
+ jobCert.CertificateEntryChain = certificates
+ .Select(c => new X509CertificateEntry(c))
+ .ToArray();
+
+ jobCert.ChainPem = certificates
+ .Select(c => PemUtilities.DERToPEM(c.GetEncoded(), PemUtilities.PemObjectType.Certificate))
+ .ToList();
+
+ _logger.LogInformation("PEM certificate(s) parsed: {Count} certificate(s), no private key", certificates.Count);
+ return jobCert;
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Services/KeystoreOperations.cs b/kubernetes-orchestrator-extension/Services/KeystoreOperations.cs
new file mode 100644
index 0000000..b977655
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Services/KeystoreOperations.cs
@@ -0,0 +1,121 @@
+// 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 System;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Services;
+
+///
+/// Result of parsing an alias that may contain a field name prefix.
+///
+/// The K8S secret field name (e.g., "mystore.jks").
+/// The actual entry alias within the keystore.
+public record AliasParseResult(string FieldName, string Alias);
+
+///
+/// Provides common operations for JKS and PKCS12 keystore handling.
+/// Eliminates duplication between HandleJksSecret and HandlePkcs12Secret methods.
+///
+public interface IKeystoreOperations
+{
+ ///
+ /// Parses an alias that may contain a field name prefix (e.g., "mystore.jks/myalias").
+ ///
+ /// The alias to parse.
+ /// The default field name to use if not specified in alias.
+ /// Tuple containing the field name and the actual alias.
+ AliasParseResult ParseAliasAndFieldName(string alias, string defaultFieldName);
+
+ ///
+ /// Extracts the StoreFileName property from a JSON properties string.
+ ///
+ /// The JSON string containing store properties.
+ /// The default file name to use if not found.
+ /// The extracted store file name, or the default.
+ string ExtractStoreFileNameFromProperties(string propertiesJson, string defaultFileName);
+}
+
+///
+/// Implementation of keystore operations for JKS and PKCS12 stores.
+///
+public class KeystoreOperations : IKeystoreOperations
+{
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of KeystoreOperations.
+ ///
+ /// Logger instance for diagnostic output.
+ public KeystoreOperations(ILogger logger)
+ {
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ public AliasParseResult ParseAliasAndFieldName(string alias, string defaultFieldName)
+ {
+ if (string.IsNullOrEmpty(alias))
+ {
+ _logger.LogDebug("Alias is null or empty, using default field name: {DefaultFieldName}", defaultFieldName);
+ return new AliasParseResult(defaultFieldName, "default");
+ }
+
+ // Check if alias contains '/' - indicates pattern is 'field-name/alias'
+ if (alias.Contains('/'))
+ {
+ _logger.LogDebug("Alias contains '/', splitting to extract field name and alias");
+ var parts = alias.Split('/');
+
+ if (parts.Length >= 2)
+ {
+ var fieldName = parts[0];
+ var actualAlias = parts[1];
+
+ _logger.LogDebug("Extracted field name: {FieldName}, alias: {Alias}", fieldName, actualAlias);
+ return new AliasParseResult(fieldName, actualAlias);
+ }
+ }
+
+ _logger.LogDebug("Using default field name: {DefaultFieldName}, alias: {Alias}", defaultFieldName, alias);
+ return new AliasParseResult(defaultFieldName, alias);
+ }
+
+ ///
+ public string ExtractStoreFileNameFromProperties(string propertiesJson, string defaultFileName)
+ {
+ if (string.IsNullOrEmpty(propertiesJson))
+ {
+ _logger.LogDebug("Properties JSON is null or empty, using default: {DefaultFileName}", defaultFileName);
+ return defaultFileName;
+ }
+
+ try
+ {
+ using var jsonDoc = System.Text.Json.JsonDocument.Parse(propertiesJson);
+
+ if (jsonDoc.RootElement.TryGetProperty("StoreFileName", out var storeFileNameElement))
+ {
+ var value = storeFileNameElement.GetString();
+ if (!string.IsNullOrEmpty(value))
+ {
+ _logger.LogDebug("Found StoreFileName in properties: {StoreFileName}", value);
+ return value;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning("Error parsing StoreFileName from Properties: {Message}. Using default '{DefaultFileName}'",
+ ex.Message, defaultFileName);
+ }
+
+ _logger.LogDebug("StoreFileName not found in properties, using default: {DefaultFileName}", defaultFileName);
+ return defaultFileName;
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Services/PasswordResolver.cs b/kubernetes-orchestrator-extension/Services/PasswordResolver.cs
new file mode 100644
index 0000000..3920294
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Services/PasswordResolver.cs
@@ -0,0 +1,163 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Text;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Extensions.Orchestrator.K8S.Utilities;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Services;
+
+///
+/// Result of password resolution containing both byte array and string forms.
+///
+public record PasswordResult(byte[] Bytes, string Value);
+
+///
+/// Resolves keystore passwords from various sources (K8S secrets, direct values, or defaults).
+/// Centralizes the password resolution logic used across PKCS12 and JKS operations.
+///
+public class PasswordResolver
+{
+ private readonly ILogger _logger;
+
+ ///
+ /// Delegate for reading a "buddy" secret (a secret in a different namespace containing the password).
+ ///
+ public delegate k8s.Models.V1Secret BuddySecretReader(string secretName, string namespaceName);
+
+ ///
+ /// Initializes a new instance of the PasswordResolver.
+ ///
+ /// Logger instance for diagnostic output.
+ public PasswordResolver(ILogger logger)
+ {
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ /// Resolves the store password from the job certificate configuration.
+ /// Supports three sources:
+ /// 1. K8S secret (same secret or "buddy" secret in different namespace)
+ /// 2. Direct password from job configuration
+ /// 3. Default password
+ ///
+ /// Job certificate containing password configuration.
+ /// Default password to use if no other source is available.
+ /// Data from the existing K8S secret (for same-secret passwords).
+ /// Name of the field containing the password.
+ /// Function to read a buddy secret from a different namespace.
+ /// PasswordResult containing the resolved password as bytes and string.
+ public PasswordResult ResolveStorePassword(
+ K8SJobCertificate jobCertificate,
+ string defaultPassword,
+ IDictionary existingSecretData = null,
+ string passwordFieldName = "password",
+ BuddySecretReader buddySecretReader = null)
+ {
+ _logger.LogDebug("Resolving store password");
+
+ byte[] passwordBytes;
+ string passwordString;
+
+ if (jobCertificate.PasswordIsK8SSecret)
+ {
+ (passwordBytes, passwordString) = ResolveFromK8sSecret(
+ jobCertificate,
+ existingSecretData,
+ passwordFieldName,
+ buddySecretReader);
+ }
+ else if (!string.IsNullOrEmpty(jobCertificate.StorePassword))
+ {
+ _logger.LogDebug("Using password from job configuration");
+ passwordBytes = Encoding.UTF8.GetBytes(jobCertificate.StorePassword);
+ passwordString = jobCertificate.StorePassword;
+ }
+ else
+ {
+ _logger.LogDebug("Using default store password");
+ passwordBytes = Encoding.UTF8.GetBytes(defaultPassword ?? "");
+ passwordString = defaultPassword ?? "";
+ }
+
+ // Trim trailing newlines (common issue with kubectl-created secrets)
+ passwordString = passwordString.TrimEnd('\r', '\n');
+ passwordBytes = Encoding.UTF8.GetBytes(passwordString);
+
+ _logger.LogTrace("Password: {Password}", LoggingUtilities.RedactPassword(passwordString));
+ _logger.LogTrace("Password correlation: {CorrelationId}", LoggingUtilities.GetPasswordCorrelationId(passwordString));
+
+ return new PasswordResult(passwordBytes, passwordString);
+ }
+
+ ///
+ /// Resolves password from a K8S secret, either from the same secret or a buddy secret.
+ ///
+ private (byte[] bytes, string value) ResolveFromK8sSecret(
+ K8SJobCertificate jobCertificate,
+ IDictionary existingSecretData,
+ string passwordFieldName,
+ BuddySecretReader buddySecretReader)
+ {
+ byte[] passwordBytes;
+
+ if (!string.IsNullOrEmpty(jobCertificate.StorePasswordPath))
+ {
+ // Password is in a separate "buddy" secret
+ _logger.LogDebug("Password is stored in K8S secret at path: {Path}", jobCertificate.StorePasswordPath);
+
+ var passwordPath = jobCertificate.StorePasswordPath.Split("/");
+ if (passwordPath.Length < 2)
+ {
+ throw new InvalidOperationException(
+ $"Invalid StorePasswordPath format: '{jobCertificate.StorePasswordPath}'. Expected format: 'namespace/secretname' or 'secretname/namespace'");
+ }
+
+ var passwordNamespace = passwordPath.Length > 1 ? passwordPath[0] : "default";
+ var passwordSecretName = passwordPath.Length > 1 ? passwordPath[1] : passwordPath[0];
+
+ _logger.LogDebug("Buddy secret metadata - Name: {Name}, Namespace: {Namespace}, Field: {Field}",
+ passwordSecretName, passwordNamespace, passwordFieldName);
+
+ if (buddySecretReader == null)
+ {
+ throw new InvalidOperationException("BuddySecretReader is required when StorePasswordPath is specified");
+ }
+
+ var buddySecret = buddySecretReader(passwordSecretName, passwordNamespace);
+ _logger.LogTrace("Buddy secret: {Summary}", LoggingUtilities.GetSecretSummary(buddySecret));
+
+ if (buddySecret?.Data == null || !buddySecret.Data.ContainsKey(passwordFieldName))
+ {
+ throw new InvalidOperationException(
+ $"Password field '{passwordFieldName}' not found in buddy secret '{passwordSecretName}'");
+ }
+
+ passwordBytes = buddySecret.Data[passwordFieldName];
+ }
+ else
+ {
+ // Password is in the same secret
+ _logger.LogDebug("Password is stored in same secret, field: {Field}", passwordFieldName);
+
+ if (existingSecretData == null || !existingSecretData.ContainsKey(passwordFieldName))
+ {
+ throw new InvalidOperationException(
+ $"Password field '{passwordFieldName}' not found in existing secret data");
+ }
+
+ passwordBytes = existingSecretData[passwordFieldName];
+ }
+
+ var passwordString = Encoding.UTF8.GetString(passwordBytes);
+ return (passwordBytes, passwordString);
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Services/StoreConfigurationParser.cs b/kubernetes-orchestrator-extension/Services/StoreConfigurationParser.cs
new file mode 100644
index 0000000..1bc0472
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Services/StoreConfigurationParser.cs
@@ -0,0 +1,292 @@
+// 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 System;
+using System.Collections.Generic;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Services;
+
+///
+/// Configuration data extracted from store properties.
+/// Contains all settings needed to configure a Kubernetes certificate store.
+///
+public class StoreConfiguration
+{
+ /// Kubernetes namespace where the secret resides.
+ public string KubeNamespace { get; set; } = "";
+
+ /// Name of the Kubernetes secret.
+ public string KubeSecretName { get; set; } = "";
+
+ /// Type of secret (tls, opaque, jks, pkcs12, etc.).
+ public string KubeSecretType { get; set; } = "";
+
+ /// Kubeconfig JSON for API authentication.
+ public string KubeSvcCreds { get; set; } = "";
+
+ /// Whether the keystore password is stored in a separate K8S secret.
+ public bool PasswordIsSeparateSecret { get; set; }
+
+ /// Field name in the secret containing the password.
+ public string PasswordFieldName { get; set; } = "password";
+
+ /// Path to a separate K8S secret containing the store password.
+ public string StorePasswordPath { get; set; } = "";
+
+ /// Field name in the secret containing the certificate/keystore data.
+ public string CertificateDataFieldName { get; set; } = "";
+
+ /// Whether the password is stored as a K8S secret (vs inline).
+ public bool PasswordIsK8SSecret { get; set; }
+
+ /// The K8S secret password value.
+ public object KubeSecretPassword { get; set; }
+
+ /// Whether to store the certificate chain in a separate field.
+ public bool SeparateChain { get; set; }
+
+ /// Whether to include the full certificate chain.
+ public bool IncludeCertChain { get; set; } = true;
+}
+
+///
+/// Parses store properties from job configuration into a StoreConfiguration object.
+/// Provides helper methods for safely extracting values with defaults.
+///
+public class StoreConfigurationParser
+{
+ private readonly ILogger _logger;
+
+ // Default field names
+ private const string DefaultPasswordFieldName = "password";
+ private const string DefaultPfxFieldName = "pfx";
+ private const string DefaultJksFieldName = "jks";
+
+ ///
+ /// Initializes a new instance of the StoreConfigurationParser.
+ ///
+ /// Logger instance for diagnostic output.
+ public StoreConfigurationParser(ILogger logger)
+ {
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ /// Gets a property value from a dynamic properties object, with a default fallback.
+ ///
+ /// The expected type of the property value.
+ /// The dynamic properties object.
+ /// The property key to look up.
+ /// The default value if key is not found.
+ /// The property value, or the default if not found.
+ public T GetPropertyOrDefault(IDictionary properties, string key, T defaultValue)
+ {
+ if (properties == null)
+ {
+ _logger.LogDebug("Properties object is null, using default for {Key}", key);
+ return defaultValue;
+ }
+
+ try
+ {
+ if (properties.ContainsKey(key))
+ {
+ var value = properties[key];
+ if (value == null)
+ {
+ _logger.LogDebug("{Key} is null, using default", key);
+ return defaultValue;
+ }
+
+ // Handle string to bool conversion
+ if (typeof(T) == typeof(bool) && value is string strValue)
+ {
+ if (bool.TryParse(strValue, out var boolResult))
+ {
+ return (T)(object)boolResult;
+ }
+ _logger.LogDebug("Could not parse {Key} as bool, using default", key);
+ return defaultValue;
+ }
+
+ // Handle string to string (with trim)
+ if (typeof(T) == typeof(string))
+ {
+ return (T)(object)(value?.ToString()?.Trim() ?? defaultValue?.ToString());
+ }
+
+ return (T)value;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug("Error reading {Key}: {Error}, using default", key, ex.Message);
+ }
+
+ _logger.LogDebug("{Key} not found in store properties, using default", key);
+ return defaultValue;
+ }
+
+ ///
+ /// Parses the store properties into a StoreConfiguration object.
+ ///
+ /// Dynamic dictionary of store properties.
+ /// The store capability string for deriving secret type.
+ /// A populated StoreConfiguration object.
+ public StoreConfiguration Parse(IDictionary storeProperties, string capability = null)
+ {
+ _logger.LogDebug("Parsing store configuration");
+
+ var config = new StoreConfiguration
+ {
+ KubeNamespace = GetPropertyOrDefault(storeProperties, "KubeNamespace", ""),
+ KubeSecretName = GetPropertyOrDefault(storeProperties, "KubeSecretName", ""),
+ KubeSvcCreds = GetPropertyOrDefault(storeProperties, "KubeSvcCreds", null),
+ PasswordIsSeparateSecret = GetPropertyOrDefault(storeProperties, "PasswordIsSeparateSecret", false),
+ PasswordFieldName = GetPropertyOrDefault(storeProperties, "PasswordFieldName", DefaultPasswordFieldName),
+ StorePasswordPath = GetPropertyOrDefault(storeProperties, "StorePasswordPath", ""),
+ CertificateDataFieldName = GetPropertyOrDefault(storeProperties, "KubeSecretKey", ""),
+ PasswordIsK8SSecret = GetPropertyOrDefault(storeProperties, "PasswordIsK8SSecret", false),
+ KubeSecretPassword = GetPropertyOrDefault(storeProperties, "KubeSecretPassword", null),
+ SeparateChain = GetPropertyOrDefault(storeProperties, "SeparateChain", false),
+ IncludeCertChain = GetPropertyOrDefault(storeProperties, "IncludeCertChain", true)
+ };
+
+ // Derive secret type from capability if available
+ if (!string.IsNullOrEmpty(capability))
+ {
+ config.KubeSecretType = DeriveSecretTypeFromCapability(capability);
+ _logger.LogTrace("Derived KubeSecretType from Capability: {Type}", config.KubeSecretType);
+ }
+
+ // Fall back to property if capability didn't provide a type
+ if (string.IsNullOrEmpty(config.KubeSecretType))
+ {
+ var propertyType = GetPropertyOrDefault(storeProperties, "KubeSecretType", null);
+ if (!string.IsNullOrEmpty(propertyType))
+ {
+ _logger.LogWarning(
+ "DEPRECATION WARNING: The 'KubeSecretType' store property is deprecated. " +
+ "The secret type should be derived from the Capability.");
+ config.KubeSecretType = propertyType;
+ }
+ }
+
+ // Validate conflicting configuration
+ if (config.SeparateChain && !config.IncludeCertChain)
+ {
+ _logger.LogWarning(
+ "Invalid configuration: SeparateChain=true but IncludeCertChain=false. " +
+ "Cannot separate a certificate chain that is not being included. " +
+ "SeparateChain will be ignored.");
+ config.SeparateChain = false;
+ }
+
+ _logger.LogDebug("Parsed store configuration: Namespace={Namespace}, SecretName={SecretName}, Type={Type}",
+ config.KubeNamespace, config.KubeSecretName, config.KubeSecretType);
+
+ return config;
+ }
+
+ ///
+ /// Applies keystore-specific defaults based on secret type.
+ ///
+ /// The configuration to update.
+ /// The original store properties for additional lookups.
+ public void ApplyKeystoreDefaults(StoreConfiguration config, IDictionary storeProperties)
+ {
+ var secretType = config.KubeSecretType?.ToLower();
+
+ switch (secretType)
+ {
+ case "pfx":
+ case "p12":
+ case "pkcs12":
+ _logger.LogDebug("Applying PKCS12 defaults");
+ if (string.IsNullOrEmpty(config.PasswordFieldName))
+ config.PasswordFieldName = DefaultPasswordFieldName;
+ if (string.IsNullOrEmpty(config.CertificateDataFieldName))
+ config.CertificateDataFieldName = DefaultPfxFieldName;
+
+ // Re-parse PKCS12-specific properties
+ config.PasswordIsSeparateSecret = GetPropertyOrDefault(storeProperties, "PasswordIsSeparateSecret", false);
+ config.StorePasswordPath = GetPropertyOrDefault(storeProperties, "StorePasswordPath", "");
+ config.PasswordIsK8SSecret = GetPropertyOrDefault(storeProperties, "PasswordIsK8SSecret", false);
+ config.KubeSecretPassword = GetPropertyOrDefault(storeProperties, "KubeSecretPassword", null);
+ config.CertificateDataFieldName = GetPropertyOrDefault(storeProperties, "CertificateDataFieldName", DefaultPfxFieldName);
+ break;
+
+ case "jks":
+ _logger.LogDebug("Applying JKS defaults");
+ if (string.IsNullOrEmpty(config.PasswordFieldName))
+ config.PasswordFieldName = DefaultPasswordFieldName;
+ if (string.IsNullOrEmpty(config.CertificateDataFieldName))
+ config.CertificateDataFieldName = DefaultJksFieldName;
+
+ // Re-parse JKS-specific properties with proper bool parsing
+ config.PasswordFieldName = GetPropertyOrDefault(storeProperties, "PasswordFieldName", DefaultPasswordFieldName);
+ config.PasswordIsSeparateSecret = ParseBoolProperty(storeProperties, "PasswordIsSeparateSecret", false);
+ config.StorePasswordPath = GetPropertyOrDefault(storeProperties, "StorePasswordPath", "");
+ config.PasswordIsK8SSecret = ParseBoolProperty(storeProperties, "PasswordIsK8SSecret", false);
+ config.KubeSecretPassword = GetPropertyOrDefault(storeProperties, "KubeSecretPassword", null);
+ config.CertificateDataFieldName = GetPropertyOrDefault(storeProperties, "CertificateDataFieldName", DefaultJksFieldName);
+ break;
+ }
+ }
+
+ ///
+ /// Parses a boolean property with proper string handling.
+ ///
+ private bool ParseBoolProperty(IDictionary properties, string key, bool defaultValue)
+ {
+ if (properties == null) return defaultValue;
+
+ try
+ {
+ if (!properties.ContainsKey(key)) return defaultValue;
+
+ var value = properties[key];
+ if (value == null || string.IsNullOrEmpty(value?.ToString()))
+ return defaultValue;
+
+ return bool.TryParse(value.ToString(), out bool result) ? result : defaultValue;
+ }
+ catch
+ {
+ return defaultValue;
+ }
+ }
+
+ ///
+ /// Derives the secret type from the capability string.
+ ///
+ private static string DeriveSecretTypeFromCapability(string capability)
+ {
+ if (string.IsNullOrEmpty(capability))
+ return null;
+
+ // Order matters - check more specific patterns first
+ if (capability.Contains("K8STLSSecr", StringComparison.OrdinalIgnoreCase))
+ return "tls_secret";
+ if (capability.Contains("K8SSecret", StringComparison.OrdinalIgnoreCase))
+ return "secret";
+ if (capability.Contains("K8SJKS", StringComparison.OrdinalIgnoreCase))
+ return "jks";
+ if (capability.Contains("K8SPKCS12", StringComparison.OrdinalIgnoreCase))
+ return "pkcs12";
+ if (capability.Contains("K8SCluster", StringComparison.OrdinalIgnoreCase))
+ return "cluster";
+ if (capability.Contains("K8SNS", StringComparison.OrdinalIgnoreCase))
+ return "namespace";
+ if (capability.Contains("K8SCert", StringComparison.OrdinalIgnoreCase))
+ return "certificate";
+
+ return null;
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Services/StorePathResolver.cs b/kubernetes-orchestrator-extension/Services/StorePathResolver.cs
new file mode 100644
index 0000000..17c136d
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Services/StorePathResolver.cs
@@ -0,0 +1,441 @@
+// 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 System;
+using System.Text.RegularExpressions;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Services;
+
+///
+/// Result of store path resolution containing namespace, secret name, and any warnings.
+///
+public record PathResolutionResult
+{
+ /// The resolved Kubernetes namespace.
+ public string Namespace { get; init; } = "";
+
+ /// The resolved Kubernetes secret name.
+ public string SecretName { get; init; } = "";
+
+ /// Whether the resolution was successful.
+ public bool Success { get; init; } = true;
+
+ /// Warning message if any path components were ignored or re-interpreted.
+ public string Warning { get; init; }
+}
+
+///
+/// Resolves store paths into Kubernetes namespace and secret name components.
+/// Handles various path formats based on store type (Cluster, Namespace, or individual secret).
+///
+///
+/// Supported path formats:
+/// - 1 part: secret_name (for regular stores), namespace_name (for K8SNS), cluster_name (for K8SCluster)
+/// - 2 parts: namespace/secret (for regular), cluster/namespace (for K8SNS)
+/// - 3 parts: cluster/namespace/secret or namespace/type/secret
+/// - 4 parts: cluster/namespace/type/secret
+///
+public class StorePathResolver
+{
+ private readonly ILogger _logger;
+ private static readonly string[] ReservedKeywords = { "secret", "secrets", "tls", "certificate", "namespace" };
+
+ ///
+ /// Initializes a new instance of StorePathResolver.
+ ///
+ /// Logger instance for diagnostic output.
+ public StorePathResolver(ILogger logger = null)
+ {
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ /// Resolves a store path into namespace and secret name components.
+ ///
+ /// The store path to resolve.
+ /// The capability string indicating store type (e.g., "K8SNS", "K8SCluster").
+ /// Current namespace value (may be overridden by path).
+ /// Current secret name value (may be overridden by path).
+ /// PathResolutionResult containing the resolved components.
+ public PathResolutionResult Resolve(
+ string storePath,
+ string capability,
+ string currentNamespace,
+ string currentSecretName)
+ {
+ _logger.LogDebug("Resolving store path: {StorePath}", storePath);
+
+ if (string.IsNullOrEmpty(storePath))
+ {
+ _logger.LogDebug("Store path is empty, using current values");
+ return new PathResolutionResult
+ {
+ Namespace = currentNamespace,
+ SecretName = currentSecretName
+ };
+ }
+
+ var parts = storePath.Split('/');
+ _logger.LogTrace("Store path has {Count} parts", parts.Length);
+
+ var isNamespaceStore = IsNamespaceStore(capability);
+ var isClusterStore = IsClusterStore(capability);
+
+ return parts.Length switch
+ {
+ 1 => ResolveSinglePart(parts[0], isNamespaceStore, isClusterStore, currentNamespace, currentSecretName),
+ 2 => ResolveTwoPart(parts, isNamespaceStore, isClusterStore, currentNamespace, currentSecretName, storePath),
+ 3 => ResolveThreePart(parts, isNamespaceStore, isClusterStore, currentNamespace, currentSecretName, storePath),
+ 4 => ResolveFourPart(parts, isNamespaceStore, isClusterStore, currentNamespace, currentSecretName, storePath),
+ _ => ResolveMultiPart(parts, currentNamespace, currentSecretName, storePath)
+ };
+ }
+
+ ///
+ /// Resolves a single-part path (just a name).
+ ///
+ private PathResolutionResult ResolveSinglePart(
+ string part,
+ bool isNamespaceStore,
+ bool isClusterStore,
+ string currentNamespace,
+ string currentSecretName)
+ {
+ if (isNamespaceStore)
+ {
+ // For K8SNS, single part is the namespace name
+ var ns = string.IsNullOrEmpty(currentNamespace) ? part : currentNamespace;
+ if (!string.IsNullOrEmpty(currentNamespace) && currentNamespace != part)
+ {
+ _logger.LogInformation(
+ "K8SNS store: KubeNamespace already set to {Current}, ignoring StorePath value {Path}",
+ currentNamespace, part);
+ }
+ else if (string.IsNullOrEmpty(currentNamespace))
+ {
+ _logger.LogInformation("K8SNS store: Setting KubeNamespace to {Namespace}", part);
+ }
+
+ ValidateK8SName("namespace", ns);
+ return new PathResolutionResult
+ {
+ Namespace = ns,
+ SecretName = "" // Namespace stores don't have a secret name
+ };
+ }
+
+ if (isClusterStore)
+ {
+ // For K8SCluster, single part is cluster name - namespace and secret should be empty
+ var warning = "";
+ if (!string.IsNullOrEmpty(currentSecretName))
+ {
+ warning = "KubeSecretName is not valid for K8SCluster and was cleared";
+ }
+ if (!string.IsNullOrEmpty(currentNamespace))
+ {
+ warning += string.IsNullOrEmpty(warning) ? "" : "; ";
+ warning += "KubeNamespace is not valid for K8SCluster and was cleared";
+ }
+
+ _logger.LogInformation("K8SCluster store: Path is cluster name, clearing namespace and secret name");
+ return new PathResolutionResult
+ {
+ Namespace = "",
+ SecretName = "",
+ Warning = string.IsNullOrEmpty(warning) ? null : warning
+ };
+ }
+
+ // Regular store - single part is the secret name
+ var secretName = string.IsNullOrEmpty(currentSecretName) ? part : currentSecretName;
+ if (!string.IsNullOrEmpty(currentSecretName))
+ {
+ _logger.LogInformation(
+ "Single-part path but KubeSecretName already set, ignoring StorePath value {Path}", part);
+ }
+ else
+ {
+ _logger.LogInformation("Single-part path: Setting KubeSecretName to {SecretName}", part);
+ }
+
+ ValidateK8SName("namespace", currentNamespace);
+ ValidateK8SName("secret", secretName);
+ return new PathResolutionResult
+ {
+ Namespace = currentNamespace,
+ SecretName = secretName
+ };
+ }
+
+ ///
+ /// Resolves a two-part path (e.g., namespace/secret).
+ ///
+ private PathResolutionResult ResolveTwoPart(
+ string[] parts,
+ bool isNamespaceStore,
+ bool isClusterStore,
+ string currentNamespace,
+ string currentSecretName,
+ string storePath)
+ {
+ if (isClusterStore)
+ {
+ _logger.LogWarning(
+ "Two-part path is not valid for K8SCluster store type, ignoring: {StorePath}", storePath);
+ return new PathResolutionResult
+ {
+ Namespace = currentNamespace,
+ SecretName = currentSecretName,
+ Warning = "Two-part path not valid for K8SCluster"
+ };
+ }
+
+ if (isNamespaceStore)
+ {
+ // For K8SNS: cluster/namespace or namespace-prefix/namespace
+ var ns = string.IsNullOrEmpty(currentNamespace) ? parts[1] : currentNamespace;
+ if (!string.IsNullOrEmpty(currentNamespace))
+ {
+ _logger.LogInformation(
+ "K8SNS store: KubeNamespace already set, ignoring StorePath value {StorePath}", storePath);
+ }
+ else
+ {
+ _logger.LogInformation("K8SNS store: Setting KubeNamespace to {Namespace}", parts[1]);
+ }
+
+ ValidateK8SName("namespace", ns);
+ return new PathResolutionResult
+ {
+ Namespace = ns,
+ SecretName = ""
+ };
+ }
+
+ // Regular store: namespace/secret
+ _logger.LogInformation(
+ "Two-part path: Interpreting as namespace/secret pattern");
+
+ var resolvedNs = string.IsNullOrEmpty(currentNamespace) ? parts[0] : currentNamespace;
+ var resolvedSecret = string.IsNullOrEmpty(currentSecretName) ? parts[1] : currentSecretName;
+
+ if (string.IsNullOrEmpty(currentNamespace))
+ {
+ _logger.LogInformation("Setting KubeNamespace to {Namespace}", parts[0]);
+ }
+ if (string.IsNullOrEmpty(currentSecretName))
+ {
+ _logger.LogInformation("Setting KubeSecretName to {SecretName}", parts[1]);
+ }
+
+ ValidateK8SName("namespace", resolvedNs);
+ ValidateK8SName("secret", resolvedSecret);
+ return new PathResolutionResult
+ {
+ Namespace = resolvedNs,
+ SecretName = resolvedSecret
+ };
+ }
+
+ ///
+ /// Resolves a three-part path (e.g., cluster/namespace/secret or namespace/type/secret).
+ ///
+ private PathResolutionResult ResolveThreePart(
+ string[] parts,
+ bool isNamespaceStore,
+ bool isClusterStore,
+ string currentNamespace,
+ string currentSecretName,
+ string storePath)
+ {
+ if (isClusterStore)
+ {
+ _logger.LogError(
+ "Three-part path is not valid for K8SCluster store type, ignoring: {StorePath}", storePath);
+ return new PathResolutionResult
+ {
+ Namespace = currentNamespace,
+ SecretName = currentSecretName,
+ Success = false,
+ Warning = "Three-part path not valid for K8SCluster"
+ };
+ }
+
+ if (isNamespaceStore)
+ {
+ // For K8SNS: cluster/namespace/namespace-name pattern
+ var ns = string.IsNullOrEmpty(currentNamespace) ? parts[2] : currentNamespace;
+ var warning = !string.IsNullOrEmpty(currentSecretName)
+ ? "KubeSecretName is not supported for K8SNS store type and was cleared"
+ : null;
+
+ if (!string.IsNullOrEmpty(currentNamespace))
+ {
+ _logger.LogInformation(
+ "K8SNS store: KubeNamespace already set, ignoring StorePath value {StorePath}", storePath);
+ }
+ else
+ {
+ _logger.LogInformation("K8SNS store: Setting KubeNamespace to {Namespace}", parts[2]);
+ }
+
+ ValidateK8SName("namespace", ns);
+ return new PathResolutionResult
+ {
+ Namespace = ns,
+ SecretName = "",
+ Warning = warning
+ };
+ }
+
+ // Regular store: cluster/namespace/secret or namespace/type/secret
+ _logger.LogInformation(
+ "Three-part path: Interpreting as cluster/namespace/secret pattern");
+
+ var kN = parts[1];
+ var kS = parts[2];
+
+ // Check if middle part is a reserved keyword (namespace/type/secret pattern)
+ if (IsReservedKeyword(parts[1]))
+ {
+ _logger.LogInformation(
+ "Middle part '{Keyword}' is a reserved keyword, re-interpreting as namespace/type/secret pattern",
+ parts[1]);
+ kN = parts[0]; // First part is actually the namespace
+ kS = parts[2]; // Third part is still the secret name
+ }
+
+ var resolvedNs = string.IsNullOrEmpty(currentNamespace) ? kN : currentNamespace;
+ var resolvedSecret = string.IsNullOrEmpty(currentSecretName) ? kS : currentSecretName;
+
+ ValidateK8SName("namespace", resolvedNs);
+ ValidateK8SName("secret", resolvedSecret);
+ return new PathResolutionResult
+ {
+ Namespace = resolvedNs,
+ SecretName = resolvedSecret
+ };
+ }
+
+ ///
+ /// Resolves a four-part path (cluster/namespace/type/secret).
+ ///
+ private PathResolutionResult ResolveFourPart(
+ string[] parts,
+ bool isNamespaceStore,
+ bool isClusterStore,
+ string currentNamespace,
+ string currentSecretName,
+ string storePath)
+ {
+ if (isClusterStore || isNamespaceStore)
+ {
+ _logger.LogError(
+ "Four-part path is not valid for {StoreType} store type: {StorePath}",
+ isClusterStore ? "K8SCluster" : "K8SNS", storePath);
+ return new PathResolutionResult
+ {
+ Namespace = currentNamespace,
+ SecretName = currentSecretName,
+ Success = false,
+ Warning = $"Four-part path not valid for {(isClusterStore ? "K8SCluster" : "K8SNS")}"
+ };
+ }
+
+ // Regular store: cluster/namespace/type/secret
+ _logger.LogTrace(
+ "Four-part path: Interpreting as cluster/namespace/type/secret pattern");
+
+ var resolvedNs = string.IsNullOrEmpty(currentNamespace) ? parts[1] : currentNamespace;
+ var resolvedSecret = string.IsNullOrEmpty(currentSecretName) ? parts[3] : currentSecretName;
+
+ if (string.IsNullOrEmpty(currentNamespace))
+ {
+ _logger.LogTrace("Setting KubeNamespace to {Namespace}", parts[1]);
+ }
+ if (string.IsNullOrEmpty(currentSecretName))
+ {
+ _logger.LogTrace("Setting KubeSecretName to {SecretName}", parts[3]);
+ }
+
+ ValidateK8SName("namespace", resolvedNs);
+ ValidateK8SName("secret", resolvedSecret);
+ return new PathResolutionResult
+ {
+ Namespace = resolvedNs,
+ SecretName = resolvedSecret
+ };
+ }
+
+ ///
+ /// Resolves paths with more than 4 parts (fallback).
+ ///
+ private PathResolutionResult ResolveMultiPart(
+ string[] parts,
+ string currentNamespace,
+ string currentSecretName,
+ string storePath)
+ {
+ _logger.LogWarning(
+ "Unable to resolve store path with {PartCount} parts: {StorePath}. Using first part as namespace and last as secret name",
+ parts.Length, storePath);
+
+ var multiNs = string.IsNullOrEmpty(currentNamespace) ? parts[0] : currentNamespace;
+ var multiSecret = string.IsNullOrEmpty(currentSecretName) ? parts[^1] : currentSecretName;
+ ValidateK8SName("namespace", multiNs);
+ ValidateK8SName("secret", multiSecret);
+ return new PathResolutionResult
+ {
+ Namespace = multiNs,
+ SecretName = multiSecret,
+ Warning = $"Path has {parts.Length} parts; using first as namespace and last as secret name"
+ };
+ }
+
+ private static readonly Regex K8SNamePattern = new(@"^[a-z0-9][a-z0-9\-.]{0,252}$", RegexOptions.Compiled);
+
+ private void ValidateK8SName(string label, string value)
+ {
+ if (!string.IsNullOrEmpty(value) && value != "*" && !K8SNamePattern.IsMatch(value))
+ _logger.LogWarning(
+ "Kubernetes {Label} name '{Value}' does not conform to DNS subdomain rules " +
+ "(must match [a-z0-9][a-z0-9-.], max 253 chars). Proceeding for backwards compatibility — " +
+ "the Kubernetes API will reject this if the name is truly invalid",
+ label, value);
+ }
+
+ ///
+ /// Checks if a string segment is a reserved keyword.
+ ///
+ private static bool IsReservedKeyword(string segment)
+ {
+ if (string.IsNullOrEmpty(segment)) return false;
+ var lower = segment.ToLowerInvariant();
+ return Array.Exists(ReservedKeywords, k => k == lower);
+ }
+
+ ///
+ /// Determines if the capability indicates a namespace-level store.
+ ///
+ private static bool IsNamespaceStore(string capability)
+ {
+ return !string.IsNullOrEmpty(capability) &&
+ capability.Contains("K8SNS", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Determines if the capability indicates a cluster-level store.
+ ///
+ private static bool IsClusterStore(string capability)
+ {
+ return !string.IsNullOrEmpty(capability) &&
+ capability.Contains("K8SCluster", StringComparison.OrdinalIgnoreCase);
+ }
+}
From cfc4233036514eb66c5c87ad8ad4e5e662b2adbd Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Wed, 15 Apr 2026 12:34:48 -0700
Subject: [PATCH 02/28] refactor: introduce handler strategy pattern for secret
operations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace inline switch/if chains in JobBase with a proper Strategy pattern:
- ISecretHandler: contract for Inventory, Management, Discovery, and
Reenrollment operations on a specific secret/store type
- SecretHandlerBase: shared infrastructure (client, logging, result helpers)
- SecretHandlerFactory: creates the correct handler from SecretType enum
- Per-type handlers: TlsSecretHandler, OpaqueSecretHandler,
JksSecretHandler, Pkcs12SecretHandler, ClusterSecretHandler,
NamespaceSecretHandler, CertificateSecretHandler (read-only)
Supporting additions:
- SecretTypes enum: typed representation of Kubernetes secret types with
normalisation and IsTlsType/IsOpaqueType helpers
- K8SJobCertificate model: replaces ad-hoc certificate data passing
- Exceptions: StoreNotFoundException, InvalidK8SSecretException,
JkSisPkcs12Exception — typed errors replace bare Exception throws
- ICertificateStoreSerializer + JKS/PKCS12 serializer implementations
moved from StoreTypes/ to Serializers/ (interface renamed for clarity)
---
.../Enums/SecretTypes.cs | 199 ++++++++
.../Exceptions/InvalidK8SSecretException.cs | 30 ++
.../Exceptions/JkSisPkcs12Exception.cs | 31 ++
.../Exceptions/StoreNotFoundException.cs | 30 ++
.../Handlers/CertificateSecretHandler.cs | 272 +++++++++++
.../Handlers/ClusterSecretHandler.cs | 307 ++++++++++++
.../Handlers/ISecretHandler.cs | 162 +++++++
.../Handlers/JksSecretHandler.cs | 318 +++++++++++++
.../Handlers/NamespaceSecretHandler.cs | 295 ++++++++++++
.../Handlers/OpaqueSecretHandler.cs | 289 +++++++++++
.../Handlers/Pkcs12SecretHandler.cs | 339 +++++++++++++
.../Handlers/SecretHandlerBase.cs | 406 ++++++++++++++++
.../Handlers/SecretHandlerFactory.cs | 108 +++++
.../Handlers/TlsSecretHandler.cs | 289 +++++++++++
.../Models/K8SJobCertificate.cs | 110 +++++
.../StoreTypes/ICertificateStoreSerializer.cs | 46 --
.../StoreTypes/K8SJKS/Store.cs | 450 ------------------
.../StoreTypes/K8SPKCS12/Store.cs | 326 -------------
18 files changed, 3185 insertions(+), 822 deletions(-)
create mode 100644 kubernetes-orchestrator-extension/Enums/SecretTypes.cs
create mode 100644 kubernetes-orchestrator-extension/Exceptions/InvalidK8SSecretException.cs
create mode 100644 kubernetes-orchestrator-extension/Exceptions/JkSisPkcs12Exception.cs
create mode 100644 kubernetes-orchestrator-extension/Exceptions/StoreNotFoundException.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/CertificateSecretHandler.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/ClusterSecretHandler.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/ISecretHandler.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/JksSecretHandler.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/NamespaceSecretHandler.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/OpaqueSecretHandler.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/Pkcs12SecretHandler.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/SecretHandlerBase.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/SecretHandlerFactory.cs
create mode 100644 kubernetes-orchestrator-extension/Handlers/TlsSecretHandler.cs
create mode 100644 kubernetes-orchestrator-extension/Models/K8SJobCertificate.cs
delete mode 100644 kubernetes-orchestrator-extension/StoreTypes/ICertificateStoreSerializer.cs
delete mode 100644 kubernetes-orchestrator-extension/StoreTypes/K8SJKS/Store.cs
delete mode 100644 kubernetes-orchestrator-extension/StoreTypes/K8SPKCS12/Store.cs
diff --git a/kubernetes-orchestrator-extension/Enums/SecretTypes.cs b/kubernetes-orchestrator-extension/Enums/SecretTypes.cs
new file mode 100644
index 0000000..211355a
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Enums/SecretTypes.cs
@@ -0,0 +1,199 @@
+// 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 System;
+using System.Linq;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Enums;
+
+///
+/// Provides constants and helper methods for Kubernetes secret type detection and normalization.
+/// Centralizes all magic strings for secret types used throughout the codebase.
+///
+public static class SecretTypes
+{
+ ///
+ /// Normalized type constant for TLS secrets (kubernetes.io/tls).
+ ///
+ public const string Tls = "tls";
+
+ ///
+ /// Normalized type constant for Opaque secrets (generic secrets).
+ ///
+ public const string Opaque = "secret";
+
+ ///
+ /// Normalized type constant for Certificate Signing Requests.
+ ///
+ public const string Certificate = "certificate";
+
+ ///
+ /// Normalized type constant for PKCS12/PFX keystores.
+ ///
+ public const string Pkcs12 = "pkcs12";
+
+ ///
+ /// Normalized type constant for JKS keystores.
+ ///
+ public const string Jks = "jks";
+
+ ///
+ /// Normalized type constant for namespace-level store operations.
+ ///
+ public const string Namespace = "namespace";
+
+ ///
+ /// Normalized type constant for cluster-level store operations.
+ ///
+ public const string Cluster = "cluster";
+
+ ///
+ /// All variant strings that map to TLS secret type.
+ ///
+ public static readonly string[] TlsVariants = { "tls_secret", "tls", "tlssecret", "tls_secrets" };
+
+ ///
+ /// All variant strings that map to Opaque secret type.
+ ///
+ public static readonly string[] OpaqueVariants = { "opaque", "secret", "secrets" };
+
+ ///
+ /// All variant strings that map to Certificate/CSR type.
+ ///
+ public static readonly string[] CsrVariants = { "certificate", "cert", "csr", "csrs", "certs", "certificates" };
+
+ ///
+ /// All variant strings that map to PKCS12 keystore type.
+ ///
+ public static readonly string[] Pkcs12Variants = { "pfx", "pkcs12", "p12" };
+
+ ///
+ /// All variant strings that map to JKS keystore type.
+ ///
+ public static readonly string[] JksVariants = { "jks" };
+
+ ///
+ /// All variant strings that map to Namespace store type.
+ ///
+ public static readonly string[] NamespaceVariants = { "namespace", "ns" };
+
+ ///
+ /// All variant strings that map to Cluster store type.
+ ///
+ public static readonly string[] ClusterVariants = { "cluster", "k8scluster" };
+
+ ///
+ /// Determines if the given type string represents a TLS secret.
+ ///
+ /// The type string to check.
+ /// True if the type is a TLS variant; otherwise, false.
+ public static bool IsTlsType(string type) =>
+ !string.IsNullOrEmpty(type) && TlsVariants.Contains(type.ToLower());
+
+ ///
+ /// Determines if the given type string represents an Opaque secret.
+ ///
+ /// The type string to check.
+ /// True if the type is an Opaque variant; otherwise, false.
+ public static bool IsOpaqueType(string type) =>
+ !string.IsNullOrEmpty(type) && OpaqueVariants.Contains(type.ToLower());
+
+ ///
+ /// Determines if the given type string represents a Certificate/CSR.
+ ///
+ /// The type string to check.
+ /// True if the type is a CSR variant; otherwise, false.
+ public static bool IsCsrType(string type) =>
+ !string.IsNullOrEmpty(type) && CsrVariants.Contains(type.ToLower());
+
+ ///
+ /// Determines if the given type string represents a PKCS12 keystore.
+ ///
+ /// The type string to check.
+ /// True if the type is a PKCS12 variant; otherwise, false.
+ public static bool IsPkcs12Type(string type) =>
+ !string.IsNullOrEmpty(type) && Pkcs12Variants.Contains(type.ToLower());
+
+ ///
+ /// Determines if the given type string represents a JKS keystore.
+ ///
+ /// The type string to check.
+ /// True if the type is a JKS variant; otherwise, false.
+ public static bool IsJksType(string type) =>
+ !string.IsNullOrEmpty(type) && JksVariants.Contains(type.ToLower());
+
+ ///
+ /// Determines if the given type string represents a Namespace store.
+ ///
+ /// The type string to check.
+ /// True if the type is a Namespace variant; otherwise, false.
+ public static bool IsNamespaceType(string type) =>
+ !string.IsNullOrEmpty(type) && NamespaceVariants.Contains(type.ToLower());
+
+ ///
+ /// Determines if the given type string represents a Cluster store.
+ ///
+ /// The type string to check.
+ /// True if the type is a Cluster variant; otherwise, false.
+ public static bool IsClusterType(string type) =>
+ !string.IsNullOrEmpty(type) && ClusterVariants.Contains(type.ToLower());
+
+ ///
+ /// Normalizes a secret type string to its canonical form.
+ ///
+ /// The type string to normalize.
+ /// The normalized type constant, or the original type if not recognized.
+ public static string Normalize(string type)
+ {
+ if (string.IsNullOrEmpty(type))
+ return type;
+
+ var lowerType = type.ToLower();
+
+ // Check from most specific to least specific
+ if (JksVariants.Contains(lowerType))
+ return Jks;
+ if (Pkcs12Variants.Contains(lowerType))
+ return Pkcs12;
+ if (TlsVariants.Contains(lowerType))
+ return Tls;
+ if (OpaqueVariants.Contains(lowerType))
+ return Opaque;
+ if (CsrVariants.Contains(lowerType))
+ return Certificate;
+ if (NamespaceVariants.Contains(lowerType))
+ return Namespace;
+ if (ClusterVariants.Contains(lowerType))
+ return Cluster;
+
+ return type;
+ }
+
+ ///
+ /// Determines if the type represents a keystore format (JKS or PKCS12) that supports multiple entries.
+ ///
+ /// The type string to check.
+ /// True if the type is a keystore format; otherwise, false.
+ public static bool IsKeystoreType(string type) =>
+ IsJksType(type) || IsPkcs12Type(type);
+
+ ///
+ /// Determines if the type represents an aggregate store (namespace or cluster level).
+ ///
+ /// The type string to check.
+ /// True if the type is an aggregate store; otherwise, false.
+ public static bool IsAggregateStoreType(string type) =>
+ IsNamespaceType(type) || IsClusterType(type);
+
+ ///
+ /// Determines if the type represents a simple secret type (TLS or Opaque).
+ ///
+ /// The type string to check.
+ /// True if the type is a simple secret; otherwise, false.
+ public static bool IsSimpleSecretType(string type) =>
+ IsTlsType(type) || IsOpaqueType(type);
+}
diff --git a/kubernetes-orchestrator-extension/Exceptions/InvalidK8SSecretException.cs b/kubernetes-orchestrator-extension/Exceptions/InvalidK8SSecretException.cs
new file mode 100644
index 0000000..561f410
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Exceptions/InvalidK8SSecretException.cs
@@ -0,0 +1,30 @@
+// 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 System;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+
+///
+/// Exception thrown when a Kubernetes secret is invalid, malformed, or missing required fields.
+///
+public class InvalidK8SSecretException : Exception
+{
+ public InvalidK8SSecretException()
+ {
+ }
+
+ public InvalidK8SSecretException(string message)
+ : base(message)
+ {
+ }
+
+ public InvalidK8SSecretException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Exceptions/JkSisPkcs12Exception.cs b/kubernetes-orchestrator-extension/Exceptions/JkSisPkcs12Exception.cs
new file mode 100644
index 0000000..b464123
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Exceptions/JkSisPkcs12Exception.cs
@@ -0,0 +1,31 @@
+// 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 System;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+
+///
+/// Exception thrown when a JKS keystore contains PKCS12 data instead of proper JKS format,
+/// or vice versa (format mismatch between expected and actual store format).
+///
+public class JkSisPkcs12Exception : Exception
+{
+ public JkSisPkcs12Exception()
+ {
+ }
+
+ public JkSisPkcs12Exception(string message)
+ : base(message)
+ {
+ }
+
+ public JkSisPkcs12Exception(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Exceptions/StoreNotFoundException.cs b/kubernetes-orchestrator-extension/Exceptions/StoreNotFoundException.cs
new file mode 100644
index 0000000..aeb6c75
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Exceptions/StoreNotFoundException.cs
@@ -0,0 +1,30 @@
+// 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 System;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+
+///
+/// Exception thrown when a certificate store cannot be found in Kubernetes.
+///
+public class StoreNotFoundException : Exception
+{
+ public StoreNotFoundException()
+ {
+ }
+
+ public StoreNotFoundException(string message)
+ : base(message)
+ {
+ }
+
+ public StoreNotFoundException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/CertificateSecretHandler.cs b/kubernetes-orchestrator-extension/Handlers/CertificateSecretHandler.cs
new file mode 100644
index 0000000..fcb0b2a
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/CertificateSecretHandler.cs
@@ -0,0 +1,272 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Handler for Kubernetes Certificate Signing Requests (CSRs).
+/// This handler is READ-ONLY - CSRs cannot be created/modified through the orchestrator.
+///
+public class CertificateSecretHandler : SecretHandlerBase
+{
+ ///
+ /// Default allowed keys (not applicable to CSRs).
+ ///
+ private static readonly string[] DefaultAllowedKeys = Array.Empty();
+
+ ///
+ public override string[] AllowedKeys => DefaultAllowedKeys;
+
+ ///
+ public override string SecretTypeName => "certificate";
+
+ ///
+ public override bool SupportsManagement => false;
+
+ ///
+ /// Initializes a new instance of the CertificateSecretHandler.
+ ///
+ public CertificateSecretHandler(
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ : base(kubeClient, logger, context)
+ {
+ }
+
+ #region Inventory Operations
+
+ ///
+ public override List GetCertificates(long jobId)
+ {
+ LogMethodEntry(nameof(GetCertificates));
+
+ try
+ {
+ // Check if this is single CSR mode or cluster-wide mode
+ if (IsSingleCsrMode())
+ {
+ return GetSingleCsrCertificates(jobId);
+ }
+ else
+ {
+ return GetClusterWideCsrCertificates(jobId);
+ }
+ }
+ finally
+ {
+ LogMethodExit(nameof(GetCertificates));
+ }
+ }
+
+ ///
+ public override Dictionary> GetCertificatesWithAliases(long jobId)
+ {
+ LogMethodEntry(nameof(GetCertificatesWithAliases));
+
+ try
+ {
+ var result = new Dictionary>();
+
+ if (IsSingleCsrMode())
+ {
+ var certs = GetSingleCsrCertificates(jobId);
+ if (certs.Count > 0)
+ {
+ result[Context.KubeSecretName] = certs;
+ }
+ }
+ else
+ {
+ // Cluster-wide: list all CSRs
+ // ListAllCertificateSigningRequests returns Dictionary (name -> certPem)
+ var allCsrs = KubeClient.ListAllCertificateSigningRequests();
+ foreach (var kvp in allCsrs)
+ {
+ if (!string.IsNullOrEmpty(kvp.Value))
+ {
+ // Parse PEM chain and convert back to individual PEM strings
+ var certList = SplitPemChainToStrings(kvp.Value);
+ if (certList.Count > 0)
+ {
+ result[kvp.Key] = certList;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+ finally
+ {
+ LogMethodExit(nameof(GetCertificatesWithAliases));
+ }
+ }
+
+ ///
+ public override List GetInventoryEntries(long jobId)
+ {
+ var aliasedCerts = GetCertificatesWithAliases(jobId);
+ var entries = new List();
+
+ foreach (var kvp in aliasedCerts)
+ {
+ entries.Add(new InventoryEntry
+ {
+ Alias = kvp.Key,
+ Certificates = kvp.Value,
+ HasPrivateKey = false // CSRs never have private keys in the orchestrator
+ });
+ }
+
+ return entries;
+ }
+
+ ///
+ public override bool HasPrivateKey()
+ {
+ // CSRs never have private keys accessible through the orchestrator
+ return false;
+ }
+
+ #endregion
+
+ #region Management Operations (Not Supported)
+
+ ///
+ public override V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite)
+ {
+ throw new NotSupportedException(
+ "Management operations are not supported for Certificate Signing Requests. " +
+ "CSRs must be created and approved through Kubernetes directly.");
+ }
+
+ ///
+ public override V1Secret HandleRemove(string alias)
+ {
+ throw new NotSupportedException(
+ "Management operations are not supported for Certificate Signing Requests. " +
+ "CSRs must be deleted through Kubernetes directly.");
+ }
+
+ ///
+ public override V1Secret CreateEmptyStore()
+ {
+ throw new NotSupportedException(
+ "Certificate Signing Requests cannot be created as empty stores.");
+ }
+
+ #endregion
+
+ #region Discovery Operations
+
+ ///
+ public override List DiscoverStores(string[] allowedKeys, string namespacesCsv)
+ {
+ LogMethodEntry(nameof(DiscoverStores));
+
+ try
+ {
+ // ListAllCertificateSigningRequests returns Dictionary (name -> certPem)
+ var allCsrs = KubeClient.ListAllCertificateSigningRequests();
+ return allCsrs.Keys.ToList();
+ }
+ finally
+ {
+ LogMethodExit(nameof(DiscoverStores));
+ }
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private bool IsSingleCsrMode()
+ {
+ // Single CSR mode when a specific CSR name is provided
+ return !string.IsNullOrEmpty(Context.KubeSecretName) &&
+ Context.KubeSecretName != "*";
+ }
+
+ private List GetSingleCsrCertificates(long jobId)
+ {
+ try
+ {
+ // GetCertificateSigningRequestStatus returns string[] (each element may contain a chain)
+ var csrCerts = KubeClient.GetCertificateSigningRequestStatus(Context.KubeSecretName);
+ if (csrCerts != null && csrCerts.Length > 0)
+ {
+ // Split each PEM chain into individual certificates
+ var allCerts = new List();
+ foreach (var certPem in csrCerts)
+ {
+ var split = SplitPemChainToStrings(certPem);
+ allCerts.AddRange(split);
+ }
+ return allCerts;
+ }
+
+ Logger.LogDebug("CSR '{Name}' has no issued certificate yet", Context.KubeSecretName);
+ return new List();
+ }
+ catch (Exception ex) when (ex.Message.Contains("NotFound") || ex.Message.Contains("404"))
+ {
+ throw new StoreNotFoundException(
+ $"Certificate Signing Request '{Context.KubeSecretName}' was not found.");
+ }
+ }
+
+ ///
+ /// Splits a PEM chain into individual certificate PEM strings using the existing
+ /// KubeClient.LoadCertificateChain method (powered by BouncyCastle's PemReader).
+ ///
+ private List SplitPemChainToStrings(string pemChain)
+ {
+ if (string.IsNullOrWhiteSpace(pemChain))
+ {
+ return new List();
+ }
+
+ var certs = KubeClient.LoadCertificateChain(pemChain);
+ var result = new List();
+
+ foreach (var cert in certs)
+ {
+ var certPem = KubeClient.ConvertToPem(cert);
+ result.Add(certPem);
+ }
+
+ Logger.LogDebug("Split PEM chain into {Count} individual certificates", result.Count);
+ return result;
+ }
+
+ private List GetClusterWideCsrCertificates(long jobId)
+ {
+ // ListAllCertificateSigningRequests returns Dictionary (name -> certPem)
+ var allCsrs = KubeClient.ListAllCertificateSigningRequests();
+
+ // Split each PEM chain into individual certificates
+ var allCerts = new List();
+ foreach (var certPem in allCsrs.Values.Where(v => !string.IsNullOrEmpty(v)))
+ {
+ var split = SplitPemChainToStrings(certPem);
+ allCerts.AddRange(split);
+ }
+ return allCerts;
+ }
+
+ #endregion
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/ClusterSecretHandler.cs b/kubernetes-orchestrator-extension/Handlers/ClusterSecretHandler.cs
new file mode 100644
index 0000000..bdab396
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/ClusterSecretHandler.cs
@@ -0,0 +1,307 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Enums;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Handler for cluster-wide certificate management.
+/// Discovers and manages all TLS and Opaque secrets across all namespaces.
+///
+public class ClusterSecretHandler : SecretHandlerBase
+{
+ ///
+ /// Allowed keys for both TLS and Opaque secrets.
+ ///
+ private static readonly string[] DefaultAllowedKeys =
+ {
+ "tls.crt", "tls.key", "ca.crt",
+ "certificate", "cert", "crt", "cert.pem"
+ };
+
+ ///
+ public override string[] AllowedKeys => DefaultAllowedKeys;
+
+ ///
+ public override string SecretTypeName => "cluster";
+
+ ///
+ public override bool SupportsManagement => true;
+
+ ///
+ /// Initializes a new instance of the ClusterSecretHandler.
+ ///
+ public ClusterSecretHandler(
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ : base(kubeClient, logger, context)
+ {
+ }
+
+ #region Inventory Operations
+
+ ///
+ public override List GetCertificates(long jobId)
+ {
+ var entries = GetInventoryEntries(jobId);
+ return entries.SelectMany(e => e.Certificates).ToList();
+ }
+
+ ///
+ public override Dictionary> GetCertificatesWithAliases(long jobId)
+ {
+ var entries = GetInventoryEntries(jobId);
+ return entries.ToDictionary(e => e.Alias, e => e.Certificates);
+ }
+
+ ///
+ public override List GetInventoryEntries(long jobId)
+ {
+ LogMethodEntry(nameof(GetInventoryEntries));
+
+ try
+ {
+ var entries = new List();
+ var errors = new List();
+
+ // Discover TLS secrets
+ var tlsSecrets = KubeClient.DiscoverSecrets(
+ new[] { "tls.crt" },
+ "kubernetes.io/tls",
+ "all");
+
+ foreach (var secretPath in tlsSecrets)
+ {
+ ProcessSecretEntry(secretPath, "tls", entries, errors, jobId);
+ }
+
+ // Discover Opaque secrets
+ var opaqueSecrets = KubeClient.DiscoverSecrets(
+ new[] { "tls.crt", "certificate", "cert", "crt" },
+ "Opaque",
+ "all");
+
+ foreach (var secretPath in opaqueSecrets)
+ {
+ ProcessSecretEntry(secretPath, "opaque", entries, errors, jobId);
+ }
+
+ if (errors.Count > 0)
+ {
+ Logger.LogWarning("Errors processing {Count} secrets: {Errors}",
+ errors.Count, string.Join("; ", errors));
+ }
+
+ return entries;
+ }
+ finally
+ {
+ LogMethodExit(nameof(GetInventoryEntries));
+ }
+ }
+
+ ///
+ public override bool HasPrivateKey()
+ {
+ // Cluster-level handler - depends on individual secrets
+ return true;
+ }
+
+ #endregion
+
+ #region Management Operations
+
+ ///
+ public override V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite)
+ {
+ LogMethodEntry(nameof(HandleAdd));
+
+ try
+ {
+ // Parse alias to determine target secret: namespace/secrets/type/name
+ var (ns, secretType, secretName) = ParseClusterAlias(alias);
+
+ // Create context for inner handler
+ var innerContext = CreateInnerContext(ns, secretName);
+ var handler = CreateInnerHandler(secretType, innerContext);
+
+ return handler.HandleAdd(certObj, alias, overwrite);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleAdd));
+ }
+ }
+
+ ///
+ public override V1Secret HandleRemove(string alias)
+ {
+ LogMethodEntry(nameof(HandleRemove));
+
+ try
+ {
+ var (ns, secretType, secretName) = ParseClusterAlias(alias);
+
+ var innerContext = CreateInnerContext(ns, secretName);
+ var handler = CreateInnerHandler(secretType, innerContext);
+
+ return handler.HandleRemove(alias);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleRemove));
+ }
+ }
+
+ ///
+ public override V1Secret CreateEmptyStore()
+ {
+ throw new NotSupportedException(
+ "Cluster-wide stores cannot be created as empty stores. " +
+ "Create individual secrets in specific namespaces instead.");
+ }
+
+ #endregion
+
+ #region Discovery Operations
+
+ ///
+ public override List DiscoverStores(string[] allowedKeys, string namespacesCsv)
+ {
+ LogMethodEntry(nameof(DiscoverStores));
+
+ try
+ {
+ var stores = new List();
+
+ // Discover TLS secrets
+ stores.AddRange(KubeClient.DiscoverSecrets(
+ new[] { "tls.crt" },
+ "kubernetes.io/tls",
+ "all"));
+
+ // Discover Opaque secrets with cert data
+ stores.AddRange(KubeClient.DiscoverSecrets(
+ new[] { "tls.crt", "certificate", "cert", "crt" },
+ "Opaque",
+ "all"));
+
+ return stores.Distinct().ToList();
+ }
+ finally
+ {
+ LogMethodExit(nameof(DiscoverStores));
+ }
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private void ProcessSecretEntry(
+ string secretPath,
+ string secretType,
+ List entries,
+ List errors,
+ long jobId)
+ {
+ try
+ {
+ // secretPath format from DiscoverSecrets: cluster/namespace/secrets/secretname
+ var parts = secretPath.Split('/');
+ if (parts.Length < 4) return;
+
+ var ns = parts[1]; // Namespace is the second part
+ var name = parts[^1]; // Secret name is the last part
+
+ var innerContext = CreateInnerContext(ns, name);
+ var handler = CreateInnerHandler(secretType, innerContext);
+
+ var innerEntries = handler.GetInventoryEntries(jobId);
+
+ // Modify aliases to include full path for cluster view
+ foreach (var entry in innerEntries)
+ {
+ entry.Alias = $"{ns}/secrets/{secretType}/{name}";
+ entries.Add(entry);
+ }
+ }
+ catch (Exception ex)
+ {
+ errors.Add($"{secretPath}: {ex.Message}");
+ }
+ }
+
+ private (string Namespace, string SecretType, string SecretName) ParseClusterAlias(string alias)
+ {
+ // Expected format: namespace/secrets/type/name
+ var parts = alias.Split('/');
+ if (parts.Length < 4)
+ {
+ throw new ArgumentException(
+ $"Invalid cluster alias format: '{alias}'. Expected: namespace/secrets/type/name");
+ }
+
+ return (parts[0], parts[2], parts[3]);
+ }
+
+ private ISecretOperationContext CreateInnerContext(string ns, string name)
+ {
+ return new SimpleSecretOperationContext
+ {
+ KubeNamespace = ns,
+ KubeSecretName = name,
+ StorePath = $"{ns}/{name}",
+ StorePassword = Context.StorePassword,
+ PasswordSecretPath = Context.PasswordSecretPath,
+ PasswordFieldName = Context.PasswordFieldName,
+ SeparateChain = Context.SeparateChain,
+ IncludeCertChain = Context.IncludeCertChain,
+ CertificateDataFieldName = Context.CertificateDataFieldName
+ };
+ }
+
+ private ISecretHandler CreateInnerHandler(string secretType, ISecretOperationContext innerContext)
+ {
+ var normalizedType = SecretTypes.Normalize(secretType);
+
+ return normalizedType switch
+ {
+ SecretTypes.Tls => new TlsSecretHandler(KubeClient, Logger, innerContext),
+ SecretTypes.Opaque => new OpaqueSecretHandler(KubeClient, Logger, innerContext),
+ _ => throw new NotSupportedException($"Inner secret type '{secretType}' not supported")
+ };
+ }
+
+ #endregion
+}
+
+///
+/// Simple implementation of ISecretOperationContext for inner handlers.
+///
+internal class SimpleSecretOperationContext : ISecretOperationContext
+{
+ public string KubeNamespace { get; init; } = string.Empty;
+ public string KubeSecretName { get; init; } = string.Empty;
+ public string StorePath { get; init; } = string.Empty;
+ public string StorePassword { get; init; } = string.Empty;
+ public string PasswordSecretPath { get; init; } = string.Empty;
+ public string PasswordFieldName { get; init; } = string.Empty;
+ public bool SeparateChain { get; init; }
+ public bool IncludeCertChain { get; init; }
+ public string CertificateDataFieldName { get; init; } = string.Empty;
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/ISecretHandler.cs b/kubernetes-orchestrator-extension/Handlers/ISecretHandler.cs
new file mode 100644
index 0000000..71ae01b
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/ISecretHandler.cs
@@ -0,0 +1,162 @@
+// 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 System.Collections.Generic;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Orchestrators.Extensions;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Represents a single inventory entry with certificate chain and private key status.
+/// Used for multi-secret inventory (K8SNS, K8SCluster) where each secret may have different private key status.
+///
+public class InventoryEntry
+{
+ /// The alias/identifier for this inventory item.
+ public string Alias { get; set; } = string.Empty;
+
+ /// The certificate chain as PEM strings (leaf cert first, then intermediates, then root).
+ public List Certificates { get; set; } = new();
+
+ /// Whether this entry has a private key in the store.
+ public bool HasPrivateKey { get; set; }
+}
+
+///
+/// Interface for secret handlers that provide store-type-specific operations.
+/// Each store type (TLS, Opaque, JKS, PKCS12, etc.) implements this interface.
+///
+public interface ISecretHandler
+{
+ #region Inventory Operations
+
+ ///
+ /// Gets certificates from the secret as a simple list of PEM strings.
+ /// Used by simple secret types (Opaque, TLS) where there's a single certificate chain.
+ ///
+ /// Job history ID for logging.
+ /// List of PEM-encoded certificates.
+ List GetCertificates(long jobId);
+
+ ///
+ /// Gets certificates from the secret with alias information.
+ /// Used by keystore types (JKS, PKCS12) where each entry has an alias.
+ ///
+ /// Job history ID for logging.
+ /// Dictionary mapping alias to certificate chain (list of PEM strings).
+ Dictionary> GetCertificatesWithAliases(long jobId);
+
+ ///
+ /// Gets inventory entries with full metadata including private key status.
+ /// Used by multi-secret types (K8SCluster, K8SNS) for per-item inventory.
+ ///
+ /// Job history ID for logging.
+ /// List of inventory entries with certificates and private key status.
+ List GetInventoryEntries(long jobId);
+
+ ///
+ /// Checks if this secret has a private key.
+ ///
+ /// True if the secret contains a private key.
+ bool HasPrivateKey();
+
+ #endregion
+
+ #region Management Operations
+
+ ///
+ /// Adds or updates a certificate in the secret.
+ ///
+ /// Certificate object containing cert data and private key.
+ /// Alias/name for the certificate entry.
+ /// Whether to overwrite existing entries.
+ /// Updated V1Secret object.
+ V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite);
+
+ ///
+ /// Removes a certificate from the secret.
+ ///
+ /// Alias of the certificate to remove.
+ /// Updated V1Secret object, or null if secret was deleted.
+ V1Secret HandleRemove(string alias);
+
+ ///
+ /// Creates an empty store (used for "create if missing" scenarios).
+ ///
+ /// New V1Secret object.
+ V1Secret CreateEmptyStore();
+
+ #endregion
+
+ #region Discovery Operations
+
+ ///
+ /// Discovers stores of this type in the cluster or namespace.
+ ///
+ /// Data keys to look for in secrets.
+ /// Comma-separated namespaces to search, or "all" for cluster-wide.
+ /// List of store paths in format "namespace/secretname".
+ List DiscoverStores(string[] allowedKeys, string namespacesCsv);
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets the default allowed data keys for this secret type.
+ ///
+ string[] AllowedKeys { get; }
+
+ ///
+ /// Gets the secret type name (e.g., "tls", "opaque", "jks").
+ ///
+ string SecretTypeName { get; }
+
+ ///
+ /// Gets whether this handler supports management operations.
+ /// Some handlers (like K8SCert) are read-only.
+ ///
+ bool SupportsManagement { get; }
+
+ #endregion
+}
+
+///
+/// Context object containing configuration and dependencies for secret handlers.
+/// Passed to handler constructors to provide access to KubeClient, Logger, and job configuration.
+///
+public interface ISecretOperationContext
+{
+ /// Kubernetes namespace for the secret.
+ string KubeNamespace { get; }
+
+ /// Secret name.
+ string KubeSecretName { get; }
+
+ /// Store path from job configuration.
+ string StorePath { get; }
+
+ /// Store password (for keystores).
+ string StorePassword { get; }
+
+ /// Password secret path (for buddy password pattern).
+ string PasswordSecretPath { get; }
+
+ /// Password field name in buddy secret.
+ string PasswordFieldName { get; }
+
+ /// Whether to store certificate chain separately.
+ bool SeparateChain { get; }
+
+ /// Whether to include certificate chain in inventory.
+ bool IncludeCertChain { get; }
+
+ /// Custom certificate data field name(s).
+ string CertificateDataFieldName { get; }
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/JksSecretHandler.cs b/kubernetes-orchestrator-extension/Handlers/JksSecretHandler.cs
new file mode 100644
index 0000000..d5ca0ed
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/JksSecretHandler.cs
@@ -0,0 +1,318 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Extensions.Orchestrator.K8S.Serializers.K8SJKS;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+using Org.BouncyCastle.Security;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Handler for JKS keystores stored in Kubernetes Opaque secrets.
+/// JKS files are stored as base64-encoded data in secret fields.
+///
+public class JksSecretHandler : SecretHandlerBase
+{
+ ///
+ /// Default allowed data keys for JKS keystores.
+ ///
+ private static readonly string[] DefaultAllowedKeys = { "jks", "keystore.jks", "truststore.jks" };
+
+ ///
+ public override string[] AllowedKeys => DefaultAllowedKeys;
+
+ ///
+ public override string SecretTypeName => "jks";
+
+ ///
+ public override bool SupportsManagement => true;
+
+ ///
+ /// Initializes a new instance of the JksSecretHandler.
+ ///
+ public JksSecretHandler(
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ : base(kubeClient, logger, context)
+ {
+ }
+
+ #region Inventory Operations
+
+ ///
+ public override Dictionary> GetCertificatesWithAliases(long jobId)
+ {
+ LogMethodEntry(nameof(GetCertificatesWithAliases));
+
+ try
+ {
+ var keys = BuildAllowedKeys(DefaultAllowedKeys);
+ var k8sData = KubeClient.GetJksSecret(
+ Context.KubeSecretName,
+ Context.KubeNamespace,
+ "", "",
+ keys.ToList());
+
+ var serializer = new JksCertificateStoreSerializer(null);
+ var result = new Dictionary>();
+
+ foreach (var (keyName, keyBytes) in k8sData.Inventory)
+ {
+ var password = ResolvePassword(k8sData.Secret);
+ var store = serializer.DeserializeRemoteCertificateStore(keyBytes, keyName, password);
+
+ foreach (var alias in store.Aliases)
+ {
+ var certChain = store.GetCertificateChain(alias);
+ if (certChain == null) continue;
+
+ var certsList = new List();
+ foreach (var cert in certChain)
+ {
+ var pem = new StringBuilder();
+ pem.AppendLine("-----BEGIN CERTIFICATE-----");
+ pem.AppendLine(Convert.ToBase64String(cert.Certificate.GetEncoded()));
+ pem.AppendLine("-----END CERTIFICATE-----");
+ certsList.Add(pem.ToString());
+ }
+
+ var fullAlias = $"{keyName}/{alias}";
+ result[fullAlias] = certsList;
+ }
+ }
+
+ return result;
+ }
+ catch (Exception ex) when (ex.Message.Contains("NotFound") || ex.Message.Contains("404"))
+ {
+ throw new StoreNotFoundException(
+ $"JKS keystore secret '{Context.KubeSecretName}' was not found in namespace '{Context.KubeNamespace}'.");
+ }
+ finally
+ {
+ LogMethodExit(nameof(GetCertificatesWithAliases));
+ }
+ }
+
+ ///
+ public override List GetInventoryEntries(long jobId)
+ {
+ var aliasedCerts = GetCertificatesWithAliases(jobId);
+ var entries = new List();
+
+ foreach (var kvp in aliasedCerts)
+ {
+ entries.Add(new InventoryEntry
+ {
+ Alias = kvp.Key,
+ Certificates = kvp.Value,
+ HasPrivateKey = true // JKS entries typically have private keys
+ });
+ }
+
+ return entries;
+ }
+
+ ///
+ public override bool HasPrivateKey()
+ {
+ // JKS keystores typically have private keys
+ return true;
+ }
+
+ #endregion
+
+ #region Management Operations
+
+ ///
+ public override V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite)
+ {
+ LogMethodEntry(nameof(HandleAdd));
+
+ try
+ {
+ // Handle "create store if missing"
+ if (string.IsNullOrEmpty(alias) && string.IsNullOrEmpty(certObj?.CertPem))
+ {
+ return HandleCreateIfMissing();
+ }
+
+ var keys = BuildAllowedKeys(DefaultAllowedKeys);
+ var serializer = new JksCertificateStoreSerializer(null);
+
+ // Get existing keystore data (or create empty if not found)
+ KubeCertificateManagerClient.JksSecret k8sData;
+ try
+ {
+ k8sData = KubeClient.GetJksSecret(
+ Context.KubeSecretName,
+ Context.KubeNamespace,
+ Context.PasswordSecretPath ?? "",
+ Context.PasswordFieldName ?? "",
+ keys.ToList());
+ }
+ catch (StoreNotFoundException)
+ {
+ Logger.LogDebug("Secret not found, will create new JKS store");
+ k8sData = new KubeCertificateManagerClient.JksSecret
+ {
+ Secret = null,
+ Inventory = new Dictionary()
+ };
+ }
+
+ // Get password
+ var storePassword = ResolvePassword(k8sData.Secret);
+
+ var (_, certAlias, existingData, existingKeyName) =
+ ParseKeystoreAlias(alias, k8sData.Inventory, "keystore.jks");
+
+ // Get certificate bytes for the serializer
+ // Use PKCS12 if available (for certificates with private keys), otherwise use raw cert bytes
+ // (for certificate-only entries like trusted CA certs)
+ byte[] newCertBytes = certObj.Pkcs12 ?? certObj.CertBytes;
+
+ // Use serializer to update the JKS store
+ var newJksBytes = serializer.CreateOrUpdateJks(
+ newCertBytes,
+ certObj.Password,
+ certAlias,
+ existingData,
+ storePassword,
+ remove: false,
+ includeChain: Context.IncludeCertChain);
+
+ // Update the k8sData inventory
+ if (k8sData.Inventory == null)
+ {
+ k8sData.Inventory = new Dictionary();
+ }
+ k8sData.Inventory[existingKeyName] = newJksBytes;
+
+ // Persist to Kubernetes
+ return KubeClient.CreateOrUpdateJksSecret(k8sData, Context.KubeSecretName, Context.KubeNamespace);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleAdd));
+ }
+ }
+
+ ///
+ public override V1Secret HandleRemove(string alias)
+ {
+ LogMethodEntry(nameof(HandleRemove));
+
+ try
+ {
+ var keys = BuildAllowedKeys(DefaultAllowedKeys);
+ var serializer = new JksCertificateStoreSerializer(null);
+
+ // Get existing keystore data
+ var k8sData = KubeClient.GetJksSecret(
+ Context.KubeSecretName,
+ Context.KubeNamespace,
+ Context.PasswordSecretPath ?? "",
+ Context.PasswordFieldName ?? "",
+ keys.ToList());
+
+ // Get password
+ var storePassword = ResolvePassword(k8sData.Secret);
+
+ var (_, certAlias, existingData, existingKeyName) =
+ ParseKeystoreAlias(alias, k8sData.Inventory, "keystore.jks");
+
+ if (existingData == null)
+ {
+ throw new InvalidOperationException($"Cannot remove from non-existent keystore field '{existingKeyName}'");
+ }
+
+ // Use serializer to remove from the JKS store
+ var newJksBytes = serializer.CreateOrUpdateJks(
+ null,
+ null,
+ certAlias,
+ existingData,
+ storePassword,
+ remove: true,
+ includeChain: false);
+
+ // Update the k8sData inventory
+ k8sData.Inventory[existingKeyName] = newJksBytes;
+
+ // Persist to Kubernetes
+ return KubeClient.CreateOrUpdateJksSecret(k8sData, Context.KubeSecretName, Context.KubeNamespace);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleRemove));
+ }
+ }
+
+ ///
+ public override V1Secret CreateEmptyStore()
+ {
+ LogMethodEntry(nameof(CreateEmptyStore));
+
+ try
+ {
+ // Create empty JKS keystore using BouncyCastle
+ // Use ResolvePassword (not Context.StorePassword directly) so buddy-secret passwords are respected
+ var jksStore = new JksStore();
+ using var ms = new System.IO.MemoryStream();
+ var password = ResolvePassword(null);
+ jksStore.Save(ms, password.ToCharArray());
+ var jksBytes = ms.ToArray();
+
+ var k8sData = new KubeCertificateManagerClient.JksSecret
+ {
+ Secret = null,
+ Inventory = new Dictionary
+ {
+ { "keystore.jks", jksBytes }
+ }
+ };
+
+ return KubeClient.CreateOrUpdateJksSecret(k8sData, Context.KubeSecretName, Context.KubeNamespace);
+ }
+ finally
+ {
+ LogMethodExit(nameof(CreateEmptyStore));
+ }
+ }
+
+ #endregion
+
+ #region Discovery Operations
+
+ ///
+ public override List DiscoverStores(string[] allowedKeys, string namespacesCsv)
+ {
+ LogMethodEntry(nameof(DiscoverStores));
+
+ try
+ {
+ var keys = allowedKeys?.Length > 0 ? allowedKeys : AllowedKeys;
+ return KubeClient.DiscoverSecrets(keys, "Opaque", namespacesCsv);
+ }
+ finally
+ {
+ LogMethodExit(nameof(DiscoverStores));
+ }
+ }
+
+ #endregion
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/NamespaceSecretHandler.cs b/kubernetes-orchestrator-extension/Handlers/NamespaceSecretHandler.cs
new file mode 100644
index 0000000..4084ce3
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/NamespaceSecretHandler.cs
@@ -0,0 +1,295 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Enums;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Handler for namespace-level certificate management.
+/// Discovers and manages all TLS and Opaque secrets within a single namespace.
+///
+public class NamespaceSecretHandler : SecretHandlerBase
+{
+ ///
+ /// Allowed keys for both TLS and Opaque secrets.
+ ///
+ private static readonly string[] DefaultAllowedKeys =
+ {
+ "tls.crt", "tls.key", "ca.crt",
+ "certificate", "cert", "crt", "cert.pem"
+ };
+
+ ///
+ public override string[] AllowedKeys => DefaultAllowedKeys;
+
+ ///
+ public override string SecretTypeName => "namespace";
+
+ ///
+ public override bool SupportsManagement => true;
+
+ ///
+ /// Initializes a new instance of the NamespaceSecretHandler.
+ ///
+ public NamespaceSecretHandler(
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ : base(kubeClient, logger, context)
+ {
+ }
+
+ #region Inventory Operations
+
+ ///
+ public override List GetCertificates(long jobId)
+ {
+ var entries = GetInventoryEntries(jobId);
+ return entries.SelectMany(e => e.Certificates).ToList();
+ }
+
+ ///
+ public override Dictionary> GetCertificatesWithAliases(long jobId)
+ {
+ var entries = GetInventoryEntries(jobId);
+ return entries.ToDictionary(e => e.Alias, e => e.Certificates);
+ }
+
+ ///
+ public override List GetInventoryEntries(long jobId)
+ {
+ LogMethodEntry(nameof(GetInventoryEntries));
+
+ try
+ {
+ var entries = new List();
+ var errors = new List();
+ var targetNamespace = Context.KubeNamespace;
+
+ // Discover TLS secrets in the namespace
+ var tlsSecrets = KubeClient.DiscoverSecrets(
+ new[] { "tls.crt" },
+ "kubernetes.io/tls",
+ targetNamespace);
+
+ foreach (var secretPath in tlsSecrets)
+ {
+ ProcessSecretEntry(secretPath, "tls", entries, errors, jobId);
+ }
+
+ // Discover Opaque secrets in the namespace
+ var opaqueSecrets = KubeClient.DiscoverSecrets(
+ new[] { "tls.crt", "certificate", "cert", "crt" },
+ "Opaque",
+ targetNamespace);
+
+ foreach (var secretPath in opaqueSecrets)
+ {
+ ProcessSecretEntry(secretPath, "opaque", entries, errors, jobId);
+ }
+
+ if (errors.Count > 0)
+ {
+ Logger.LogWarning("Errors processing {Count} secrets: {Errors}",
+ errors.Count, string.Join("; ", errors));
+ }
+
+ return entries;
+ }
+ finally
+ {
+ LogMethodExit(nameof(GetInventoryEntries));
+ }
+ }
+
+ ///
+ public override bool HasPrivateKey()
+ {
+ // Namespace-level handler - depends on individual secrets
+ return true;
+ }
+
+ #endregion
+
+ #region Management Operations
+
+ ///
+ public override V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite)
+ {
+ LogMethodEntry(nameof(HandleAdd));
+
+ try
+ {
+ // Parse alias to determine target secret: type/name
+ var (secretType, secretName) = ParseNamespaceAlias(alias);
+
+ // Create context for inner handler
+ var innerContext = CreateInnerContext(secretName);
+ var handler = CreateInnerHandler(secretType, innerContext);
+
+ return handler.HandleAdd(certObj, alias, overwrite);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleAdd));
+ }
+ }
+
+ ///
+ public override V1Secret HandleRemove(string alias)
+ {
+ LogMethodEntry(nameof(HandleRemove));
+
+ try
+ {
+ var (secretType, secretName) = ParseNamespaceAlias(alias);
+
+ var innerContext = CreateInnerContext(secretName);
+ var handler = CreateInnerHandler(secretType, innerContext);
+
+ return handler.HandleRemove(alias);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleRemove));
+ }
+ }
+
+ ///
+ public override V1Secret CreateEmptyStore()
+ {
+ throw new NotSupportedException(
+ "Namespace-wide stores cannot be created as empty stores. " +
+ "Create individual secrets instead.");
+ }
+
+ #endregion
+
+ #region Discovery Operations
+
+ ///
+ public override List DiscoverStores(string[] allowedKeys, string namespacesCsv)
+ {
+ LogMethodEntry(nameof(DiscoverStores));
+
+ try
+ {
+ var targetNamespace = string.IsNullOrEmpty(namespacesCsv)
+ ? Context.KubeNamespace
+ : namespacesCsv;
+
+ var stores = new List();
+
+ // Discover TLS secrets
+ stores.AddRange(KubeClient.DiscoverSecrets(
+ new[] { "tls.crt" },
+ "kubernetes.io/tls",
+ targetNamespace));
+
+ // Discover Opaque secrets with cert data
+ stores.AddRange(KubeClient.DiscoverSecrets(
+ new[] { "tls.crt", "certificate", "cert", "crt" },
+ "Opaque",
+ targetNamespace));
+
+ return stores.Distinct().ToList();
+ }
+ finally
+ {
+ LogMethodExit(nameof(DiscoverStores));
+ }
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ private void ProcessSecretEntry(
+ string secretPath,
+ string secretType,
+ List entries,
+ List errors,
+ long jobId)
+ {
+ try
+ {
+ // secretPath format: namespace/secretname
+ var parts = secretPath.Split('/');
+ var name = parts.Length >= 2 ? parts[^1] : secretPath;
+
+ var innerContext = CreateInnerContext(name);
+ var handler = CreateInnerHandler(secretType, innerContext);
+
+ var innerEntries = handler.GetInventoryEntries(jobId);
+
+ // Modify aliases for namespace view: type/name
+ foreach (var entry in innerEntries)
+ {
+ entry.Alias = $"{secretType}/{name}";
+ entries.Add(entry);
+ }
+ }
+ catch (Exception ex)
+ {
+ errors.Add($"{secretPath}: {ex.Message}");
+ }
+ }
+
+ private (string SecretType, string SecretName) ParseNamespaceAlias(string alias)
+ {
+ // Expected format: [secrets/]/ - uses last two parts
+ // Examples: "opaque/my-secret" or "secrets/opaque/my-secret"
+ var parts = alias.Split('/');
+ if (parts.Length < 2)
+ {
+ throw new ArgumentException(
+ $"Invalid namespace alias format: '{alias}'. Expected: / or secrets//");
+ }
+
+ // Use ^2 and ^1 to get second-to-last (type) and last (name)
+ return (parts[^2], parts[^1]);
+ }
+
+ private ISecretOperationContext CreateInnerContext(string name)
+ {
+ return new SimpleSecretOperationContext
+ {
+ KubeNamespace = Context.KubeNamespace,
+ KubeSecretName = name,
+ StorePath = $"{Context.KubeNamespace}/{name}",
+ StorePassword = Context.StorePassword,
+ PasswordSecretPath = Context.PasswordSecretPath,
+ PasswordFieldName = Context.PasswordFieldName,
+ SeparateChain = Context.SeparateChain,
+ IncludeCertChain = Context.IncludeCertChain,
+ CertificateDataFieldName = Context.CertificateDataFieldName
+ };
+ }
+
+ private ISecretHandler CreateInnerHandler(string secretType, ISecretOperationContext innerContext)
+ {
+ var normalizedType = SecretTypes.Normalize(secretType);
+
+ return normalizedType switch
+ {
+ SecretTypes.Tls => new TlsSecretHandler(KubeClient, Logger, innerContext),
+ SecretTypes.Opaque => new OpaqueSecretHandler(KubeClient, Logger, innerContext),
+ _ => throw new NotSupportedException($"Inner secret type '{secretType}' not supported")
+ };
+ }
+
+ #endregion
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/OpaqueSecretHandler.cs b/kubernetes-orchestrator-extension/Handlers/OpaqueSecretHandler.cs
new file mode 100644
index 0000000..800ccdf
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/OpaqueSecretHandler.cs
@@ -0,0 +1,289 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Text;
+using k8s.Autorest;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Handler for Opaque secrets containing PEM-encoded certificates.
+/// Opaque secrets can have various field names for certificate and key data.
+///
+public class OpaqueSecretHandler : SecretHandlerBase
+{
+ ///
+ /// Default allowed data keys for Opaque secrets containing certificates.
+ ///
+ private static readonly string[] DefaultAllowedKeys =
+ {
+ "tls.crt", "certificate", "cert", "crt", "cert.pem", "certificate.pem",
+ "tls.key", "key", "private-key", "key.pem", "private-key.pem",
+ "ca.crt", "ca", "ca-bundle", "ca-bundle.crt"
+ };
+
+ ///
+ public override string[] AllowedKeys => DefaultAllowedKeys;
+
+ ///
+ public override string SecretTypeName => "opaque";
+
+ ///
+ public override bool SupportsManagement => true;
+
+ ///
+ protected override string[] PrivateKeyFieldNames =>
+ new[] { "tls.key", "key", "private-key", "key.pem", "private-key.pem" };
+
+ ///
+ /// Initializes a new instance of the OpaqueSecretHandler.
+ ///
+ public OpaqueSecretHandler(
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ : base(kubeClient, logger, context)
+ {
+ }
+
+ #region Inventory Operations
+
+ ///
+ public override List GetCertificates(long jobId)
+ {
+ LogMethodEntry(nameof(GetCertificates));
+
+ try
+ {
+ var secret = GetSecret();
+ return ExtractCertificatesFromSecret(secret);
+ }
+ catch (HttpOperationException)
+ {
+ Logger.LogError("Kubernetes Opaque secret '{Name}' was not found in namespace '{Namespace}'",
+ Context.KubeSecretName, Context.KubeNamespace);
+ throw new StoreNotFoundException(
+ $"Kubernetes Opaque secret '{Context.KubeSecretName}' was not found in namespace '{Context.KubeNamespace}'.");
+ }
+ finally
+ {
+ LogMethodExit(nameof(GetCertificates));
+ }
+ }
+
+ ///
+ public override Dictionary> GetCertificatesWithAliases(long jobId)
+ {
+ // Opaque secrets don't use aliases - return single entry with secret name as alias
+ var certs = GetCertificates(jobId);
+ return new Dictionary>
+ {
+ { Context.KubeSecretName, certs }
+ };
+ }
+
+ ///
+ public override List GetInventoryEntries(long jobId)
+ {
+ var certs = GetCertificates(jobId);
+ var hasKey = HasPrivateKey();
+
+ return new List
+ {
+ new InventoryEntry
+ {
+ Alias = Context.KubeSecretName,
+ Certificates = certs,
+ HasPrivateKey = hasKey
+ }
+ };
+ }
+
+ ///
+ public override bool HasPrivateKey()
+ {
+ try
+ {
+ var secret = GetSecret();
+ if (secret.Data == null) return false;
+
+ // Check various key field names
+ var keyFields = new[] { "tls.key", "key", "private-key", "key.pem", "private-key.pem" };
+ foreach (var field in keyFields)
+ {
+ if (secret.Data.TryGetValue(field, out var keyBytes) &&
+ keyBytes != null &&
+ keyBytes.Length > 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Management Operations
+
+ ///
+ public override V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite)
+ {
+ LogMethodEntry(nameof(HandleAdd));
+
+ try
+ {
+ // Handle "create store if missing" - when no certificate data is provided
+ if (string.IsNullOrEmpty(alias) && string.IsNullOrEmpty(certObj?.CertPem))
+ {
+ return HandleCreateIfMissing();
+ }
+
+ // Check if secret exists
+ V1Secret existingSecret = null;
+ try
+ {
+ existingSecret = GetSecret();
+ }
+ catch (StoreNotFoundException)
+ {
+ // Secret doesn't exist, will create new one
+ }
+
+ if (existingSecret != null && !overwrite)
+ {
+ if (IsSecretEmpty(existingSecret))
+ {
+ Logger.LogDebug("Secret '{Name}' exists but is empty; overwriting implicitly", Context.KubeSecretName);
+ }
+ else
+ {
+ Logger.LogWarning("Secret already exists and overwrite is false");
+ throw new InvalidOperationException(
+ $"Secret '{Context.KubeSecretName}' already exists. Set overwrite=true to replace.");
+ }
+ }
+
+ // Validate cert-only updates: prevent deploying certificate without private key
+ // to an existing secret that has a key (would cause key/cert mismatch)
+ var incomingHasNoPrivateKey = string.IsNullOrEmpty(certObj?.PrivateKeyPem);
+ if (existingSecret != null && overwrite && incomingHasNoPrivateKey)
+ {
+ ValidateCertOnlyUpdate(existingSecret);
+ }
+
+ // Create or update secret using the PEM helper
+ return CreateOrUpdatePemSecret(
+ certObj.PrivateKeyPem,
+ certObj.CertPem,
+ certObj.ChainPem ?? new List(),
+ "opaque",
+ Context.SeparateChain,
+ Context.IncludeCertChain);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleAdd));
+ }
+ }
+
+ ///
+ public override V1Secret HandleRemove(string alias)
+ {
+ LogMethodEntry(nameof(HandleRemove));
+
+ try
+ {
+ // Opaque secrets are single-entry, so remove means delete the whole secret
+ DeleteSecret(alias);
+ return null;
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleRemove));
+ }
+ }
+
+ ///
+ public override V1Secret CreateEmptyStore()
+ {
+ LogMethodEntry(nameof(CreateEmptyStore));
+
+ try
+ {
+ // Create empty Opaque secret
+ return CreateOrUpdatePemSecret(
+ "",
+ "",
+ new List(),
+ "opaque",
+ separateChain: false,
+ includeChain: false);
+ }
+ finally
+ {
+ LogMethodExit(nameof(CreateEmptyStore));
+ }
+ }
+
+ #endregion
+
+ #region Discovery Operations
+
+ ///
+ public override List DiscoverStores(string[] allowedKeys, string namespacesCsv)
+ {
+ LogMethodEntry(nameof(DiscoverStores));
+
+ try
+ {
+ var keys = allowedKeys?.Length > 0 ? allowedKeys : AllowedKeys;
+ return KubeClient.DiscoverSecrets(keys, "Opaque", namespacesCsv);
+ }
+ finally
+ {
+ LogMethodExit(nameof(DiscoverStores));
+ }
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ // ValidateCertOnlyUpdate is inherited from SecretHandlerBase.
+ // OpaqueSecretHandler overrides PrivateKeyFieldNames to include all common key field names.
+
+ private List ExtractCertificatesFromSecret(V1Secret secret)
+ {
+ if (secret.Data == null)
+ {
+ Logger.LogWarning("Secret '{Name}' has no data", Context.KubeSecretName);
+ return new List();
+ }
+
+ var keys = BuildAllowedKeys(DefaultAllowedKeys);
+ return CertExtractor.ExtractFromSecretData(
+ secret.Data,
+ keys,
+ Context.KubeSecretName,
+ Context.KubeNamespace);
+ }
+
+ #endregion
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/Pkcs12SecretHandler.cs b/kubernetes-orchestrator-extension/Handlers/Pkcs12SecretHandler.cs
new file mode 100644
index 0000000..0328a8d
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/Pkcs12SecretHandler.cs
@@ -0,0 +1,339 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Extensions.Orchestrator.K8S.Serializers.K8SPKCS12;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.Security;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Handler for PKCS12/PFX keystores stored in Kubernetes Opaque secrets.
+/// PKCS12 files are stored as base64-encoded data in secret fields.
+///
+public class Pkcs12SecretHandler : SecretHandlerBase
+{
+ ///
+ /// Default allowed data keys for PKCS12 keystores.
+ ///
+ private static readonly string[] DefaultAllowedKeys = { "pkcs12", "p12", "pfx", "keystore.p12", "keystore.pfx" };
+
+ ///
+ public override string[] AllowedKeys => DefaultAllowedKeys;
+
+ ///
+ public override string SecretTypeName => "pkcs12";
+
+ ///
+ public override bool SupportsManagement => true;
+
+ ///
+ /// Initializes a new instance of the Pkcs12SecretHandler.
+ ///
+ public Pkcs12SecretHandler(
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ : base(kubeClient, logger, context)
+ {
+ }
+
+ #region Inventory Operations
+
+ ///
+ public override Dictionary> GetCertificatesWithAliases(long jobId)
+ {
+ LogMethodEntry(nameof(GetCertificatesWithAliases));
+
+ try
+ {
+ var keys = BuildAllowedKeys(DefaultAllowedKeys);
+ var k8sData = KubeClient.GetPkcs12Secret(
+ Context.KubeSecretName,
+ Context.KubeNamespace,
+ "", "",
+ keys.ToList());
+
+ var serializer = new Pkcs12CertificateStoreSerializer(null);
+ var result = new Dictionary>();
+
+ foreach (var (keyName, keyBytes) in k8sData.Inventory)
+ {
+ var password = ResolvePassword(k8sData.Secret);
+ var store = serializer.DeserializeRemoteCertificateStore(keyBytes, keyName, password);
+
+ foreach (var alias in store.Aliases)
+ {
+ var certsList = new List();
+
+ // For key entries, get the certificate chain
+ // For certificate-only entries (trusted certs), get the single certificate
+ if (store.IsKeyEntry(alias))
+ {
+ var certChain = store.GetCertificateChain(alias);
+ if (certChain == null) continue;
+
+ foreach (var cert in certChain)
+ {
+ var pem = new StringBuilder();
+ pem.AppendLine("-----BEGIN CERTIFICATE-----");
+ pem.AppendLine(Convert.ToBase64String(cert.Certificate.GetEncoded()));
+ pem.AppendLine("-----END CERTIFICATE-----");
+ certsList.Add(pem.ToString());
+ }
+ }
+ else
+ {
+ // Certificate-only entry (trusted cert)
+ var certEntry = store.GetCertificate(alias);
+ if (certEntry == null) continue;
+
+ var pem = new StringBuilder();
+ pem.AppendLine("-----BEGIN CERTIFICATE-----");
+ pem.AppendLine(Convert.ToBase64String(certEntry.Certificate.GetEncoded()));
+ pem.AppendLine("-----END CERTIFICATE-----");
+ certsList.Add(pem.ToString());
+ }
+
+ var fullAlias = $"{keyName}/{alias}";
+ result[fullAlias] = certsList;
+ }
+ }
+
+ return result;
+ }
+ catch (Exception ex) when (ex.Message.Contains("NotFound") || ex.Message.Contains("404"))
+ {
+ throw new StoreNotFoundException(
+ $"PKCS12 keystore secret '{Context.KubeSecretName}' was not found in namespace '{Context.KubeNamespace}'.");
+ }
+ finally
+ {
+ LogMethodExit(nameof(GetCertificatesWithAliases));
+ }
+ }
+
+ ///
+ public override List GetInventoryEntries(long jobId)
+ {
+ var aliasedCerts = GetCertificatesWithAliases(jobId);
+ var entries = new List();
+
+ foreach (var kvp in aliasedCerts)
+ {
+ entries.Add(new InventoryEntry
+ {
+ Alias = kvp.Key,
+ Certificates = kvp.Value,
+ // PKCS12 keystores typically contain private keys
+ HasPrivateKey = true
+ });
+ }
+
+ return entries;
+ }
+
+ ///
+ public override bool HasPrivateKey()
+ {
+ // PKCS12 keystores typically have private keys
+ return true;
+ }
+
+ #endregion
+
+ #region Management Operations
+
+ ///
+ public override V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite)
+ {
+ LogMethodEntry(nameof(HandleAdd));
+
+ try
+ {
+ // Handle "create store if missing"
+ if (string.IsNullOrEmpty(alias) && string.IsNullOrEmpty(certObj?.CertPem))
+ {
+ return HandleCreateIfMissing();
+ }
+
+ var keys = BuildAllowedKeys(DefaultAllowedKeys);
+ var serializer = new Pkcs12CertificateStoreSerializer(null);
+
+ // Get existing keystore data (or create empty if not found)
+ KubeCertificateManagerClient.Pkcs12Secret k8sData;
+ try
+ {
+ k8sData = KubeClient.GetPkcs12Secret(
+ Context.KubeSecretName,
+ Context.KubeNamespace,
+ Context.PasswordSecretPath ?? "",
+ Context.PasswordFieldName ?? "",
+ keys.ToList());
+ }
+ catch (StoreNotFoundException)
+ {
+ Logger.LogDebug("Secret not found, will create new PKCS12 store");
+ k8sData = new KubeCertificateManagerClient.Pkcs12Secret
+ {
+ Secret = null,
+ Inventory = new Dictionary()
+ };
+ }
+
+ // Get password
+ var storePassword = ResolvePassword(k8sData.Secret);
+
+ var (_, certAlias, existingData, existingKeyName) =
+ ParseKeystoreAlias(alias, k8sData.Inventory, "keystore.pfx");
+
+ // Get certificate bytes for the serializer
+ // Use PKCS12 if available (for certificates with private keys), otherwise use raw cert bytes
+ // (for certificate-only entries like trusted CA certs)
+ byte[] newCertBytes = certObj.Pkcs12 ?? certObj.CertBytes;
+
+ // Use serializer to update the PKCS12 store
+ var newPkcs12Bytes = serializer.CreateOrUpdatePkcs12(
+ newCertBytes,
+ certObj.Password,
+ certAlias,
+ existingData,
+ storePassword,
+ remove: false,
+ includeChain: Context.IncludeCertChain);
+
+ // Update the k8sData inventory
+ if (k8sData.Inventory == null)
+ {
+ k8sData.Inventory = new Dictionary();
+ }
+ k8sData.Inventory[existingKeyName] = newPkcs12Bytes;
+
+ // Persist to Kubernetes
+ return KubeClient.CreateOrUpdatePkcs12Secret(k8sData, Context.KubeSecretName, Context.KubeNamespace);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleAdd));
+ }
+ }
+
+ ///
+ public override V1Secret HandleRemove(string alias)
+ {
+ LogMethodEntry(nameof(HandleRemove));
+
+ try
+ {
+ var keys = BuildAllowedKeys(DefaultAllowedKeys);
+ var serializer = new Pkcs12CertificateStoreSerializer(null);
+
+ // Get existing keystore data
+ var k8sData = KubeClient.GetPkcs12Secret(
+ Context.KubeSecretName,
+ Context.KubeNamespace,
+ Context.PasswordSecretPath ?? "",
+ Context.PasswordFieldName ?? "",
+ keys.ToList());
+
+ // Get password
+ var storePassword = ResolvePassword(k8sData.Secret);
+
+ var (_, certAlias, existingData, existingKeyName) =
+ ParseKeystoreAlias(alias, k8sData.Inventory, "keystore.pfx");
+
+ if (existingData == null)
+ {
+ throw new InvalidOperationException($"Cannot remove from non-existent keystore field '{existingKeyName}'");
+ }
+
+ // Use serializer to remove from the PKCS12 store
+ var newPkcs12Bytes = serializer.CreateOrUpdatePkcs12(
+ null,
+ null,
+ certAlias,
+ existingData,
+ storePassword,
+ remove: true,
+ includeChain: false);
+
+ // Update the k8sData inventory
+ k8sData.Inventory[existingKeyName] = newPkcs12Bytes;
+
+ // Persist to Kubernetes
+ return KubeClient.CreateOrUpdatePkcs12Secret(k8sData, Context.KubeSecretName, Context.KubeNamespace);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleRemove));
+ }
+ }
+
+ ///
+ public override V1Secret CreateEmptyStore()
+ {
+ LogMethodEntry(nameof(CreateEmptyStore));
+
+ try
+ {
+ // Create empty PKCS12 keystore
+ // Use ResolvePassword (not Context.StorePassword directly) so buddy-secret passwords are respected
+ var storeBuilder = new Pkcs12StoreBuilder();
+ var store = storeBuilder.Build();
+ using var ms = new System.IO.MemoryStream();
+ var password = ResolvePassword(null);
+ store.Save(ms, password.ToCharArray(), new SecureRandom());
+ var pkcs12Bytes = ms.ToArray();
+
+ var k8sData = new KubeCertificateManagerClient.Pkcs12Secret
+ {
+ Secret = null,
+ Inventory = new Dictionary
+ {
+ { "keystore.pfx", pkcs12Bytes }
+ }
+ };
+
+ return KubeClient.CreateOrUpdatePkcs12Secret(k8sData, Context.KubeSecretName, Context.KubeNamespace);
+ }
+ finally
+ {
+ LogMethodExit(nameof(CreateEmptyStore));
+ }
+ }
+
+ #endregion
+
+ #region Discovery Operations
+
+ ///
+ public override List DiscoverStores(string[] allowedKeys, string namespacesCsv)
+ {
+ LogMethodEntry(nameof(DiscoverStores));
+
+ try
+ {
+ var keys = allowedKeys?.Length > 0 ? allowedKeys : AllowedKeys;
+ return KubeClient.DiscoverSecrets(keys, "Opaque", namespacesCsv);
+ }
+ finally
+ {
+ LogMethodExit(nameof(DiscoverStores));
+ }
+ }
+
+ #endregion
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/SecretHandlerBase.cs b/kubernetes-orchestrator-extension/Handlers/SecretHandlerBase.cs
new file mode 100644
index 0000000..c2f76d0
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/SecretHandlerBase.cs
@@ -0,0 +1,406 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Extensions.Orchestrator.K8S.Services;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Base class for secret handlers providing common functionality.
+/// Subclasses implement store-type-specific logic.
+///
+public abstract class SecretHandlerBase : ISecretHandler
+{
+ ///
+ /// Kubernetes client for API operations.
+ ///
+ protected readonly KubeCertificateManagerClient KubeClient;
+
+ ///
+ /// Logger for diagnostic output.
+ ///
+ protected readonly ILogger Logger;
+
+ ///
+ /// Operation context with configuration and job parameters.
+ ///
+ protected readonly ISecretOperationContext Context;
+
+ ///
+ /// Certificate chain extractor service.
+ ///
+ protected readonly CertificateChainExtractor CertExtractor;
+
+ ///
+ /// Initializes a new instance of the handler.
+ ///
+ /// Kubernetes client.
+ /// Logger instance.
+ /// Operation context.
+ protected SecretHandlerBase(
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ {
+ KubeClient = kubeClient ?? throw new ArgumentNullException(nameof(kubeClient));
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ Context = context ?? throw new ArgumentNullException(nameof(context));
+ CertExtractor = new CertificateChainExtractor(kubeClient, logger);
+ }
+
+ #region Abstract Members
+
+ ///
+ public abstract string[] AllowedKeys { get; }
+
+ ///
+ public abstract string SecretTypeName { get; }
+
+ ///
+ public abstract bool SupportsManagement { get; }
+
+ ///
+ public virtual List GetCertificates(long jobId)
+ {
+ var aliasedCerts = GetCertificatesWithAliases(jobId);
+ var allCerts = new List();
+ foreach (var kvp in aliasedCerts)
+ allCerts.AddRange(kvp.Value);
+ return allCerts;
+ }
+
+ ///
+ public abstract Dictionary> GetCertificatesWithAliases(long jobId);
+
+ ///
+ public abstract List GetInventoryEntries(long jobId);
+
+ ///
+ public abstract bool HasPrivateKey();
+
+ ///
+ public abstract V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite);
+
+ ///
+ public abstract V1Secret HandleRemove(string alias);
+
+ ///
+ public abstract V1Secret CreateEmptyStore();
+
+ ///
+ public abstract List DiscoverStores(string[] allowedKeys, string namespacesCsv);
+
+ #endregion
+
+ #region Protected Helpers
+
+ ///
+ /// Resolves the store password, checking a buddy secret first then falling back to the configured password.
+ ///
+ /// The primary secret (unused, kept for signature compatibility).
+ /// The resolved password string.
+ protected string ResolvePassword(V1Secret secret)
+ {
+ if (!string.IsNullOrEmpty(Context.PasswordSecretPath))
+ {
+ var pathParts = Context.PasswordSecretPath.Split('/');
+ var passwordNamespace = pathParts.Length > 1 ? pathParts[0] : Context.KubeNamespace;
+ var passwordSecretName = pathParts.Length > 1 ? pathParts[^1] : pathParts[0];
+
+ var buddySecret = KubeClient.ReadBuddyPass(passwordSecretName, passwordNamespace);
+ if (buddySecret?.Data != null)
+ {
+ var fieldName = Context.PasswordFieldName ?? "password";
+ if (buddySecret.Data.TryGetValue(fieldName, out var passwordBytes) && passwordBytes != null)
+ return Encoding.UTF8.GetString(passwordBytes).TrimEnd('\n', '\r');
+ }
+ }
+
+ return Context.StorePassword ?? "";
+ }
+
+ ///
+ /// Returns true if the secret has no meaningful certificate data (e.g. created via "create if missing").
+ /// An empty secret can be implicitly overwritten without the overwrite flag.
+ ///
+ public static bool IsSecretEmpty(V1Secret secret)
+ {
+ if (secret?.Data == null || secret.Data.Count == 0)
+ return true;
+
+ return secret.Data.Values.All(v => v == null || v.Length == 0);
+ }
+
+ ///
+ /// Handles the "create store if missing" case: returns existing secret if present, otherwise creates an empty store.
+ ///
+ /// The existing or newly created secret.
+ protected V1Secret HandleCreateIfMissing()
+ {
+ try
+ {
+ var existingSecret = GetSecret();
+ Logger.LogInformation("Secret already exists, nothing to do for empty certificate data");
+ return existingSecret;
+ }
+ catch (StoreNotFoundException)
+ {
+ Logger.LogDebug("Secret not found, creating empty {Type} store", SecretTypeName);
+ return CreateEmptyStore();
+ }
+ }
+
+ ///
+ /// Gets the secret from Kubernetes.
+ ///
+ /// The V1Secret object.
+ /// Thrown if secret doesn't exist.
+ protected V1Secret GetSecret()
+ {
+ Logger.LogDebug("Getting secret {Name} from namespace {Namespace}",
+ Context.KubeSecretName, Context.KubeNamespace);
+
+ return KubeClient.GetCertificateStoreSecret(Context.KubeSecretName, Context.KubeNamespace);
+ }
+
+ ///
+ /// Creates or updates a TLS or Opaque secret in Kubernetes.
+ /// For JKS/PKCS12, use specialized KubeClient methods instead.
+ ///
+ /// Private key in PEM format.
+ /// Certificate in PEM format.
+ /// Certificate chain as list of PEM strings.
+ /// Secret type (tls or opaque).
+ /// Whether to store chain separately.
+ /// Whether to include chain.
+ /// The created/updated secret.
+ protected V1Secret CreateOrUpdatePemSecret(
+ string keyPem,
+ string certPem,
+ List chainPem,
+ string secretType,
+ bool separateChain = true,
+ bool includeChain = true)
+ {
+ Logger.LogDebug("Creating/updating {Type} secret {Name} in namespace {Namespace}",
+ secretType, Context.KubeSecretName, Context.KubeNamespace);
+
+ return KubeClient.CreateOrUpdateCertificateStoreSecret(
+ keyPem,
+ certPem,
+ chainPem,
+ Context.KubeSecretName,
+ Context.KubeNamespace,
+ secretType,
+ append: false,
+ overwrite: true,
+ remove: false,
+ separateChain: separateChain,
+ includeChain: includeChain);
+ }
+
+ ///
+ /// Deletes a secret from Kubernetes.
+ ///
+ /// Optional alias for keystore entries.
+ protected void DeleteSecret(string alias = "")
+ {
+ Logger.LogDebug("Deleting secret {Name} from namespace {Namespace}",
+ Context.KubeSecretName, Context.KubeNamespace);
+
+ KubeClient.DeleteCertificateStoreSecret(
+ Context.KubeSecretName,
+ Context.KubeNamespace,
+ SecretTypeName,
+ alias);
+ }
+
+ ///
+ /// Checks if the secret exists in Kubernetes.
+ ///
+ /// True if the secret exists.
+ protected bool SecretExists()
+ {
+ try
+ {
+ GetSecret();
+ return true;
+ }
+ catch (StoreNotFoundException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// The private key field names to check during cert-only update validation.
+ /// TLS handlers check only "tls.key"; Opaque handlers check all common key field names.
+ /// Override in subclasses to customize which fields are considered private key fields.
+ ///
+ protected virtual string[] PrivateKeyFieldNames => new[] { "tls.key" };
+
+ ///
+ /// Validates that a cert-only update won't create a key/cert mismatch.
+ /// Throws if the existing secret contains a private key but the incoming cert doesn't.
+ ///
+ protected void ValidateCertOnlyUpdate(V1Secret existingSecret)
+ {
+ Logger.LogDebug("Validating cert-only update for {Type} secret '{SecretName}' in namespace '{Namespace}'",
+ SecretTypeName, Context.KubeSecretName, Context.KubeNamespace);
+ ValidateCertOnlyUpdateCore(existingSecret, PrivateKeyFieldNames,
+ SecretTypeName, Context.KubeSecretName, Context.KubeNamespace, Logger);
+ }
+
+ ///
+ /// Core cert-only update validation logic, separated for testability.
+ /// Iterates and throws if any contains a PEM private key.
+ ///
+ internal static void ValidateCertOnlyUpdateCore(
+ V1Secret existingSecret,
+ string[] privateKeyFieldNames,
+ string secretTypeName,
+ string secretName,
+ string secretNamespace,
+ ILogger logger)
+ {
+ if (existingSecret?.Data == null) return;
+
+ foreach (var field in privateKeyFieldNames)
+ {
+ if (!existingSecret.Data.TryGetValue(field, out var existingKeyBytes) ||
+ existingKeyBytes == null || existingKeyBytes.Length == 0)
+ continue;
+
+ var existingKeyPem = Encoding.UTF8.GetString(existingKeyBytes).Trim();
+ if (!string.IsNullOrEmpty(existingKeyPem) && existingKeyPem.Contains("PRIVATE KEY"))
+ {
+ var errorMsg = $"Cannot update {secretTypeName} secret '{secretName}' in namespace '{secretNamespace}' " +
+ $"with a certificate that has no private key. The existing secret contains a private key ({field}) " +
+ $"which would become mismatched with the new certificate. " +
+ $"Either include the private key with the certificate, or delete the existing secret first.";
+ logger?.LogError(errorMsg);
+ throw new InvalidOperationException(errorMsg);
+ }
+ }
+
+ logger?.LogDebug("Validation passed: existing secret has no private key");
+ }
+
+ ///
+ /// Builds allowed keys list from context and defaults.
+ ///
+ /// Default keys for this handler type.
+ /// Combined list of allowed keys.
+ protected string[] BuildAllowedKeys(string[] defaultKeys)
+ {
+ var keys = new List();
+
+ // Add custom field name if specified
+ if (!string.IsNullOrEmpty(Context.CertificateDataFieldName))
+ {
+ keys.AddRange(Context.CertificateDataFieldName.Split(','));
+ }
+
+ // Add default keys
+ keys.AddRange(defaultKeys);
+
+ return keys.ToArray();
+ }
+
+ ///
+ /// Parses a keystore alias of the form <fieldName>/<certAlias> and resolves the
+ /// corresponding existing data and key name from the supplied inventory.
+ ///
+ /// The raw alias string (e.g. "keystore.jks/mycert" or just "mycert" ).
+ /// The current K8S secret inventory (field → bytes). May be null or empty.
+ /// Field name to use when no prefix is present and the inventory is empty.
+ ///
+ /// A tuple containing:
+ ///
+ /// fieldName — the K8S secret field name extracted from the alias, or null if no separator was found.
+ /// certAlias — the alias inside the keystore file.
+ /// existingData — the current bytes for the resolved field, or null if the field does not yet exist.
+ /// existingKeyName — the resolved field name to write to.
+ ///
+ ///
+ protected (string fieldName, string certAlias, byte[] existingData, string existingKeyName) ParseKeystoreAlias(
+ string alias,
+ Dictionary inventory,
+ string defaultFieldName)
+ {
+ var result = ParseKeystoreAliasCore(alias, inventory, defaultFieldName);
+ Logger.LogDebug("Parsed alias '{Alias}' → field='{Field}', certAlias='{CertAlias}'",
+ alias, result.fieldName ?? "(none)", result.certAlias);
+ return result;
+ }
+
+ ///
+ /// Core alias parsing logic, separated for testability.
+ ///
+ internal static (string fieldName, string certAlias, byte[] existingData, string existingKeyName) ParseKeystoreAliasCore(
+ string alias,
+ Dictionary inventory,
+ string defaultFieldName)
+ {
+ var separatorIdx = alias.IndexOf('/');
+ var fieldName = separatorIdx > 0 ? alias[..separatorIdx] : null;
+ var certAlias = separatorIdx > 0 ? alias[(separatorIdx + 1)..] : alias;
+
+ byte[] existingData = null;
+ string existingKeyName = fieldName ?? defaultFieldName;
+
+ if (inventory != null && inventory.Count > 0)
+ {
+ if (fieldName != null && inventory.TryGetValue(fieldName, out var fieldBytes))
+ {
+ existingData = fieldBytes;
+ }
+ else if (fieldName == null)
+ {
+ var firstKey = inventory.Keys.First();
+ existingData = inventory[firstKey];
+ existingKeyName = firstKey;
+ }
+ // else: fieldName specified but not yet in inventory → existingData stays null (new field)
+ }
+
+ return (fieldName, certAlias, existingData, existingKeyName);
+ }
+
+ ///
+ /// Logs entry to a method.
+ ///
+ /// Name of the method.
+ protected void LogMethodEntry(string methodName)
+ {
+ Logger.MethodEntry(LogLevel.Debug);
+ Logger.LogDebug("Entering {Method} for {Type} in {Namespace}/{Secret}",
+ methodName, SecretTypeName, Context.KubeNamespace, Context.KubeSecretName);
+ }
+
+ ///
+ /// Logs exit from a method.
+ ///
+ /// Name of the method.
+ protected void LogMethodExit(string methodName)
+ {
+ Logger.LogDebug("Exiting {Method}", methodName);
+ Logger.MethodExit(LogLevel.Debug);
+ }
+
+ #endregion
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/SecretHandlerFactory.cs b/kubernetes-orchestrator-extension/Handlers/SecretHandlerFactory.cs
new file mode 100644
index 0000000..793e2bf
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/SecretHandlerFactory.cs
@@ -0,0 +1,108 @@
+// 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 System;
+using System.Collections.Generic;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Enums;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Factory for creating store-type-specific secret handlers.
+/// Maps normalized secret types to their corresponding handler implementations.
+///
+public static class SecretHandlerFactory
+{
+ private static readonly Dictionary> _factories = new()
+ {
+ [SecretTypes.Tls] = (c, l, ctx) => new TlsSecretHandler(c, l, ctx),
+ [SecretTypes.Opaque] = (c, l, ctx) => new OpaqueSecretHandler(c, l, ctx),
+ [SecretTypes.Jks] = (c, l, ctx) => new JksSecretHandler(c, l, ctx),
+ [SecretTypes.Pkcs12] = (c, l, ctx) => new Pkcs12SecretHandler(c, l, ctx),
+ [SecretTypes.Certificate] = (c, l, ctx) => new CertificateSecretHandler(c, l, ctx),
+ [SecretTypes.Cluster] = (c, l, ctx) => new ClusterSecretHandler(c, l, ctx),
+ [SecretTypes.Namespace] = (c, l, ctx) => new NamespaceSecretHandler(c, l, ctx),
+ };
+
+ private static readonly Dictionary _handlerTypeNames = new()
+ {
+ [SecretTypes.Tls] = nameof(TlsSecretHandler),
+ [SecretTypes.Opaque] = nameof(OpaqueSecretHandler),
+ [SecretTypes.Jks] = nameof(JksSecretHandler),
+ [SecretTypes.Pkcs12] = nameof(Pkcs12SecretHandler),
+ [SecretTypes.Certificate] = nameof(CertificateSecretHandler),
+ [SecretTypes.Cluster] = nameof(ClusterSecretHandler),
+ [SecretTypes.Namespace] = nameof(NamespaceSecretHandler),
+ };
+
+ ///
+ /// Creates a secret handler for the specified secret type.
+ ///
+ /// The secret type (will be normalized).
+ /// Kubernetes client for API operations.
+ /// Logger for diagnostic output.
+ /// Operation context with configuration and job parameters.
+ /// An ISecretHandler implementation for the specified type.
+ /// Thrown when the secret type is not supported.
+ public static ISecretHandler Create(
+ string secretType,
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ {
+ if (string.IsNullOrEmpty(secretType))
+ throw new ArgumentNullException(nameof(secretType), "Secret type cannot be null or empty");
+
+ var normalizedType = SecretTypes.Normalize(secretType);
+ if (_factories.TryGetValue(normalizedType, out var factory))
+ return factory(kubeClient, logger, context);
+
+ throw new NotSupportedException($"Secret type '{secretType}' (normalized: '{normalizedType}') is not supported");
+ }
+
+ ///
+ /// Determines if a handler exists for the specified secret type.
+ ///
+ /// The secret type to check.
+ /// True if a handler exists for this type; otherwise, false.
+ public static bool HasHandler(string secretType)
+ {
+ if (string.IsNullOrEmpty(secretType))
+ return false;
+
+ return _factories.ContainsKey(SecretTypes.Normalize(secretType));
+ }
+
+ ///
+ /// Determines if the secret type supports management operations (add/remove).
+ ///
+ /// The secret type to check.
+ /// True if management operations are supported; otherwise, false.
+ public static bool SupportsManagement(string secretType)
+ {
+ if (string.IsNullOrEmpty(secretType))
+ return false;
+
+ // K8SCert (Certificate) is read-only - no management
+ return SecretTypes.Normalize(secretType) is not SecretTypes.Certificate;
+ }
+
+ ///
+ /// Gets the handler type name for the specified secret type (for logging/debugging).
+ ///
+ /// The secret type.
+ /// The handler class name.
+ public static string GetHandlerTypeName(string secretType)
+ {
+ var normalizedType = SecretTypes.Normalize(secretType);
+ return _handlerTypeNames.TryGetValue(normalizedType, out var name)
+ ? name
+ : $"Unknown({secretType})";
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Handlers/TlsSecretHandler.cs b/kubernetes-orchestrator-extension/Handlers/TlsSecretHandler.cs
new file mode 100644
index 0000000..4efa400
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Handlers/TlsSecretHandler.cs
@@ -0,0 +1,289 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Text;
+using k8s.Autorest;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Clients;
+using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+
+///
+/// Handler for kubernetes.io/tls secrets.
+/// TLS secrets contain tls.crt (certificate chain) and tls.key (private key) fields.
+///
+public class TlsSecretHandler : SecretHandlerBase
+{
+ ///
+ /// Default allowed data keys for TLS secrets.
+ ///
+ private static readonly string[] DefaultAllowedKeys = { "tls.crt", "tls.key", "ca.crt" };
+
+ ///
+ public override string[] AllowedKeys => DefaultAllowedKeys;
+
+ ///
+ public override string SecretTypeName => "tls";
+
+ ///
+ public override bool SupportsManagement => true;
+
+ ///
+ /// Initializes a new instance of the TlsSecretHandler.
+ ///
+ public TlsSecretHandler(
+ KubeCertificateManagerClient kubeClient,
+ ILogger logger,
+ ISecretOperationContext context)
+ : base(kubeClient, logger, context)
+ {
+ }
+
+ #region Inventory Operations
+
+ ///
+ public override List GetCertificates(long jobId)
+ {
+ LogMethodEntry(nameof(GetCertificates));
+
+ try
+ {
+ var secret = GetSecret();
+ return ExtractCertificatesFromSecret(secret);
+ }
+ catch (HttpOperationException)
+ {
+ Logger.LogError("Kubernetes TLS secret '{Name}' was not found in namespace '{Namespace}'",
+ Context.KubeSecretName, Context.KubeNamespace);
+ throw new StoreNotFoundException(
+ $"Kubernetes TLS secret '{Context.KubeSecretName}' was not found in namespace '{Context.KubeNamespace}'.");
+ }
+ finally
+ {
+ LogMethodExit(nameof(GetCertificates));
+ }
+ }
+
+ ///
+ public override Dictionary> GetCertificatesWithAliases(long jobId)
+ {
+ // TLS secrets don't use aliases - return single entry with secret name as alias
+ var certs = GetCertificates(jobId);
+ return new Dictionary>
+ {
+ { Context.KubeSecretName, certs }
+ };
+ }
+
+ ///
+ public override List GetInventoryEntries(long jobId)
+ {
+ var certs = GetCertificates(jobId);
+ var hasKey = HasPrivateKey();
+
+ return new List
+ {
+ new InventoryEntry
+ {
+ Alias = Context.KubeSecretName,
+ Certificates = certs,
+ HasPrivateKey = hasKey
+ }
+ };
+ }
+
+ ///
+ public override bool HasPrivateKey()
+ {
+ try
+ {
+ var secret = GetSecret();
+ return secret.Data != null &&
+ secret.Data.TryGetValue("tls.key", out var keyBytes) &&
+ keyBytes != null &&
+ keyBytes.Length > 0;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Management Operations
+
+ ///
+ public override V1Secret HandleAdd(K8SJobCertificate certObj, string alias, bool overwrite)
+ {
+ LogMethodEntry(nameof(HandleAdd));
+
+ try
+ {
+ // Handle "create store if missing" - when no certificate data is provided
+ if (string.IsNullOrEmpty(alias) && string.IsNullOrEmpty(certObj?.CertPem))
+ {
+ return HandleCreateIfMissing();
+ }
+
+ // Check if secret exists
+ V1Secret existingSecret = null;
+ try
+ {
+ existingSecret = GetSecret();
+ }
+ catch (StoreNotFoundException)
+ {
+ // Secret doesn't exist, will create new one
+ }
+
+ if (existingSecret != null && !overwrite)
+ {
+ if (IsSecretEmpty(existingSecret))
+ {
+ Logger.LogDebug("Secret '{Name}' exists but is empty; overwriting implicitly", Context.KubeSecretName);
+ }
+ else
+ {
+ Logger.LogWarning("Secret already exists and overwrite is false");
+ throw new InvalidOperationException(
+ $"Secret '{Context.KubeSecretName}' already exists. Set overwrite=true to replace.");
+ }
+ }
+
+ // Validate cert-only updates: prevent deploying certificate without private key
+ // to an existing secret that has a key (would cause key/cert mismatch)
+ var incomingHasNoPrivateKey = string.IsNullOrEmpty(certObj?.PrivateKeyPem);
+ if (existingSecret != null && overwrite && incomingHasNoPrivateKey)
+ {
+ ValidateCertOnlyUpdate(existingSecret);
+ }
+
+ // Create or update secret using the PEM helper
+ return CreateOrUpdatePemSecret(
+ certObj.PrivateKeyPem,
+ certObj.CertPem,
+ certObj.ChainPem ?? new List(),
+ "tls",
+ Context.SeparateChain,
+ Context.IncludeCertChain);
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleAdd));
+ }
+ }
+
+ ///
+ public override V1Secret HandleRemove(string alias)
+ {
+ LogMethodEntry(nameof(HandleRemove));
+
+ try
+ {
+ // TLS secrets are single-entry, so remove means delete the whole secret
+ DeleteSecret(alias);
+ return null;
+ }
+ finally
+ {
+ LogMethodExit(nameof(HandleRemove));
+ }
+ }
+
+ ///
+ public override V1Secret CreateEmptyStore()
+ {
+ LogMethodEntry(nameof(CreateEmptyStore));
+
+ try
+ {
+ // Create empty TLS secret
+ return CreateOrUpdatePemSecret(
+ "",
+ "",
+ new List(),
+ "tls",
+ separateChain: false,
+ includeChain: false);
+ }
+ finally
+ {
+ LogMethodExit(nameof(CreateEmptyStore));
+ }
+ }
+
+ #endregion
+
+ #region Discovery Operations
+
+ ///
+ public override List DiscoverStores(string[] allowedKeys, string namespacesCsv)
+ {
+ LogMethodEntry(nameof(DiscoverStores));
+
+ try
+ {
+ var keys = allowedKeys?.Length > 0 ? allowedKeys : AllowedKeys;
+ return KubeClient.DiscoverSecrets(keys, "kubernetes.io/tls", namespacesCsv);
+ }
+ finally
+ {
+ LogMethodExit(nameof(DiscoverStores));
+ }
+ }
+
+ #endregion
+
+ #region Private Helpers
+
+ // ValidateCertOnlyUpdate is inherited from SecretHandlerBase.
+ // TlsSecretHandler uses the default PrivateKeyFieldNames = { "tls.key" }.
+
+ private List ExtractCertificatesFromSecret(V1Secret secret)
+ {
+ // Check if tls.crt exists and has data
+ if (secret.Data == null ||
+ !secret.Data.TryGetValue("tls.crt", out var certBytes) ||
+ certBytes == null ||
+ certBytes.Length == 0)
+ {
+ Logger.LogWarning("Secret '{Name}' has no certificate data (tls.crt is empty or missing)",
+ Context.KubeSecretName);
+ return new List();
+ }
+
+ // Extract certificates from tls.crt
+ var sourceDesc = $"secret '{Context.KubeSecretName}' key 'tls.crt'";
+ var certsList = CertExtractor.ExtractCertificates(certBytes, sourceDesc);
+
+ if (certsList.Count == 0)
+ {
+ throw new InvalidOperationException(
+ $"Failed to parse certificate from secret '{Context.KubeSecretName}'. " +
+ "The certificate data could not be parsed as PEM or DER format.");
+ }
+
+ // Add CA chain certificates from ca.crt if present (avoiding duplicates)
+ if (secret.Data.TryGetValue("ca.crt", out var caBytes))
+ {
+ CertExtractor.ExtractAndAppendUnique(
+ caBytes,
+ certsList,
+ $"secret '{Context.KubeSecretName}' key 'ca.crt'");
+ }
+
+ return certsList;
+ }
+
+ #endregion
+}
diff --git a/kubernetes-orchestrator-extension/Models/K8SJobCertificate.cs b/kubernetes-orchestrator-extension/Models/K8SJobCertificate.cs
new file mode 100644
index 0000000..418c0bc
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Models/K8SJobCertificate.cs
@@ -0,0 +1,110 @@
+// 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 System.Collections.Generic;
+using System.Linq;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.X509;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+
+///
+/// Comprehensive data model for a certificate processed during a Keyfactor orchestrator job.
+/// Contains certificate data in multiple formats (PEM, bytes, base64), private key data,
+/// certificate chain information, and password details.
+///
+public class K8SJobCertificate
+{
+ /// Alias/friendly name for the certificate entry.
+ public string Alias { get; set; } = "";
+
+ /// Base64 encoded certificate data.
+ public string CertB64 { get; set; } = "";
+
+ /// Certificate in PEM format.
+ public string CertPem { get; set; } = "";
+
+ /// SHA-1 thumbprint of the certificate for identification.
+ public string CertThumbprint { get; set; } = "";
+
+ /// Raw certificate bytes (DER encoded).
+ public byte[] CertBytes { get; set; }
+
+ /// Private key in PEM format (unencrypted).
+ public string PrivateKeyPem { get; set; } = "";
+
+ /// Raw private key bytes (PKCS#8 format).
+ public byte[] PrivateKeyBytes { get; set; }
+
+ /// BouncyCastle AsymmetricKeyParameter for the private key. Used for format-preserving re-export.
+ public AsymmetricKeyParameter PrivateKeyParameter { get; set; }
+
+ /// Password protecting the private key (if encrypted).
+ public string Password { get; set; } = "";
+
+ /// Indicates if the password is stored in a separate Kubernetes secret.
+ public bool PasswordIsK8SSecret { get; set; } = false;
+
+ /// Password for the certificate store (JKS/PKCS12).
+ public string StorePassword { get; set; } = "";
+
+ /// Path to a separate Kubernetes secret containing the store password.
+ public string StorePasswordPath { get; set; } = "";
+
+ /// Indicates whether this certificate has an associated private key.
+ public bool HasPrivateKey { get; set; } = false;
+
+ /// Indicates whether the certificate/key is password protected.
+ public bool HasPassword { get; set; } = false;
+
+ /// BouncyCastle X509CertificateEntry containing the certificate.
+ public X509CertificateEntry CertificateEntry { get; set; }
+
+ /// BouncyCastle X509CertificateEntry array containing the certificate chain.
+ public X509CertificateEntry[] CertificateEntryChain { get; set; }
+
+ public byte[] Pkcs12 { get; set; }
+
+ public List ChainPem { get; set; }
+
+ ///
+ /// Optional: K8SCertificateContext providing BouncyCastle-based certificate operations.
+ ///
+ public Models.K8SCertificateContext CertificateContext { get; set; }
+
+ ///
+ /// Factory method to create K8SCertificateContext from this job certificate's data.
+ ///
+ public Models.K8SCertificateContext GetCertificateContext()
+ {
+ if (CertificateEntry?.Certificate == null)
+ return null;
+
+ var context = new Models.K8SCertificateContext
+ {
+ Certificate = CertificateEntry.Certificate,
+ CertPem = CertPem,
+ PrivateKeyPem = PrivateKeyPem
+ };
+
+ if (CertificateEntryChain != null && CertificateEntryChain.Length > 0)
+ {
+ context.Chain = CertificateEntryChain
+ .Skip(1)
+ .Select(entry => entry.Certificate)
+ .ToList();
+
+ if (ChainPem != null && ChainPem.Count > 0)
+ {
+ context.ChainPem = ChainPem.Skip(1).ToList();
+ }
+ }
+
+ return context;
+ }
+}
diff --git a/kubernetes-orchestrator-extension/StoreTypes/ICertificateStoreSerializer.cs b/kubernetes-orchestrator-extension/StoreTypes/ICertificateStoreSerializer.cs
deleted file mode 100644
index 7ce63e4..0000000
--- a/kubernetes-orchestrator-extension/StoreTypes/ICertificateStoreSerializer.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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.Collections.Generic;
-using Keyfactor.Extensions.Orchestrator.K8S.Models;
-using Org.BouncyCastle.Pkcs;
-
-namespace Keyfactor.Extensions.Orchestrator.K8S.StoreTypes;
-
-///
-/// Interface for certificate store serializers that handle different keystore formats.
-/// Implemented by JKS and PKCS12 serializers to provide a consistent API for
-/// reading and writing certificate stores.
-///
-internal interface ICertificateStoreSerializer
-{
- ///
- /// Deserializes a certificate store from raw bytes into a Pkcs12Store for manipulation.
- ///
- /// The raw store bytes.
- /// Path to the store (for logging context).
- /// Password to decrypt the store.
- /// A Pkcs12Store containing the certificates and keys.
- Pkcs12Store DeserializeRemoteCertificateStore(byte[] storeContents, string storePath, string storePassword);
-
- ///
- /// Serializes a Pkcs12Store back to the appropriate format for storage.
- ///
- /// The store to serialize.
- /// Directory path for the store.
- /// Filename for the serialized store.
- /// Password to encrypt the store.
- /// List of SerializedStoreInfo containing the serialized bytes and path.
- List SerializeRemoteCertificateStore(Pkcs12Store certificateStore, string storePath,
- string storeFileName, string storePassword);
-
- ///
- /// Gets the path for the private key file (for stores that separate private keys).
- ///
- /// The private key path, or null if not applicable.
- string GetPrivateKeyPath();
-}
\ No newline at end of file
diff --git a/kubernetes-orchestrator-extension/StoreTypes/K8SJKS/Store.cs b/kubernetes-orchestrator-extension/StoreTypes/K8SJKS/Store.cs
deleted file mode 100644
index 4c71de9..0000000
--- a/kubernetes-orchestrator-extension/StoreTypes/K8SJKS/Store.cs
+++ /dev/null
@@ -1,450 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
-using Keyfactor.Extensions.Orchestrator.K8S.Models;
-using Keyfactor.Extensions.Orchestrator.K8S.Utilities;
-using Keyfactor.Logging;
-using Microsoft.Extensions.Logging;
-using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
-using Org.BouncyCastle.Pkcs;
-using Org.BouncyCastle.Security;
-using Org.BouncyCastle.X509;
-
-namespace Keyfactor.Extensions.Orchestrator.K8S.StoreTypes.K8SJKS;
-
-///
-/// Serializer for Java KeyStore (JKS) certificate stores in Kubernetes secrets.
-/// Handles conversion between JKS format and BouncyCastle's Pkcs12Store for internal processing.
-///
-///
-/// JKS stores are converted to PKCS12 internally because BouncyCastle provides better
-/// manipulation capabilities for PKCS12 stores. The conversion is transparent to callers.
-///
-internal class JksCertificateStoreSerializer : ICertificateStoreSerializer
-{
- /// Logger instance for diagnostic output.
- private readonly ILogger _logger;
-
- ///
- /// Initializes a new instance of the JKS certificate store serializer.
- ///
- /// JSON string of store properties (currently unused).
- public JksCertificateStoreSerializer(string storeProperties)
- {
- _logger = LogHandler.GetClassLogger(GetType());
- }
-
- ///
- /// Deserializes a JKS keystore from byte data into a Pkcs12Store for manipulation.
- /// Handles both true JKS format and PKCS12 format that may have been stored as JKS.
- ///
- /// The JKS keystore bytes.
- /// Path to the store (for logging context).
- /// Password to decrypt the keystore.
- /// A Pkcs12Store containing the certificates and keys from the JKS.
- /// Thrown when store password is null or empty.
- /// Thrown when the data is actually PKCS12 format.
- public Pkcs12Store DeserializeRemoteCertificateStore(byte[] storeContents, string storePath, string storePassword)
- {
- _logger.MethodEntry(MsLogLevel.Debug);
- var storeBuilder = new Pkcs12StoreBuilder();
- var pkcs12Store = storeBuilder.Build();
- var pkcs12StoreNew = storeBuilder.Build();
-
- _logger.LogTrace("storePath: {Path}", storePath);
-
- if (string.IsNullOrEmpty(storePassword))
- {
- _logger.LogError("JKS store password is null or empty for store at path '{Path}'", storePath);
- throw new ArgumentException("JKS store password is null or empty");
- }
-
- _logger.LogTrace("StorePassword: {Password}", LoggingUtilities.RedactPassword(storePassword));
- _logger.LogTrace("Password correlation: {CorrelationId}", LoggingUtilities.GetPasswordCorrelationId(storePassword));
-
- var jksStore = new JksStore();
-
- _logger.LogDebug("Loading JKS store");
- try
- {
- _logger.LogTrace("Attempting to load JKS store with provided password");
-
- using (var ms = new MemoryStream(storeContents))
- {
- jksStore.Load(ms, string.IsNullOrEmpty(storePassword) ? [] : storePassword.ToCharArray());
- }
-
- _logger.LogDebug("JKS store loaded");
- }
- catch (Exception ex)
- {
- _logger.LogError("Error loading JKS store: {Ex}", ex.Message);
- if (ex.Message.Contains("password incorrect or store tampered with"))
- {
- if (storePassword == string.Empty)
- {
- _logger.LogError("Unable to load JKS store using empty password, please provide a valid password");
- }
- else
- {
- _logger.LogError("Unable to load JKS store using provided password: {Password}", LoggingUtilities.RedactPassword(storePassword));
- _logger.LogTrace("Password correlation: {CorrelationId}", LoggingUtilities.GetPasswordCorrelationId(storePassword));
- }
-
- throw;
- }
-
- // Attempt to read JKS store as Pkcs12Store
- try
- {
- if (string.IsNullOrEmpty(storePassword))
- {
- _logger.LogError("JKS store password is null or empty for store at path '{Path}'", storePath);
- throw new ArgumentException("JKS store password is null or empty");
- }
-
- _logger.LogDebug("Attempting to load JKS store as Pkcs12Store using provided password");
-
- using (var ms = new MemoryStream(storeContents))
- {
- pkcs12Store.Load(ms, string.IsNullOrEmpty(storePassword) ? [] : storePassword.ToCharArray());
- }
-
- _logger.LogDebug("JKS store loaded as Pkcs12Store");
- // return pkcs12Store;
- throw new JkSisPkcs12Exception("JKS store is actually a Pkcs12Store");
- }
- catch (Exception ex2)
- {
- _logger.LogError("Error loading JKS store as Jks or Pkcs12Store: {Ex}", ex2.Message);
- throw;
- }
- }
-
- _logger.LogDebug("Converting JKS store to Pkcs12Store ny iterating over aliases");
- foreach (var alias in jksStore.Aliases)
- {
- _logger.LogDebug("Processing alias '{Alias}'", alias);
-
- _logger.LogDebug("Getting key for alias '{Alias}'", alias);
- var keyParam = jksStore.GetKey(alias,
- string.IsNullOrEmpty(storePassword) ? [] : storePassword.ToCharArray());
-
- _logger.LogDebug("Creating AsymmetricKeyEntry for alias '{Alias}'", alias);
- var keyEntry = new AsymmetricKeyEntry(keyParam);
-
- if (jksStore.IsKeyEntry(alias))
- {
- _logger.LogDebug("Alias '{Alias}' is a key entry", alias);
- _logger.LogDebug("Getting certificate chain for alias '{Alias}'", alias);
- var certificateChain = jksStore.GetCertificateChain(alias);
-
- _logger.LogDebug("Adding key entry and certificate chain to Pkcs12Store");
- pkcs12Store.SetKeyEntry(alias, keyEntry,
- certificateChain.Select(certificate => new X509CertificateEntry(certificate)).ToArray());
- }
- else
- {
- _logger.LogDebug("Alias '{Alias}' is a certificate entry", alias);
- _logger.LogDebug("Setting certificate for alias '{Alias}'", alias);
- pkcs12Store.SetCertificateEntry(alias, new X509CertificateEntry(jksStore.GetCertificate(alias)));
- }
- }
-
- // Second Pkcs12Store necessary because of an obscure BC bug where creating a Pkcs12Store without .Load (code above using "Set" methods only) does not set all
- // internal hashtables necessary to avoid an error later when processing store.
- var ms2 = new MemoryStream();
- _logger.LogDebug("Saving Pkcs12Store to MemoryStream using provided password");
- pkcs12Store.Save(ms2, string.IsNullOrEmpty(storePassword) ? [] : storePassword.ToCharArray(),
- new SecureRandom());
- ms2.Position = 0;
-
- _logger.LogDebug("Loading Pkcs12Store from MemoryStream");
- pkcs12StoreNew.Load(ms2, string.IsNullOrEmpty(storePassword) ? [] : storePassword.ToCharArray());
-
- _logger.LogDebug("Returning Pkcs12Store");
- _logger.MethodExit(MsLogLevel.Debug);
- return pkcs12StoreNew;
- }
-
- ///
- /// Serializes a Pkcs12Store back to JKS format for storage in Kubernetes.
- ///
- /// The Pkcs12Store to serialize.
- /// Directory path for the store.
- /// Filename for the serialized store.
- /// Password to encrypt the keystore.
- /// List of SerializedStoreInfo containing the JKS bytes and path.
- public List SerializeRemoteCertificateStore(Pkcs12Store certificateStore, string storePath,
- string storeFileName, string storePassword)
- {
- _logger.MethodEntry(MsLogLevel.Debug);
-
- var jksStore = new JksStore();
-
- foreach (var alias in certificateStore.Aliases)
- {
- var keyEntry = certificateStore.GetKey(alias);
- var certificateChain = certificateStore.GetCertificateChain(alias);
- var certificates = new List();
- if (certificateStore.IsKeyEntry(alias))
- {
- certificates.AddRange(certificateChain.Select(certificateEntry => certificateEntry.Certificate));
- _logger.LogDebug("Processing key entry for alias '{Alias}' using provided password", alias);
- jksStore.SetKeyEntry(alias, keyEntry.Key,
- string.IsNullOrEmpty(storePassword) ? [] : storePassword.ToCharArray(), certificates.ToArray());
- }
- else
- {
- jksStore.SetCertificateEntry(alias, certificateStore.GetCertificate(alias).Certificate);
- }
- }
-
- using var outStream = new MemoryStream();
- _logger.LogDebug("Saving JKS store to MemoryStream using provided password");
- jksStore.Save(outStream, string.IsNullOrEmpty(storePassword) ? [] : storePassword.ToCharArray());
-
- var storeInfo = new List
- { new() { FilePath = Path.Combine(storePath, storeFileName), Contents = outStream.ToArray() } };
-
- _logger.MethodExit(MsLogLevel.Debug);
- return storeInfo;
- }
-
- ///
- /// Returns the private key path (not applicable for JKS stores).
- ///
- /// Always returns null for JKS stores.
- public string GetPrivateKeyPath()
- {
- return null;
- }
-
- ///
- /// Creates a new JKS store or updates an existing one with a new certificate.
- /// Handles both add and remove operations.
- ///
- /// PKCS12 bytes containing the new certificate to add.
- /// Password for the new certificate's private key.
- /// Alias for the certificate entry in the JKS.
- /// Existing JKS store bytes (null for new store).
- /// Password for the existing store.
- /// True to remove the certificate, false to add.
- /// Whether to include the certificate chain.
- /// The updated JKS store as byte array.
- /// Thrown when the existing store is actually PKCS12 format.
- public byte[] CreateOrUpdateJks(byte[] newPkcs12Bytes, string newCertPassword, string alias,
- byte[] existingStore = null, string existingStorePassword = null,
- bool remove = false, bool includeChain = true)
- {
- _logger.MethodEntry(MsLogLevel.Debug);
- // If existingStore is null, create a new store
- var existingJksStore = new JksStore();
- var newJksStore = new JksStore();
- var createdNewStore = false;
-
- _logger.LogTrace("alias: {Alias}", alias);
- _logger.LogTrace("newCertPassword: {Password}", LoggingUtilities.RedactPassword(newCertPassword));
- _logger.LogTrace("existingStorePassword: {Password}", LoggingUtilities.RedactPassword(existingStorePassword));
-
- // If existingStore is not null, load it into jksStore
- if (existingStore != null)
- {
- _logger.LogDebug("Loading existing JKS store");
- using var ms = new MemoryStream(existingStore);
-
- try
- {
- existingJksStore.Load(ms,
- string.IsNullOrEmpty(existingStorePassword) ? [] : existingStorePassword.ToCharArray());
- }
- catch (Exception ex)
- {
- _logger.LogError("Error loading existing JKS store: {Ex}", ex.Message);
-
- if (ex.Message.Contains("password incorrect or store tampered with"))
- {
- _logger.LogError("Unable to load existing JKS store using provided password: {Password}", LoggingUtilities.RedactPassword(existingStorePassword));
- _logger.LogTrace("Password correlation: {CorrelationId}", LoggingUtilities.GetPasswordCorrelationId(existingStorePassword));
- throw;
- }
-
- try
- {
- _logger.LogDebug("Attempting to load existing JKS store as Pkcs12Store");
- var pkcs12Store = new Pkcs12StoreBuilder().Build();
- using (var ms2 = new MemoryStream(existingStore))
- {
- pkcs12Store.Load(ms2,
- string.IsNullOrEmpty(existingStorePassword) ? [] : existingStorePassword.ToCharArray());
- }
-
- _logger.LogDebug("Existing JKS store loaded as Pkcs12Store");
- // return pkcs12Store;
- throw new JkSisPkcs12Exception("Existing JKS store is actually a Pkcs12Store");
- }
- catch (Exception ex2)
- {
- _logger.LogError("Error loading existing JKS store as Jks or Pkcs12Store: {Ex}", ex2.Message);
- throw;
- }
- }
-
- if (existingJksStore.ContainsAlias(alias))
- {
- // If alias exists, delete it from existingJksStore
- _logger.LogDebug("Alias '{Alias}' exists in existing JKS store, deleting it", alias);
- existingJksStore.DeleteEntry(alias);
- if (remove)
- {
- // If remove is true, save existingJksStore and return
- _logger.LogDebug("This is a removal operation, saving existing JKS store");
- using var mms = new MemoryStream();
- existingJksStore.Save(mms,
- string.IsNullOrEmpty(existingStorePassword) ? [] : existingStorePassword.ToCharArray());
- _logger.LogDebug("Returning existing JKS store");
- return mms.ToArray();
- }
- }
- else if (remove)
- {
- // If alias does not exist and remove is true, return existingStore
- _logger.LogDebug(
- "Alias '{Alias}' does not exist in existing JKS store and this is a removal operation, returning existing JKS store as-is",
- alias);
- using var mms = new MemoryStream();
- existingJksStore.Save(mms,
- string.IsNullOrEmpty(existingStorePassword) ? [] : existingStorePassword.ToCharArray());
- return mms.ToArray();
- }
- }
- else
- {
- _logger.LogDebug("Existing JKS store is null, creating new JKS store");
- createdNewStore = true;
- }
-
- // Create new Pkcs12Store from newPkcs12Bytes
- var storeBuilder = new Pkcs12StoreBuilder();
- var newCert = storeBuilder.Build();
-
- try
- {
- _logger.LogDebug("Loading new Pkcs12Store from newPkcs12Bytes");
- _logger.LogTrace("PKCS12 data: {Data}", LoggingUtilities.RedactPkcs12Bytes(newPkcs12Bytes));
- using var pkcs12Ms = new MemoryStream(newPkcs12Bytes);
- if (pkcs12Ms.Length != 0) newCert.Load(pkcs12Ms, (newCertPassword ?? string.Empty).ToCharArray());
- }
- catch (Exception)
- {
- _logger.LogDebug("Loading new Pkcs12Store from newPkcs12Bytes failed, trying to load as X509Certificate");
- var certificateParser = new X509CertificateParser();
- var certificate = certificateParser.ReadCertificate(newPkcs12Bytes);
-
- _logger.LogDebug("Creating new Pkcs12Store from certificate");
- // create new Pkcs12Store from certificate
- storeBuilder = new Pkcs12StoreBuilder();
- newCert = storeBuilder.Build();
- _logger.LogDebug("Setting certificate entry in new Pkcs12Store as alias '{Alias}'", alias);
- newCert.SetCertificateEntry(alias, new X509CertificateEntry(certificate));
- }
-
-
- // Iterate through newCert aliases.
- _logger.LogDebug("Iterating through new Pkcs12Store aliases");
- foreach (var al in newCert.Aliases)
- {
- _logger.LogTrace("Alias: {Alias}", al);
- if (newCert.IsKeyEntry(al))
- {
- _logger.LogDebug("Alias '{Alias}' is a key entry, getting key entry and certificate chain", al);
- var keyEntry = newCert.GetKey(al);
- _logger.LogDebug("Getting certificate chain for alias '{Alias}'", al);
- var certificateChain = newCert.GetCertificateChain(al);
- if (!includeChain)
- {
- _logger.LogDebug("includeChain is false, reducing certificate chain to only the end-entity certificate");
- // If includeChain is false, reduce certificate chain to only the end-entity certificate
- certificateChain =
- [
- new X509CertificateEntry(certificateChain[0].Certificate)
- ];
- }
-
- _logger.LogDebug("Creating certificate list from certificate chain");
- var certificates = certificateChain.Select(certificateEntry => certificateEntry.Certificate).ToList();
-
- if (createdNewStore)
- {
- // If createdNewStore is true, create a new store
- _logger.LogDebug("Created new JKS store, setting key entry for alias '{Alias}'", al);
- newJksStore.SetKeyEntry(alias,
- keyEntry.Key,
- string.IsNullOrEmpty(existingStorePassword) ? [] : existingStorePassword.ToCharArray(),
- certificates.ToArray());
- }
- else
- {
- // If createdNewStore is false, add to existingJksStore
- // check if alias exists in existingJksStore
- if (existingJksStore.ContainsAlias(alias))
- {
- // If alias exists, delete it from existingJksStore
- _logger.LogDebug("Alias '{Alias}' exists in existing JKS store, deleting it", alias);
- existingJksStore.DeleteEntry(alias);
- }
-
- _logger.LogDebug("Setting key entry for alias '{Alias}'", alias);
- existingJksStore.SetKeyEntry(alias,
- keyEntry.Key,
- string.IsNullOrEmpty(existingStorePassword) ? [] : existingStorePassword.ToCharArray(),
- certificates.ToArray());
- }
- }
- else
- {
- if (createdNewStore)
- {
- _logger.LogDebug("Created new JKS store, setting certificate entry for alias '{Alias}'", alias);
- _logger.LogDebug("Setting certificate entry for new JKS store, alias '{Alias}'", alias);
- newJksStore.SetCertificateEntry(alias, newCert.GetCertificate(alias).Certificate);
- }
- else
- {
- _logger.LogDebug("Setting certificate entry for existing JKS store, alias '{Alias}'", alias);
- existingJksStore.SetCertificateEntry(alias, newCert.GetCertificate(alias).Certificate);
- }
- }
- }
-
- using var outStream = new MemoryStream();
- if (createdNewStore)
- {
- _logger.LogDebug("Created new JKS store, saving it to outStream");
- newJksStore.Save(outStream,
- string.IsNullOrEmpty(existingStorePassword) ? [] : existingStorePassword.ToCharArray());
- }
- else
- {
- _logger.LogDebug("Saving existing JKS store to outStream");
- existingJksStore.Save(outStream,
- string.IsNullOrEmpty(existingStorePassword) ? [] : existingStorePassword.ToCharArray());
- }
-
- // Return existingJksStore as byte[]
- _logger.LogDebug("JKS store operation complete");
- _logger.MethodExit(MsLogLevel.Debug);
- return outStream.ToArray();
- }
-}
\ No newline at end of file
diff --git a/kubernetes-orchestrator-extension/StoreTypes/K8SPKCS12/Store.cs b/kubernetes-orchestrator-extension/StoreTypes/K8SPKCS12/Store.cs
deleted file mode 100644
index 825904f..0000000
--- a/kubernetes-orchestrator-extension/StoreTypes/K8SPKCS12/Store.cs
+++ /dev/null
@@ -1,326 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.IO;
-using Keyfactor.Extensions.Orchestrator.K8S.Models;
-using Keyfactor.Logging;
-using Microsoft.Extensions.Logging;
-using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
-using Org.BouncyCastle.Pkcs;
-using Org.BouncyCastle.Security;
-using Org.BouncyCastle.X509;
-
-namespace Keyfactor.Extensions.Orchestrator.K8S.StoreTypes.K8SPKCS12;
-
-///
-/// Serializer for PKCS12/PFX certificate stores in Kubernetes secrets.
-/// Handles loading, saving, and manipulation of PKCS12 stores.
-///
-internal class Pkcs12CertificateStoreSerializer : ICertificateStoreSerializer
-{
- /// Logger instance for diagnostic output.
- private readonly ILogger _logger;
-
- ///
- /// Initializes a new instance of the PKCS12 certificate store serializer.
- ///
- /// JSON string of store properties (currently unused).
- public Pkcs12CertificateStoreSerializer(string storeProperties)
- {
- _logger = LogHandler.GetClassLogger(GetType());
- }
-
- ///
- /// Deserializes a PKCS12 keystore from byte data.
- ///
- /// The PKCS12 keystore bytes.
- /// Path to the store (for logging context).
- /// Password to decrypt the keystore.
- /// A Pkcs12Store containing the certificates and keys.
- public Pkcs12Store DeserializeRemoteCertificateStore(byte[] storeContents, string storePath, string storePassword)
- {
- _logger.MethodEntry(MsLogLevel.Debug);
-
- var storeBuilder = new Pkcs12StoreBuilder();
- var store = storeBuilder.Build();
-
- using var ms = new MemoryStream(storeContents);
- _logger.LogDebug("Loading Pkcs12Store from MemoryStream from {Path}", storePath);
- store.Load(ms, string.IsNullOrEmpty(storePassword) ? Array.Empty() : storePassword.ToCharArray());
- _logger.LogDebug("Pkcs12Store loaded from {Path}", storePath);
- _logger.MethodExit(MsLogLevel.Debug);
- return store;
- }
-
- ///
- /// Serializes a Pkcs12Store back to PKCS12 format for storage in Kubernetes.
- ///
- /// The Pkcs12Store to serialize.
- /// Directory path for the store.
- /// Filename for the serialized store.
- /// Password to encrypt the keystore.
- /// List of SerializedStoreInfo containing the PKCS12 bytes and path.
- public List SerializeRemoteCertificateStore(Pkcs12Store certificateStore, string storePath,
- string storeFileName, string storePassword)
- {
- _logger.MethodEntry(MsLogLevel.Debug);
-
- var storeBuilder = new Pkcs12StoreBuilder();
- var pkcs12Store = storeBuilder.Build();
-
- foreach (var alias in certificateStore.Aliases)
- {
- _logger.LogDebug("Processing alias '{Alias}'", alias);
- var keyEntry = certificateStore.GetKey(alias);
-
- if (certificateStore.IsKeyEntry(alias))
- {
- _logger.LogDebug("Alias '{Alias}' is a key entry", alias);
- pkcs12Store.SetKeyEntry(alias, keyEntry, certificateStore.GetCertificateChain(alias));
- }
- else
- {
- _logger.LogDebug("Alias '{Alias}' is a certificate entry", alias);
- var certEntry = certificateStore.GetCertificate(alias);
- _logger.LogTrace("Certificate entry '{Entry}'", certEntry.Certificate.SubjectDN.ToString());
- _logger.LogDebug("Attempting to SetCertificateEntry for '{Alias}'", alias);
- pkcs12Store.SetCertificateEntry(alias, certEntry);
- }
- }
-
- using var outStream = new MemoryStream();
- _logger.LogDebug("Saving Pkcs12Store to MemoryStream");
- pkcs12Store.Save(outStream,
- string.IsNullOrEmpty(storePassword) ? Array.Empty() : storePassword.ToCharArray(),
- new SecureRandom());
-
- var storeInfo = new List();
-
- _logger.LogDebug("Adding store to list of serialized stores");
- var filePath = Path.Combine(storePath, storeFileName);
- _logger.LogDebug("Filepath '{Path}'", filePath);
- storeInfo.Add(new SerializedStoreInfo
- {
- FilePath = filePath,
- Contents = outStream.ToArray()
- });
-
- _logger.MethodExit(MsLogLevel.Debug);
- return storeInfo;
- }
-
- ///
- /// Returns the private key path (not applicable for PKCS12 stores).
- ///
- /// Always returns null for PKCS12 stores.
- public string GetPrivateKeyPath()
- {
- return null;
- }
-
- ///
- /// Creates a new PKCS12 store or updates an existing one with a new certificate.
- /// Handles both add and remove operations.
- ///
- /// PKCS12 bytes containing the new certificate to add.
- /// Password for the new certificate's private key.
- /// Alias for the certificate entry in the store.
- /// Existing PKCS12 store bytes (null for new store).
- /// Password for the existing store.
- /// True to remove the certificate, false to add.
- /// Whether to include the certificate chain.
- /// The updated PKCS12 store as byte array.
- public byte[] CreateOrUpdatePkcs12(byte[] newPkcs12Bytes, string newCertPassword, string alias,
- byte[] existingStore = null, string existingStorePassword = null,
- bool remove = false, bool includeChain = true)
- {
- _logger.MethodEntry(MsLogLevel.Debug);
-
- _logger.LogDebug("Creating or updating PKCS12 store for alias '{Alias}'", alias);
- // If existingStore is null, create a new store
- var storeBuilder = new Pkcs12StoreBuilder();
- var existingPkcs12Store = storeBuilder.Build();
- var pkcs12StoreNew = storeBuilder.Build();
- var createdNewStore = false;
-
- // If existingStore is not null, load it into pkcs12Store
- if (existingStore != null)
- {
- _logger.LogDebug("Attempting to load existing Pkcs12Store");
- using var ms = new MemoryStream(existingStore);
- existingPkcs12Store.Load(ms,
- string.IsNullOrEmpty(existingStorePassword)
- ? Array.Empty()
- : existingStorePassword.ToCharArray());
- _logger.LogDebug("Existing Pkcs12Store loaded");
-
- _logger.LogDebug("Checking if alias '{Alias}' exists in existingPkcs12Store", alias);
- if (existingPkcs12Store.ContainsAlias(alias))
- {
- // If alias exists, delete it from existingPkcs12Store
- _logger.LogDebug("Alias '{Alias}' exists in existingPkcs12Store", alias);
- _logger.LogDebug("Deleting alias '{Alias}' from existingPkcs12Store", alias);
- existingPkcs12Store.DeleteEntry(alias);
- if (remove)
- {
- // If remove is true, save existingPkcs12Store and return
- _logger.LogDebug("Alias '{Alias}' was removed from existing store", alias);
- using var mms = new MemoryStream();
- _logger.LogDebug("Saving removal operation");
- existingPkcs12Store.Save(mms,
- string.IsNullOrEmpty(existingStorePassword)
- ? Array.Empty()
- : existingStorePassword.ToCharArray(), new SecureRandom());
-
- _logger.LogDebug("Converting existingPkcs12Store to byte[] and returning");
- _logger.MethodExit(MsLogLevel.Debug);
- return mms.ToArray();
- }
- }
- else if (remove)
- {
- // If alias does not exist and remove is true, return existingStore
- _logger.LogDebug("Alias '{Alias}' does not exist in existingPkcs12Store, nothing to remove", alias);
- using var existingPkcs12StoreMs = new MemoryStream();
- existingPkcs12Store.Save(existingPkcs12StoreMs,
- string.IsNullOrEmpty(existingStorePassword)
- ? Array.Empty()
- : existingStorePassword.ToCharArray(),
- new SecureRandom());
-
- _logger.LogDebug("Converting existingPkcs12Store to byte[] and returning");
- _logger.MethodExit(MsLogLevel.Debug);
- return existingPkcs12StoreMs.ToArray();
- }
- }
- else
- {
- _logger.LogDebug("Attempting to create new Pkcs12Store");
- createdNewStore = true;
- }
-
- var newCert = storeBuilder.Build();
-
- try
- {
- _logger.LogDebug("Attempting to load pkcs12 bytes");
- using var newPkcs12Ms = new MemoryStream(newPkcs12Bytes);
- newCert.Load(newPkcs12Ms,
- string.IsNullOrEmpty(newCertPassword) ? Array.Empty() : newCertPassword.ToCharArray());
- _logger.LogDebug("pkcs12 bytes loaded");
- }
- catch (Exception)
- {
- _logger.LogError("Unknown error loading pkcs12 bytes, attempting to parse certificate");
- var certificateParser = new X509CertificateParser();
- var certificate = certificateParser.ReadCertificate(newPkcs12Bytes);
- _logger.LogDebug("Certificate parse successful, attempting to create new Pkcs12Store from certificate");
-
- // create new Pkcs12Store from certificate
- storeBuilder = new Pkcs12StoreBuilder();
- newCert = storeBuilder.Build();
-
- _logger.LogDebug("Attempting to set PKCS12 certificate entry using alias '{Alias}'", alias);
- newCert.SetCertificateEntry(alias, new X509CertificateEntry(certificate));
- _logger.LogDebug("PKCS12 certificate entry set using alias '{Alias}'", alias);
- }
-
-
- // Iterate through newCert aliases. WARNING: This assumes there is only one alias in the newCert
- _logger.LogTrace("Iterating through PKCS12 certificate aliases");
- foreach (var al in newCert.Aliases)
- {
- _logger.LogTrace("Handling alias {Alias}", al);
- if (newCert.IsKeyEntry(al))
- {
- _logger.LogDebug("Attempting to parse key for alias {Alias}", al);
- var keyEntry = newCert.GetKey(al);
- _logger.LogDebug("Key parsed for alias {Alias}", al);
-
- _logger.LogDebug("Attempting to parse certificate chain for alias {Alias}", al);
- var certificateChain = newCert.GetCertificateChain(al);
- if (!includeChain)
- {
- _logger.LogDebug("includeChain is false, reducing certificate chain to only the end-entity certificate");
- // If includeChain is false, reduce certificate chain to only the end-entity certificate
- certificateChain =
- [
- new X509CertificateEntry(certificateChain[0].Certificate)
- ];
- }
- _logger.LogDebug("Certificate chain parsed for alias {Alias}", al);
- if (createdNewStore)
- {
- // If createdNewStore is true, create a new store
- _logger.LogDebug("Attempting to set key entry for alias '{Alias}'", alias);
- pkcs12StoreNew.SetKeyEntry(
- alias,
- keyEntry,
- certificateChain
- );
- }
- else
- {
- // If createdNewStore is false, add to existingPkcs12Store
- // check if alias exists in existingPkcs12Store
- if (existingPkcs12Store.ContainsAlias(alias))
- {
- _logger.LogDebug("Removing existing entry for alias '{Alias}'", alias);
- // If alias exists, delete it from existingPkcs12Store
- existingPkcs12Store.DeleteEntry(alias);
- }
-
- _logger.LogDebug("Attempting to set key entry for alias '{Alias}'", alias);
- existingPkcs12Store.SetKeyEntry(
- alias,
- keyEntry,
- // string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(),
- certificateChain
- );
- }
- }
- else
- {
- if (createdNewStore)
- {
- _logger.LogDebug("Attempting to set certificate entry for alias '{Alias}'", alias);
- pkcs12StoreNew.SetCertificateEntry(alias, newCert.GetCertificate(alias));
- }
- else
- {
- _logger.LogDebug("Attempting to set certificate entry for alias '{Alias}'", alias);
- existingPkcs12Store.SetCertificateEntry(alias, newCert.GetCertificate(alias));
- }
- }
- }
-
- using var outStream = new MemoryStream();
- if (createdNewStore)
- {
- _logger.LogDebug("Attempting to save new Pkcs12Store");
- pkcs12StoreNew.Save(outStream,
- string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(),
- new SecureRandom());
- _logger.LogDebug("New Pkcs12Store saved");
- }
- else
- {
- _logger.LogDebug("Attempting to save existing Pkcs12Store");
- existingPkcs12Store.Save(outStream,
- string.IsNullOrEmpty(existingStorePassword) ? Array.Empty() : existingStorePassword.ToCharArray(),
- new SecureRandom());
- _logger.LogDebug("Existing Pkcs12Store saved");
- }
- // Return existingPkcs12Store as byte[]
-
- _logger.LogDebug("Converting existingPkcs12Store to byte[] and returning");
- _logger.MethodExit(MsLogLevel.Debug);
- return outStream.ToArray();
- }
-}
\ No newline at end of file
From ab3fe8aa67cf405ff65da03f26760b770bd1e5ce Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Wed, 15 Apr 2026 12:35:02 -0700
Subject: [PATCH 03/28] refactor: split monolithic KubeClient into focused
client components
KubeClient.cs was a 3000+ line file mixing authentication, kubeconfig
parsing, secret CRUD, and CSR operations. Split into:
- KubeconfigParser: parses kubeconfig JSON into typed configuration,
validates required fields, provides clear error messages
- SecretOperations: Kubernetes secret CRUD (create, read, update, delete,
list) with retry logic and structured logging
- CertificateOperations: CSR-specific operations (list, read, approve,
inject certificate status)
- KubeClient (KubeCertificateManagerClient): now a thin coordinator
that initialises the authenticated client and delegates to the above
Also removes unreachable code branches, converts string interpolation
log calls to structured logging throughout, and adds retry logic with
configurable backoff.
---
.../Clients/CertificateOperations.cs | 154 ++
.../Clients/KubeClient.cs | 2118 ++---------------
.../Clients/KubeconfigParser.cs | 334 +++
.../Clients/SecretOperations.cs | 337 +++
4 files changed, 985 insertions(+), 1958 deletions(-)
create mode 100644 kubernetes-orchestrator-extension/Clients/CertificateOperations.cs
create mode 100644 kubernetes-orchestrator-extension/Clients/KubeconfigParser.cs
create mode 100644 kubernetes-orchestrator-extension/Clients/SecretOperations.cs
diff --git a/kubernetes-orchestrator-extension/Clients/CertificateOperations.cs b/kubernetes-orchestrator-extension/Clients/CertificateOperations.cs
new file mode 100644
index 0000000..08b0b6e
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Clients/CertificateOperations.cs
@@ -0,0 +1,154 @@
+// 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 System;
+using System.Collections.Generic;
+using Keyfactor.Extensions.Orchestrator.K8S.Enums;
+using Keyfactor.Extensions.Orchestrator.K8S.Utilities;
+using Keyfactor.Logging;
+using Keyfactor.PKI.PEM;
+using Microsoft.Extensions.Logging;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.X509;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Clients;
+
+///
+/// Provides certificate parsing, conversion, and chain operations.
+/// Delegates to for core logic.
+///
+public class CertificateOperations
+{
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of CertificateOperations.
+ ///
+ /// Logger instance for diagnostic output. If null, creates a default logger.
+ public CertificateOperations(ILogger logger = null)
+ {
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ /// Reads a DER-encoded certificate from a base64 string.
+ ///
+ /// Base64-encoded DER certificate data.
+ /// Parsed X509Certificate object.
+ public X509Certificate ReadDerCertificate(string derString)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+ var derData = Convert.FromBase64String(derString);
+ var cert = CertificateUtilities.ParseCertificateFromDer(derData);
+ _logger.LogDebug("Parsed DER certificate: {Summary}", LoggingUtilities.GetCertificateSummary(cert));
+ _logger.MethodExit(LogLevel.Debug);
+ return cert;
+ }
+
+ ///
+ /// Reads a PEM-encoded certificate from a string.
+ /// Returns null if the input is not a valid certificate (unlike which throws).
+ ///
+ /// PEM-encoded certificate string.
+ /// Parsed X509Certificate object, or null if not a valid certificate.
+ public X509Certificate ReadPemCertificate(string pemString)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+ try
+ {
+ var cert = CertificateUtilities.ParseCertificateFromPem(pemString);
+ _logger.LogDebug("Parsed PEM certificate: {Summary}", LoggingUtilities.GetCertificateSummary(cert));
+ _logger.MethodExit(LogLevel.Debug);
+ return cert;
+ }
+ catch
+ {
+ _logger.LogDebug("PEM object is not a valid certificate, returning null");
+ _logger.MethodExit(LogLevel.Debug);
+ return null;
+ }
+ }
+
+ ///
+ /// Loads a certificate chain from PEM data containing multiple certificates.
+ ///
+ /// PEM string potentially containing multiple certificates.
+ /// List of parsed X509Certificate objects.
+ public List LoadCertificateChain(string pemData)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+ var certificates = CertificateUtilities.LoadCertificateChain(pemData);
+ _logger.LogDebug("Loaded {Count} certificates from chain", certificates.Count);
+ _logger.MethodExit(LogLevel.Debug);
+ return certificates;
+ }
+
+ ///
+ /// Converts a BouncyCastle X509Certificate to PEM format.
+ ///
+ /// The certificate to convert.
+ /// PEM-formatted certificate string.
+ public string ConvertToPem(X509Certificate certificate)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+ var pem = PemUtilities.DERToPEM(certificate.GetEncoded(), PemUtilities.PemObjectType.Certificate);
+ _logger.MethodExit(LogLevel.Debug);
+ return pem;
+ }
+
+ ///
+ /// Extracts a private key from a PKCS12 store and converts it to PEM format.
+ /// Supports RSA, EC, Ed25519, and Ed448 private keys.
+ ///
+ /// The PKCS12 store containing the private key.
+ /// Password for the store (currently unused, key is already decrypted).
+ /// The desired PEM format (PKCS1 or PKCS8). Defaults to PKCS8.
+ /// PEM-formatted private key string.
+ /// Thrown when no private key is found or key type is unsupported.
+ public string ExtractPrivateKeyAsPem(Pkcs12Store store, string password, PrivateKeyFormat format = PrivateKeyFormat.Pkcs8)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+
+ var privateKey = CertificateUtilities.ExtractPrivateKey(store);
+ var keyTypeName = PrivateKeyFormatUtilities.GetAlgorithmName(privateKey);
+ _logger.LogDebug("Private key type: {KeyType}, requested format: {Format}", keyTypeName, format);
+
+ var pem = PrivateKeyFormatUtilities.ExportPrivateKeyAsPem(privateKey, format);
+
+ _logger.LogTrace("Private key: {Key}", LoggingUtilities.RedactPrivateKeyPem(pem));
+ _logger.MethodExit(LogLevel.Debug);
+ return pem;
+ }
+
+ ///
+ /// Parses a certificate from PEM string using BouncyCastle.
+ ///
+ /// PEM-encoded certificate string.
+ /// Parsed X509Certificate object.
+ public X509Certificate ParseCertificateFromPem(string pemCertificate)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+ var cert = CertificateUtilities.ParseCertificateFromPem(pemCertificate);
+ _logger.LogDebug("Parsed certificate: {Summary}", LoggingUtilities.GetCertificateSummary(cert));
+ _logger.MethodExit(LogLevel.Debug);
+ return cert;
+ }
+
+ ///
+ /// Parses a certificate from DER bytes using BouncyCastle.
+ ///
+ /// DER-encoded certificate bytes.
+ /// Parsed X509Certificate object.
+ public X509Certificate ParseCertificateFromDer(byte[] derBytes)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+ var cert = CertificateUtilities.ParseCertificateFromDer(derBytes);
+ _logger.LogDebug("Parsed certificate: {Summary}", LoggingUtilities.GetCertificateSummary(cert));
+ _logger.MethodExit(LogLevel.Debug);
+ return cert;
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Clients/KubeClient.cs b/kubernetes-orchestrator-extension/Clients/KubeClient.cs
index b3fc117..70a50b4 100644
--- a/kubernetes-orchestrator-extension/Clients/KubeClient.cs
+++ b/kubernetes-orchestrator-extension/Clients/KubeClient.cs
@@ -8,10 +8,10 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Reflection;
using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
@@ -23,9 +23,11 @@
using k8s.Models;
using Keyfactor.Extensions.Orchestrator.K8S.Enums;
using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
+using Keyfactor.Extensions.Orchestrator.K8S.Services;
using Keyfactor.Extensions.Orchestrator.K8S.Utilities;
using Keyfactor.Logging;
using Keyfactor.Orchestrators.Extensions;
+using Keyfactor.PKI.Extensions;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -46,6 +48,10 @@ namespace Keyfactor.Extensions.Orchestrator.K8S.Clients;
public class KubeCertificateManagerClient
{
private readonly ILogger _logger;
+ private readonly KubeconfigParser _kubeconfigParser;
+ private readonly PasswordResolver _passwordResolver;
+ private readonly CertificateOperations _certificateOperations;
+ private SecretOperations _secretOperations;
///
/// Initializes a new instance of the class.
@@ -55,15 +61,19 @@ public class KubeCertificateManagerClient
public KubeCertificateManagerClient(string kubeconfig, bool useSSL = true)
{
_logger = LogHandler.GetClassLogger(MethodBase.GetCurrentMethod()?.DeclaringType);
+ _kubeconfigParser = new KubeconfigParser(_logger);
+ _passwordResolver = new PasswordResolver(_logger);
+ _certificateOperations = new CertificateOperations(_logger);
_logger.MethodEntry(LogLevel.Debug);
_logger.LogTrace("Kubeconfig: {Kubeconfig}", LoggingUtilities.RedactKubeconfig(kubeconfig));
_logger.LogTrace("UseSSL: {UseSSL}", useSSL);
Client = GetKubeClient(kubeconfig);
+ _secretOperations = new SecretOperations(Client, _logger);
ConfigJson = kubeconfig;
try
{
- ConfigObj = ParseKubeConfig(kubeconfig, !useSSL); // invert useSSL to skip TLS verification
+ ConfigObj = _kubeconfigParser.Parse(kubeconfig, !useSSL); // invert useSSL to skip TLS verification
_logger.LogDebug("Successfully parsed kubeconfig for cluster: {ClusterName}", ConfigObj.CurrentContext ?? "unknown");
}
catch (Exception ex)
@@ -151,184 +161,6 @@ public string GetHost()
return host;
}
- ///
- /// Parses a kubeconfig JSON string into a K8SConfiguration object.
- /// Extracts cluster, user, and context information for API authentication.
- ///
- /// JSON-formatted kubeconfig string.
- /// When true, skips TLS certificate verification.
- /// Parsed K8SConfiguration object.
- private K8SConfiguration ParseKubeConfig(string kubeconfig, bool skipTLSVerify = false)
- {
- _logger.MethodEntry(LogLevel.Debug);
- _logger.LogTrace("Kubeconfig length: {Length}, skipTLSVerify: {SkipTLS}", kubeconfig?.Length ?? 0, skipTLSVerify);
- _logger.LogTrace("Kubeconfig: {Kubeconfig}", LoggingUtilities.RedactKubeconfig(kubeconfig));
-
- try
- {
- var k8SConfiguration = new K8SConfiguration();
- _logger.LogTrace("K8SConfiguration object created");
-
- _logger.LogTrace("Checking if kubeconfig is null or empty");
- if (string.IsNullOrEmpty(kubeconfig))
- {
- _logger.LogError("kubeconfig is null or empty");
- throw new KubeConfigException(
- "kubeconfig is null or empty, please provide a valid kubeconfig in JSON format. For more information on how to create a kubeconfig file, please visit https://github.com/Keyfactor/k8s-orchestrator/tree/main/scripts/kubernetes#example-service-account-json");
- }
-
- try
- {
- // test if kubeconfig is base64 encoded
- _logger.LogDebug("Testing if kubeconfig is base64 encoded");
- var decodedKubeconfig = Encoding.UTF8.GetString(Convert.FromBase64String(kubeconfig));
- kubeconfig = decodedKubeconfig;
- _logger.LogDebug("Successfully decoded kubeconfig from base64");
- }
- catch
- {
- _logger.LogTrace("Kubeconfig is not base64 encoded");
- }
-
- _logger.LogTrace("Checking if kubeconfig is escaped JSON");
- if (kubeconfig.StartsWith("\\"))
- {
- _logger.LogDebug("Un-escaping kubeconfig JSON");
- kubeconfig = kubeconfig.Replace("\\", "");
- kubeconfig = kubeconfig.Replace("\\n", "\n");
- _logger.LogDebug("Successfully un-escaped kubeconfig JSON");
- }
-
- // parse kubeconfig as a dictionary of string, string
- if (!kubeconfig.StartsWith("{"))
- {
- _logger.LogError("kubeconfig is not a JSON object");
- throw new KubeConfigException(
- "kubeconfig is not a JSON object, please provide a valid kubeconfig in JSON format. For more information on how to create a kubeconfig file, please visit: https://github.com/Keyfactor/k8s-orchestrator/tree/main/scripts/kubernetes#get_service_account_credssh");
- // return k8SConfiguration;
- }
-
-
- _logger.LogDebug("Parsing kubeconfig as a dictionary of string, string");
-
- //load json into dictionary of string, string
- _logger.LogTrace("Deserializing kubeconfig JSON");
- var configDict = JsonConvert.DeserializeObject>(kubeconfig);
- _logger.LogTrace("Deserialized kubeconfig JSON successfully");
-
- _logger.LogTrace("Creating K8SConfiguration object");
- k8SConfiguration = new K8SConfiguration
- {
- ApiVersion = configDict["apiVersion"].ToString(),
- Kind = configDict["kind"].ToString(),
- CurrentContext = configDict["current-context"].ToString(),
- Clusters = new List(),
- Users = new List(),
- Contexts = new List()
- };
-
- // parse clusters
- _logger.LogDebug("Parsing clusters");
- var cl = configDict["clusters"];
-
- _logger.LogTrace("Entering foreach loop to parse clusters...");
- foreach (var clusterMetadata in JsonConvert.DeserializeObject(cl.ToString() ?? string.Empty))
- {
- _logger.LogTrace("Creating Cluster object for cluster '{Name}'", clusterMetadata["name"]?.ToString());
- // get environment variable for skip tls verify and convert to bool
- var skipTlsEnvStr = Environment.GetEnvironmentVariable("KEYFACTOR_ORCHESTRATOR_SKIP_TLS_VERIFY");
- _logger.LogTrace("KEYFACTOR_ORCHESTRATOR_SKIP_TLS_VERIFY environment variable: {SkipTlsVerify}",
- skipTlsEnvStr);
- if (!string.IsNullOrEmpty(skipTlsEnvStr) &&
- (bool.TryParse(skipTlsEnvStr, out var skipTlsVerifyEnv) || skipTlsEnvStr == "1"))
- {
- if (skipTlsEnvStr == "1") skipTlsVerifyEnv = true;
- _logger.LogDebug("Setting skip-tls-verify to {SkipTlsVerify}", skipTlsVerifyEnv);
- if (skipTlsVerifyEnv && !skipTLSVerify)
- {
- _logger.LogWarning(
- "Skipping TLS verification is enabled in environment variable KEYFACTOR_ORCHESTRATOR_SKIP_TLS_VERIFY this takes the highest precedence and verification will be skipped. To disable this, set the environment variable to 'false' or remove it");
- skipTLSVerify = true;
- }
- }
-
- var clusterObj = new Cluster
- {
- Name = clusterMetadata["name"]?.ToString(),
- ClusterEndpoint = new ClusterEndpoint
- {
- Server = clusterMetadata["cluster"]?["server"]?.ToString(),
- CertificateAuthorityData = clusterMetadata["cluster"]?["certificate-authority-data"]?.ToString(),
- SkipTlsVerify = skipTLSVerify
- }
- };
- _logger.LogDebug("Cluster metadata - Name: {Name}, Server: {Server}, SkipTlsVerify: {SkipTls}",
- clusterObj.Name, clusterObj.ClusterEndpoint?.Server, skipTLSVerify);
- _logger.LogTrace("Certificate authority data: {CaDataPresence}",
- LoggingUtilities.GetFieldPresence("certificate-authority-data", clusterObj.ClusterEndpoint?.CertificateAuthorityData));
- k8SConfiguration.Clusters = new List { clusterObj };
- }
-
- _logger.LogTrace("Finished parsing clusters");
-
- _logger.LogDebug("Parsing users");
- _logger.LogTrace("Entering foreach loop to parse users...");
- // parse users
- foreach (var user in JsonConvert.DeserializeObject(configDict["users"].ToString() ?? string.Empty))
- {
- var token = user["user"]?["token"]?.ToString();
- var userObj = new User
- {
- Name = user["name"]?.ToString(),
- UserCredentials = new UserCredentials
- {
- UserName = user["name"]?.ToString(),
- Token = token
- }
- };
- _logger.LogDebug("User metadata - Name: {Name}, HasToken: {HasToken}",
- userObj.Name, !string.IsNullOrEmpty(token));
- _logger.LogTrace("Token: {Token}", LoggingUtilities.RedactToken(token));
- k8SConfiguration.Users = new List { userObj };
- }
-
- _logger.LogTrace("Finished parsing users");
-
- _logger.LogDebug("Parsing contexts");
- _logger.LogTrace("Entering foreach loop to parse contexts...");
- foreach (var ctx in JsonConvert.DeserializeObject(configDict["contexts"].ToString() ?? string.Empty))
- {
- _logger.LogTrace("Creating Context object");
- var contextObj = new Context
- {
- Name = ctx["name"]?.ToString(),
- ContextDetails = new ContextDetails
- {
- Cluster = ctx["context"]?["cluster"]?.ToString(),
- Namespace = ctx["context"]?["namespace"]?.ToString(),
- User = ctx["context"]?["user"]?.ToString()
- }
- };
- _logger.LogDebug("Context metadata - Name: {Name}, Cluster: {Cluster}, Namespace: {Namespace}, User: {User}",
- contextObj.Name, contextObj.ContextDetails?.Cluster, contextObj.ContextDetails?.Namespace, contextObj.ContextDetails?.User);
- k8SConfiguration.Contexts = new List { contextObj };
- }
-
- _logger.LogTrace("Finished parsing contexts");
- _logger.LogDebug("Finished parsing kubeconfig");
-
- _logger.MethodExit(LogLevel.Debug);
- return k8SConfiguration;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "CRITICAL ERROR in ParseKubeConfig: {Message}", ex.Message);
- _logger.LogError("Exception Type: {Type}", ex.GetType().FullName);
- _logger.LogError("Stack Trace: {StackTrace}", ex.StackTrace);
- throw;
- }
- }
-
///
/// Creates and configures a Kubernetes API client from the provided kubeconfig.
/// Implements retry logic for transient connection failures.
@@ -338,55 +170,24 @@ private K8SConfiguration ParseKubeConfig(string kubeconfig, bool skipTLSVerify =
private IKubernetes GetKubeClient(string kubeconfig)
{
_logger.MethodEntry(LogLevel.Debug);
- _logger.LogTrace("Getting executing assembly location");
- var strExeFilePath = Assembly.GetExecutingAssembly().Location;
- _logger.LogTrace("Executing assembly location: {ExeFilePath}", strExeFilePath);
-
- _logger.LogTrace("Getting executing assembly directory");
- var strWorkPath = Path.GetDirectoryName(strExeFilePath);
- _logger.LogTrace("Executing assembly directory: {WorkPath}", strWorkPath);
- var credentialFileName = kubeconfig;
- // Logger.LogDebug($"credentialFileName: {credentialFileName}");
- _logger.LogDebug("Calling ParseKubeConfig()");
- var k8SConfiguration = ParseKubeConfig(kubeconfig);
- _logger.LogDebug("Finished calling ParseKubeConfig()");
+ // Use the parser; handle initialization order (parser may not be set yet in constructor)
+ var parser = _kubeconfigParser ?? new KubeconfigParser(_logger);
+ _logger.LogDebug("Calling KubeconfigParser.Parse()");
+ var k8SConfiguration = parser.Parse(kubeconfig);
+ _logger.LogDebug("Finished calling KubeconfigParser.Parse()");
- // use k8sConfiguration over credentialFileName
KubernetesClientConfiguration config;
- if (k8SConfiguration != null) // Config defined in store parameters takes highest precedence
+ try
{
- try
- {
- _logger.LogDebug(
- "Config defined in store parameters takes highest precedence - calling BuildConfigFromConfigObject()");
- config = KubernetesClientConfiguration.BuildConfigFromConfigObject(k8SConfiguration);
- _logger.LogDebug("Finished calling BuildConfigFromConfigObject()");
- }
- catch (Exception e)
- {
- _logger.LogError("Error building config from config object: {Error}", e.Message);
- config = KubernetesClientConfiguration.BuildDefaultConfig();
- }
+ _logger.LogDebug("Calling BuildConfigFromConfigObject()");
+ config = KubernetesClientConfiguration.BuildConfigFromConfigObject(k8SConfiguration);
+ _logger.LogDebug("Finished calling BuildConfigFromConfigObject()");
}
- else if
- (string.IsNullOrEmpty(
- credentialFileName)) // If no config defined in store parameters, use default config. This should never happen though.
+ catch (Exception e)
{
- _logger.LogWarning(
- "No config defined in store parameters, using default config. This should never happen!");
+ _logger.LogError("Error building config from config object: {Error}", e.Message);
config = KubernetesClientConfiguration.BuildDefaultConfig();
- _logger.LogDebug("Finished calling BuildDefaultConfig()");
- }
- else
- {
- _logger.LogDebug("Calling BuildConfigFromConfigFile()");
- config = KubernetesClientConfiguration.BuildConfigFromConfigFile(
- strWorkPath != null && !credentialFileName.Contains(strWorkPath)
- ? Path.Join(strWorkPath, credentialFileName)
- : // Else attempt to load config from file
- credentialFileName); // Else attempt to load config from file
- _logger.LogDebug("Finished calling BuildConfigFromConfigFile()");
}
_logger.LogDebug("Creating Kubernetes client");
@@ -395,7 +196,6 @@ private IKubernetes GetKubeClient(string kubeconfig)
IKubernetes client = new Kubernetes(config);
_logger.LogDebug("Finished creating Kubernetes client");
- _logger.LogTrace("Setting Client property");
Client = client;
_logger.MethodExit(LogLevel.Debug);
return client;
@@ -408,865 +208,14 @@ private IKubernetes GetKubeClient(string kubeconfig)
}
}
- ///
- /// Finds an alias in a PKCS12 store by matching the certificate's Common Name.
- ///
- /// The PKCS12 store to search.
- /// The Common Name to match (case-insensitive, partial match).
- /// The matching alias, or null if not found.
- private string FindAliasByCN(Pkcs12Store store, string cn)
- {
- _logger.MethodEntry(LogLevel.Debug);
- _logger.LogTrace("Searching for CN: {CN}", cn);
- if (store == null || string.IsNullOrEmpty(cn))
- {
- _logger.LogDebug("Store or CN is null/empty, returning null");
- _logger.MethodExit(LogLevel.Debug);
- return null;
- }
-
- foreach (var alias in store.Aliases)
- {
- if (!store.IsKeyEntry(alias))
- continue;
-
- var certEntry = store.GetCertificate(alias);
- if (certEntry?.Certificate == null)
- continue;
-
- var subjectCN = Keyfactor.Extensions.Orchestrator.K8S.Utilities.CertificateUtilities.GetSubjectCN(certEntry.Certificate);
- if (!string.IsNullOrEmpty(subjectCN) && subjectCN.Contains(cn, StringComparison.OrdinalIgnoreCase))
- return alias;
- }
-
- return null;
- }
-
- ///
- /// Find an alias in a PKCS12 store by thumbprint
- ///
- private string FindAliasByThumbprint(Pkcs12Store store, string thumbprint)
- {
- if (store == null || string.IsNullOrEmpty(thumbprint))
- return null;
-
- foreach (var alias in store.Aliases)
- {
- var certEntry = store.GetCertificate(alias);
- if (certEntry?.Certificate == null)
- continue;
-
- var certThumbprint = Keyfactor.Extensions.Orchestrator.K8S.Utilities.CertificateUtilities.GetThumbprint(certEntry.Certificate);
- if (certThumbprint.Equals(thumbprint, StringComparison.OrdinalIgnoreCase))
- return alias;
- }
-
- return null;
- }
-
- ///
- /// Find an alias in a PKCS12 store by alias name (partial match on subject DN)
- ///
- private string FindAliasByName(Pkcs12Store store, string aliasSearch)
- {
- if (store == null || string.IsNullOrEmpty(aliasSearch))
- return null;
-
- // First try exact match
- if (store.ContainsAlias(aliasSearch))
- return aliasSearch;
-
- // Then try partial match on subject DN
- foreach (var alias in store.Aliases)
- {
- var certEntry = store.GetCertificate(alias);
- if (certEntry?.Certificate == null)
- continue;
-
- var subjectDN = certEntry.Certificate.SubjectDN.ToString();
- if (!string.IsNullOrEmpty(subjectDN) && subjectDN.Contains(aliasSearch, StringComparison.OrdinalIgnoreCase))
- return alias;
- }
-
- return null;
- }
-
- [Obsolete("Use FindAliasByCN with Pkcs12Store instead")]
- public X509Certificate2 FindCertificateByCN(X509Certificate2Collection certificates, string cn)
- {
- var foundCertificate = certificates
- .OfType()
- .FirstOrDefault(cert => cert.SubjectName.Name.Contains($"CN={cn}", StringComparison.OrdinalIgnoreCase));
-
- return foundCertificate;
- }
-
- [Obsolete("Use FindAliasByThumbprint with Pkcs12Store instead")]
- public X509Certificate2 FindCertificateByThumbprint(X509Certificate2Collection certificates, string thumbprint)
- {
- var foundCertificate = certificates
- .OfType()
- .FirstOrDefault(cert => cert.Thumbprint == thumbprint);
-
- return foundCertificate;
- }
-
- [Obsolete("Use FindAliasByName with Pkcs12Store instead")]
- public X509Certificate2 FindCertificateByAlias(X509Certificate2Collection certificates, string alias)
- {
- var foundCertificate = certificates
- .OfType()
- .FirstOrDefault(cert => cert.SubjectName.Name != null && cert.SubjectName.Name.Contains(alias));
-
- return foundCertificate;
- }
-
- ///
- /// Removes a certificate from a PKCS12 secret store in Kubernetes.
- /// Loads the existing store, removes the matching certificate entry, and updates the secret.
- ///
- /// The certificate to remove, containing thumbprint or alias for matching.
- /// Name of the Kubernetes secret containing the PKCS12 store.
- /// Kubernetes namespace where the secret resides.
- /// Type of secret (e.g., "pkcs12", "pfx").
- /// Field name within the secret containing the PKCS12 data.
- /// Password for the PKCS12 store.
- /// Existing secret data object.
- /// When true, appends to existing entries.
- /// When true, overwrites existing entries.
- /// When true, password is stored in a separate Kubernetes secret.
- /// Path to the password secret if passwdIsK8SSecret is true.
- /// Field name containing the password in the password secret.
- /// Array of allowed field names to process.
- /// The updated V1Secret object.
- public V1Secret RemoveFromPKCS12SecretStore(K8SJobCertificate jobCertificate, string secretName,
- string namespaceName, string secretType, string certDataFieldName,
- string storePasswd, V1Secret k8SSecretData,
- bool append = false, bool overwrite = true, bool passwdIsK8SSecret = false, string passwordSecretPath = "",
- string passwordFieldName = "password",
- string[] certdataFieldNames = null)
- {
- _logger.MethodEntry(LogLevel.Debug);
- _logger.LogTrace("Parameters - SecretName: {SecretName}, Namespace: {Namespace}, SecretType: {SecretType}",
- secretName, namespaceName, secretType);
- _logger.LogTrace("Password: {Password}", LoggingUtilities.RedactPassword(storePasswd));
- _logger.LogTrace("Calling GetSecret()");
- var existingPkcs12DataObj = Client.CoreV1.ReadNamespacedSecret(secretName, namespaceName);
-
-
- // Load existing PKCS12 store
- var storeBuilder = new Pkcs12StoreBuilder();
- Pkcs12Store existingStore = null;
- var storePasswordBytes = Encoding.UTF8.GetBytes("");
-
- if (existingPkcs12DataObj?.Data == null)
- {
- _logger.LogTrace("existingPkcs12DataObj.Data is null");
- }
- else
- {
- _logger.LogTrace("existingPkcs12DataObj.Data is not null");
-
- foreach (var fieldName in existingPkcs12DataObj?.Data.Keys)
- {
- //check if key is in certdataFieldNames
- //if fieldname contains a . then split it and use the last part
- var searchFieldName = fieldName;
- certDataFieldName = fieldName;
- if (fieldName.Contains("."))
- {
- var splitFieldName = fieldName.Split(".");
- searchFieldName = splitFieldName[splitFieldName.Length - 1];
- }
-
- if (certdataFieldNames != null && !certdataFieldNames.Contains(searchFieldName)) continue;
-
- _logger.LogTrace($"Loading PKCS12 store from field '{fieldName}'");
- if (jobCertificate.PasswordIsK8SSecret)
- {
- if (!string.IsNullOrEmpty(jobCertificate.StorePasswordPath))
- {
- _logger.LogDebug("Password is stored in K8S secret at path: {Path}", jobCertificate.StorePasswordPath);
- var passwordPath = jobCertificate.StorePasswordPath.Split("/");
- var passwordNamespace = passwordPath[0];
- var passwordSecretName = passwordPath[1];
- _logger.LogDebug("Buddy secret metadata - Name: {Name}, Namespace: {Namespace}, Field: {Field}",
- passwordSecretName, passwordNamespace, passwordFieldName);
-
- // Get password from k8s secret
- var k8sPasswordObj = ReadBuddyPass(passwordSecretName, passwordNamespace);
- _logger.LogTrace("Buddy secret: {Summary}", LoggingUtilities.GetSecretSummary(k8sPasswordObj));
-
- storePasswordBytes = k8sPasswordObj.Data[passwordFieldName];
- var storePasswdString = Encoding.UTF8.GetString(storePasswordBytes).TrimEnd('\r', '\n');
- _logger.LogTrace("Password from buddy secret: {Password}", LoggingUtilities.RedactPassword(storePasswdString));
- _logger.LogTrace("Password correlation: {CorrelationId}", LoggingUtilities.GetPasswordCorrelationId(storePasswdString));
-
- existingStore = storeBuilder.Build();
- using var ms = new MemoryStream(existingPkcs12DataObj.Data[fieldName]);
- existingStore.Load(ms, storePasswdString.ToCharArray());
- }
- else
- {
- _logger.LogDebug("Password is stored in same secret, field: {Field}", passwordFieldName);
- storePasswordBytes = existingPkcs12DataObj.Data[passwordFieldName];
- var storePasswdString = Encoding.UTF8.GetString(storePasswordBytes).TrimEnd('\r', '\n');
- _logger.LogTrace("Password from secret field: {Password}", LoggingUtilities.RedactPassword(storePasswdString));
- _logger.LogTrace("Password correlation: {CorrelationId}", LoggingUtilities.GetPasswordCorrelationId(storePasswdString));
-
- existingStore = storeBuilder.Build();
- using var ms = new MemoryStream(existingPkcs12DataObj.Data[fieldName]);
- existingStore.Load(ms, storePasswdString.ToCharArray());
- }
- }
- else if (!string.IsNullOrEmpty(jobCertificate.StorePassword))
- {
- _logger.LogDebug("Using password from job configuration");
- storePasswordBytes = Encoding.UTF8.GetBytes(jobCertificate.StorePassword);
- var storePasswdString = Encoding.UTF8.GetString(storePasswordBytes);
- _logger.LogTrace("Password: {Password}", LoggingUtilities.RedactPassword(storePasswdString));
- _logger.LogTrace("Password correlation: {CorrelationId}", LoggingUtilities.GetPasswordCorrelationId(storePasswdString));
-
- existingStore = storeBuilder.Build();
- using var ms = new MemoryStream(existingPkcs12DataObj.Data[fieldName]);
- existingStore.Load(ms, storePasswdString.ToCharArray());
- }
- else
- {
- _logger.LogDebug("Using default store password");
- storePasswordBytes = Encoding.UTF8.GetBytes(storePasswd);
- var storePasswdString = Encoding.UTF8.GetString(storePasswordBytes);
- _logger.LogTrace("Password: {Password}", LoggingUtilities.RedactPassword(storePasswdString));
- _logger.LogTrace("Password correlation: {CorrelationId}", LoggingUtilities.GetPasswordCorrelationId(storePasswdString));
-
- existingStore = storeBuilder.Build();
- using var ms = new MemoryStream(existingPkcs12DataObj.Data[fieldName]);
- existingStore.Load(ms, storePasswdString.ToCharArray());
- }
- }
-
- if (existingStore != null && existingStore.Count > 0)
- {
- // Check if overwrite is true, if so, remove the certificate
- if (overwrite)
- {
- _logger.LogTrace("Overwrite is true, removing existing cert");
-
- var foundAlias = FindAliasByName(existingStore, jobCertificate.Alias);
- if (foundAlias != null)
- {
- // Certificate found
- // remove the found certificate
- _logger.LogTrace($"Certificate found with alias '{foundAlias}', removing it");
- existingStore.DeleteEntry(foundAlias);
- }
- }
- }
- }
-
-
- _logger.LogTrace("Creating V1Secret object");
-
- byte[] p12bytes;
- if (existingStore != null)
- {
- using var outStream = new MemoryStream();
- existingStore.Save(outStream, Encoding.UTF8.GetString(storePasswordBytes).ToCharArray(), new SecureRandom());
- p12bytes = outStream.ToArray();
- }
- else
- {
- p12bytes = Array.Empty();
- }
-
- var secret = new V1Secret
- {
- ApiVersion = "v1",
- Kind = "Secret",
- Metadata = new V1ObjectMeta
- {
- Name = secretName,
- NamespaceProperty = namespaceName
- },
- Type = "Opaque",
- Data = new Dictionary
- {
- { certDataFieldName, p12bytes }
- }
- };
- switch (string.IsNullOrEmpty(storePasswd))
- {
- case false
- when string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8SSecret
- : // password is not empty and passwordSecretPath is empty
- {
- _logger.LogDebug("Adding password to secret...");
- if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password";
- secret.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(storePasswd));
- break;
- }
- case false
- when !string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8SSecret
- : // password is not empty and passwordSecretPath is not empty
- {
- _logger.LogDebug("Adding password secret path to secret...");
- if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "passwordSecretPath";
- secret.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordSecretPath));
-
- // Lookup password secret path on cluster to see if it exists
- _logger.LogDebug("Attempting to lookup password secret path on cluster...");
- var splitPasswordPath = passwordSecretPath.Split("/");
- // Assume secret pattern is namespace/secretName
- var passwordSecretName = splitPasswordPath[^1];
- var passwordSecretNamespace = splitPasswordPath[0];
- _logger.LogDebug(
- $"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- try
- {
- var passwordSecret =
- Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace);
- // storePasswd = Encoding.UTF8.GetString(passwordSecret.Data[passwordFieldName]);
- _logger.LogDebug(
- $"Successfully found secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- // Update secret
- _logger.LogDebug(
- $"Attempting to update secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- passwordSecret.Data[passwordFieldName] = Encoding.UTF8.GetBytes(storePasswd);
- var updatedPasswordSecret = Client.CoreV1.ReplaceNamespacedSecret(passwordSecret,
- passwordSecretName, passwordSecretNamespace);
- _logger.LogDebug(
- $"Successfully updated secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- }
- catch (HttpOperationException e)
- {
- _logger.LogError(
- $"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- _logger.LogError(e.Message);
- // Attempt to create a new secret
- _logger.LogDebug(
- $"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- var passwordSecretData = new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = passwordSecretName,
- NamespaceProperty = passwordSecretNamespace
- },
- Data = new Dictionary
- {
- { passwordFieldName, Encoding.UTF8.GetBytes(storePasswd) }
- }
- };
- var createdPasswordSecret =
- Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace);
- _logger.LogDebug("Successfully created secret " + passwordSecretPath);
- }
-
- break;
- }
- }
-
- // Update secret on K8S
- _logger.LogTrace("Calling UpdateSecret()");
- var updatedSecret = Client.CoreV1.ReplaceNamespacedSecret(secret, secretName, namespaceName);
-
- _logger.LogTrace("Finished creating V1Secret object");
-
- _logger.MethodExit(LogLevel.Debug);
- return updatedSecret;
- }
-
- ///
- /// Updates a PKCS12 secret store in Kubernetes by adding or modifying certificate entries.
- /// Supports password storage in a separate "buddy" secret for security.
- ///
- /// The certificate to add/update in the store.
- /// Name of the Kubernetes secret containing the PKCS12 store.
- /// Kubernetes namespace where the secret resides.
- /// Type of secret (e.g., "pkcs12", "pfx").
- /// Field name within the secret containing the PKCS12 data.
- /// Password for the PKCS12 store.
- /// Existing secret data object.
- /// When true, appends to existing entries.
- /// When true, overwrites existing entries with same alias.
- /// When true, password is stored in a separate Kubernetes secret.
- /// Path to the password secret if passwdIsK8sSecret is true.
- /// Field name containing the password in the password secret.
- /// Array of allowed field names to process.
- /// When true, removes the certificate instead of adding.
- /// The updated V1Secret object.
- public V1Secret UpdatePKCS12SecretStore(K8SJobCertificate jobCertificate, string secretName, string namespaceName,
- string secretType, string certdataFieldName,
- string storePasswd, V1Secret k8SSecretData,
- bool append = false, bool overwrite = true, bool passwdIsK8sSecret = false, string passwordSecretPath = "",
- string passwordFieldName = "password",
- string[] certdataFieldNames = null, bool remove = false)
- {
- _logger.MethodEntry(LogLevel.Debug);
- _logger.LogTrace("Parameters - SecretName: {SecretName}, Namespace: {Namespace}, Overwrite: {Overwrite}, Append: {Append}",
- secretName, namespaceName, overwrite, append);
- _logger.LogTrace("Calling GetSecret()");
- var existingPkcs12DataObj = Client.CoreV1.ReadNamespacedSecret(secretName, namespaceName);
- // var existingPkcs12Bytes = existingPkcs12DataObj.Data[certdataFieldName];
- // var existingPkcs12 = new X509Certificate2Collection();
- // existingPkcs12.Import(existingPkcs12Bytes, storePasswd, X509KeyStorageFlags.Exportable);
-
- // Load existing PKCS12 store
- var storeBuilder = new Pkcs12StoreBuilder();
- Pkcs12Store existingStore = null;
- var storePasswordBytes = Encoding.UTF8.GetBytes("");
-
- if (existingPkcs12DataObj?.Data == null)
- {
- _logger.LogTrace("existingPkcs12DataObj.Data is null");
- }
- else
- {
- _logger.LogTrace("existingPkcs12DataObj.Data is not null");
-
- // KeyValuePair updated_data = new KeyValuePair();
-
- foreach (var fieldName in existingPkcs12DataObj?.Data.Keys)
- {
- //check if key is in certdataFieldNames
- //if fieldname contains a . then split it and use the last part
- var searchFieldName = fieldName;
- if (fieldName.Contains("."))
- {
- var splitFieldName = fieldName.Split(".");
- searchFieldName = splitFieldName[splitFieldName.Length - 1];
- }
-
- if (certdataFieldNames != null && !certdataFieldNames.Contains(searchFieldName)) continue;
-
- certdataFieldName = fieldName;
- _logger.LogTrace("Adding cert '{FieldName}' to existingPkcs12", fieldName);
- if (jobCertificate.PasswordIsK8SSecret)
- {
- _logger.LogDebug("Job certificate password is a K8S secret");
- if (!string.IsNullOrEmpty(jobCertificate.StorePasswordPath))
- {
- _logger.LogDebug("Job certificate store password path is {StorePasswordPath}",
- jobCertificate.StorePasswordPath);
-
- _logger.LogDebug("Splitting store password path into namespace and secret name");
- var passwordPath = jobCertificate.StorePasswordPath.Split("/");
-
- string passwordNamespace;
- string passwordSecretName;
-
- if (passwordPath.Length == 1)
- {
- _logger.LogDebug("Password path length is 1, using KubeNamespace");
- passwordNamespace = namespaceName;
- _logger.LogTrace("Password namespace: {Namespace}", passwordNamespace);
- passwordSecretName = passwordPath[0];
- }
- else
- {
- _logger.LogDebug(
- "Password path length is not 1, using passwordPath[0] and passwordPath[^1]");
- passwordNamespace = passwordPath[0];
- _logger.LogTrace("Password namespace: {Namespace}", passwordNamespace);
- passwordSecretName = passwordPath[^1];
- }
-
- _logger.LogDebug("Password namespace: {PasswordNamespace}", passwordNamespace);
- _logger.LogDebug("Password secret name: {PasswordSecretName}", passwordSecretName);
-
- var k8sPasswordObj = ReadBuddyPass(passwordSecretName, passwordNamespace);
- _logger.LogDebug(
- "Successfully read password secret {PasswordSecretName} in namespace {PasswordNamespace}",
- passwordSecretName, passwordNamespace);
-
- if (k8sPasswordObj?.Data == null)
- {
- _logger.LogError("Unable to read K8S buddy secret {SecretName} in namespace {Namespace}",
- passwordSecretName, passwordNamespace);
- throw new InvalidK8SSecretException(
- $"Unable to read K8S buddy secret {passwordSecretName} in namespace {passwordNamespace}");
- }
-
- _logger.LogTrace("Secret response fields: {Keys}", k8sPasswordObj.Data.Keys);
-
- if (!k8sPasswordObj.Data.TryGetValue(passwordFieldName, out storePasswordBytes) ||
- storePasswordBytes == null)
- {
- _logger.LogError("Unable to find password field {FieldName}", passwordFieldName);
- throw new InvalidK8SSecretException(
- $"Unable to find password field '{passwordFieldName}' in secret '{passwordSecretName}' in namespace '{passwordNamespace}'"
- );
- }
-
- // storePasswordBytes = k8sPasswordObj.Data[passwordFieldName];
- if (storePasswordBytes == null || storePasswordBytes.Length == 0)
- {
- _logger.LogError(
- "Password field {FieldName} in secret {SecretName} in namespace {Namespace} is empty",
- passwordFieldName, passwordSecretName, passwordNamespace);
- throw new InvalidK8SSecretException(
- $"Password field '{passwordFieldName}' in secret '{passwordSecretName}' in namespace '{passwordNamespace}' is empty"
- );
- }
-
- var storePasswdString = Encoding.UTF8.GetString(storePasswordBytes);
- // _logger.LogTrace("Loading existing PKCS12 store with password");
-
- existingStore = storeBuilder.Build();
- using var ms = new MemoryStream(existingPkcs12DataObj.Data[fieldName]);
- existingStore.Load(ms, storePasswdString.ToCharArray());
- }
- else
- {
- _logger.LogDebug("Job certificate store password path is empty, using existing secret data");
- storePasswordBytes = existingPkcs12DataObj.Data[passwordFieldName];
- if (storePasswordBytes == null || storePasswordBytes.Length == 0)
- {
- _logger.LogError(
- "Password field {FieldName} in secret {SecretName} in namespace {Namespace} is empty",
- passwordFieldName, secretName, namespaceName);
- throw new InvalidK8SSecretException(
- $"Password field '{passwordFieldName}' in secret '{secretName}' in namespace '{namespaceName}' is empty"
- );
- }
-
- // _logger.LogTrace("Loading existing PKCS12 store with password");
- existingStore = storeBuilder.Build();
- using var ms = new MemoryStream(existingPkcs12DataObj.Data[fieldName]);
- existingStore.Load(ms, Encoding.UTF8.GetString(storePasswordBytes).ToCharArray());
- }
- }
- else if (!string.IsNullOrEmpty(jobCertificate.StorePassword))
- {
- _logger.LogDebug(
- "Job certificate store password is not empty, using job certificate store password");
- storePasswordBytes = Encoding.UTF8.GetBytes(jobCertificate.StorePassword);
- // _logger.LogTrace("Loading existing PKCS12 store with password");
-
- existingStore = storeBuilder.Build();
- using var ms = new MemoryStream(existingPkcs12DataObj.Data[fieldName]);
- existingStore.Load(ms, Encoding.UTF8.GetString(storePasswordBytes).ToCharArray());
- }
- else
- {
- _logger.LogDebug("Job certificate store password is empty, using provided store password");
- storePasswordBytes = Encoding.UTF8.GetBytes(storePasswd);
- // _logger.LogTrace("Loading existing PKCS12 store with password");
-
- existingStore = storeBuilder.Build();
- using var ms = new MemoryStream(existingPkcs12DataObj.Data[fieldName]);
- existingStore.Load(ms, Encoding.UTF8.GetString(storePasswordBytes).ToCharArray());
- }
- }
-
- if (existingStore != null && existingStore.Count > 0)
- {
- // Process existing store
- if (remove)
- {
- var foundAlias = FindAliasByName(existingStore, jobCertificate.Alias);
- if (foundAlias != null)
- {
- // Certificate found - remove it
- _logger.LogTrace($"Certificate found with alias '{foundAlias}', removing it");
- existingStore.DeleteEntry(foundAlias);
- }
- }
- else
- {
- // Load new certificate to get its CN
- var newCertStore = storeBuilder.Build();
- using var newCertMs = new MemoryStream(jobCertificate.Pkcs12 ?? jobCertificate.CertBytes);
- newCertStore.Load(newCertMs, storePasswd.ToCharArray());
-
- var newCertAlias = newCertStore.Aliases.FirstOrDefault(newCertStore.IsKeyEntry);
- if (newCertAlias != null)
- {
- var newCertEntry = newCertStore.GetCertificate(newCertAlias);
- var newCertCn = Keyfactor.Extensions.Orchestrator.K8S.Utilities.CertificateUtilities.GetSubjectCN(newCertEntry.Certificate);
-
- // Check if overwrite is true, if so, replace existing cert with new cert
- if (overwrite)
- {
- _logger.LogTrace("Overwrite is true, replacing existing cert with new cert");
-
- var foundAlias = FindAliasByCN(existingStore, newCertCn);
- if (foundAlias != null)
- {
- // Certificate found - replace it
- _logger.LogTrace($"Certificate found with alias '{foundAlias}', replacing it");
- existingStore.DeleteEntry(foundAlias);
- }
-
- // Add new certificate with its alias or jobCertificate.Alias
- var targetAlias = string.IsNullOrEmpty(jobCertificate.Alias) ? newCertAlias : jobCertificate.Alias;
- var newKey = newCertStore.GetKey(newCertAlias);
- var newChain = newCertStore.GetCertificateChain(newCertAlias);
- existingStore.SetKeyEntry(targetAlias, newKey, newChain);
- }
- else
- {
- // Check if certificate doesn't exist, then add
- var foundAlias = FindAliasByCN(existingStore, newCertCn);
- if (foundAlias == null)
- {
- _logger.LogDebug("Certificate not found, adding the new certificate to the store");
- var targetAlias = string.IsNullOrEmpty(jobCertificate.Alias) ? newCertAlias : jobCertificate.Alias;
- var newKey = newCertStore.GetKey(newCertAlias);
- var newChain = newCertStore.GetCertificateChain(newCertAlias);
- existingStore.SetKeyEntry(targetAlias, newKey, newChain);
- }
- }
- }
- }
- }
- else
- {
- // No existing store - create new one from jobCertificate data
- _logger.LogDebug("No existing PKCS12 data found, creating new PKCS12 store");
- existingStore = storeBuilder.Build();
- using var newStoreMs = new MemoryStream(jobCertificate.Pkcs12 ?? jobCertificate.CertBytes);
- existingStore.Load(newStoreMs, storePasswd.ToCharArray());
- }
- }
-
- // Export PKCS12 store to bytes
- byte[] p12Bytes;
- if (existingStore != null)
- {
- using var outStream = new MemoryStream();
- existingStore.Save(outStream, Encoding.UTF8.GetString(storePasswordBytes).ToCharArray(), new SecureRandom());
- p12Bytes = outStream.ToArray();
- }
- else
- {
- p12Bytes = Array.Empty();
- }
-
- _logger.LogDebug("Creating V1Secret object for PKCS12 data with name {SecretName} in namespace {NamespaceName}",
- secretName, namespaceName);
- var secret = new V1Secret
- {
- ApiVersion = "v1",
- Kind = "Secret",
- Metadata = new V1ObjectMeta
- {
- Name = secretName,
- NamespaceProperty = namespaceName
- },
- Type = "Opaque",
- Data = new Dictionary
- {
- { certdataFieldName, p12Bytes }
- }
- };
-
- if (existingPkcs12DataObj?.Data != null)
- {
- secret.Data = existingPkcs12DataObj.Data;
- secret.Data[certdataFieldName] = p12Bytes;
- }
-
- // Convert p12bytes to pkcs12store
- var pkcs12StoreBuilder = new Pkcs12StoreBuilder();
- var pkcs12Store = pkcs12StoreBuilder.Build();
- pkcs12Store.Load(new MemoryStream(p12Bytes), storePasswd.ToCharArray());
-
-
- switch (string.IsNullOrEmpty(storePasswd))
- {
- case false
- when string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8sSecret
- : // password is not empty and passwordSecretPath is empty
- {
- _logger.LogDebug("Adding password to secret...");
- if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password";
- secret.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(storePasswd));
- break;
- }
- case false
- when !string.IsNullOrEmpty(passwordSecretPath) && passwdIsK8sSecret
- : // password is not empty and passwordSecretPath is not empty
- {
- _logger.LogDebug("Adding password secret path to secret...");
- if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "passwordSecretPath";
- secret.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordSecretPath));
-
- // Lookup password secret path on cluster to see if it exists
- _logger.LogDebug("Attempting to lookup password secret path on cluster...");
- var splitPasswordPath = passwordSecretPath.Split("/");
- // Assume secret pattern is namespace/secretName
- var passwordSecretName = splitPasswordPath[^1];
- var passwordSecretNamespace = splitPasswordPath[0];
- _logger.LogDebug(
- $"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- try
- {
- var passwordSecret =
- Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace);
- // storePasswd = Encoding.UTF8.GetString(passwordSecret.Data[passwordFieldName]);
- _logger.LogDebug(
- $"Successfully found secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- // Update secret
- _logger.LogDebug(
- $"Attempting to update secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- passwordSecret.Data[passwordFieldName] = Encoding.UTF8.GetBytes(storePasswd);
- var updatedPasswordSecret = Client.CoreV1.ReplaceNamespacedSecret(passwordSecret,
- passwordSecretName, passwordSecretNamespace);
- _logger.LogDebug(
- $"Successfully updated secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- }
- catch (HttpOperationException e)
- {
- _logger.LogError(
- $"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- _logger.LogError(e.Message);
- // Attempt to create a new secret
- _logger.LogDebug(
- $"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- var passwordSecretData = new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = passwordSecretName,
- NamespaceProperty = passwordSecretNamespace
- },
- Data = new Dictionary
- {
- { passwordFieldName, Encoding.UTF8.GetBytes(storePasswd) }
- }
- };
- var createdPasswordSecret =
- Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace);
- _logger.LogDebug("Successfully created secret " + passwordSecretPath);
- }
-
- break;
- }
- }
-
- // Update secret on K8S
- _logger.LogTrace("Calling UpdateSecret()");
- var updatedSecret = Client.CoreV1.ReplaceNamespacedSecret(secret, secretName, namespaceName);
-
- _logger.LogTrace("Finished creating V1Secret object");
-
- _logger.MethodExit(LogLevel.Debug);
- return updatedSecret;
- }
-
- ///
- /// Creates or updates a certificate store secret in Kubernetes.
- /// Routes to appropriate handler based on secret type (PKCS12, PFX, JKS).
- ///
- /// The certificate to store.
- /// Name of the Kubernetes secret.
- /// Kubernetes namespace.
- /// Type of store (pkcs12, pfx, jks).
- /// When true, overwrites existing entries.
- /// Field name for certificate data.
- /// Field name for password.
- /// Path to password secret if stored separately.
- /// When true, password is in a separate secret.
- /// Store password.
- /// Allowed field names to process.
- /// When true, removes instead of adds.
- /// The created or updated V1Secret.
- public V1Secret CreateOrUpdateCertificateStoreSecret(K8SJobCertificate jobCertificate, string secretName,
- string namespaceName, string secretType, bool overwrite = false, string certDataFieldName = "pkcs12",
- string passwordFieldName = "password",
- string passwordSecretPath = "", bool passwordIsK8SSecret = false, string password = "",
- string[] allowedKeys = null, bool remove = false)
- {
- _logger.MethodEntry(LogLevel.Debug);
- _logger.LogTrace("Parameters - SecretName: {SecretName}, Namespace: {Namespace}, SecretType: {SecretType}, Remove: {Remove}",
- secretName, namespaceName, secretType, remove);
- var storePasswd = string.IsNullOrEmpty(password) ? jobCertificate.Password : password;
- _logger.LogTrace("Password: {Password}", LoggingUtilities.RedactPassword(storePasswd));
- _logger.LogTrace("Calling CreateNewSecret()");
- V1Secret k8SSecretData;
- switch (secretType)
- {
- case "pkcs12":
- case "pfx":
- case "jks":
- if (remove)
- k8SSecretData = new V1Secret();
- else
- k8SSecretData = CreateOrUpdatePKCS12Secret(secretName,
- namespaceName,
- jobCertificate,
- certDataFieldName,
- storePasswd,
- passwordFieldName,
- passwordSecretPath,
- allowedKeys);
- break;
- default:
- k8SSecretData = new V1Secret();
- break;
- }
-
- _logger.LogTrace("Finished calling CreateNewSecret()");
-
- _logger.LogTrace("Entering try/catch block to create secret...");
- try
- {
- _logger.LogDebug("Calling CreateNamespacedSecret()");
- var secretResponse = Client.CoreV1.CreateNamespacedSecret(k8SSecretData, namespaceName);
- _logger.LogDebug("Finished calling CreateNamespacedSecret()");
- _logger.LogTrace(secretResponse.ToString());
- _logger.LogTrace("Exiting CreateOrUpdateCertificateStoreSecret()");
- return secretResponse;
- }
- catch (HttpOperationException e)
- {
- _logger.LogWarning("Error while attempting to create secret: " + e.Message);
- if (e.Message.Contains("Conflict") || e.Message.Contains("Unprocessable"))
- {
- _logger.LogDebug(
- $"Secret {secretName} already exists in namespace {namespaceName}, attempting to update secret...");
- _logger.LogTrace("Calling UpdateSecretStore()");
- switch (secretType)
- {
- case "pkcs12":
- case "pfx":
- case "jks":
- return UpdatePKCS12SecretStore(jobCertificate,
- secretName,
- namespaceName,
- secretType,
- certDataFieldName,
- storePasswd,
- k8SSecretData,
- true,
- overwrite,
- passwordIsK8SSecret,
- passwordSecretPath,
- passwordFieldName,
- null,
- remove);
- default:
- return UpdateSecretStore(secretName, namespaceName, secretType, "", "", k8SSecretData, false,
- overwrite);
- }
- }
- }
-
- _logger.LogError("Unable to create secret for unknown reason.");
- return k8SSecretData;
- }
-
public V1Secret CreateOrUpdateCertificateStoreSecret(string keyPem, string certPem, List chainPem,
string secretName,
string namespaceName, string secretType, bool append = false, bool overwrite = false, bool remove = false, bool separateChain = true, bool includeChain = true)
{
_logger.LogTrace("Entered CreateOrUpdateCertificateStoreSecret()");
- _logger.LogDebug($"Attempting to create new secret {secretName} in namespace {namespaceName}");
- _logger.LogTrace("Calling CreateNewSecret()");
- var k8SSecretData = CreateNewSecret(secretName, namespaceName, keyPem, certPem, chainPem, secretType, separateChain, includeChain);
- _logger.LogTrace("Finished calling CreateNewSecret()");
+ _logger.LogDebug("Attempting to create new secret {SecretName} in namespace {Namespace}", secretName, namespaceName);
+ var k8SSecretData = _secretOperations.BuildNewSecret(secretName, namespaceName, secretType, keyPem, certPem, chainPem, separateChain, includeChain);
_logger.LogTrace("Entering try/catch block to create secret...");
try
@@ -1283,7 +232,7 @@ public V1Secret CreateOrUpdateCertificateStoreSecret(string keyPem, string certP
}
catch (HttpOperationException e)
{
- _logger.LogWarning("Error while attempting to create secret: " + e.Message);
+ _logger.LogWarning("Error while attempting to create secret: {Message}", e.Message);
if (e.Message.Contains("Conflict"))
{
_logger.LogDebug(
@@ -1299,232 +248,35 @@ public V1Secret CreateOrUpdateCertificateStoreSecret(string keyPem, string certP
}
- public Pkcs12Store CreatePKCS12Collection(byte[] pkcs12bytes, string currentPassword, string newPassword)
+ ///
+ /// Parses a password secret path into namespace and secret name components.
+ ///
+ /// Path in format "namespace/secretName".
+ /// Tuple of (namespace, secretName).
+ private (string Namespace, string SecretName) ParsePasswordSecretPath(string passwordSecretPath)
{
- try
- {
- var storeBuilder = new Pkcs12StoreBuilder();
- var certs = storeBuilder.Build();
-
- var newEntry = storeBuilder.Build();
-
- // Load the PKCS12 data directly with BouncyCastle
- using (var ms = new MemoryStream(pkcs12bytes))
- {
- newEntry.Load(ms, string.IsNullOrEmpty(currentPassword) ? new char[0] : currentPassword.ToCharArray());
- }
-
- var checkAliasExists = string.Empty;
- string alias = null;
-
- // Get the first certificate to use its thumbprint as alias
- foreach (var newEntryAlias in newEntry.Aliases)
- {
- var certEntry = newEntry.GetCertificate(newEntryAlias);
- if (certEntry?.Certificate != null && alias == null)
- {
- // Use thumbprint as alias
- alias = Keyfactor.Extensions.Orchestrator.K8S.Utilities.CertificateUtilities.GetThumbprint(certEntry.Certificate);
- }
-
- if (!newEntry.IsKeyEntry(newEntryAlias))
- continue;
-
- checkAliasExists = newEntryAlias;
-
- if (certs.ContainsAlias(alias)) certs.DeleteEntry(alias);
- certs.SetKeyEntry(alias, newEntry.GetKey(newEntryAlias), newEntry.GetCertificateChain(newEntryAlias));
- }
-
- if (string.IsNullOrEmpty(checkAliasExists))
- {
- // No private key found, add certificate only
- var firstAlias = newEntry.Aliases.FirstOrDefault();
- if (firstAlias != null)
- {
- var certEntry = newEntry.GetCertificate(firstAlias);
- if (certEntry != null)
- {
- if (certs.ContainsAlias(alias)) certs.DeleteEntry(alias);
- certs.SetCertificateEntry(alias, certEntry);
- }
- }
- }
-
- using (var outStream = new MemoryStream())
- {
- certs.Save(outStream, string.IsNullOrEmpty(newPassword) ? new char[0] : newPassword.ToCharArray(),
- new SecureRandom());
- }
-
- return certs;
- }
- catch (Exception ex)
- {
- throw new Exception("Error attempting to add certficate for store path=StorePath, file name=StoreFileName.",
- ex);
- }
+ var parts = passwordSecretPath.Split("/");
+ var secretNamespace = parts[0];
+ var secretName = parts[^1];
+ _logger.LogTrace("Parsed password path: {Namespace}/{SecretName}", secretNamespace, secretName);
+ return (secretNamespace, secretName);
}
- private V1Secret CreateOrUpdatePKCS12Secret(string secretName, string namespaceName, K8SJobCertificate certObj,
- string secretFieldName, string password,
- string passwordFieldName, string passwordSecretPath = "", string[] allowedKeys = null)
+ public V1Secret ReadBuddyPass(string secretName, string passwordSecretPath)
{
- _logger.LogTrace("Entered CreateOrUpdatePKCS12Secret()");
-
- _logger.LogDebug("Attempting to read existing k8s secret...");
- var existingSecret = new V1Secret();
- try
- {
- existingSecret = Client.CoreV1.ReadNamespacedSecret(secretName, namespaceName);
- }
- catch (HttpOperationException e)
- {
- _logger.LogDebug("Error while attempting to read existing secret: " + e.Message);
- if (e.Message.Contains("Not Found")) _logger.LogDebug("No existing secret found.");
- existingSecret = null;
- }
-
- _logger.LogDebug("Finished reading existing k8s secret.");
-
- if (existingSecret != null)
- {
- _logger.LogDebug("Existing secret found, attempting to update...");
- return UpdatePKCS12SecretStore(certObj,
- secretName,
- namespaceName,
- "pkcs12",
- secretFieldName,
- password,
- existingSecret,
- false,
- true,
- false,
- passwordSecretPath,
- passwordFieldName,
- allowedKeys);
- }
-
- _logger.LogDebug("Attempting to create new secret...");
-
- //convert cert obj pkcs12 to base64
- _logger.LogDebug("Converting certificate to base64...");
-
- _logger.LogDebug("Creating X509Certificate2 from certificate object...");
-
- var passwordToWrite = !string.IsNullOrEmpty(certObj.StorePassword) ? certObj.StorePassword : password;
-
- var pkcs12Data = CreatePKCS12Collection(certObj.Pkcs12, password, passwordToWrite);
-
- byte[] p12Bytes;
- using (var stream = new MemoryStream())
- {
- pkcs12Data.Save(stream, passwordToWrite.ToCharArray(), new SecureRandom());
-
- // Get the PKCS12 bytes
- p12Bytes = stream.ToArray();
-
- // Use the pkcs12Bytes as desired
- }
-
- if (string.IsNullOrEmpty(secretFieldName)) secretFieldName = "pkcs12";
- var k8SSecretData = new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = secretName,
- NamespaceProperty = namespaceName
- },
- Data = new Dictionary
- {
- { secretFieldName, p12Bytes }
- }
- };
+ _logger.MethodEntry();
+ var (passwordNamespace, passwordSecretName) = ParsePasswordSecretPath(passwordSecretPath);
+ _logger.LogDebug("Looking up buddy secret {SecretName} in namespace {Namespace}",
+ passwordSecretName, passwordNamespace);
- switch (string.IsNullOrEmpty(password))
+ var passwordSecretResponse = _secretOperations.GetSecret(secretName, passwordNamespace);
+ if (passwordSecretResponse == null)
{
- case false
- when certObj.PasswordIsK8SSecret && string.IsNullOrEmpty(certObj.StorePasswordPath)
- : // This means the password is expected to be on the secret so add it
- {
- _logger.LogDebug("Adding password to secret...");
- if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password";
-
- // var passwordToWrite = !string.IsNullOrEmpty(certObj.StorePassword) ? certObj.StorePassword : password;
-
- k8SSecretData.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordToWrite));
- break;
- }
- case false when !string.IsNullOrEmpty(passwordSecretPath):
- {
- _logger.LogDebug("Adding password secret path to secret...");
- if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password";
- // k8SSecretData.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordSecretPath));
-
- // Lookup password secret path on cluster to see if it exists
- _logger.LogDebug("Attempting to lookup password secret path on cluster...");
- var splitPasswordPath = passwordSecretPath.Split("/");
- // Assume secret pattern is namespace/secretName
- var passwordSecretName = splitPasswordPath[splitPasswordPath.Length - 1];
- var passwordSecretNamespace = splitPasswordPath[0];
- _logger.LogDebug(
- $"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- try
- {
- var passwordSecret =
- Client.CoreV1.ReadNamespacedSecret(passwordSecretName, passwordSecretNamespace);
- password = Encoding.UTF8.GetString(passwordSecret.Data[passwordFieldName]);
- }
- catch (HttpOperationException e)
- {
- _logger.LogError(
- $"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- _logger.LogError(e.Message);
- // Attempt to create a new secret
- _logger.LogDebug(
- $"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- // var passwordToWrite = !string.IsNullOrEmpty(certObj.StorePassword) ? certObj.StorePassword : password;
- var passwordSecretData = new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = passwordSecretName,
- NamespaceProperty = passwordSecretNamespace
- },
- Data = new Dictionary
- {
- { passwordFieldName, Encoding.UTF8.GetBytes(passwordToWrite) }
- }
- };
- _logger.LogDebug("Calling CreateNamespacedSecret()");
- var passwordSecretResponse =
- Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace);
- _logger.LogDebug("Finished calling CreateNamespacedSecret()");
- _logger.LogDebug("Successfully created secret " + passwordSecretPath);
- }
-
- break;
- }
+ throw new StoreNotFoundException($"K8S password secret NotFound: {passwordNamespace}/secrets/{secretName}");
}
- _logger.LogTrace("Exiting CreateNewSecret()");
- return k8SSecretData;
- }
-
- public V1Secret ReadBuddyPass(string secretName, string passwordSecretPath)
- {
- _logger.MethodEntry();
- // Lookup password secret path on cluster to see if it exists
- _logger.LogDebug("Attempting to lookup password secret path on cluster...");
- var splitPasswordPath = passwordSecretPath.Split("/");
- _logger.LogDebug("Split password secret path: {SplitPasswordPath}", string.Join("/", splitPasswordPath));
- var passwordSecretName = splitPasswordPath[^1];
- var passwordSecretNamespace = splitPasswordPath[0];
- _logger.LogDebug("Attempting to lookup secret {PasswordSecretName} in namespace {PasswordSecretNamespace}",
- passwordSecretName, passwordSecretNamespace);
- var passwordSecretResponse = Client.CoreV1.ReadNamespacedSecret(secretName, passwordSecretNamespace);
- _logger.LogDebug("Successfully found secret {PasswordSecretName} in namespace {PasswordSecretNamespace}",
- passwordSecretName, passwordSecretNamespace);
+ _logger.LogDebug("Successfully found buddy secret {SecretName} in namespace {Namespace}",
+ passwordSecretName, passwordNamespace);
_logger.MethodExit();
return passwordSecretResponse;
}
@@ -1532,180 +284,26 @@ public V1Secret ReadBuddyPass(string secretName, string passwordSecretPath)
public V1Secret CreateOrUpdateBuddyPass(string secretName, string passwordFieldName, string passwordSecretPath,
string password)
{
- _logger.LogDebug("Adding password secret path to secret...");
if (string.IsNullOrEmpty(passwordFieldName)) passwordFieldName = "password";
- // k8SSecretData.Data.Add(passwordFieldName, Encoding.UTF8.GetBytes(passwordSecretPath));
-
- // Lookup password secret path on cluster to see if it exists
- _logger.LogDebug("Attempting to lookup password secret path on cluster...");
- var splitPasswordPath = passwordSecretPath.Split("/");
- // Assume secret pattern is namespace/secretName
- var passwordSecretName = splitPasswordPath[splitPasswordPath.Length - 1];
- var passwordSecretNamespace = splitPasswordPath[0];
- _logger.LogDebug($"Attempting to lookup secret {passwordSecretName} in namespace {passwordSecretNamespace}");
+ var (passwordNamespace, passwordSecretName) = ParsePasswordSecretPath(passwordSecretPath);
+ _logger.LogDebug("Creating/updating buddy secret {SecretName} in namespace {Namespace}",
+ passwordSecretName, passwordNamespace);
+
var passwordSecretData = new V1Secret
{
Metadata = new V1ObjectMeta
{
Name = passwordSecretName,
- NamespaceProperty = passwordSecretNamespace
+ NamespaceProperty = passwordNamespace
},
Data = new Dictionary
{
{ passwordFieldName, Encoding.UTF8.GetBytes(password) }
}
};
- try
- {
- var passwordSecretResponse =
- Client.CoreV1.CreateNamespacedSecret(passwordSecretData, passwordSecretNamespace);
- return passwordSecretResponse;
- }
- catch (HttpOperationException e)
- {
- _logger.LogError($"Unable to find secret {passwordSecretName} in namespace {passwordSecretNamespace}");
- _logger.LogError(e.Message);
- // Attempt to create a new secret
- _logger.LogDebug(
- $"Attempting to create secret {passwordSecretName} in namespace {passwordSecretNamespace}");
-
- _logger.LogDebug("Calling CreateNamespacedSecret()");
- var passwordSecretResponse =
- Client.CoreV1.ReplaceNamespacedSecret(passwordSecretData, secretName, passwordSecretNamespace);
- _logger.LogDebug("Finished calling CreateNamespacedSecret()");
- _logger.LogDebug("Successfully created secret " + passwordSecretPath);
- return passwordSecretResponse;
- }
- }
-
- private V1Secret CreateNewSecret(string secretName, string namespaceName, string keyPem, string certPem,
- List chainPem, string secretType, bool separateChain = true, bool includeChain = true)
- {
- _logger.LogTrace("Entered CreateNewSecret()");
- _logger.LogDebug("Attempting to create new secret...");
-
- switch (secretType)
- {
- case "secret":
- case "opaque":
- case "opaque_secret":
- secretType = "secret";
- break;
- case "tls_secret":
- case "tls":
- secretType = "tls_secret";
- break;
- case "pfx":
- case "pkcs12":
- secretType = "pkcs12";
- break;
- case "jks":
- secretType = "jks";
- break;
- default:
- _logger.LogError("Unknown secret type: " + secretType);
- break;
- }
-
- var k8SSecretData = new V1Secret();
-
- switch (secretType)
- {
- case "secret":
- // Opaque secrets can store certificate-only (no private key)
- var opaqueData = new Dictionary
- {
- { "tls.crt", Encoding.UTF8.GetBytes(certPem ?? "") }
- };
- if (!string.IsNullOrEmpty(keyPem))
- {
- opaqueData["tls.key"] = Encoding.UTF8.GetBytes(keyPem);
- }
- else
- {
- _logger.LogDebug("No private key provided for Opaque secret - storing certificate only");
- }
- k8SSecretData = new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = secretName,
- NamespaceProperty = namespaceName
- },
- Data = opaqueData
- };
- break;
- case "tls_secret":
- // TLS secrets require both tls.crt and tls.key per Kubernetes specification
- if (string.IsNullOrEmpty(keyPem))
- {
- _logger.LogWarning("TLS secrets require a private key. Certificate was provided without private key - creating with empty tls.key field");
- }
- k8SSecretData = new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = secretName,
- NamespaceProperty = namespaceName
- },
-
- Type = "kubernetes.io/tls",
-
- Data = new Dictionary
- {
- { "tls.key", Encoding.UTF8.GetBytes(keyPem ?? "") },
- { "tls.crt", Encoding.UTF8.GetBytes(certPem ?? "") }
- }
- };
- break;
- case "pkcs12":
- case "pfx":
- // PKCS12/PFX secrets are stored as Opaque secrets with the keystore data
- // For "create store if missing", create an empty Opaque secret
- _logger.LogDebug("Creating empty Opaque secret for PKCS12/PFX store");
- k8SSecretData = new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = secretName,
- NamespaceProperty = namespaceName
- },
- Type = "Opaque",
- Data = new Dictionary()
- };
- break;
- case "jks":
- // JKS secrets are stored as Opaque secrets with the keystore data
- // For "create store if missing", create an empty Opaque secret
- _logger.LogDebug("Creating empty Opaque secret for JKS store");
- k8SSecretData = new V1Secret
- {
- Metadata = new V1ObjectMeta
- {
- Name = secretName,
- NamespaceProperty = namespaceName
- },
- Type = "Opaque",
- Data = new Dictionary()
- };
- break;
- default:
- throw new NotImplementedException(
- $"Secret type {secretType} not implemented. Unable to create or update certificate store {secretName} in {namespaceName} on {GetHost()}.");
- }
-
- if (chainPem is { Count: > 0 } && includeChain)
- {
- var caCert = chainPem.Where(cer => cer != certPem).Aggregate("", (current, cer) => current + cer);
- if (separateChain)
- k8SSecretData.Data.Add("ca.crt", Encoding.UTF8.GetBytes(caCert));
- else
- //update tls.crt w/ full chain
- k8SSecretData.Data["tls.crt"] = Encoding.UTF8.GetBytes(certPem + caCert);
- }
- _logger.LogTrace("Exiting CreateNewSecret()");
- return k8SSecretData;
+ // Use SecretOperations for upsert
+ return _secretOperations.CreateOrUpdateSecret(passwordSecretData, passwordNamespace);
}
private V1Secret UpdateOpaqueSecret(string secretName, string namespaceName, V1Secret existingSecret,
@@ -1726,103 +324,22 @@ private V1Secret UpdateOpaqueSecret(string secretName, string namespaceName, V1S
// Always update tls.crt
existingSecret.Data["tls.crt"] = newSecret.Data["tls.crt"];
- //check if existing secret has ca.crt and if new secret has ca.crt
- if (existingSecret.Data.ContainsKey("ca.crt") && newSecret.Data.ContainsKey("ca.crt"))
+ // Use the new secret's ca.crt field as the source of truth for whether the chain should be separate.
+ // Do NOT gate on whether the existing secret already has ca.crt — on first write to an empty store
+ // the existing secret will never have ca.crt, which caused the chain to be concatenated into tls.crt
+ // even when SeparateChain=true.
+ if (newSecret.Data.TryGetValue("ca.crt", out var chainBytes))
{
- _logger.LogDebug("Existing secret '{Namespace}/{Name}' has ca.crt adding chain to this field",
+ _logger.LogDebug("New secret has ca.crt, storing chain separately in '{Namespace}/{Name}'",
namespaceName, secretName);
- _logger.LogTrace("existing ca.crt:\n {CaCrt}", existingSecret.Data["ca.crt"]);
- existingSecret.Data["ca.crt"] = newSecret.Data["ca.crt"];
- _logger.LogTrace("new ca.crt:\n {CaCrt}", newSecret.Data["ca.crt"]);
+ existingSecret.Data["ca.crt"] = chainBytes;
+ _logger.LogTrace("ca.crt:\n {CaCrt}", chainBytes);
}
else
{
- //Append to tls.crt
- _logger.LogDebug("Existing secret '{Namespace}/{Name}' does not have ca.crt, appending to tls.crt",
+ _logger.LogDebug("No separate chain in new secret, only updating tls.crt for '{Namespace}/{Name}'",
namespaceName, secretName);
- if (newSecret.Data.TryGetValue("ca.crt", out var value))
- {
- _logger.LogDebug("Appending ca.crt to tls.crt");
- existingSecret.Data["tls.crt"] =
- Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(newSecret.Data["tls.crt"]) +
- Encoding.UTF8.GetString(value));
- _logger.LogTrace("New tls.crt:\n {TlsCrt}", existingSecret.Data["tls.crt"]);
- }
- else
- {
- _logger.LogDebug("No chain was provided, only updating leaf certificate for '{Namespace}/{Name}'",
- namespaceName, secretName);
- _logger.LogTrace("existing tls.crt:\n {TlsCrt}", existingSecret.Data["tls.crt"]);
- existingSecret.Data["tls.crt"] =
- Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(newSecret.Data["tls.crt"]));
- _logger.LogTrace("updated tls.crt:\n {TlsCrt}", existingSecret.Data["tls.crt"]);
- }
- }
-
- _logger.LogDebug($"Attempting to update secret {secretName} in namespace {namespaceName}");
- _logger.LogTrace("Calling ReplaceNamespacedSecret()");
- var secretResponse = Client.CoreV1.ReplaceNamespacedSecret(existingSecret, secretName, namespaceName);
- _logger.LogTrace("Finished calling ReplaceNamespacedSecret()");
- _logger.LogTrace("Exiting UpdateOpaqueSecret()");
- return secretResponse;
- }
-
- private V1Secret UpdateOpaqueSecretMultiple(string secretName, string namespaceName, V1Secret existingSecret,
- string certPem, string keyPem)
- {
- _logger.LogTrace("Entered UpdateOpaqueSecret()");
-
- var existingCerts = existingSecret.Data.ContainsKey("certificates")
- ? Encoding.UTF8.GetString(existingSecret.Data["certificates"])
- : "";
-
- _logger.LogTrace("Existing certificates: " + existingCerts);
-
- var existingKeys = existingSecret.Data.ContainsKey("tls.key")
- ? Encoding.UTF8.GetString(existingSecret.Data["tls.key"])
- : "";
- // Logger.LogTrace("Existing private keys: " + existingKeys);
-
- if (existingCerts.Contains(certPem) && existingKeys.Contains(keyPem))
- {
- // certificate already exists, return existing secret
- _logger.LogDebug($"Certificate already exists in secret {secretName} in namespace {namespaceName}");
- _logger.LogTrace("Exiting UpdateOpaqueSecret()");
- return existingSecret;
- }
-
- if (!existingCerts.Contains(certPem))
- {
- _logger.LogDebug("Certificate does not exist in secret, adding certificate to secret");
- var newCerts = existingCerts;
- if (existingCerts.Length > 0)
- {
- _logger.LogTrace("Adding comma to existing certificates");
- newCerts += ",";
- }
-
- _logger.LogTrace("Adding certificate to existing certificates");
- newCerts += certPem;
-
- _logger.LogTrace("Updating 'certificates' secret data");
- existingSecret.Data["certificates"] = Encoding.UTF8.GetBytes(newCerts);
- }
-
- if (!existingKeys.Contains(keyPem))
- {
- _logger.LogDebug("Private key does not exist in secret, adding private key to secret");
- var newKeys = existingKeys;
- if (existingKeys.Length > 0)
- {
- _logger.LogTrace("Adding comma to existing private keys");
- newKeys += ",";
- }
-
- _logger.LogTrace("Adding private key to existing private keys");
- newKeys += keyPem;
-
- _logger.LogTrace("Updating 'private_keys' secret data");
- existingSecret.Data["tls.key"] = Encoding.UTF8.GetBytes(newKeys);
+ _logger.LogTrace("updated tls.crt:\n {TlsCrt}", existingSecret.Data["tls.crt"]);
}
_logger.LogDebug($"Attempting to update secret {secretName} in namespace {namespaceName}");
@@ -1849,243 +366,74 @@ private V1Secret UpdateSecretStore(string secretName, string namespaceName, stri
throw new Exception(errMsg);
}
- _logger.LogTrace($"Entering switch statement for secret type {secretType}");
- switch (secretType)
- {
- // check if certificate already exists in "certificates" field
- // case "secret" when !overwrite:
- // Logger.LogInformation($"Attempting to create opaque secret {secretName} in namespace {namespaceName}");
- // Logger.LogInformation("Overwrite is not specified, checking if certificate already exists in secret");
- //
- //
- // return CreateNewSecret(secretName, namespaceName, keyPem,certPem,"","",secretType);
- case "secret":
- {
- _logger.LogInformation($"Attempting to update opaque secret {secretName} in namespace {namespaceName}");
- _logger.LogTrace("Calling UpdateOpaqueSecret()");
- return UpdateOpaqueSecret(secretName, namespaceName, existingSecret, newData);
- }
- // case "tls_secret" when !overwrite:
- // var errMsg = "Overwrite is not specified, cannot add multiple certificates to a Kubernetes secret type 'tls_secret'.";
- // Logger.LogError(errMsg);
- // Logger.LogTrace("Exiting UpdateSecretStore()");
- // throw new Exception(errMsg);
- case "tls_secret":
- {
- _logger.LogInformation($"Attempting to update tls secret {secretName} in namespace {namespaceName}");
- _logger.LogTrace("Calling ReplaceNamespacedSecret()");
- var secretResponse = Client.CoreV1.ReplaceNamespacedSecret(newData, secretName, namespaceName);
- _logger.LogTrace("Finished calling ReplaceNamespacedSecret()");
- _logger.LogTrace("Exiting UpdateSecretStore()");
- return secretResponse;
- }
- default:
- var dErrMsg =
- $"Secret type not implemented. Unable to create or update certificate store {secretName} in {namespaceName} on {GetHost()}.";
- _logger.LogError(dErrMsg);
- _logger.LogTrace("Exiting UpdateSecretStore()");
- throw new NotImplementedException(dErrMsg);
- }
- }
-
- public V1Secret GetCertificateStoreSecret(string secretName, string namespaceName)
- {
- _logger.LogTrace("Entered GetCertificateStoreSecret()");
- _logger.LogTrace("Calling ReadNamespacedSecret()");
- _logger.LogDebug($"Attempting to read secret {secretName} in namespace {namespaceName} from {GetHost()}");
- return Client.CoreV1.ReadNamespacedSecret(secretName, namespaceName);
- }
+ // Normalize the secret type to handle variants (e.g., "opaque" -> "secret", "tls" stays "tls")
+ var normalizedType = SecretTypes.Normalize(secretType);
+ _logger.LogTrace("Entering switch statement for secret type {OriginalType} (normalized: {NormalizedType})",
+ secretType, normalizedType);
- private string CleanOpaqueStore(string existingEntries, string pemString)
- {
- _logger.LogTrace("Entered CleanOpaqueStore()");
- // Logger.LogTrace($"pemString: {pemString}");
- _logger.LogTrace("Entering try/catch block to remove existing certificate from opaque secret");
- try
+ // Route based on normalized type using SecretTypes helpers
+ if (SecretTypes.IsOpaqueType(normalizedType))
{
- _logger.LogDebug("Attempting to remove existing certificate from opaque secret");
- existingEntries = existingEntries.Replace(pemString, "").Replace(",,", ",");
-
- if (existingEntries.StartsWith(","))
- {
- _logger.LogDebug("Removing leading comma from existing certificates.");
- existingEntries = existingEntries.Substring(1);
- }
-
- if (existingEntries.EndsWith(","))
- {
- _logger.LogDebug("Removing trailing comma from existing certificates.");
- existingEntries = existingEntries.Substring(0, existingEntries.Length - 1);
- }
+ _logger.LogInformation("Attempting to update opaque secret {SecretName} in namespace {Namespace}",
+ secretName, namespaceName);
+ _logger.LogTrace("Calling UpdateOpaqueSecret()");
+ return UpdateOpaqueSecret(secretName, namespaceName, existingSecret, newData);
}
- catch (Exception)
+
+ if (SecretTypes.IsTlsType(normalizedType))
{
- // Didn't find existing key for whatever reason so no need to delete.
- _logger.LogWarning("Unable to find existing certificate in opaque secret. No need to remove.");
+ _logger.LogInformation("Attempting to update tls secret {SecretName} in namespace {Namespace}",
+ secretName, namespaceName);
+ newData.Metadata.ResourceVersion = existingSecret.Metadata.ResourceVersion;
+ _logger.LogTrace("Calling ReplaceNamespacedSecret()");
+ var secretResponse = Client.CoreV1.ReplaceNamespacedSecret(newData, secretName, namespaceName);
+ _logger.LogTrace("Finished calling ReplaceNamespacedSecret()");
+ _logger.LogTrace("Exiting UpdateSecretStore()");
+ return secretResponse;
}
- _logger.LogTrace("Exiting CleanOpaqueStore()");
- return existingEntries;
+ var dErrMsg =
+ $"Secret type '{secretType}' not implemented. Unable to create or update certificate store {secretName} in {namespaceName} on {GetHost()}.";
+ _logger.LogError(dErrMsg);
+ _logger.LogTrace("Exiting UpdateSecretStore()");
+ throw new NotImplementedException(dErrMsg);
}
- private V1Secret DeleteCertificateStoreSecret(string secretName, string namespaceName, string alias)
+ public V1Secret GetCertificateStoreSecret(string secretName, string namespaceName)
{
- _logger.LogTrace("Entered DeleteCertificateStoreSecret()");
- _logger.LogTrace("secretName: " + secretName);
- _logger.LogTrace("namespaceName: " + namespaceName);
- _logger.LogTrace("alias: " + alias);
-
- _logger.LogDebug($"Attempting to read secret {secretName} in namespace {namespaceName} from {GetHost()}");
- _logger.LogTrace("Calling ReadNamespacedSecret()");
- var existingSecret = Client.CoreV1.ReadNamespacedSecret(secretName, namespaceName, true);
- _logger.LogTrace("Finished calling ReadNamespacedSecret()");
- if (existingSecret == null)
+ _logger.LogDebug("Reading secret {SecretName} in namespace {Namespace} from {Host}",
+ secretName, namespaceName, GetHost());
+ var secret = _secretOperations.GetSecret(secretName, namespaceName);
+ if (secret == null)
{
- var errMsg =
- $"Delete secret {secretName} in Kubernetes namespace {namespaceName} failed. Unable unable to read secret, please verify credentials have correct access.";
- _logger.LogError(errMsg);
- throw new Exception(errMsg);
- }
-
- // handle cert removal
- _logger.LogDebug("Parsing existing certificates from secret into a string.");
- foreach (var sKey in existingSecret.Data.Keys)
- {
- var existingCerts = Encoding.UTF8.GetString(existingSecret.Data[sKey]);
- _logger.LogTrace("existingCerts: " + existingCerts);
-
- _logger.LogDebug("Parsing existing private keys from secret into a string.");
- var existingKeys = Encoding.UTF8.GetString(existingSecret.Data["tls.key"]);
- // Logger.LogTrace("existingKeys: " + existingKeys);
-
- _logger.LogDebug("Splitting existing certificates into an array.");
- var certs = existingCerts.Split(",");
- _logger.LogTrace("certs: " + certs);
-
- _logger.LogDebug("Splitting existing private keys into an array.");
- var keys = existingKeys.Split(",");
- // Logger.LogTrace("keys: " + keys);
-
- var index = 0; //Currently keys are assumed to be in the same order as certs.
- _logger.LogTrace("Entering foreach loop to remove existing certificate from opaque secret");
- foreach (var cer in certs)
- {
- _logger.LogTrace("pkey index: " + index);
- _logger.LogTrace("cer: " + cer);
- _logger.LogTrace("alias: " + alias);
- if (string.IsNullOrEmpty(cer))
- {
- _logger.LogDebug("Found empty certificate string. Skipping.");
- continue;
- }
-
- _logger.LogDebug("Creating X509Certificate2 from certificate string.");
- var sCert = new X509Certificate2();
- try
- {
- sCert = new X509Certificate2(Encoding.UTF8.GetBytes(cer));
- }
- catch (Exception e)
- {
- _logger.LogWarning(
- $"Unable to create X509Certificate2 from string in '{sKey}' field. Skipping. Error: {e.Message}");
- continue;
- }
-
- _logger.LogDebug("sCert.Thumbprint: " + sCert.Thumbprint);
-
- if (sCert.Thumbprint == alias)
- {
- _logger.LogDebug("Found matching certificate thumbprint. Removing certificate from opaque secret.");
- _logger.LogTrace("Calling CleanOpaqueStore()");
- existingCerts = CleanOpaqueStore(existingCerts, cer);
- _logger.LogTrace("Finished calling CleanOpaqueStore()");
- _logger.LogTrace("Updated existingCerts: " + existingCerts);
- _logger.LogTrace("Calling CleanOpaqueStore()");
- try
- {
- existingKeys = CleanOpaqueStore(existingKeys, keys[index]);
- }
- catch (IndexOutOfRangeException)
- {
- // Didn't find existing key for whatever reason so no need to delete.
- // Find the corresponding key the the keys array and by checking if the private key corresponds to the cert public key.
- _logger.LogWarning(
- $"Unable to find corresponding private key in opaque secret for certificate {sCert.Thumbprint}. No need to remove.");
- }
- }
-
- _logger.LogTrace("Incrementing pkey index...");
- index++; //Currently keys are assumed to be in the same order as certs.
- }
-
- _logger.LogDebug("Updating existing secret with new certificate data.");
- existingSecret.Data[sKey] = Encoding.UTF8.GetBytes(existingCerts);
- _logger.LogDebug("Updating existing secret with new key data.");
- try
- {
- existingSecret.Data["tls.key"] = Encoding.UTF8.GetBytes(existingKeys);
- }
- catch (Exception)
- {
- _logger.LogWarning(
- "Unable to update private_keys in opaque secret. This is expected if the secret did not contain private keys to begin with.");
- }
-
-
- // Update Kubernetes secret
- _logger.LogDebug(
- $"Updating secret {secretName} in namespace {namespaceName} on {GetHost()} with new certificate data.");
- _logger.LogTrace("Calling ReplaceNamespacedSecret()");
+ throw new StoreNotFoundException($"K8S secret NotFound: {namespaceName}/secrets/{secretName}");
}
-
- return Client.CoreV1.ReplaceNamespacedSecret(existingSecret, secretName, namespaceName);
+ return secret;
}
public V1Status DeleteCertificateStoreSecret(string secretName, string namespaceName, string storeType,
string alias)
{
_logger.LogTrace("Entered DeleteCertificateStoreSecret()");
- _logger.LogTrace("secretName: " + secretName);
- _logger.LogTrace("namespaceName: " + namespaceName);
- _logger.LogTrace("storeType: " + storeType);
- _logger.LogTrace("alias: " + alias);
- _logger.LogTrace("Entering switch statement to determine which delete method to use.");
+ _logger.LogDebug("Deleting secret {SecretName} in namespace {Namespace}, type: {StoreType}",
+ secretName, namespaceName, storeType);
+
switch (storeType)
{
case "secret":
case "opaque":
- // check the current inventory and only remove the cert if it is found else throw not found exception
- _logger.LogDebug(
- $"Attempting to delete certificate from opaque secret {secretName} in namespace {namespaceName} on {GetHost()}");
- _logger.LogTrace("Calling DeleteCertificateStoreSecret()");
- // _ = DeleteCertificateStoreSecret(secretName, namespaceName, alias);
- return Client.CoreV1.DeleteNamespacedSecret(
- secretName,
- namespaceName,
- new V1DeleteOptions()
- );
- // Logger.LogTrace("Finished calling DeleteCertificateStoreSecret()");
- // return new V1Status("v1", 0, status: "Success");
case "tls_secret":
case "tls":
- _logger.LogDebug($"Deleting TLS secret {secretName} in namespace {namespaceName} on {GetHost()}");
- _logger.LogTrace("Calling DeleteNamespacedSecret()");
- return Client.CoreV1.DeleteNamespacedSecret(
- secretName,
- namespaceName,
- new V1DeleteOptions()
- );
+ _logger.LogDebug("Deleting secret via SecretOperations");
+ return _secretOperations.DeleteSecret(secretName, namespaceName);
+
case "certificate":
- _logger.LogDebug($"Deleting Certificate Signing Request {secretName} on {GetHost()}");
- _logger.LogTrace("Calling CertificatesV1.DeleteCertificateSigningRequest()");
- _ = Client.CertificatesV1.DeleteCertificateSigningRequest(
- secretName,
- new V1DeleteOptions()
- );
+ _logger.LogDebug("Deleting Certificate Signing Request {SecretName} on {Host}", secretName, GetHost());
+ _ = Client.CertificatesV1.DeleteCertificateSigningRequest(secretName, new V1DeleteOptions());
var errMsg = "DeleteCertificateStoreSecret not implemented for 'certificate' type.";
_logger.LogError(errMsg);
throw new NotImplementedException(errMsg);
+
default:
var dErrMsg = $"DeleteCertificateStoreSecret not implemented for type '{storeType}'.";
_logger.LogError(dErrMsg);
@@ -2101,13 +449,13 @@ public List DiscoverCertificates()
_logger.LogTrace("Calling CertificatesV1.ListCertificateSigningRequest()");
var csr = Client.CertificatesV1.ListCertificateSigningRequest();
_logger.LogTrace("Finished calling CertificatesV1.ListCertificateSigningRequest()");
- _logger.LogTrace("csr.Items.Count: " + csr.Items.Count);
+ _logger.LogTrace("csr.Items.Count: {Count}", csr.Items.Count);
_logger.LogTrace("Entering foreach loop to add certificate locations to list.");
var clusterName = GetClusterName();
foreach (var cr in csr)
{
- _logger.LogTrace("cr.Metadata.Name: " + cr.Metadata.Name);
+ _logger.LogTrace("cr.Metadata.Name: {Name}", cr.Metadata.Name);
_logger.LogDebug("Parsing certificate from certificate resource.");
var utfCert = cr.Status.Certificate != null ? Encoding.UTF8.GetString(cr.Status.Certificate) : "";
_logger.LogDebug("Parsing certificate signing request from certificate resource.");
@@ -2115,7 +463,7 @@ public List DiscoverCertificates()
? Encoding.UTF8.GetString(cr.Spec.Request, 0, cr.Spec.Request.Length)
: "";
- if (utfCsr != "") _logger.LogTrace("utfCsr: " + utfCsr);
+ if (utfCsr != "") _logger.LogTrace("utfCsr length: {Length}", utfCsr.Length);
if (utfCert == "")
{
_logger.LogWarning("CSR has not been signed yet. Skipping.");
@@ -2124,19 +472,18 @@ public List DiscoverCertificates()
_logger.LogDebug("Parsing certificate using BouncyCastle.");
var cert = Keyfactor.Extensions.Orchestrator.K8S.Utilities.CertificateUtilities.ParseCertificateFromPem(utfCert);
- _logger.LogTrace("cert: " + cert);
+ _logger.LogTrace("cert subject: {Subject}", cert?.SubjectDN?.ToString());
_logger.LogDebug("Getting certificate Common Name.");
- var certName = Keyfactor.Extensions.Orchestrator.K8S.Utilities.CertificateUtilities.GetSubjectCN(cert);
- _logger.LogTrace("certName: " + certName);
+ var certName = cert.CommonName();
+ _logger.LogTrace("certName: {CertName}", certName);
- _logger.LogDebug($"Adding certificate {certName} discovered location to list.");
+ _logger.LogDebug("Adding certificate {CertName} discovered location to list", certName);
locations.Add($"{clusterName}/certificate/{certName}");
}
_logger.LogDebug("Completed discovering certificates from k8s certificate resources.");
- _logger.LogTrace("locations.Count: " + locations.Count);
- _logger.LogTrace("locations: " + locations);
+ _logger.LogTrace("locations.Count: {Count}", locations.Count);
_logger.MethodExit(LogLevel.Debug);
return locations;
}
@@ -2153,8 +500,8 @@ public string[] GetCertificateSigningRequestStatus(string name)
_logger.LogTrace("CSR Name: {Name}", name);
_logger.LogDebug("Attempting to read {Name} certificate signing request from {Host}...", name, GetHost());
var cr = Client.CertificatesV1.ReadCertificateSigningRequest(name);
- _logger.LogDebug($"Successfully read {name} certificate signing request from {GetHost()}.");
- _logger.LogTrace("cr: " + cr);
+ _logger.LogDebug("Successfully read {Name} certificate signing request from {Host}", name, GetHost());
+ _logger.LogTrace("cr status: {Status}", cr?.Status?.Conditions?.FirstOrDefault()?.Type);
_logger.LogTrace("Attempting to parse certificate from certificate resource.");
// Check if CSR has been signed yet
@@ -2166,11 +513,11 @@ public string[] GetCertificateSigningRequestStatus(string name)
}
var utfCert = Encoding.UTF8.GetString(cr.Status.Certificate);
- _logger.LogTrace("utfCert: " + utfCert);
+ _logger.LogTrace("utfCert length: {Length}", utfCert.Length);
- _logger.LogDebug($"Attempting to parse certificate from certificate resource {name}.");
+ _logger.LogDebug("Attempting to parse certificate from certificate resource {Name}", name);
var cert = Keyfactor.Extensions.Orchestrator.K8S.Utilities.CertificateUtilities.ParseCertificateFromPem(utfCert);
- _logger.LogTrace("cert: " + cert);
+ _logger.LogTrace("cert subject: {Subject}", cert?.SubjectDN?.ToString());
_logger.MethodExit(LogLevel.Debug);
return new[] { utfCert };
}
@@ -2218,42 +565,14 @@ public Dictionary ListAllCertificateSigningRequests()
///
/// Base64-encoded DER certificate data.
/// Parsed X509Certificate object.
- public X509Certificate ReadDerCertificate(string derString)
- {
- _logger.MethodEntry(LogLevel.Debug);
- var derData = Convert.FromBase64String(derString);
- var certificateParser = new X509CertificateParser();
- var cert = certificateParser.ReadCertificate(derData);
- _logger.LogDebug("Parsed DER certificate: {Summary}", LoggingUtilities.GetCertificateSummary(cert));
- _logger.MethodExit(LogLevel.Debug);
- return cert;
- }
+ public X509Certificate ReadDerCertificate(string derString) => _certificateOperations.ReadDerCertificate(derString);
///
/// Reads a PEM-encoded certificate from a string.
///
/// PEM-encoded certificate string.
/// Parsed X509Certificate object, or null if not a valid certificate.
- public X509Certificate ReadPemCertificate(string pemString)
- {
- _logger.MethodEntry(LogLevel.Debug);
- using var reader = new StringReader(pemString);
- var pemReader = new PemReader(reader);
- var pemObject = pemReader.ReadPemObject();
- if (pemObject is not { Type: "CERTIFICATE" })
- {
- _logger.LogDebug("PEM object is not a certificate, returning null");
- _logger.MethodExit(LogLevel.Debug);
- return null;
- }
-
- var certificateBytes = pemObject.Content;
- var certificateParser = new X509CertificateParser();
- var cert = certificateParser.ReadCertificate(certificateBytes);
- _logger.LogDebug("Parsed PEM certificate: {Summary}", LoggingUtilities.GetCertificateSummary(cert));
- _logger.MethodExit(LogLevel.Debug);
- return cert;
- }
+ public X509Certificate ReadPemCertificate(string pemString) => _certificateOperations.ReadPemCertificate(pemString);
///
/// Extracts a private key from a PKCS12 store and converts it to PEM format.
@@ -2265,75 +584,21 @@ public X509Certificate ReadPemCertificate(string pemString)
/// PEM-formatted private key string.
/// Thrown when no private key is found or key type is unsupported.
public string ExtractPrivateKeyAsPem(Pkcs12Store store, string password, PrivateKeyFormat format = PrivateKeyFormat.Pkcs8)
- {
- _logger.MethodEntry(LogLevel.Debug);
- // Get the first private key entry
- var alias = store.Aliases.FirstOrDefault(entryAlias => store.IsKeyEntry(entryAlias));
-
- if (alias == null)
- {
- _logger.LogError("No private key found in the provided PFX/P12 file");
- throw new Exception("No private key found in the provided PFX/P12 file.");
- }
-
- _logger.LogDebug("Found private key with alias: {Alias}", alias);
- // Get the private key
- var keyEntry = store.GetKey(alias);
- var privateKeyParams = keyEntry.Key;
-
- var keyTypeName = PrivateKeyFormatUtilities.GetAlgorithmName(privateKeyParams);
- _logger.LogDebug("Private key type: {KeyType}, requested format: {Format}", keyTypeName, format);
-
- // Use PrivateKeyFormatUtilities to export in the requested format
- // It will automatically fall back to PKCS8 if PKCS1 is not supported for the key type
- var pem = PrivateKeyFormatUtilities.ExportPrivateKeyAsPem(privateKeyParams, format);
-
- _logger.LogTrace("Private key: {Key}", LoggingUtilities.RedactPrivateKeyPem(pem));
- _logger.MethodExit(LogLevel.Debug);
- return pem;
- }
+ => _certificateOperations.ExtractPrivateKeyAsPem(store, password, format);
///
/// Loads a certificate chain from PEM data containing multiple certificates.
///
/// PEM string potentially containing multiple certificates.
/// List of parsed X509Certificate objects.
- public List LoadCertificateChain(string pemData)
- {
- _logger.MethodEntry(LogLevel.Debug);
- var pemReader = new PemReader(new StringReader(pemData));
- var certificates = new List();
-
- PemObject pemObject;
- while ((pemObject = pemReader.ReadPemObject()) != null)
- if (pemObject.Type == "CERTIFICATE")
- {
- var certificateParser = new X509CertificateParser();
- var certificate = certificateParser.ReadCertificate(pemObject.Content);
- certificates.Add(certificate);
- }
-
- _logger.LogDebug("Loaded {Count} certificates from chain", certificates.Count);
- _logger.MethodExit(LogLevel.Debug);
- return certificates;
- }
+ public List LoadCertificateChain(string pemData) => _certificateOperations.LoadCertificateChain(pemData);
///
/// Converts a BouncyCastle X509Certificate to PEM format.
///
/// The certificate to convert.
/// PEM-formatted certificate string.
- public string ConvertToPem(X509Certificate certificate)
- {
- _logger.MethodEntry(LogLevel.Debug);
- var pemObject = new PemObject("CERTIFICATE", certificate.GetEncoded());
- using var stringWriter = new StringWriter();
- var pemWriter = new PemWriter(stringWriter);
- pemWriter.WriteObject(pemObject);
- pemWriter.Writer.Flush();
- _logger.MethodExit(LogLevel.Debug);
- return stringWriter.ToString();
- }
+ public string ConvertToPem(X509Certificate certificate) => _certificateOperations.ConvertToPem(certificate);
///
/// Discovers secrets across namespaces in the Kubernetes cluster.
@@ -2450,7 +715,7 @@ private void DiscoverSecretsInNamespace(
_logger.LogDebug("Discovering secrets in namespace: {Namespace}", namespaceName);
var secrets = RetryPolicy(() =>
- Client.CoreV1.ListNamespacedSecret(namespaceName).Items);
+ _secretOperations.ListSecrets(namespaceName).Items);
foreach (var secret in secrets)
ProcessSecretIfSupported(secret, secType, allowedKeys, clusterName, namespaceName, locations);
@@ -2468,10 +733,20 @@ private void ProcessSecretIfSupported(
return;
}
- var secretData = RetryPolicy(() =>
- Client.CoreV1.ReadNamespacedSecret(secret.Metadata.Name, namespaceName));
+ try
+ {
+ var secretData = RetryPolicy(() =>
+ Client.CoreV1.ReadNamespacedSecret(secret.Metadata.Name, namespaceName));
- ProcessSecret(secret, secretData, allowedKeys, clusterName, namespaceName, locations);
+ ProcessSecret(secret, secretData, allowedKeys, clusterName, namespaceName, locations);
+ }
+ catch (k8s.Autorest.HttpOperationException ex) when (ex.Response?.StatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ // Secret was deleted between listing and reading - this can happen in dynamic environments
+ _logger.LogDebug(
+ "Secret '{SecretName}' in namespace '{Namespace}' was deleted before it could be read, skipping.",
+ secret.Metadata.Name, namespaceName);
+ }
}
private T RetryPolicy(Func action)
@@ -2623,8 +898,7 @@ public JksSecret GetJksSecret(string secretName, string namespaceName, string pa
// Logger.LogTrace("secret.Data: " + secret.Data);
if (secret.Data != null)
{
- _logger.LogTrace("secret.Data.Keys: {Name}", secret.Data.Keys);
- _logger.LogTrace("secret.Data.Keys.Count: " + secret.Data.Keys.Count);
+ _logger.LogTrace("secret.Data.Keys.Count: {Count}", secret.Data.Keys.Count);
allowedKeys ??= new List { "jks", "JKS", "Jks" };
@@ -2639,9 +913,9 @@ public JksSecret GetJksSecret(string secretName, string namespaceName, string pa
if (!isJksField) continue;
- _logger.LogTrace("Key " + secretFieldName + " is in list of allowed keys" + allowedKeys);
+ _logger.LogTrace("Key {FieldName} is in list of allowed keys", secretFieldName);
var data = secret.Data[secretFieldName];
- _logger.LogTrace("data: " + data);
+ _logger.LogTrace("data length: {Length}", data?.Length);
secretData.Add(secretFieldName, data);
}
@@ -2707,28 +981,24 @@ public Pkcs12Secret GetPkcs12Secret(string secretName, string namespaceName, str
{
var secret = Client.CoreV1.ReadNamespacedSecret(secretName, namespaceName);
_logger.LogTrace("Finished calling CoreV1.ReadNamespacedSecret()");
- // Logger.LogTrace("secret: " + secret);
- // Logger.LogTrace("secret.Data: " + secret.Data);
- _logger.LogTrace("secret.Data.Keys: " + secret.Data.Keys);
- _logger.LogTrace("secret.Data.Keys.Count: " + secret.Data.Keys.Count);
+ _logger.LogTrace("secret.Data.Keys.Count: {Count}", secret.Data.Keys.Count);
allowedKeys ??= new List { "pkcs12", "p12", "P12", "PKCS12", "pfx", "PFX" };
-
var secretData = new Dictionary();
foreach (var secretFieldName in secret?.Data.Keys)
{
- _logger.LogTrace("secretFieldName: " + secretFieldName);
+ _logger.LogTrace("secretFieldName: {FieldName}", secretFieldName);
var sField = secretFieldName;
if (secretFieldName.Contains('.')) sField = secretFieldName.Split(".")[^1];
var isPkcs12Field = allowedKeys.Any(allowedKey => sField.Contains(allowedKey));
if (!isPkcs12Field) continue;
- _logger.LogTrace("Key " + secretFieldName + " is in list of allowed keys" + allowedKeys);
+ _logger.LogTrace("Key {FieldName} is in list of allowed keys", secretFieldName);
var data = secret.Data[secretFieldName];
- _logger.LogTrace("data: " + data);
+ _logger.LogTrace("data length: {Length}", data?.Length);
secretData.Add(secretFieldName, data);
}
@@ -2782,7 +1052,7 @@ public V1CertificateSigningRequest CreateCertificateSigningRequest(string name,
SignerName = "kubernetes.io/kube-apiserver-client"
}
};
- _logger.LogTrace("request: " + request);
+ _logger.LogTrace("Request: {Request}", request);
_logger.LogTrace("Calling CertificatesV1.CreateCertificateSigningRequest()");
var result = Client.CertificatesV1.CreateCertificateSigningRequest(request);
_logger.MethodExit(LogLevel.Debug);
@@ -2805,14 +1075,14 @@ public CsrObject GenerateCertificateRequest(string name, string[] sans, IPAddres
_logger.MethodEntry(LogLevel.Debug);
_logger.LogTrace("Name: {Name}, KeyType: {KeyType}, KeyBits: {KeyBits}", name, keyType, keyBits);
var sanBuilder = new SubjectAlternativeNameBuilder();
- _logger.LogDebug($"Building IP and SAN lists for CSR {name}");
+ _logger.LogDebug("Building IP and SAN lists for CSR {Name}", name);
foreach (var ip in ips) sanBuilder.AddIpAddress(ip);
foreach (var san in sans) sanBuilder.AddDnsName(san);
- _logger.LogTrace("sanBuilder: " + sanBuilder);
+ _logger.LogTrace("SanBuilder: {SanBuilder}", sanBuilder);
- _logger.LogTrace("Setting DN to CN=" + name);
+ _logger.LogTrace("Setting DN to CN={Name}", name);
var distinguishedName = new X500DistinguishedName(name);
_logger.LogDebug("Generating private key and CSR");
@@ -2855,45 +1125,6 @@ public CsrObject GenerateCertificateRequest(string name, string[] sans, IPAddres
}
- ///
- /// Gets certificate inventory from Opaque secrets.
- /// Currently returns empty list - placeholder for future implementation.
- ///
- /// Empty list of inventory items.
- public IEnumerable GetOpaqueSecretCertificateInventory()
- {
- _logger.MethodEntry(LogLevel.Debug);
- var inventoryItems = new List();
- _logger.MethodExit(LogLevel.Debug);
- return inventoryItems;
- }
-
- ///
- /// Gets certificate inventory from TLS secrets.
- /// Currently returns empty list - placeholder for future implementation.
- ///
- /// Empty list of inventory items.
- public IEnumerable GetTlsSecretCertificateInventory()
- {
- _logger.MethodEntry(LogLevel.Debug);
- var inventoryItems = new List();
- _logger.MethodExit(LogLevel.Debug);
- return inventoryItems;
- }
-
- ///
- /// Gets certificate inventory from all certificate resources.
- /// Currently returns empty list - placeholder for future implementation.
- ///
- /// Empty list of inventory items.
- public IEnumerable GetCertificateInventory()
- {
- _logger.MethodEntry(LogLevel.Debug);
- var inventoryItems = new List();
- _logger.MethodExit(LogLevel.Debug);
- return inventoryItems;
- }
-
///
/// Creates or updates a JKS secret in Kubernetes.
/// Preserves existing data fields while updating the inventory items.
@@ -2907,7 +1138,7 @@ public V1Secret CreateOrUpdateJksSecret(JksSecret k8SData, string kubeSecretName
_logger.MethodEntry(LogLevel.Debug);
_logger.LogTrace("kubeSecretName: {Name}", kubeSecretName);
_logger.LogTrace("kubeNamespace: {Namespace}", kubeNamespace);
- var s1 = new V1Secret
+ var secret = new V1Secret
{
ApiVersion = "v1",
Kind = "Secret",
@@ -2917,36 +1148,19 @@ public V1Secret CreateOrUpdateJksSecret(JksSecret k8SData, string kubeSecretName
Name = kubeSecretName,
NamespaceProperty = kubeNamespace
},
- Data = k8SData.Secret?.Data //This preserves any existing data/fields we didn't modify
+ Data = k8SData.Secret?.Data // Preserves any existing data/fields we didn't modify
};
-
// Update the fields/data we did modify
- s1.Data ??= new Dictionary();
+ secret.Data ??= new Dictionary();
foreach (var inventoryItem in k8SData.Inventory)
{
_logger.LogTrace("Adding inventory item {Key} to secret", inventoryItem.Key);
- s1.Data[inventoryItem.Key] = inventoryItem.Value;
+ secret.Data[inventoryItem.Key] = inventoryItem.Value;
}
- // Create secret if it doesn't exist
- try
- {
- _logger.LogDebug("Checking if secret {Name} exists in namespace {Namespace}", kubeSecretName,
- kubeNamespace);
- Client.CoreV1.ReadNamespacedSecret(kubeSecretName, kubeNamespace);
- }
- catch (HttpOperationException e)
- {
- if (e.Response.StatusCode == HttpStatusCode.NotFound)
- return Client.CoreV1.CreateNamespacedSecret(s1, kubeNamespace);
- _logger.LogError("Error checking if secret {Name} exists in namespace {Namespace}: {Message}",
- kubeSecretName, kubeNamespace, e.Message);
- }
-
- // Replace existing secret
- _logger.LogDebug("Replacing secret {Name} in namespace {Namespace}", kubeSecretName, kubeNamespace);
- var result = Client.CoreV1.ReplaceNamespacedSecret(s1, kubeSecretName, kubeNamespace);
+ // Use SecretOperations for upsert
+ var result = _secretOperations.CreateOrUpdateSecret(secret, kubeNamespace);
_logger.MethodExit(LogLevel.Debug);
return result;
}
@@ -2963,8 +1177,7 @@ public V1Secret CreateOrUpdatePkcs12Secret(Pkcs12Secret k8SData, string kubeSecr
{
_logger.MethodEntry(LogLevel.Debug);
_logger.LogTrace("SecretName: {Name}, Namespace: {Namespace}", kubeSecretName, kubeNamespace);
- // Create V1Secret object and replace existing secret
- var s1 = new V1Secret
+ var secret = new V1Secret
{
ApiVersion = "v1",
Kind = "Secret",
@@ -2977,23 +1190,12 @@ public V1Secret CreateOrUpdatePkcs12Secret(Pkcs12Secret k8SData, string kubeSecr
Data = k8SData.Secret?.Data
};
- s1.Data ??= new Dictionary();
- foreach (var inventoryItem in k8SData.Inventory) s1.Data[inventoryItem.Key] = inventoryItem.Value;
-
- // Create secret if it doesn't exist
- try
- {
- Client.CoreV1.ReadNamespacedSecret(kubeSecretName, kubeNamespace);
- }
- catch (HttpOperationException e)
- {
- if (e.Response.StatusCode == HttpStatusCode.NotFound)
- return Client.CoreV1.CreateNamespacedSecret(s1, kubeNamespace);
- }
+ secret.Data ??= new Dictionary();
+ foreach (var inventoryItem in k8SData.Inventory)
+ secret.Data[inventoryItem.Key] = inventoryItem.Value;
- // Replace existing secret
- _logger.LogDebug("Replacing secret {Name} in namespace {Namespace}", kubeSecretName, kubeNamespace);
- var result = Client.CoreV1.ReplaceNamespacedSecret(s1, kubeSecretName, kubeNamespace);
+ // Use SecretOperations for upsert
+ var result = _secretOperations.CreateOrUpdateSecret(secret, kubeNamespace);
_logger.MethodExit(LogLevel.Debug);
return result;
}
diff --git a/kubernetes-orchestrator-extension/Clients/KubeconfigParser.cs b/kubernetes-orchestrator-extension/Clients/KubeconfigParser.cs
new file mode 100644
index 0000000..e2c2e8c
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Clients/KubeconfigParser.cs
@@ -0,0 +1,334 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Text;
+using k8s.Exceptions;
+using k8s.KubeConfigModels;
+using Keyfactor.Extensions.Orchestrator.K8S.Utilities;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Clients;
+
+///
+/// Parses kubeconfig JSON strings into K8SConfiguration objects.
+/// Handles base64 decoding, JSON escaping, and environment variable overrides.
+///
+public class KubeconfigParser
+{
+ private readonly ILogger _logger;
+
+ ///
+ /// Environment variable name for overriding TLS verification.
+ ///
+ public const string SkipTlsVerifyEnvVar = "KEYFACTOR_ORCHESTRATOR_SKIP_TLS_VERIFY";
+
+ ///
+ /// Initializes a new instance of the KubeconfigParser.
+ ///
+ /// Logger instance for diagnostic output.
+ public KubeconfigParser(ILogger logger = null)
+ {
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ /// Parses a kubeconfig JSON string into a K8SConfiguration object.
+ ///
+ /// JSON-formatted kubeconfig string (may be base64 encoded).
+ /// When true, skips TLS certificate verification.
+ /// Parsed K8SConfiguration object.
+ /// Thrown when kubeconfig is invalid or missing required fields.
+ public K8SConfiguration Parse(string kubeconfig, bool skipTlsVerify = false)
+ {
+ _logger.MethodEntry(LogLevel.Debug);
+ _logger.LogTrace("Kubeconfig length: {Length}, skipTlsVerify: {SkipTLS}", kubeconfig?.Length ?? 0, skipTlsVerify);
+
+ try
+ {
+ ValidateInput(kubeconfig);
+
+ // Decode and normalize the kubeconfig
+ kubeconfig = DecodeAndNormalize(kubeconfig);
+
+ // Check for environment variable override
+ skipTlsVerify = CheckTlsVerifyOverride(skipTlsVerify);
+
+ // Parse the JSON
+ var configDict = ParseJson(kubeconfig);
+
+ // Build the configuration object
+ var config = BuildConfiguration(configDict, skipTlsVerify);
+
+ _logger.LogDebug("Finished parsing kubeconfig");
+ _logger.MethodExit(LogLevel.Debug);
+ return config;
+ }
+ catch (KubeConfigException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "CRITICAL ERROR in ParseKubeConfig: {Message}", ex.Message);
+ throw new KubeConfigException($"Failed to parse kubeconfig: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Validates the kubeconfig input is not null or empty.
+ ///
+ private void ValidateInput(string kubeconfig)
+ {
+ if (string.IsNullOrEmpty(kubeconfig))
+ {
+ _logger.LogError("kubeconfig is null or empty");
+ throw new KubeConfigException(
+ "kubeconfig is null or empty, please provide a valid kubeconfig in JSON format. " +
+ "For more information on how to create a kubeconfig file, please visit " +
+ "https://github.com/Keyfactor/k8s-orchestrator/tree/main/scripts/kubernetes#example-service-account-json");
+ }
+ }
+
+ ///
+ /// Decodes base64 encoding and normalizes escaped JSON.
+ ///
+ private string DecodeAndNormalize(string kubeconfig)
+ {
+ // Try to decode from base64
+ kubeconfig = TryDecodeBase64(kubeconfig);
+
+ // Handle escaped JSON (fixes bug where all backslashes were removed before newline handling)
+ kubeconfig = NormalizeEscapedJson(kubeconfig);
+
+ // Validate it's a JSON object
+ if (!kubeconfig.TrimStart().StartsWith("{"))
+ {
+ _logger.LogError("kubeconfig is not a JSON object");
+ throw new KubeConfigException(
+ "kubeconfig is not a JSON object, please provide a valid kubeconfig in JSON format. " +
+ "For more information on how to create a kubeconfig file, please visit: " +
+ "https://github.com/Keyfactor/k8s-orchestrator/tree/main/scripts/kubernetes#get_service_account_credssh");
+ }
+
+ return kubeconfig;
+ }
+
+ ///
+ /// Attempts to decode a base64-encoded kubeconfig.
+ ///
+ private string TryDecodeBase64(string kubeconfig)
+ {
+ try
+ {
+ _logger.LogDebug("Testing if kubeconfig is base64 encoded");
+ var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(kubeconfig));
+ _logger.LogDebug("Successfully decoded kubeconfig from base64");
+ return decoded;
+ }
+ catch
+ {
+ _logger.LogTrace("Kubeconfig is not base64 encoded");
+ return kubeconfig;
+ }
+ }
+
+ ///
+ /// Normalizes escaped JSON by handling backslash escaping properly.
+ ///
+ private string NormalizeEscapedJson(string kubeconfig)
+ {
+ if (!kubeconfig.StartsWith("\\"))
+ return kubeconfig;
+
+ _logger.LogDebug("Un-escaping kubeconfig JSON");
+
+ // First convert escaped newlines to actual newlines, then remove escape characters
+ // Note: Order matters - handle \\n before removing backslashes
+ kubeconfig = kubeconfig.Replace("\\n", "\n");
+ kubeconfig = kubeconfig.Replace("\\\"", "\"");
+ kubeconfig = kubeconfig.Replace("\\\\", "\\");
+
+ // Remove leading backslash if still present
+ if (kubeconfig.StartsWith("\\"))
+ kubeconfig = kubeconfig.TrimStart('\\');
+
+ _logger.LogDebug("Successfully un-escaped kubeconfig JSON");
+ return kubeconfig;
+ }
+
+ ///
+ /// Checks for TLS verification override from environment variable.
+ ///
+ private bool CheckTlsVerifyOverride(bool skipTlsVerify)
+ {
+ var skipTlsEnvStr = Environment.GetEnvironmentVariable(SkipTlsVerifyEnvVar);
+ if (string.IsNullOrEmpty(skipTlsEnvStr))
+ return skipTlsVerify;
+
+ _logger.LogTrace("{EnvVar} environment variable: {Value}", SkipTlsVerifyEnvVar, skipTlsEnvStr);
+
+ if (bool.TryParse(skipTlsEnvStr, out var skipTlsVerifyEnv) || skipTlsEnvStr == "1")
+ {
+ if (skipTlsEnvStr == "1") skipTlsVerifyEnv = true;
+
+ if (skipTlsVerifyEnv && !skipTlsVerify)
+ {
+ _logger.LogWarning(
+ "Skipping TLS verification is enabled in environment variable {EnvVar}. " +
+ "This takes the highest precedence and verification will be skipped. " +
+ "To disable this, set the environment variable to 'false' or remove it",
+ SkipTlsVerifyEnvVar);
+ return true;
+ }
+ }
+
+ return skipTlsVerify;
+ }
+
+ ///
+ /// Parses the kubeconfig JSON string into a dictionary.
+ ///
+ private Dictionary ParseJson(string kubeconfig)
+ {
+ _logger.LogDebug("Parsing kubeconfig as JSON");
+ var configDict = JsonConvert.DeserializeObject>(kubeconfig);
+
+ if (configDict == null)
+ throw new KubeConfigException("Failed to deserialize kubeconfig JSON");
+
+ return configDict;
+ }
+
+ ///
+ /// Builds the K8SConfiguration object from the parsed JSON.
+ ///
+ private K8SConfiguration BuildConfiguration(Dictionary configDict, bool skipTlsVerify)
+ {
+ var config = new K8SConfiguration
+ {
+ ApiVersion = configDict["apiVersion"]?.ToString(),
+ Kind = configDict["kind"]?.ToString(),
+ CurrentContext = configDict["current-context"]?.ToString(),
+ Clusters = ParseClusters(configDict, skipTlsVerify),
+ Users = ParseUsers(configDict),
+ Contexts = ParseContexts(configDict)
+ };
+
+ return config;
+ }
+
+ ///
+ /// Parses the clusters array from the configuration.
+ ///
+ private List ParseClusters(Dictionary configDict, bool skipTlsVerify)
+ {
+ _logger.LogDebug("Parsing clusters");
+ var clusters = new List();
+
+ var clustersJson = configDict["clusters"]?.ToString();
+ if (string.IsNullOrEmpty(clustersJson))
+ return clusters;
+
+ foreach (var clusterMetadata in JsonConvert.DeserializeObject(clustersJson))
+ {
+ var clusterObj = new Cluster
+ {
+ Name = clusterMetadata["name"]?.ToString(),
+ ClusterEndpoint = new ClusterEndpoint
+ {
+ Server = clusterMetadata["cluster"]?["server"]?.ToString(),
+ CertificateAuthorityData = clusterMetadata["cluster"]?["certificate-authority-data"]?.ToString(),
+ SkipTlsVerify = skipTlsVerify
+ }
+ };
+
+ _logger.LogDebug("Cluster metadata - Name: {Name}, Server: {Server}, SkipTlsVerify: {SkipTls}",
+ clusterObj.Name, clusterObj.ClusterEndpoint?.Server, skipTlsVerify);
+
+ clusters.Add(clusterObj);
+ }
+
+ _logger.LogTrace("Finished parsing clusters");
+ return clusters;
+ }
+
+ ///
+ /// Parses the users array from the configuration.
+ ///
+ private List ParseUsers(Dictionary configDict)
+ {
+ _logger.LogDebug("Parsing users");
+ var users = new List();
+
+ var usersJson = configDict["users"]?.ToString();
+ if (string.IsNullOrEmpty(usersJson))
+ return users;
+
+ foreach (var user in JsonConvert.DeserializeObject(usersJson))
+ {
+ var token = user["user"]?["token"]?.ToString();
+ var userObj = new User
+ {
+ Name = user["name"]?.ToString(),
+ UserCredentials = new UserCredentials
+ {
+ UserName = user["name"]?.ToString(),
+ Token = token
+ }
+ };
+
+ _logger.LogDebug("User metadata - Name: {Name}, HasToken: {HasToken}",
+ userObj.Name, !string.IsNullOrEmpty(token));
+
+ users.Add(userObj);
+ }
+
+ _logger.LogTrace("Finished parsing users");
+ return users;
+ }
+
+ ///
+ /// Parses the contexts array from the configuration.
+ ///
+ private List ParseContexts(Dictionary configDict)
+ {
+ _logger.LogDebug("Parsing contexts");
+ var contexts = new List();
+
+ var contextsJson = configDict["contexts"]?.ToString();
+ if (string.IsNullOrEmpty(contextsJson))
+ return contexts;
+
+ foreach (var ctx in JsonConvert.DeserializeObject(contextsJson))
+ {
+ var contextObj = new Context
+ {
+ Name = ctx["name"]?.ToString(),
+ ContextDetails = new ContextDetails
+ {
+ Cluster = ctx["context"]?["cluster"]?.ToString(),
+ Namespace = ctx["context"]?["namespace"]?.ToString(),
+ User = ctx["context"]?["user"]?.ToString()
+ }
+ };
+
+ _logger.LogDebug("Context metadata - Name: {Name}, Cluster: {Cluster}, Namespace: {Namespace}, User: {User}",
+ contextObj.Name, contextObj.ContextDetails?.Cluster,
+ contextObj.ContextDetails?.Namespace, contextObj.ContextDetails?.User);
+
+ contexts.Add(contextObj);
+ }
+
+ _logger.LogTrace("Finished parsing contexts");
+ return contexts;
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Clients/SecretOperations.cs b/kubernetes-orchestrator-extension/Clients/SecretOperations.cs
new file mode 100644
index 0000000..993bfc3
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Clients/SecretOperations.cs
@@ -0,0 +1,337 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using k8s;
+using k8s.Models;
+using Keyfactor.Extensions.Orchestrator.K8S.Enums;
+using Keyfactor.Logging;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Clients;
+
+///
+/// Handles Kubernetes secret CRUD operations.
+/// Provides methods for creating, reading, updating, and deleting secrets.
+///
+public class SecretOperations
+{
+ private readonly ILogger _logger;
+ private readonly IKubernetes _client;
+
+ ///
+ /// Initializes a new instance of SecretOperations.
+ ///
+ /// Kubernetes API client.
+ /// Logger instance for diagnostic output.
+ public SecretOperations(IKubernetes client, ILogger logger = null)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ _logger = logger ?? LogHandler.GetClassLogger();
+ }
+
+ ///
+ /// Creates a new Kubernetes secret with the specified data.
+ ///
+ /// Name of the secret to create.
+ /// Namespace where the secret will be created.
+ /// Type of secret (tls, opaque, pkcs12, jks).
+ /// Private key in PEM format (optional for opaque).
+ /// Certificate in PEM format.
+ /// Certificate chain in PEM format.
+ /// Whether to store chain in separate ca.crt field.
+ /// Whether to include the certificate chain.
+ /// The created V1Secret object ready for API submission.
+ public V1Secret BuildNewSecret(
+ string secretName,
+ string namespaceName,
+ string secretType,
+ string keyPem = null,
+ string certPem = null,
+ IList chainPem = null,
+ bool separateChain = true,
+ bool includeChain = true)
+ {
+ _logger.LogTrace("Building new secret: {SecretName} in {Namespace}", secretName, namespaceName);
+
+ // Normalize the secret type
+ var normalizedType = SecretTypes.Normalize(secretType);
+ _logger.LogDebug("Normalized secret type: {OriginalType} -> {NormalizedType}", secretType, normalizedType);
+
+ V1Secret secret;
+
+ if (SecretTypes.IsTlsType(normalizedType))
+ {
+ secret = BuildTlsSecret(secretName, namespaceName, keyPem, certPem);
+ }
+ else if (SecretTypes.IsOpaqueType(normalizedType))
+ {
+ secret = BuildOpaqueSecret(secretName, namespaceName, keyPem, certPem);
+ }
+ else if (SecretTypes.IsKeystoreType(normalizedType))
+ {
+ // Keystore secrets start as empty Opaque secrets
+ secret = BuildEmptyOpaqueSecret(secretName, namespaceName);
+ _logger.LogDebug("Created empty Opaque secret for {Type} store", normalizedType);
+ }
+ else
+ {
+ throw new NotSupportedException($"Secret type '{secretType}' is not supported for new secret creation.");
+ }
+
+ // Add chain if provided and requested
+ if (chainPem != null && chainPem.Count > 0 && includeChain)
+ {
+ AddChainToSecret(secret, certPem, chainPem, separateChain);
+ }
+
+ _logger.LogTrace("Finished building secret");
+ return secret;
+ }
+
+ ///
+ /// Creates a TLS secret (kubernetes.io/tls type).
+ ///
+ private V1Secret BuildTlsSecret(string secretName, string namespaceName, string keyPem, string certPem)
+ {
+ if (string.IsNullOrEmpty(keyPem))
+ {
+ _logger.LogWarning("TLS secrets require a private key. Certificate was provided without private key - creating with empty tls.key field");
+ }
+
+ return new V1Secret
+ {
+ Metadata = new V1ObjectMeta
+ {
+ Name = secretName,
+ NamespaceProperty = namespaceName
+ },
+ Type = "kubernetes.io/tls",
+ Data = new Dictionary
+ {
+ { "tls.key", Encoding.UTF8.GetBytes(keyPem ?? "") },
+ { "tls.crt", Encoding.UTF8.GetBytes(certPem ?? "") }
+ }
+ };
+ }
+
+ ///
+ /// Creates an Opaque secret with certificate data.
+ ///
+ private V1Secret BuildOpaqueSecret(string secretName, string namespaceName, string keyPem, string certPem)
+ {
+ var data = new Dictionary
+ {
+ { "tls.crt", Encoding.UTF8.GetBytes(certPem ?? "") }
+ };
+
+ if (!string.IsNullOrEmpty(keyPem))
+ {
+ data["tls.key"] = Encoding.UTF8.GetBytes(keyPem);
+ }
+ else
+ {
+ _logger.LogDebug("No private key provided for Opaque secret - storing certificate only");
+ }
+
+ return new V1Secret
+ {
+ Metadata = new V1ObjectMeta
+ {
+ Name = secretName,
+ NamespaceProperty = namespaceName
+ },
+ Type = "Opaque",
+ Data = data
+ };
+ }
+
+ ///
+ /// Creates an empty Opaque secret (for keystore initialization).
+ ///
+ private V1Secret BuildEmptyOpaqueSecret(string secretName, string namespaceName)
+ {
+ return new V1Secret
+ {
+ Metadata = new V1ObjectMeta
+ {
+ Name = secretName,
+ NamespaceProperty = namespaceName
+ },
+ Type = "Opaque",
+ Data = new Dictionary()
+ };
+ }
+
+ ///
+ /// Adds certificate chain to an existing secret.
+ ///
+ private void AddChainToSecret(V1Secret secret, string certPem, IList chainPem, bool separateChain)
+ {
+ // Filter out the leaf certificate from the chain
+ var chainCerts = chainPem.Where(c => c != certPem).ToList();
+ if (chainCerts.Count == 0)
+ return;
+
+ var chainPemString = string.Join("", chainCerts);
+
+ if (separateChain)
+ {
+ secret.Data["ca.crt"] = Encoding.UTF8.GetBytes(chainPemString);
+ _logger.LogDebug("Added certificate chain to ca.crt field");
+ }
+ else
+ {
+ // Bundle chain with the certificate in tls.crt
+ var existingCert = Encoding.UTF8.GetString(secret.Data["tls.crt"]);
+ secret.Data["tls.crt"] = Encoding.UTF8.GetBytes(existingCert + chainPemString);
+ _logger.LogDebug("Bundled certificate chain into tls.crt field");
+ }
+ }
+
+ ///
+ /// Updates an existing Opaque secret with new certificate data.
+ ///
+ /// The existing secret to update.
+ /// New private key (null to keep existing).
+ /// New certificate.
+ /// Certificate chain.
+ /// Whether to store chain separately.
+ /// Whether to include the chain.
+ /// The updated V1Secret object.
+ public V1Secret UpdateOpaqueSecretData(
+ V1Secret existingSecret,
+ string newKeyPem,
+ string newCertPem,
+ IList chainPem = null,
+ bool separateChain = true,
+ bool includeChain = true)
+ {
+ _logger.LogTrace("Updating Opaque secret data");
+
+ // Update private key only if provided
+ if (!string.IsNullOrEmpty(newKeyPem))
+ {
+ existingSecret.Data["tls.key"] = Encoding.UTF8.GetBytes(newKeyPem);
+ }
+ else
+ {
+ _logger.LogDebug("No private key provided in update - keeping existing tls.key if present");
+ }
+
+ // Update certificate
+ if (!string.IsNullOrEmpty(newCertPem))
+ {
+ existingSecret.Data["tls.crt"] = Encoding.UTF8.GetBytes(newCertPem);
+ }
+
+ // Handle chain
+ if (chainPem != null && chainPem.Count > 0 && includeChain)
+ {
+ AddChainToSecret(existingSecret, newCertPem, chainPem, separateChain);
+ }
+
+ return existingSecret;
+ }
+
+ ///
+ /// Reads a secret from the Kubernetes API.
+ ///
+ /// Name of the secret.
+ /// Namespace of the secret.
+ /// The V1Secret if found, null otherwise.
+ public V1Secret GetSecret(string secretName, string namespaceName)
+ {
+ _logger.LogTrace("Reading secret {SecretName} from namespace {Namespace}", secretName, namespaceName);
+
+ try
+ {
+ return _client.CoreV1.ReadNamespacedSecret(secretName, namespaceName);
+ }
+ catch (k8s.Autorest.HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ _logger.LogDebug("Secret {SecretName} not found in namespace {Namespace}", secretName, namespaceName);
+ return null;
+ }
+ }
+
+ ///
+ /// Creates a new secret in Kubernetes.
+ ///
+ /// The secret to create.
+ /// Namespace where to create the secret.
+ /// The created secret.
+ public V1Secret CreateSecret(V1Secret secret, string namespaceName)
+ {
+ _logger.LogDebug("Creating secret {SecretName} in namespace {Namespace}",
+ secret.Metadata?.Name, namespaceName);
+
+ return _client.CoreV1.CreateNamespacedSecret(secret, namespaceName);
+ }
+
+ ///
+ /// Updates an existing secret in Kubernetes.
+ ///
+ /// The secret to update.
+ /// Namespace of the secret.
+ /// The updated secret.
+ public V1Secret UpdateSecret(V1Secret secret, string namespaceName)
+ {
+ _logger.LogDebug("Updating secret {SecretName} in namespace {Namespace}",
+ secret.Metadata?.Name, namespaceName);
+
+ return _client.CoreV1.ReplaceNamespacedSecret(secret, secret.Metadata.Name, namespaceName);
+ }
+
+ ///
+ /// Deletes a secret from Kubernetes.
+ ///
+ /// Name of the secret to delete.
+ /// Namespace of the secret.
+ /// Status of the delete operation.
+ public V1Status DeleteSecret(string secretName, string namespaceName)
+ {
+ _logger.LogDebug("Deleting secret {SecretName} from namespace {Namespace}", secretName, namespaceName);
+
+ return _client.CoreV1.DeleteNamespacedSecret(secretName, namespaceName);
+ }
+
+ ///
+ /// Lists all secrets in a namespace.
+ ///
+ /// Namespace to list secrets from.
+ /// List of secrets in the namespace.
+ public V1SecretList ListSecrets(string namespaceName)
+ {
+ _logger.LogTrace("Listing secrets in namespace {Namespace}", namespaceName);
+
+ return _client.CoreV1.ListNamespacedSecret(namespaceName);
+ }
+
+ ///
+ /// Creates or updates a secret (upsert operation).
+ ///
+ /// The secret to create or update.
+ /// Namespace for the operation.
+ /// The created or updated secret.
+ public V1Secret CreateOrUpdateSecret(V1Secret secret, string namespaceName)
+ {
+ var existing = GetSecret(secret.Metadata.Name, namespaceName);
+
+ if (existing != null)
+ {
+ // Preserve resource version for update
+ secret.Metadata.ResourceVersion = existing.Metadata.ResourceVersion;
+ return UpdateSecret(secret, namespaceName);
+ }
+
+ return CreateSecret(secret, namespaceName);
+ }
+}
From 1ee4a8323736a9879cfbfcb6c8b7df6e65bbced2 Mon Sep 17 00:00:00 2001
From: spbsoluble <1661003+spbsoluble@users.noreply.github.com>
Date: Wed, 15 Apr 2026 12:35:19 -0700
Subject: [PATCH 04/28] refactor: restructure job classes by store type, remove
X509Certificate2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Job structure (flat → per-store-type):
- Remove Jobs/Inventory.cs, Management.cs, Discovery.cs, Reenrollment.cs
(monolithic files with large switch statements on store type)
- Add Jobs/Base/: K8SJobBase, InventoryBase, ManagementBase, DiscoveryBase,
ReenrollmentBase — shared logic each job type delegates to its handler
- Add Jobs/StoreTypes//: one class per operation per store type
(7 store types × up to 4 operations = 26 concrete job classes)
- manifest.json updated to route each capability to its dedicated class
X509Certificate2 removal:
- Replace X509Certificate2 usage throughout with BouncyCastle types
- K8SCertificateContext replaces X509Certificate2-based SerializedStoreInfo
- LoggingUtilities updated: GetCertificateSummary now accepts BouncyCastle
X509Certificate; RedactPassword no longer leaks password length
Version logging:
- JobBase reads AssemblyInformationalVersionAttribute at startup and logs
"K8S Orchestrator Extension version: {Version}" on every job execution
(baked in at build time by GitHub Actions via -p:Version=)
Also removes TestConsole (superseded by integration test suite) and
store_types.json (superseded by integration-manifest.json).
---
Keyfactor.Orchestrators.K8S.sln | 2 -
TestConsole/Program.cs | 717 -----
TestConsole/TestConsole.csproj | 17 -
TestConsole/generate_vault_certs.sh | 185 --
TestConsole/tests.json | 0
TestConsole/tests.yml | 0
TestConsole/yaml2json.sh | 5 -
.../Jobs/Base/DiscoveryBase.cs | 153 +
.../Jobs/Base/InventoryBase.cs | 139 +
.../Jobs/Base/K8SJobBase.cs | 159 ++
.../Jobs/Base/ManagementBase.cs | 154 +
.../Jobs/Base/ReenrollmentBase.cs | 83 +
.../Jobs/Discovery.cs | 294 --
.../Jobs/Inventory.cs | 2141 --------------
.../Jobs/JobBase.cs | 2491 ++---------------
.../Jobs/Management.cs | 1220 --------
.../Jobs/Reenrollment.cs | 108 -
.../Jobs/StoreTypes/K8SCert/Discovery.cs | 20 +
.../Jobs/StoreTypes/K8SCert/Inventory.cs | 21 +
.../Jobs/StoreTypes/K8SCluster/Discovery.cs | 20 +
.../Jobs/StoreTypes/K8SCluster/Inventory.cs | 20 +
.../Jobs/StoreTypes/K8SCluster/Management.cs | 20 +
.../StoreTypes/K8SCluster/Reenrollment.cs | 20 +
.../Jobs/StoreTypes/K8SJKS/Discovery.cs | 20 +
.../Jobs/StoreTypes/K8SJKS/Inventory.cs | 20 +
.../Jobs/StoreTypes/K8SJKS/Management.cs | 20 +
.../Jobs/StoreTypes/K8SJKS/Reenrollment.cs | 20 +
.../Jobs/StoreTypes/K8SNS/Discovery.cs | 20 +
.../Jobs/StoreTypes/K8SNS/Inventory.cs | 20 +
.../Jobs/StoreTypes/K8SNS/Management.cs | 20 +
.../Jobs/StoreTypes/K8SNS/Reenrollment.cs | 20 +
.../Jobs/StoreTypes/K8SPKCS12/Discovery.cs | 20 +
.../Jobs/StoreTypes/K8SPKCS12/Inventory.cs | 20 +
.../Jobs/StoreTypes/K8SPKCS12/Management.cs | 20 +
.../Jobs/StoreTypes/K8SPKCS12/Reenrollment.cs | 20 +
.../Jobs/StoreTypes/K8SSecret/Discovery.cs | 20 +
.../Jobs/StoreTypes/K8SSecret/Inventory.cs | 20 +
.../Jobs/StoreTypes/K8SSecret/Management.cs | 20 +
.../Jobs/StoreTypes/K8SSecret/Reenrollment.cs | 20 +
.../Jobs/StoreTypes/K8STLSSecr/Discovery.cs | 20 +
.../Jobs/StoreTypes/K8STLSSecr/Inventory.cs | 20 +
.../Jobs/StoreTypes/K8STLSSecr/Management.cs | 20 +
.../StoreTypes/K8STLSSecr/Reenrollment.cs | 20 +
.../Utilities/LoggingUtilities.cs | 4 +-
.../manifest.json | 64 +-
45 files changed, 1536 insertions(+), 6921 deletions(-)
delete mode 100644 TestConsole/Program.cs
delete mode 100644 TestConsole/TestConsole.csproj
delete mode 100644 TestConsole/generate_vault_certs.sh
delete mode 100644 TestConsole/tests.json
delete mode 100644 TestConsole/tests.yml
delete mode 100644 TestConsole/yaml2json.sh
create mode 100644 kubernetes-orchestrator-extension/Jobs/Base/DiscoveryBase.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/Base/InventoryBase.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/Base/K8SJobBase.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/Base/ManagementBase.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/Base/ReenrollmentBase.cs
delete mode 100644 kubernetes-orchestrator-extension/Jobs/Discovery.cs
delete mode 100644 kubernetes-orchestrator-extension/Jobs/Inventory.cs
delete mode 100644 kubernetes-orchestrator-extension/Jobs/Management.cs
delete mode 100644 kubernetes-orchestrator-extension/Jobs/Reenrollment.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SCert/Discovery.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SCert/Inventory.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SCluster/Discovery.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SCluster/Inventory.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SCluster/Management.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SCluster/Reenrollment.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SJKS/Discovery.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SJKS/Inventory.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SJKS/Management.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SJKS/Reenrollment.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SNS/Discovery.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SNS/Inventory.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SNS/Management.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SNS/Reenrollment.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SPKCS12/Discovery.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SPKCS12/Inventory.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SPKCS12/Management.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SPKCS12/Reenrollment.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SSecret/Discovery.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SSecret/Inventory.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SSecret/Management.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8SSecret/Reenrollment.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8STLSSecr/Discovery.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8STLSSecr/Inventory.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8STLSSecr/Management.cs
create mode 100644 kubernetes-orchestrator-extension/Jobs/StoreTypes/K8STLSSecr/Reenrollment.cs
diff --git a/Keyfactor.Orchestrators.K8S.sln b/Keyfactor.Orchestrators.K8S.sln
index c1eb62b..b7e2c55 100644
--- a/Keyfactor.Orchestrators.K8S.sln
+++ b/Keyfactor.Orchestrators.K8S.sln
@@ -5,8 +5,6 @@ VisualStudioVersion = 17.3.32929.385
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keyfactor.Orchestrators.K8S", "kubernetes-orchestrator-extension\Keyfactor.Orchestrators.K8S.csproj", "{F497D7FA-AC9F-4BB2-935F-6A7569ACC173}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestConsole", "TestConsole\TestConsole.csproj", "{8C2C6B52-E386-4DAE-B596-7EE4E64EB0F4}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "kubernetes-orchestrator-extension.Tests", "kubernetes-orchestrator-extension.Tests", "{4D988838-9BAF-C253-004D-7C7673F12805}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keyfactor.Orchestrators.K8S.Tests", "kubernetes-orchestrator-extension.Tests\Keyfactor.Orchestrators.K8S.Tests.csproj", "{7976404A-58D7-4709-99A9-DBBA31431C69}"
diff --git a/TestConsole/Program.cs b/TestConsole/Program.cs
deleted file mode 100644
index 87eee0c..0000000
--- a/TestConsole/Program.cs
+++ /dev/null
@@ -1,717 +0,0 @@
-// Copyright 2022 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;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
-using Keyfactor.Extensions.Orchestrator.K8S.Jobs;
-using Keyfactor.Orchestrators.Common.Enums;
-using Keyfactor.Orchestrators.Extensions;
-using Keyfactor.Orchestrators.Extensions.Interfaces;
-using Moq;
-using Newtonsoft.Json;
-
-namespace TestConsole;
-
-public class OrchTestCase
-{
- public string TestName { get; set; }
-
- public string Description { get; set; }
-
- public bool Fail { get; set; }
-
- public string ExpectedValue { get; set; }
-
- public JobConfig JobConfig { get; set; }
-}
-
-public class CertificateStoreDetails
-{
- public string ClientMachine { get; set; }
-
- public string StorePath { get; set; }
-
- public string StorePassword { get; set; }
-
- public string Properties { get; set; }
-
- public int Type { get; set; }
-}
-
-public class JobCertificate
-{
- public object Thumbprint { get; set; }
-
- public string Contents { get; set; }
-
- public string Alias { get; set; }
-
- public string PrivateKeyPassword { get; set; }
-}
-
-public class JobConfig
-{
- public List LastInventory { get; set; }
-
- public CertificateStoreDetails CertificateStoreDetails { get; set; }
-
- public bool JobCancelled { get; set; }
-
- public object ServerError { get; set; }
-
- public int JobHistoryId { get; set; }
-
- public int RequestStatus { get; set; }
-
- public string ServerUsername { get; set; }
-
- public string ServerPassword { get; set; }
-
- public bool UseSSL { get; set; }
-
- public object JobProperties { get; set; }
-
- public string JobTypeId { get; set; }
-
- public string JobId { get; set; }
-
- public string Capability { get; set; }
-
- public int OperationType { get; set; }
-
- public bool Overwrite { get; set; }
-
- public JobCertificate JobCertificate { get; set; }
-}
-
-public class JobProperties
-{
- [JsonProperty("Trusted Root")] public bool TrustedRoot { get; set; }
-}
-
-public class OrchestratorTestConfig
-{
- public List inventory { get; set; }
-
- public List add { get; set; }
-
- public List remove { get; set; }
-
- public List discovery { get; set; }
-}
-
-internal class Program
-{
- private const string EnvironmentVariablePrefix = "TEST_";
- private const string KubeConfigEnvVar = "TEST_KUBECONFIG";
- private const string KubeNamespaceEnvVar = "TEST_KUBE_NAMESPACE";
-
- public static int tableWidth = 200;
-
- private static readonly TestEnvironmentalVariable[] _envVariables;
-
- static Program()
- {
- _envVariables = new[]
- {
- new TestEnvironmentalVariable
- {
- Name = "TEST_KUBECONFIG",
- Description = "Kubeconfig file contents",
- Default = "kubeconfig",
- Type = "string",
- Secret = true
- },
- new TestEnvironmentalVariable
- {
- Name = "TEST_KUBE_NAMESPACE",
- Description = "Kubernetes namespace",
- Default = "default",
- Type = "string"
- },
- new TestEnvironmentalVariable
- {
- Name = "TEST_CERT_MGMT_TYPE",
- Description = "Certificate management type",
- Default = "inv",
- Choices = new[] { "inv", "add", "remove" },
- Type = "string"
- },
- new TestEnvironmentalVariable
- {
- Name = "TEST_MANUAL",
- Description = "Manual test",
- Default = "false",
- Type = "bool"
- },
- new TestEnvironmentalVariable
- {
- Name = "TEST_ORCH_OPERATION",
- Description = "Orchestrator operation",
- Default = "inv",
- Type = "string",
- Choices = new[] { "inv", "mgmt" }
- }
- };
- }
-
- public static string ShowEnvConfig(string format = "json")
- {
- var envConfig = new Dictionary();
- var showSecrets = Environment.GetEnvironmentVariable("TEST_SHOW_SECRETS") == "true";
- foreach (var testVar in _envVariables)
- {
- if (testVar.Secret)
- {
- if (showSecrets)
- {
- envConfig.Add(testVar.Name, Environment.GetEnvironmentVariable(testVar.Name));
- continue;
- }
-
- envConfig.Add(testVar.Name, "********");
- continue;
- }
-
- envConfig.Add(testVar.Name, Environment.GetEnvironmentVariable(testVar.Name));
- }
-
- return format == "json" ? JsonConvert.SerializeObject(envConfig, Formatting.Indented) : envConfig.ToString();
- }
-
-
- public static OrchTestCase[] GetTestConfig(string testFileName, string jobType = "inventory")
- {
- // Read test config from file as JSON and deserialize to TestConfiguration
- var testConfig = JsonConvert.DeserializeObject(File.ReadAllText(testFileName));
-
- //convert testList to array of objects
- switch (jobType)
- {
- case "inventory":
- case "inv":
- case "i":
- return testConfig.inventory.ToArray();
- case "add":
- case "a":
- return testConfig.add.ToArray();
- case "remove":
- case "rem":
- case "r":
- return testConfig.remove.ToArray();
- case "discovery":
- case "discover":
- case "disc":
- case "d":
- return testConfig.discovery.ToArray();
- }
-
- throw new Exception("Invalid job type");
- }
-
- private static async Task Main(string[] args)
- {
- var runTypeStr = Environment.GetEnvironmentVariable("TEST_MANUAL");
- var isManualTest = !string.IsNullOrEmpty(runTypeStr) && bool.Parse(runTypeStr);
- var hasFailure = false;
-
- var testOutputDict = new Dictionary();
-
- Console.WriteLine("====KubeTestConsole====");
- Console.WriteLine("Environment Variables:");
- Console.WriteLine(ShowEnvConfig());
- Console.WriteLine("====End Environmental Variables====");
-
- var pamUserNameField = Environment.GetEnvironmentVariable("TEST_PAM_USERNAME_FIELD") ?? "ServerUsername";
- var pamPasswordField = Environment.GetEnvironmentVariable("TEST_PAM_PASSWORD_FIELD") ?? "ServerPassword";
-
- if (args.Length == 0)
- {
- // check TEST_OPERATION env var and use that if it else prompt user
- var testOperation = Environment.GetEnvironmentVariable("TEST_ORCH_OPERATION");
- var input = testOperation;
- if (string.IsNullOrEmpty(testOperation) || isManualTest)
- {
- Console.WriteLine("Enter Operation: (I)nventory, or (M)anagement");
- input = Console.ReadLine();
- }
-
- var testConfigPath = Environment.GetEnvironmentVariable("TEST_CONFIG_PATH") ?? "tests.json";
-
- var pamMockUsername = Environment.GetEnvironmentVariable("TEST_PAM_MOCK_USERNAME") ?? string.Empty;
- var pamMockPassword = Environment.GetEnvironmentVariable("TEST_PAM_MOCK_PASSWORD") ?? string.Empty;
-
- Console.WriteLine("TEST_PAM_USERNAME_FIELD: " + pamUserNameField);
- Console.WriteLine("TEST_PAM_MOCK_USERNAME: " + pamMockUsername);
-
- Console.WriteLine("TEST_PAM_PASSWORD_FIELD: " + pamPasswordField);
- Console.WriteLine("TEST_PAM_MOCK_PASSWORD: " + pamMockPassword);
-
- var secretResolver = new Mock();
- // Get from env var TEST_KUBECONFIG
- // setup resolver for "Server Username" to return "kubeconfig"
- secretResolver.Setup(m =>
- m.Resolve(It.Is(s => s == pamUserNameField))).Returns(() => pamMockUsername);
- // setup resolver for "Server Password" to return the value of the env var TEST_KUBECONFIG
- secretResolver.Setup(m =>
- m.Resolve(It.Is(s => s == pamPasswordField))).Returns(() => pamMockPassword);
-
-
- var tests = new OrchTestCase[] { };
-
- input = input.ToLower();
- switch (input)
- {
- case "inventory":
- case "inv":
- case "i":
- // Get test configurations from testConfigPath
-
- tests = GetTestConfig(testConfigPath, input);
- var inv = new Inventory(secretResolver.Object);
-
- Console.WriteLine("Running Inventory Job Test Cases");
- foreach (var testCase in tests)
- {
- testOutputDict.Add(testCase.TestName, "Running");
- try
- {
- //convert testCase to InventoryJobConfig
- Console.WriteLine($"=============={testCase.TestName}==================");
- Console.WriteLine($"Description: {testCase.Description}");
- Console.WriteLine($"Expected Fail: {testCase.Fail.ToString()}");
- Console.WriteLine($"Expected Result: {testCase.ExpectedValue}");
-
-
- var invJobConfig =
- GetInventoryJobConfiguration(JsonConvert.SerializeObject(testCase.JobConfig));
- SubmitInventoryUpdate sui = GetItems;
-
- var jobResult = inv.ProcessJob(invJobConfig, sui);
-
- if (jobResult.Result == OrchestratorJobStatusJobResult.Success ||
- (jobResult.Result == OrchestratorJobStatusJobResult.Failure && testCase.Fail))
- {
- testOutputDict[testCase.TestName] = $"Success {jobResult.FailureMessage}";
- Console.ForegroundColor = ConsoleColor.Green;
- }
- else
- {
- testOutputDict[testCase.TestName] = $"Failure - {jobResult.FailureMessage}";
- Console.ForegroundColor = ConsoleColor.Red;
- hasFailure = true;
- }
-
- Console.WriteLine(
- $"Job Hist ID:{jobResult.JobHistoryId}\nStorePath:{invJobConfig.CertificateStoreDetails.StorePath}\nStore Properties:\n{invJobConfig.CertificateStoreDetails.Properties}\nMessage: {jobResult.FailureMessage}\nResult: {jobResult.Result}");
- Console.ResetColor();
- }
- catch (Exception e)
- {
- testOutputDict[testCase.TestName] = $"Failure - {e.Message}";
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine(e);
- Console.WriteLine($"Failed to run inventory test case: {testCase.TestName}");
- Console.ResetColor();
- }
- }
-
- Console.WriteLine("Finished Running Inventory Job Test Cases");
- break;
- case "management":
- case "man":
- case "m":
- // Get from env var TEST_CERT_MGMT_TYPE or prompt for it if not set
- var testMgmtType = Environment.GetEnvironmentVariable("TEST_CERT_MGMT_TYPE");
-
- if (string.IsNullOrEmpty(testMgmtType) || isManualTest)
- {
- Console.WriteLine("Select Management Type Add or Remove");
- testMgmtType = Console.ReadLine();
- }
-
- tests = GetTestConfig(testConfigPath, testMgmtType);
-
- Console.WriteLine("Running Management Job Test Cases");
- foreach (var testCase in tests)
- {
- testOutputDict.Add(testCase.TestName, "Running");
- try
- {
- //convert testCase to InventoryJobConfig
- Console.WriteLine($"=============={testCase.TestName}==================");
- Console.WriteLine($"Description: {testCase.Description}");
- Console.WriteLine($"Expected Fail: {testCase.Fail.ToString()}");
- Console.WriteLine($"Expected Result: {testCase.ExpectedValue}");
- // var jobConfig = GetManagementJobConfiguration(JsonConvert.SerializeObject(testCase.JobConfig), testCase.JobConfig.JobCertificate.Alias);
-
- //======================================================================================================
-
- var jobResult = new JobResult();
- switch (testMgmtType)
- {
- case "Add":
- case "add":
- case "a":
- {
- // Get from env var TEST_PKEY_PASSWORD or prompt for it if not set
- var testPrivateKeyPwd = Environment.GetEnvironmentVariable("TEST_PKEY_PASSWORD") ??
- testCase.JobConfig.JobCertificate.PrivateKeyPassword;
- var privateKeyPwd = testPrivateKeyPwd;
- if (string.IsNullOrEmpty(testPrivateKeyPwd) &&
- isManualTest) //Only prompt on explicit set of TEST_USE_PKEY_PASS and that password has not been provided
- {
- Console.WriteLine(
- "Enter private key password or leave blank if no private key");
- privateKeyPwd = Console.ReadLine();
- }
- else
- {
- Console.WriteLine(
- "Using Private Key Password from env var 'TEST_PKEY_PASSWORD'");
- Console.WriteLine("Password: " + testPrivateKeyPwd);
- }
-
- var isOverwriteStr = Environment.GetEnvironmentVariable("TEST_JOB_OVERWRITE") ??
- "true";
- var isOverwrite = !string.IsNullOrEmpty(isOverwriteStr) &&
- bool.Parse(isOverwriteStr);
- if (string.IsNullOrEmpty(isOverwriteStr) && isManualTest)
- {
- Console.WriteLine("Overwrite? Enter true or false");
- isOverwriteStr = Console.ReadLine();
- isOverwrite = bool.Parse(isOverwriteStr);
- }
-
- var certAlias = Environment.GetEnvironmentVariable("TEST_CERT_ALIAS") ??
- testCase.JobConfig.JobCertificate.Alias;
- if (string.IsNullOrEmpty(certAlias) && isManualTest)
- {
- Console.WriteLine("Enter cert alias. This is usually the cert thumbprint.");
- certAlias = Console.ReadLine();
- }
-
- var isTrustedRootStr = Environment.GetEnvironmentVariable("TEST_IS_TRUSTED_ROOT") ??
- "false";
- var isTrustedRoot = !string.IsNullOrEmpty(isTrustedRootStr) &&
- bool.Parse(isTrustedRootStr);
- if (string.IsNullOrEmpty(isTrustedRootStr) && isManualTest)
- {
- Console.WriteLine("Trusted Root? Enter true or false");
- isTrustedRootStr = Console.ReadLine();
- isTrustedRoot = bool.Parse(isTrustedRootStr);
- }
-
- var mgmt = new Management(secretResolver.Object);
-
- var jobConfig = GetJobManagementConfiguration(
- JsonConvert.SerializeObject(testCase.JobConfig),
- certAlias,
- privateKeyPwd,
- isOverwrite,
- isTrustedRoot
- );
-
- jobResult = mgmt.ProcessJob(jobConfig);
- if (testCase.Fail && jobResult.Result == OrchestratorJobStatusJobResult.Success)
- {
- testOutputDict[testCase.TestName] =
- $"Failure - {jobResult.FailureMessage} This test case was expected to fail but succeeded.";
- Console.ForegroundColor = ConsoleColor.Red;
- hasFailure = true;
- }
- else if (!testCase.Fail &&
- jobResult.Result == OrchestratorJobStatusJobResult.Failure)
- {
- testOutputDict[testCase.TestName] =
- $"Failure - {jobResult.FailureMessage} This test case was expected to succeed but failed.";
- Console.ForegroundColor = ConsoleColor.Red;
- hasFailure = true;
- }
- else
- {
- testOutputDict[testCase.TestName] = $"Success {jobResult.FailureMessage}";
- Console.ForegroundColor = ConsoleColor.Green;
- }
-
- Console.WriteLine(
- $"Job Hist ID:{jobResult.JobHistoryId}\nStorePath:{jobConfig.CertificateStoreDetails.StorePath}\nStore Properties:\n{jobConfig.CertificateStoreDetails.Properties}\nMessage: {jobResult.FailureMessage}\nResult: {jobResult.Result}");
-
- Console.ResetColor();
- break;
- }
- case "Remove":
- case "remove":
- case "rem":
- case "r":
- {
- // Get alias from env TEST_CERT_REMOVE_ALIAS or prompt for it if not set
- var alias = Environment.GetEnvironmentVariable("TEST_CERT_ALIAS") ??
- testCase.JobConfig.JobCertificate.Thumbprint?.ToString() ??
- testCase.JobConfig.JobCertificate.Alias;
- if (string.IsNullOrEmpty(alias) && isManualTest)
- {
- Console.WriteLine("Alias Enter Alias Name");
- alias = Console.ReadLine();
- }
-
- var mgmt = new Management(secretResolver.Object);
-
- var jobConfig =
- GetJobManagementConfiguration(JsonConvert.SerializeObject(testCase.JobConfig),
- alias);
-
- jobResult = mgmt.ProcessJob(jobConfig);
- if (jobResult.Result == OrchestratorJobStatusJobResult.Success ||
- (jobResult.Result == OrchestratorJobStatusJobResult.Failure && testCase.Fail))
- {
- testOutputDict[testCase.TestName] = $"Success {jobResult.FailureMessage}";
- Console.ForegroundColor = ConsoleColor.Green;
- }
- else
- {
- testOutputDict[testCase.TestName] = $"Failure - {jobResult.FailureMessage}";
- Console.ForegroundColor = ConsoleColor.Red;
- hasFailure = true;
- }
-
- Console.ResetColor();
- break;
- }
- default:
- testOutputDict[testCase.TestName] =
- $"Invalid Management Type {testMgmtType}. Valid types are 'Add' or 'Remove'.";
- // Console.WriteLine($"Invalid Management Type {testMgmtType}. Valid types are 'Add' or 'Remove'.");
- break;
- }
- }
- catch (Exception e)
- {
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine(e);
- Console.WriteLine(
- $"Failed to run inventory test case: {testCase.JobConfig.JobId}({testCase.JobConfig.CertificateStoreDetails.StorePath})");
- Console.ResetColor();
- }
- }
-
- Console.WriteLine("Finished Running Management Job Test Cases");
- break;
- case "discovery":
- case "discover":
- case "disc":
- case "d":
- tests = GetTestConfig(testConfigPath, input);
- var discovery = new Discovery(secretResolver.Object);
-
- Console.WriteLine("Running Discovery Job Test Cases");
- foreach (var testCase in tests)
- {
- testOutputDict.Add(testCase.TestName, "Running");
- try
- {
- //convert testCase to DiscoveryJobConfig
- Console.WriteLine($"=============={testCase.TestName}==================");
- Console.WriteLine($"Description: {testCase.Description}");
- Console.WriteLine($"Expected Fail: {testCase.Fail.ToString()}");
- Console.WriteLine($"Expected Result: {testCase.ExpectedValue}");
-
-
- var discoveryJobConfiguration =
- GetDiscoveryJobConfiguration(JsonConvert.SerializeObject(testCase.JobConfig));
- // create array of strings for discovery paths
- var discPaths = new List();
- // foreach (var path in invJobConfig.DiscoveryPaths)
- // {
- // dicoveryPaths.Add(path.Path);
- // }
- discPaths.Add("tls");
- SubmitDiscoveryUpdate dui = DiscoverItems;
- var jobResult = discovery.ProcessJob(discoveryJobConfiguration, dui);
-
- if (jobResult.Result == OrchestratorJobStatusJobResult.Success ||
- (jobResult.Result == OrchestratorJobStatusJobResult.Failure && testCase.Fail))
- {
- testOutputDict[testCase.TestName] = $"Success {jobResult.FailureMessage}";
- Console.ForegroundColor = ConsoleColor.Green;
- }
- else
- {
- testOutputDict[testCase.TestName] = $"Failure - {jobResult.FailureMessage}";
- Console.ForegroundColor = ConsoleColor.Red;
- hasFailure = true;
- }
-
- // Console.WriteLine(
- // $"Job Hist ID:{jobResult.JobHistoryId}\nStorePath:{invJobConfig.CertificateStoreDetails.StorePath}\nStore Properties:\n{invJobConfig.CertificateStoreDetails.Properties}\nMessage: {jobResult.FailureMessage}\nResult: {jobResult.Result}");
- Console.ResetColor();
- }
- catch (Exception e)
- {
- testOutputDict[testCase.TestName] = $"Failure - {e.Message}";
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine(e);
- Console.WriteLine($"Failed to run inventory test case: {testCase.TestName}");
- Console.ResetColor();
- }
- }
-
- Console.WriteLine("Finished Running Inventory Job Test Cases");
- break;
- }
-
- if (input == "SerializeTest")
- {
- // Example XML for testing serialization (currently disabled)
- // var xml = " cannot be deleted because of references from: certificate-profile -> Keyfactor -> CA -> Boingy ";
- // using System.Xml.Serialization;
- // var serializer = new XmlSerializer(typeof(ErrorSuccessResponse));
- // using var reader = new StringReader(xml);
- // var test = (ErrorSuccessResponse)serializer.Deserialize(reader);
- // Console.Write(test);
- }
- else
- {
- // output test results as a table to the console
-
- //write output to csv file
- var csv = new StringBuilder();
- csv.AppendLine("Test Name,Result");
- PrintLine();
- PrintRow("Test Name", "Result");
- PrintLine();
- foreach (var res in testOutputDict)
- {
- PrintRow(res.Key, res.Value);
- csv.AppendLine($"{res.Key},{res.Value}");
- }
-
- PrintLine();
- var resultFilePath = Environment.GetEnvironmentVariable("TEST_OUTPUT_FILE_PATH") ?? "testResults.csv";
- try
- {
- File.WriteAllText(resultFilePath, csv.ToString());
- }
- catch (Exception e)
- {
- var currentColor = Console.ForegroundColor;
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine(
- $"Unable to write test results to file {resultFilePath}. Please check the file path and try again.");
- Console.WriteLine(e.Message);
- Console.ForegroundColor = currentColor;
- }
- }
-
- if (hasFailure)
- {
- // Send a failure exit code
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine("Some tests failed please check the output above.");
- Environment.Exit(1);
- }
- else
- {
- Console.ForegroundColor = ConsoleColor.Green;
- Console.WriteLine("All tests passed.");
- }
- }
- }
-
-
- private static void PrintLine()
- {
- Console.WriteLine(new string('-', tableWidth));
- }
-
- private static void PrintRow(params string[] columns)
- {
- var width = (tableWidth - columns.Length) / columns.Length;
- var row = "|";
-
- foreach (var column in columns) row += AlignLeft(column, width) + "|";
-
- Console.WriteLine(row);
- }
-
- private static string AlignCentre(string text, int width)
- {
- text = text.Length > width ? text.Substring(0, width - 3) + "..." : text;
-
- if (string.IsNullOrEmpty(text)) return new string(' ', width);
- return text.PadRight(width - (width - text.Length) / 2).PadLeft(width);
- }
-
- private static string AlignLeft(string text, int width)
- {
- text = text.Length > width ? text.Substring(0, width - 3) + "..." : text;
-
- return text.PadRight(width);
- }
-
- public static bool GetItems(IEnumerable items)
- {
- return true;
- }
-
- public static bool DiscoverItems(IEnumerable items)
- {
- return true;
- }
-
- public static ManagementJobConfiguration GetJobManagementConfiguration(string jobConfigString, string alias,
- string privateKeyPwd = "", bool overWrite = true,
- bool trustedRoot = false)
- {
- var result = JsonConvert.DeserializeObject(jobConfigString);
- return result;
- }
-
- public static InventoryJobConfiguration GetInventoryJobConfiguration(string jobConfigString)
- {
- var result = JsonConvert.DeserializeObject(jobConfigString);
- return result;
- }
-
- public static DiscoveryJobConfiguration GetDiscoveryJobConfiguration(string jobConfigString)
- {
- var result = JsonConvert.DeserializeObject(jobConfigString);
- return result;
- }
-
- public static ManagementJobConfiguration GetManagementJobConfiguration(string jobConfigString, string alias = null)
- {
- if (alias != null) jobConfigString = jobConfigString.Replace("{{alias}}", alias);
- var result = JsonConvert.DeserializeObject(jobConfigString);
- return result;
- }
-
- public struct TestEnvironmentalVariable
- {
- public string Name { get; set; }
-
- public string Description { get; set; }
-
- public string Default { get; set; }
-
- public string Type { get; set; }
-
- public string[] Choices { get; set; }
-
- public bool Secret { get; set; }
- }
-}
\ No newline at end of file
diff --git a/TestConsole/TestConsole.csproj b/TestConsole/TestConsole.csproj
deleted file mode 100644
index cbdf802..0000000
--- a/TestConsole/TestConsole.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- Exe
- net8.0
- TestConsole
-
-
-
-
-
-
-
-
-
-
-
diff --git a/TestConsole/generate_vault_certs.sh b/TestConsole/generate_vault_certs.sh
deleted file mode 100644
index 79f8feb..0000000
--- a/TestConsole/generate_vault_certs.sh
+++ /dev/null
@@ -1,185 +0,0 @@
-#!/usr/bin/env bash
-
-root_ca_name="K8S Orchestrator Dev Root CA"
-intermediate_ca_name="K8S Orchestrator Dev Intermediate CA"
-export VAULT_ADDR="http://localhost:8200"
-#export VAULT_TOKEN="" # If you have a token, you can set it here
-export CN_PREFIX="k8s-"
-export CN_SUFFIX="-vca"
-
-# Enable the PKI secrets engine
-vault secrets enable pki
-
-# Tune the secrets engine so that certificates are valid for ten years
-vault secrets tune -max-lease-ttl=87600h pki
-
-# Generate the root CA
-vault write -format=json pki/root/generate/internal \
- common_name="$root_ca_name" \
- ttl=87600h > pki_root_root-ca.json
-
-# Tell Vault where to find the root CA for signing
-vault write pki/config/urls issuing_certificates="$VAULT_ADDR/v1/pki/ca" crl_distribution_points="$VAULT_ADDR/v1/pki/crl"
-
-# Generate the intermediate CA
-vault secrets enable -path=pki_int pki
-vault secrets tune -max-lease-ttl=43800h pki_int
-vault write -format=json pki_int/intermediate/generate/internal \
- common_name="$intermediate_ca_name" \
- ttl=43800h > pki_int_intermediate_intermediate-ca.json
-
-# Extract CSR from Vault response
-jq -r .data.csr pki_int_intermediate_intermediate-ca.json > pki_int_intermediate_intermediate.csr
-
-# Sign the intermediate CA's CSR
-vault write -format=json pki/root/sign-intermediate csr=@pki_int_intermediate_intermediate.csr \
- format=pem_bundle ttl="43800h" \
- common_name="$intermediate_ca_name" > pki_int_intermediate_signed-intermediate.json
-
-# Extract the intermediate CA certificate from Vault response
-jq -r .data.certificate pki_int_intermediate_signed-intermediate.json > pki_int_intermediate_intermediate.cert.pem
-
-# Tell Vault where to find the intermediate CA for signing
-vault write pki_int/intermediate/set-signed certificate=@pki_int_intermediate_intermediate.cert.pem
-
-# Create a role using an RSA 2048 key
-vault write pki_int/roles/rsa-2048 \
- allow_any_name=true \
- max_ttl=72h \
- key_type=rsa \
- key_bits=2048
-
-# Create a role using an RSA 4096 key
-vault write pki_int/roles/rsa-4096 \
- allow_any_name=true \
- max_ttl=72h \
- key_type=rsa \
- key_bits=4096
-
-# Create a role using an ECDSA P256 key
-vault write pki_int/roles/ecdsa-256 \
- allow_any_name=true \
- max_ttl=72h \
- key_type=ec \
- key_bits=256
-
-# Create a role using an ECDSA P384 key
-vault write pki_int/roles/ecdsa-384 \
- allow_any_name=true \
- max_ttl=72h \
- key_type=ec \
- key_bits=384
-
-# Create a role using an ECDSA P521 key
-vault write pki_int/roles/ecdsa-521 \
- allow_any_name=true \
- max_ttl=72h \
- key_type=ec \
- key_bits=521
-
-# Create a role using an Ed25519 key
-vault write pki_int/roles/ed25519 \
- allow_any_name=true \
- max_ttl=72h \
- key_type=ed25519 \
- key_bits=0
-
-# Issue a certificate from the RSA 2048 role
-vault write -format=json pki_int/issue/rsa-2048 common_name="${CN_PREFIX}rsa-2048${CN_SUFFIX}" > rsa-2048.json
-# Extract the certificate from Vault response
-jq -r .data.certificate rsa-2048.json > rsa-2048.cert.pem
-# Extract the private key from Vault response
-jq -r .data.private_key rsa-2048.json > rsa-2048.key.pem
-
-# Issue a certificate from the RSA 4096 role
-vault write -format=json pki_int/issue/rsa-4096 common_name="${CN_PREFIX}rsa-4096${CN_SUFFIX}" > rsa-4096.json
-# Extract the certificate from Vault response
-jq -r .data.certificate rsa-4096.json > rsa-4096.cert.pem
-# Extract the private key from Vault response
-jq -r .data.private_key rsa-4096.json > rsa-4096.key.pem
-
-# Issue a certificate from the ECDSA P256 role
-vault write -format=json pki_int/issue/ecdsa-256 common_name="${CN_PREFIX}ecdsa-256${CN_SUFFIX}" > ecdsa-256.json
-# Extract the certificate from Vault response
-jq -r .data.certificate ecdsa-256.json > ecdsa-256.cert.pem
-# Extract the private key from Vault response
-jq -r .data.private_key ecdsa-256.json > ecdsa-256.key.pem
-
-# Issue a certificate from the ECDSA P384 role
-vault write -format=json pki_int/issue/ecdsa-384 common_name="${CN_PREFIX}ecdsa-384${CN_SUFFIX}" > ecdsa-384.json
-# Extract the certificate from Vault response
-jq -r .data.certificate ecdsa-384.json > ecdsa-384.cert.pem
-# Extract the private key from Vault response
-jq -r .data.private_key ecdsa-384.json > ecdsa-384.key.pem
-
-# Issue a certificate from the ECDSA P521 role
-vault write -format=json pki_int/issue/ecdsa-521 common_name="${CN_PREFIX}ecdsa-521${CN_SUFFIX}" > ecdsa-521.json
-# Extract the certificate from Vault response
-jq -r .data.certificate ecdsa-521.json > ecdsa-521.cert.pem
-# Extract the private key from Vault response
-jq -r .data.private_key ecdsa-521.json > ecdsa-521.key.pem
-
-# Issue a certificate from the Ed25519 role
-vault write -format=json pki_int/issue/ed25519 common_name="${CN_PREFIX}ed25519${CN_SUFFIX}" > ed25519.json
-# Extract the certificate from Vault response
-jq -r .data.certificate ed25519.json > ed25519.cert.pem
-# Extract the private key from Vault response
-jq -r .data.private_key ed25519.json > ed25519.key.pem
-
-# Write all certs and private keys to kubeneretes secrets
-kubectl create secret generic rsa-2048 --from-file=tls.crt=rsa-2048.cert.pem --from-file=tls.key=rsa-2048.key.pem
-kubectl create secret generic rsa-4096 --from-file=tls.crt=rsa-4096.cert.pem --from-file=tls.key=rsa-4096.key.pem
-kubectl create secret generic ecdsa-256 --from-file=tls.crt=ecdsa-256.cert.pem --from-file=tls.key=ecdsa-256.key.pem
-kubectl create secret generic ecdsa-384 --from-file=tls.crt=ecdsa-384.cert.pem --from-file=tls.key=ecdsa-384.key.pem
-kubectl create secret generic ecdsa-521 --from-file=tls.crt=ecdsa-521.cert.pem --from-file=tls.key=ecdsa-521.key.pem
-kubectl create secret generic ed25519 --from-file=tls.crt=ed25519.cert.pem --from-file=tls.key=ed25519.key.pem
-
-# Write all certs and private keys to kubeneretes tls secrets
-kubectl create secret tls tls-rsa-2048 --cert=rsa-2048.cert.pem --key=rsa-2048.key.pem
-kubectl create secret tls tls-rsa-4096 --cert=rsa-4096.cert.pem --key=rsa-4096.key.pem
-kubectl create secret tls tls-ecdsa-256 --cert=ecdsa-256.cert.pem --key=ecdsa-256.key.pem
-kubectl create secret tls tls-ecdsa-384 --cert=ecdsa-384.cert.pem --key=ecdsa-384.key.pem
-kubectl create secret tls tls-ecdsa-521 --cert=ecdsa-521.cert.pem --key=ecdsa-521.key.pem
-kubectl create secret tls tls-ed25519 --cert=ed25519.cert.pem --key=ed25519.key.pem
-
-# Prompt y/n if you want to delete all generated files then run the following commands
-read -p "Do you want to delete all generated files? (y/n) " answer
-
-if [[ $answer =~ ^[Yy]$ ]]; then
-
- echo "Deleting all k8s opa secrets..."
- # Delete all kubernetes secrets
- kubectl delete secret rsa-2048
- kubectl delete secret rsa-4096
- kubectl delete secret ecdsa-256
- kubectl delete secret ecdsa-384
- kubectl delete secret ecdsa-521
- kubectl delete secret ed25519
-
- echo "Deleting all k8s opa tls secrets..."
- # Delete all kubernetes tls secrets
- kubectl delete secret tls-rsa-2048
- kubectl delete secret tls-rsa-4096
- kubectl delete secret tls-ecdsa-256
- kubectl delete secret tls-ecdsa-384
- kubectl delete secret tls-ecdsa-521
- kubectl delete secret tls-ed25519
-
- echo "Deleting all generated files..."
- # Delete all generated files
- rm rsa-2048.cert.pem rsa-2048.key.pem rsa-2048.json
- rm rsa-4096.cert.pem rsa-4096.key.pem rsa-4096.json
- rm ecdsa-256.cert.pem ecdsa-256.key.pem ecdsa-256.json
- rm ecdsa-384.cert.pem ecdsa-384.key.pem ecdsa-384.json
- rm ecdsa-521.cert.pem ecdsa-521.key.pem ecdsa-521.json
- rm ed25519.cert.pem ed25519.key.pem ed25519.json
-
- echo "Completed. All generated files are deleted."
-else
- echo "Completed. All generated files are in the current directory $(pwd)."
-fi
-
-
-
-
-
\ No newline at end of file
diff --git a/TestConsole/tests.json b/TestConsole/tests.json
deleted file mode 100644
index e69de29..0000000
diff --git a/TestConsole/tests.yml b/TestConsole/tests.yml
deleted file mode 100644
index e69de29..0000000
diff --git a/TestConsole/yaml2json.sh b/TestConsole/yaml2json.sh
deleted file mode 100644
index 7df3cff..0000000
--- a/TestConsole/yaml2json.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-# Convert YAML to JSON
-# Usage: yaml2json.sh
-# Example: yaml2json.sh tests.yaml > tests.json
-yq -p yaml -o json "$1"
\ No newline at end of file
diff --git a/kubernetes-orchestrator-extension/Jobs/Base/DiscoveryBase.cs b/kubernetes-orchestrator-extension/Jobs/Base/DiscoveryBase.cs
new file mode 100644
index 0000000..112c379
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Jobs/Base/DiscoveryBase.cs
@@ -0,0 +1,153 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+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.K8S.Jobs.Base;
+
+///
+/// Base class for store-type-specific discovery jobs.
+/// Handles common discovery workflow: initialize, discover stores via handler, return locations.
+/// Store-type-specific classes inherit from this and may override methods as needed.
+///
+public abstract class DiscoveryBase : K8SJobBase, IDiscoveryJobExtension
+{
+ ///
+ /// Initializes a new instance with the specified PAM resolver.
+ ///
+ /// PAM secret resolver for credential retrieval.
+ protected DiscoveryBase(IPAMSecretResolver resolver)
+ {
+ _resolver = resolver;
+ }
+
+ ///
+ /// Gets the allowed keys for this store type's discovery.
+ /// Override in subclasses to specify store-type-specific keys.
+ ///
+ protected virtual string[] AllowedKeys => Handler?.AllowedKeys ?? Array.Empty();
+
+ ///
+ /// Processes the discovery job by delegating to the appropriate handler.
+ ///
+ /// The discovery job configuration.
+ /// Callback to submit discovered stores.
+ /// Job result indicating success or failure.
+ public virtual JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate submitDiscovery)
+ {
+ Logger ??= LogHandler.GetClassLogger(GetType());
+ Logger.MethodEntry(LogLevel.Debug);
+
+ try
+ {
+ Logger.LogDebug("Initializing store for discovery job {JobId}", config.JobId);
+ InitializeStore(config);
+
+ Logger.LogDebug("Initializing handler for discovery");
+ InitializeHandler(config);
+
+ if (Handler == null)
+ {
+ return FailJob($"No handler available for store type: {KubeSecretType}", config.JobHistoryId);
+ }
+
+ Logger.LogInformation("Begin DISCOVERY for {StoreType} job {JobId}", KubeSecretType, config.JobId);
+
+ // Get namespaces to search from job properties
+ var namespacesCsv = GetNamespacesToSearch(config);
+
+ // Get custom allowed keys from job properties
+ var customKeys = GetCustomAllowedKeys(config);
+
+ // Discover stores via handler
+ var discoveredStores = Handler.DiscoverStores(customKeys, namespacesCsv);
+
+ Logger.LogInformation("Discovered {Count} stores", discoveredStores.Count);
+
+ // Submit discovered stores
+ submitDiscovery.Invoke(discoveredStores);
+
+ return SuccessJob(config.JobHistoryId);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Discovery failed: {Message}", ex.Message);
+ return FailJob(ex, config.JobHistoryId);
+ }
+ finally
+ {
+ Logger.LogInformation("End DISCOVERY for job {JobId}", config.JobId);
+ Logger.MethodExit(LogLevel.Debug);
+ }
+ }
+
+ ///
+ /// Gets the namespaces to search from the job configuration.
+ ///
+ /// The discovery job configuration.
+ /// Comma-separated list of namespaces, or empty for all.
+ protected virtual string GetNamespacesToSearch(DiscoveryJobConfiguration config)
+ {
+ if (config.JobProperties == null)
+ return "";
+
+ try
+ {
+ var props = JsonConvert.DeserializeObject>(config.JobProperties.ToString());
+ if (props != null && props.TryGetValue("Directories", out var dirs))
+ {
+ return dirs?.ToString() ?? "";
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning("Failed to parse discovery directories: {Message}", ex.Message);
+ }
+
+ return "";
+ }
+
+ ///
+ /// Gets custom allowed keys from the job configuration.
+ ///
+ /// The discovery job configuration.
+ /// Array of custom allowed keys, or null to use defaults.
+ protected virtual string[] GetCustomAllowedKeys(DiscoveryJobConfiguration config)
+ {
+ if (config.JobProperties == null)
+ return null;
+
+ try
+ {
+ var props = JsonConvert.DeserializeObject>(config.JobProperties.ToString());
+ if (props != null && props.TryGetValue("Extensions", out var extensions))
+ {
+ var extString = extensions?.ToString();
+ if (!string.IsNullOrEmpty(extString))
+ {
+ return extString.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(s => s.Trim())
+ .ToArray();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning("Failed to parse discovery extensions: {Message}", ex.Message);
+ }
+
+ return null;
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Jobs/Base/InventoryBase.cs b/kubernetes-orchestrator-extension/Jobs/Base/InventoryBase.cs
new file mode 100644
index 0000000..ca4312d
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Jobs/Base/InventoryBase.cs
@@ -0,0 +1,139 @@
+// 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Keyfactor.Orchestrators.Extensions.Interfaces;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs.Base;
+
+///
+/// Base class for store-type-specific inventory jobs.
+/// Handles common inventory workflow: initialize, get certificates via handler, submit to Keyfactor.
+/// Store-type-specific classes inherit from this and may override methods as needed.
+///
+public abstract class InventoryBase : K8SJobBase, IInventoryJobExtension
+{
+ ///
+ /// Initializes a new instance with the specified PAM resolver.
+ ///
+ /// PAM secret resolver for credential retrieval.
+ protected InventoryBase(IPAMSecretResolver resolver)
+ {
+ _resolver = resolver;
+ }
+
+ ///
+ /// Processes the inventory job by delegating to the appropriate handler.
+ ///
+ /// The inventory job configuration.
+ /// Callback to submit inventory to Keyfactor.
+ /// The job result indicating success or failure.
+ public virtual JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate submitInventory)
+ {
+ Logger ??= LogHandler.GetClassLogger(GetType());
+ Logger.MethodEntry(LogLevel.Debug);
+
+ try
+ {
+ Logger.LogDebug("Initializing store for inventory job {JobId}", config.JobId);
+ InitializeStore(config);
+
+ Logger.LogDebug("Initializing handler for store type: {StoreType}", KubeSecretType);
+ InitializeHandler(config);
+
+ if (Handler == null)
+ {
+ return FailJob($"No handler available for store type: {KubeSecretType}", config.JobHistoryId);
+ }
+
+ Logger.LogInformation("Begin INVENTORY for {StoreType} job {JobId}", KubeSecretType, config.JobId);
+
+ // Get inventory entries from handler
+ // JobHistoryId is the long identifier used by Keyfactor
+ var entries = GetInventoryEntries(config.JobHistoryId);
+
+ // Submit to Keyfactor
+ return SubmitInventory(config.JobHistoryId, submitInventory, entries);
+ }
+ catch (StoreNotFoundException ex)
+ {
+ Logger.LogWarning("Store not found: {Message}", ex.Message);
+ // Return empty inventory for not found stores (common during initial setup)
+ submitInventory.Invoke(new List());
+ return SuccessJob(config.JobHistoryId, $"Store not found: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Inventory failed: {Message}", ex.Message);
+ return FailJob(ex, config.JobHistoryId);
+ }
+ finally
+ {
+ Logger.LogInformation("End INVENTORY for job {JobId}", config.JobId);
+ Logger.MethodExit(LogLevel.Debug);
+ }
+ }
+
+ ///
+ /// Gets inventory entries from the handler.
+ /// Override in subclasses to customize inventory retrieval logic.
+ ///
+ /// The job ID for logging.
+ /// List of inventory entries.
+ protected virtual List GetInventoryEntries(long jobId)
+ {
+ Logger.LogDebug("Getting inventory entries via handler");
+ return Handler.GetInventoryEntries(jobId);
+ }
+
+ ///
+ /// Submits inventory entries to Keyfactor.
+ ///
+ /// The job history ID.
+ /// The submission callback.
+ /// The inventory entries to submit.
+ /// The job result.
+ protected virtual JobResult SubmitInventory(
+ long jobHistoryId,
+ SubmitInventoryUpdate submitInventory,
+ List entries)
+ {
+ Logger.LogDebug("Submitting {Count} inventory entries to Keyfactor", entries.Count);
+
+ var inventoryItems = entries
+ .Where(e => e.Certificates != null && e.Certificates.Count > 0)
+ .Select(e => new CurrentInventoryItem
+ {
+ Alias = e.Alias,
+ Certificates = e.Certificates,
+ PrivateKeyEntry = e.HasPrivateKey,
+ UseChainLevel = e.Certificates.Count > 1
+ })
+ .ToList();
+
+ Logger.LogInformation("Submitting {Count} certificates to Keyfactor", inventoryItems.Count);
+
+ try
+ {
+ submitInventory.Invoke(inventoryItems);
+ Logger.LogInformation("Successfully submitted inventory");
+ return SuccessJob(jobHistoryId);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Failed to submit inventory: {Message}", ex.Message);
+ return FailJob($"Failed to submit inventory: {ex.Message}", jobHistoryId);
+ }
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Jobs/Base/K8SJobBase.cs b/kubernetes-orchestrator-extension/Jobs/Base/K8SJobBase.cs
new file mode 100644
index 0000000..c8913cf
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Jobs/Base/K8SJobBase.cs
@@ -0,0 +1,159 @@
+// 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 System;
+using Keyfactor.Extensions.Orchestrator.K8S.Handlers;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs.Base;
+
+///
+/// Simplified base class for store-type-specific jobs.
+/// Provides common infrastructure for Kubernetes client access, handler creation, and job results.
+/// Store-type-specific jobs inherit from this to get shared functionality while implementing
+/// their own ProcessJob methods.
+///
+public abstract class K8SJobBase : JobBase
+{
+ ///
+ /// Gets or sets the secret handler for the current store type.
+ /// Lazily initialized based on the store configuration.
+ ///
+ protected ISecretHandler Handler { get; set; }
+
+ ///
+ /// Creates the operation context from the current job configuration.
+ /// Override in subclasses to provide store-type-specific context.
+ ///
+ protected virtual ISecretOperationContext CreateOperationContext()
+ {
+ return new SecretOperationContext
+ {
+ KubeNamespace = KubeNamespace,
+ KubeSecretName = KubeSecretName,
+ KubeSecretType = KubeSecretType,
+ StorePath = StorePath,
+ StorePassword = StorePassword,
+ CertificateDataFieldName = CertificateDataFieldName,
+ PasswordFieldName = PasswordFieldName,
+ PasswordSecretPath = StorePasswordPath,
+ SeparateChain = SeparateChain,
+ IncludeCertChain = IncludeCertChain
+ };
+ }
+
+ ///
+ /// Initializes the handler for inventory operations.
+ ///
+ protected void InitializeHandler(InventoryJobConfiguration config)
+ {
+ InitializeHandlerCore();
+ }
+
+ ///
+ /// Initializes the handler for management operations.
+ ///
+ protected void InitializeHandler(ManagementJobConfiguration config)
+ {
+ InitializeHandlerCore();
+ }
+
+ ///
+ /// Initializes the handler for discovery operations.
+ ///
+ protected void InitializeHandler(DiscoveryJobConfiguration config)
+ {
+ Logger ??= LogHandler.GetClassLogger(GetType());
+ Logger.LogDebug("Creating handler for discovery");
+
+ // For discovery, we may not have full store context yet
+ var context = new SecretOperationContext
+ {
+ KubeNamespace = KubeNamespace ?? "",
+ KubeSecretName = KubeSecretName ?? "",
+ KubeSecretType = KubeSecretType ?? "secret"
+ };
+
+ Handler = SecretHandlerFactory.Create(KubeSecretType ?? "secret", KubeClient, Logger, context);
+ Logger.LogDebug("Handler created: {HandlerType}", Handler?.GetType().Name ?? "null");
+ }
+
+ ///
+ /// Shared handler initialization for inventory and management operations.
+ ///
+ private void InitializeHandlerCore()
+ {
+ Logger ??= LogHandler.GetClassLogger(GetType());
+ Logger.LogDebug("Creating handler for store type: {StoreType}", KubeSecretType);
+
+ var context = CreateOperationContext();
+ Handler = SecretHandlerFactory.Create(KubeSecretType, KubeClient, Logger, context);
+
+ Logger.LogDebug("Handler created: {HandlerType}", Handler?.GetType().Name ?? "null");
+ }
+
+ ///
+ /// Creates a success job result.
+ ///
+ protected static JobResult SuccessJob(long jobHistoryId, string message = null)
+ {
+ return new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Success,
+ JobHistoryId = jobHistoryId,
+ FailureMessage = message
+ };
+ }
+
+ ///
+ /// Creates a failure job result.
+ ///
+ protected JobResult FailJob(string message, long jobHistoryId)
+ {
+ Logger?.LogError("Job failed: {Message}", message);
+ return new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Failure,
+ JobHistoryId = jobHistoryId,
+ FailureMessage = message
+ };
+ }
+
+ ///
+ /// Creates a failure job result from an exception.
+ ///
+ protected JobResult FailJob(Exception ex, long jobHistoryId)
+ {
+ Logger?.LogError(ex, "Job failed with exception: {Message}", ex.Message);
+ return new JobResult
+ {
+ Result = OrchestratorJobStatusJobResult.Failure,
+ JobHistoryId = jobHistoryId,
+ FailureMessage = ex.Message
+ };
+ }
+}
+
+///
+/// Simple implementation of ISecretOperationContext for handler initialization.
+///
+internal class SecretOperationContext : ISecretOperationContext
+{
+ public string KubeNamespace { get; set; } = "";
+ public string KubeSecretName { get; set; } = "";
+ public string KubeSecretType { get; set; } = "";
+ public string StorePath { get; set; } = "";
+ public string StorePassword { get; set; }
+ public string CertificateDataFieldName { get; set; }
+ public string PasswordFieldName { get; set; }
+ public string PasswordSecretPath { get; set; }
+ public bool SeparateChain { get; set; }
+ public bool IncludeCertChain { get; set; } = true;
+}
diff --git a/kubernetes-orchestrator-extension/Jobs/Base/ManagementBase.cs b/kubernetes-orchestrator-extension/Jobs/Base/ManagementBase.cs
new file mode 100644
index 0000000..af8bbc7
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Jobs/Base/ManagementBase.cs
@@ -0,0 +1,154 @@
+// 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 System;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Keyfactor.Orchestrators.Extensions.Interfaces;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs.Base;
+
+///
+/// Base class for store-type-specific management jobs (Add/Remove certificates).
+/// Handles common management workflow: initialize, validate, delegate to handler.
+/// Store-type-specific classes inherit from this and may override methods as needed.
+///
+public abstract class ManagementBase : K8SJobBase, IManagementJobExtension
+{
+ ///
+ /// Initializes a new instance with the specified PAM resolver.
+ ///
+ /// PAM secret resolver for credential retrieval.
+ protected ManagementBase(IPAMSecretResolver resolver)
+ {
+ _resolver = resolver;
+ }
+
+ ///
+ /// Processes the management job by delegating to the appropriate handler.
+ ///
+ /// The management job configuration.
+ /// The job result indicating success or failure.
+ public virtual JobResult ProcessJob(ManagementJobConfiguration config)
+ {
+ Logger ??= LogHandler.GetClassLogger(GetType());
+ Logger.MethodEntry(LogLevel.Debug);
+
+ try
+ {
+ Logger.LogDebug("Initializing store for management job {JobId}", config.JobId);
+ InitializeStore(config);
+
+ // Ensure StorePassword is set from config (Management jobs need this for keystore types)
+ if (!string.IsNullOrEmpty(config.CertificateStoreDetails?.StorePassword))
+ {
+ StorePassword = config.CertificateStoreDetails.StorePassword;
+ }
+
+ Logger.LogDebug("Initializing handler for store type: {StoreType}", KubeSecretType);
+ InitializeHandler(config);
+
+ if (Handler == null)
+ {
+ return FailJob($"No handler available for store type: {KubeSecretType}", config.JobHistoryId);
+ }
+
+ if (!Handler.SupportsManagement)
+ {
+ return FailJob($"Management operations are not supported for store type: {KubeSecretType}", config.JobHistoryId);
+ }
+
+ Logger.LogInformation("Begin MANAGEMENT ({OperationType}) for {StoreType} job {JobId}",
+ config.OperationType, KubeSecretType, config.JobId);
+
+ // Route to appropriate operation
+ return RouteOperation(config);
+ }
+ catch (StoreNotFoundException ex)
+ {
+ Logger.LogError("Store not found: {Message}", ex.Message);
+ return FailJob($"Store not found: {ex.Message}", config.JobHistoryId);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Management job failed: {Message}", ex.Message);
+ return FailJob(ex, config.JobHistoryId);
+ }
+ finally
+ {
+ Logger.LogInformation("End MANAGEMENT for job {JobId}", config.JobId);
+ Logger.MethodExit(LogLevel.Debug);
+ }
+ }
+
+ ///
+ /// Routes the management job to the appropriate handler method based on OperationType.
+ /// Create is treated identically to Add — both add a certificate to the store.
+ /// Extracted as an internal method to allow direct unit testing without K8S infrastructure.
+ ///
+ internal JobResult RouteOperation(ManagementJobConfiguration config)
+ {
+ return config.OperationType switch
+ {
+ CertStoreOperationType.Add or CertStoreOperationType.Create => HandleAdd(config),
+ CertStoreOperationType.Remove => HandleRemove(config),
+ _ => FailJob($"Unknown operation type: {config.OperationType}", config.JobHistoryId)
+ };
+ }
+
+ ///
+ /// Handles the Add operation by delegating to the handler.
+ /// Override in subclasses to customize add logic.
+ ///
+ /// The management job configuration.
+ /// The job result.
+ protected virtual JobResult HandleAdd(ManagementJobConfiguration config)
+ {
+ Logger.LogDebug("Processing Add operation");
+
+ // Initialize certificate from job configuration (parses PKCS12, extracts keys, etc.)
+ K8SCertificate = InitJobCertificate(config);
+ var alias = config.JobCertificate?.Alias ?? "";
+ var overwrite = config.Overwrite;
+
+ Logger.LogDebug("Adding certificate with alias: {Alias}, overwrite: {Overwrite}", alias, overwrite);
+
+ Handler.HandleAdd(K8SCertificate, alias, overwrite);
+ Logger.LogInformation("Successfully added certificate to {SecretName}", KubeSecretName);
+ return SuccessJob(config.JobHistoryId);
+ }
+
+ ///
+ /// Handles the Remove operation by delegating to the handler.
+ /// Override in subclasses to customize remove logic.
+ ///
+ /// The management job configuration.
+ /// The job result.
+ protected virtual JobResult HandleRemove(ManagementJobConfiguration config)
+ {
+ Logger.LogDebug("Processing Remove operation");
+
+ var alias = config.JobCertificate?.Alias ?? "";
+
+ Logger.LogDebug("Removing certificate with alias: {Alias}", alias);
+
+ try
+ {
+ Handler.HandleRemove(alias);
+ Logger.LogInformation("Successfully removed certificate from {SecretName}", KubeSecretName);
+ return SuccessJob(config.JobHistoryId);
+ }
+ catch (StoreNotFoundException)
+ {
+ // Store doesn't exist - nothing to remove
+ Logger.LogWarning("Store not found, nothing to remove");
+ return SuccessJob(config.JobHistoryId);
+ }
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Jobs/Base/ReenrollmentBase.cs b/kubernetes-orchestrator-extension/Jobs/Base/ReenrollmentBase.cs
new file mode 100644
index 0000000..c3df993
--- /dev/null
+++ b/kubernetes-orchestrator-extension/Jobs/Base/ReenrollmentBase.cs
@@ -0,0 +1,83 @@
+// 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 System;
+using Keyfactor.Logging;
+using Keyfactor.Orchestrators.Common.Enums;
+using Keyfactor.Orchestrators.Extensions;
+using Keyfactor.Orchestrators.Extensions.Interfaces;
+using Microsoft.Extensions.Logging;
+
+namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs.Base;
+
+///
+/// Base class for store-type-specific reenrollment jobs.
+/// Reenrollment generates a new key pair and CSR for an existing certificate entry.
+/// Currently not implemented for Kubernetes stores - subclasses can override to add support.
+///
+public abstract class ReenrollmentBase : K8SJobBase, IReenrollmentJobExtension
+{
+ ///
+ /// Initializes a new instance with the specified PAM resolver.
+ ///
+ /// PAM secret resolver for credential retrieval.
+ protected ReenrollmentBase(IPAMSecretResolver resolver)
+ {
+ _resolver = resolver;
+ }
+
+ ///
+ /// Processes the reenrollment job.
+ /// Default implementation returns "not implemented" - override in store types that support reenrollment.
+ ///
+ /// The reenrollment job configuration.
+ /// Callback to submit the CSR.
+ /// The job result indicating success or failure.
+ public virtual JobResult ProcessJob(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReenrollment)
+ {
+ Logger ??= LogHandler.GetClassLogger(GetType());
+ Logger.MethodEntry(LogLevel.Debug);
+
+ try
+ {
+ Logger.LogDebug("Processing reenrollment job {JobId} for capability {Capability}",
+ config.JobId, config.Capability);
+
+ // Reenrollment is not implemented for most Kubernetes store types
+ // Subclasses can override PerformReenrollment to provide implementation
+ return PerformReenrollment(config, submitReenrollment);
+ }
+ catch (NotSupportedException ex)
+ {
+ Logger.LogWarning("Reenrollment not supported: {Message}", ex.Message);
+ return FailJob(ex.Message, config.JobHistoryId);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Reenrollment failed: {Message}", ex.Message);
+ return FailJob(ex, config.JobHistoryId);
+ }
+ finally
+ {
+ Logger.MethodExit(LogLevel.Debug);
+ }
+ }
+
+ ///
+ /// Performs the actual reenrollment operation.
+ /// Override in store types that support reenrollment (JKS, PKCS12).
+ /// Default implementation returns "not implemented".
+ ///
+ /// The reenrollment job configuration.
+ /// Callback to submit the CSR.
+ /// The job result.
+ protected virtual JobResult PerformReenrollment(ReenrollmentJobConfiguration config, SubmitReenrollmentCSR submitReenrollment)
+ {
+ Logger.LogWarning("Re-enrollment not implemented for {Capability}", config.Capability);
+ return FailJob($"Re-enrollment not implemented for {config.Capability}", config.JobHistoryId);
+ }
+}
diff --git a/kubernetes-orchestrator-extension/Jobs/Discovery.cs b/kubernetes-orchestrator-extension/Jobs/Discovery.cs
deleted file mode 100644
index 2e3ef8a..0000000
--- a/kubernetes-orchestrator-extension/Jobs/Discovery.cs
+++ /dev/null
@@ -1,294 +0,0 @@
-// 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 System;
-using System.Collections.Generic;
-using System.Linq;
-using Keyfactor.Extensions.Orchestrator.K8S.Clients;
-using Keyfactor.Logging;
-using Keyfactor.Orchestrators.Common.Enums;
-using Keyfactor.Orchestrators.Extensions;
-using Keyfactor.Orchestrators.Extensions.Interfaces;
-using Microsoft.Extensions.Logging;
-using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
-
-namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs;
-
-///
-/// Discovery job implementation for Kubernetes certificate stores.
-/// Finds all certificate stores (secrets, JKS, PKCS12) in specified namespaces
-/// based on job configuration and returns them to Keyfactor Command for approval.
-///
-///
-/// Supports discovery for the following store types:
-/// - K8SCluster: Cluster-wide secret discovery
-/// - K8SNS: Namespace-level secret discovery
-/// - K8STLSSecr: TLS secrets (kubernetes.io/tls)
-/// - K8SSecret: Opaque secrets
-/// - K8SPKCS12/K8SPFX: PKCS12 keystores
-/// - K8SJKS: JKS keystores
-///
-/// Discovery parameters from job properties:
-/// - dirs: Namespaces to search (comma-separated)
-/// - extensions: Secret data keys to check
-/// - ignoreddirs: Namespaces to ignore
-/// - patterns: File name patterns to match
-///
-public class Discovery : JobBase, IDiscoveryJobExtension
-{
- ///
- /// Initializes a new instance of the Discovery job with the specified PAM resolver.
- ///
- /// PAM secret resolver for credential retrieval.
- public Discovery(IPAMSecretResolver resolver)
- {
- _resolver = resolver;
- }
-
- ///
- /// Main entry point for the discovery job. Searches for certificate stores
- /// in Kubernetes based on the job configuration.
- ///
- /// Discovery job configuration containing search parameters.
- /// Callback delegate to submit discovered store locations to Keyfactor Command.
- /// JobResult indicating success or failure of the discovery operation.
- ///
- /// Configuration parameters available in config:
- /// - config.ServerUsername, config.ServerPassword - credentials for K8S API authentication
- /// - config.JobProperties["dirs"] - Namespaces to search (comma-separated, defaults to "default")
- /// - config.JobProperties["extensions"] - Secret data keys to check for certificate data
- /// - config.JobProperties["ignoreddirs"] - Namespaces to ignore
- /// - config.JobProperties["patterns"] - File name patterns to match
- ///
- public JobResult ProcessJob(DiscoveryJobConfiguration config, SubmitDiscoveryUpdate submitDiscovery)
- {
- Logger = LogHandler.GetClassLogger(GetType());
- Logger.MethodEntry(MsLogLevel.Debug);
- Logger.LogInformation("Begin Discovery for K8S Orchestrator Extension for job {JobID}", config.JobId);
- Logger.LogInformation("Discovery for store type: {Capability}", config.Capability);
- try
- {
- Logger.LogDebug("Calling InitializeStore()");
- InitializeStore(config);
- Logger.LogDebug("Store initialized successfully");
- }
- catch (Exception ex)
- {
- Logger.LogError("Failed to initialize store: {Error}", ex.Message);
- return FailJob("Failed to initialize store: " + ex.Message, config.JobHistoryId);
- }
-
-
- var locations = new List();
-
- KubeSvcCreds = ServerPassword;
- Logger.LogDebug("Calling KubeCertificateManagerClient()");
- KubeClient = new KubeCertificateManagerClient(KubeSvcCreds, config.UseSSL); //todo does this throw an exception?
- Logger.LogDebug("Returned from KubeCertificateManagerClient()");
- if (KubeClient == null)
- {
- Logger.LogError("Failed to create KubeCertificateManagerClient");
- return FailJob("Failed to create KubeCertificateManagerClient", config.JobHistoryId);
- }
-
- var namespaces = config.JobProperties["dirs"].ToString()?.Split(',') ?? Array.Empty();
- if (namespaces is null or { Length: 0 })
- {
- Logger.LogDebug("No namespaces provided, using `default` namespace");
- namespaces = new[] { "default" };
- }
-
- Logger.LogDebug("Namespaces: {Namespaces}", string.Join(",", namespaces));
-
- var ignoreNamespace = config.JobProperties["ignoreddirs"].ToString()?.Split(',') ?? Array.Empty();
- Logger.LogDebug("Ignored Namespaces: {Namespaces}", string.Join(",", ignoreNamespace));
-
- var secretAllowedKeys = config.JobProperties["patterns"].ToString()?.Split(',') ?? Array.Empty();
- Logger.LogDebug("Secret Allowed Keys: {AllowedKeys}", string.Join(",", secretAllowedKeys));
-
- Logger.LogTrace("Discovery entering switch block based on capability {Capability}", config.Capability);
- 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
- switch (config.Capability)
- {
- case "CertStores.K8SCluster.Discovery":
- // Combine the allowed keys with the default keys
- Logger.LogTrace("Entering case: {Capability}", config.Capability);
- secretAllowedKeys = secretAllowedKeys.Concat(TLSAllowedKeys).ToArray();
-
- Logger.LogInformation(
- "Discovering k8s secrets for cluster `{ClusterName}` with allowed keys: `{AllowedKeys}` and secret types: `kubernetes.io/tls, Opaque`",
- KubeHost, string.Join(",", secretAllowedKeys));
- Logger.LogDebug("Calling KubeClient.DiscoverSecrets()");
- locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "cluster", string.Join(",", namespaces));
- Logger.LogDebug("Returned from KubeClient.DiscoverSecrets()");
-
- break;
- case "CertStores.K8SNS.Discovery":
- // Combine the allowed keys with the default keys
- Logger.LogTrace("Entering case: {Capability}", config.Capability);
- secretAllowedKeys = secretAllowedKeys.Concat(TLSAllowedKeys).ToArray();
- Logger.LogInformation(
- "Discovering k8s secrets in k8s namespaces `{Namespaces}` with allowed keys: `{AllowedKeys}` and secret types: `kubernetes.io/tls, Opaque`",
- string.Join(",", namespaces), string.Join(",", secretAllowedKeys));
- Logger.LogDebug("Calling KubeClient.DiscoverSecrets()");
- locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "namespace",
- string.Join(",", namespaces));
- Logger.LogDebug("Returned from KubeClient.DiscoverSecrets()");
- break;
- case "CertStores.K8STLSSecr.Discovery":
- // Combine the allowed keys with the default keys
- Logger.LogTrace("Entering case: {Capability}", config.Capability);
- secretAllowedKeys = secretAllowedKeys.Concat(TLSAllowedKeys).ToArray();
- Logger.LogInformation(
- "Discovering k8s secrets in k8s namespaces `{Namespaces}` with allowed keys: `{AllowedKeys}` and secret type: `kubernetes.io/tls`",
- string.Join(",", namespaces), string.Join(",", secretAllowedKeys));
- Logger.LogDebug("Calling KubeClient.DiscoverSecrets()");
- locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "kubernetes.io/tls",
- string.Join(",", namespaces));
- Logger.LogDebug("Returned from KubeClient.DiscoverSecrets()");
- break;
- case "CertStores.K8SSecret.Discovery":
- Logger.LogTrace("Entering case: {Capability}", config.Capability);
- secretAllowedKeys = secretAllowedKeys.Concat(OpaqueAllowedKeys).ToArray();
- Logger.LogInformation("Discovering secrets with allowed keys: `{AllowedKeys}` and type: `Opaque`",
- string.Join(",", secretAllowedKeys));
- locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "Opaque", string.Join(",", namespaces));
- break;
- case "CertStores.K8SPFX.Discovery":
- case "CertStores.K8SPKCS12.Discovery":
- // 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
- Logger.LogTrace("Entering case: {Capability}", config.Capability);
-
- var secretAllowedKeysStr = config.JobProperties["extensions"].ToString();
- var allowedPatterns = config.JobProperties["patterns"].ToString();
-
- var additionalKeyPatterns = string.IsNullOrEmpty(allowedPatterns)
- ? new[] { "p12" }
- : allowedPatterns.Split(',');
- secretAllowedKeys = string.IsNullOrEmpty(secretAllowedKeysStr)
- ? new[] { "p12" }
- : secretAllowedKeysStr.Split(',');
-
- //append pkcs12AllowedKeys to secretAllowedKeys
- secretAllowedKeys = secretAllowedKeys.Concat(additionalKeyPatterns).ToArray();
- secretAllowedKeys = secretAllowedKeys.Concat(Pkcs12AllowedKeys).ToArray();
-
- //make secretAllowedKeys unique
- secretAllowedKeys = secretAllowedKeys.Distinct().ToArray();
-
- Logger.LogInformation(
- "Discovering k8s secrets with allowed keys: `{AllowedKeys}` and type: `pkcs12`",
- string.Join(",", secretAllowedKeys));
- Logger.LogDebug("Calling KubeClient.DiscoverSecrets()");
- locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "pkcs12",
- string.Join(",", namespaces));
- Logger.LogDebug("Returned from KubeClient.DiscoverSecrets()");
- break;
- case "CertStores.K8SJKS.Discovery":
- // 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
-
- Logger.LogTrace("Entering case: {Capability}", config.Capability);
- var jksSecretAllowedKeysStr = config.JobProperties["extensions"].ToString();
- var jksAllowedPatterns = config.JobProperties["patterns"].ToString();
-
- var jksAdditionalKeyPatterns = string.IsNullOrEmpty(jksAllowedPatterns)
- ? new[] { "jks" }
- : jksAllowedPatterns.Split(',');
- secretAllowedKeys = string.IsNullOrEmpty(jksSecretAllowedKeysStr)
- ? new[] { "jks" }
- : jksSecretAllowedKeysStr.Split(',');
-
- //append pkcs12AllowedKeys to secretAllowedKeys
- secretAllowedKeys = secretAllowedKeys.Concat(jksAdditionalKeyPatterns).ToArray();
- secretAllowedKeys = secretAllowedKeys.Concat(JksAllowedKeys).ToArray();
-
- //make secretAllowedKeys unique
- secretAllowedKeys = secretAllowedKeys.Distinct().ToArray();
-
- Logger.LogInformation("Discovering k8s secrets with allowed keys: `{AllowedKeys}` and type: `jks`",
- string.Join(",", secretAllowedKeys));
- locations = KubeClient.DiscoverSecrets(secretAllowedKeys, "jks", string.Join(",", namespaces));
- break;
- case "CertStores.K8SCert.Discovery":
- Logger.LogError("Capability not supported: CertStores.K8SCert.Discovery");
- return FailJob("Discovery not supported for store type `K8SCert`", config.JobHistoryId);
- }
- }
- catch (Exception ex)
- {
- //Status: 2=Success, 3=Warning, 4=Error
- Logger.LogError("Discovery job has failed due to an unknown error");
- Logger.LogError("{Message}", ex.Message);
- Logger.LogTrace("{Message}", ex.ToString());
- // iterate through the inner exceptions
- var inner = ex.InnerException;
- while (inner != null)
- {
- Logger.LogError("Inner Exception: {Message}", inner.Message);
- Logger.LogTrace("{Message}", inner.ToString());
- inner = inner.InnerException;
- }
-
- Logger.LogInformation("End DISCOVERY for K8S Orchestrator Extension for job '{JobID}' with failure",
- config.JobId);
- return FailJob(ex.Message, config.JobHistoryId);
- }
-
- try
- {
- //Sends store locations back to KF command where they can be approved or rejected
- Logger.LogInformation("Submitting discovered locations to Keyfactor Command...");
- Logger.LogTrace("Discovery locations: {Locations}", string.Join(",", locations));
- Logger.LogDebug("Calling submitDiscovery.Invoke()");
- submitDiscovery.Invoke(locations.Distinct().ToArray());
- Logger.LogDebug("Returned from submitDiscovery.Invoke()");
- //Status: 2=Success, 3=Warning, 4=Error
- Logger.LogInformation("Discovery job {JobId} completed successfully with {Count} locations", config.JobId, locations.Count);
- Logger.MethodExit(MsLogLevel.Debug);
- return new JobResult
- {
- Result = OrchestratorJobStatusJobResult.Success,
- JobHistoryId = config.JobHistoryId,
- FailureMessage = "Discovered the following locations: " + string.Join(",\n", locations)
- };
- }
- 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.
- Logger.LogError("Discovery job has failed due to an unknown error: {Error}", ex.Message);
- Logger.LogTrace("Exception details: {Details}", ex.ToString());
- var inner = ex.InnerException;
- while (inner != null)
- {
- Logger.LogError("Inner Exception: {Message}", inner.Message);
- Logger.LogTrace("Inner exception details: {Details}", inner.ToString());
- inner = inner.InnerException;
- }
-
- Logger.LogInformation("End DISCOVERY for K8S Orchestrator Extension for job '{JobID}' with failure",
- config.JobId);
- Logger.MethodExit(MsLogLevel.Debug);
- return FailJob(ex.Message, config.JobHistoryId);
- }
- }
-}
\ No newline at end of file
diff --git a/kubernetes-orchestrator-extension/Jobs/Inventory.cs b/kubernetes-orchestrator-extension/Jobs/Inventory.cs
deleted file mode 100644
index c2bc387..0000000
--- a/kubernetes-orchestrator-extension/Jobs/Inventory.cs
+++ /dev/null
@@ -1,2141 +0,0 @@
-// 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.
-
-// Suppress warnings for variables used for state tracking but not read (future functionality)
-#pragma warning disable CS0219
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using k8s.Autorest;
-using Keyfactor.Extensions.Orchestrator.K8S.StoreTypes.K8SJKS;
-using Keyfactor.Extensions.Orchestrator.K8S.StoreTypes.K8SPKCS12;
-using Keyfactor.Extensions.Orchestrator.K8S.Utilities;
-using Keyfactor.Logging;
-using Keyfactor.Orchestrators.Common.Enums;
-using Keyfactor.Orchestrators.Extensions;
-using Keyfactor.Orchestrators.Extensions.Interfaces;
-using Microsoft.Extensions.Logging;
-using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
-using Org.BouncyCastle.Pkcs;
-
-namespace Keyfactor.Extensions.Orchestrator.K8S.Jobs;
-
-///
-/// Inventory job implementation for Kubernetes certificate stores.
-/// Finds all certificates in a given Kubernetes certificate store (secrets, CSRs, JKS, PKCS12)
-/// and returns them to Keyfactor Command for storage in its database.
-/// Private keys are NOT passed back to Keyfactor Command.
-///
-///
-/// Supports the following store types:
-/// - Opaque secrets (K8SSecret)
-/// - TLS secrets (K8STLSSecr)
-/// - Certificate Signing Requests (K8SCert)
-/// - JKS keystores (K8SJKS)
-/// - PKCS12 keystores (K8SPKCS12)
-/// - Cluster-wide inventory (K8SCluster)
-/// - Namespace-wide inventory (K8SNS)
-///
-public class Inventory : JobBase, IInventoryJobExtension
-{
- ///
- /// Represents a single inventory entry with per-item private key status and certificate chain.
- /// Used for K8SNS and K8SCluster inventory where each secret may have different private key status.
- ///
- private class InventoryEntry
- {
- /// The alias/identifier for this inventory item.
- public string Alias { get; set; } = string.Empty;
-
- /// The certificate chain (leaf cert first, then intermediates, then root).
- public List Certificates { get; set; } = new();
-
- /// Whether this entry has a private key in the store.
- public bool HasPrivateKey { get; set; }
- }
-
- ///
- /// Stores the original KubeSecretName value from the job config properties.
- /// This is needed for K8SCert cluster-wide mode detection because InitializeStore
- /// may modify KubeSecretName by setting it from StorePath if empty.
- ///
- private string _originalKubeSecretName;
-
- ///
- /// Initializes a new instance of the Inventory job with the specified PAM resolver.
- ///
- /// PAM secret resolver for credential retrieval.
- public Inventory(IPAMSecretResolver resolver)
- {
- _resolver = resolver;
- }
-
- ///
- /// Main entry point for the inventory job. Processes the job configuration and returns
- /// all certificates found in the specified Kubernetes certificate store.
- ///
- /// Inventory job configuration containing store details and credentials.
- /// Callback delegate to submit discovered certificates to Keyfactor Command.
- /// JobResult indicating success or failure of the inventory operation.
- ///
- /// Configuration parameters available in config:
- /// - config.ServerUsername, config.ServerPassword - credentials for K8S API authentication
- /// - config.CertificateStoreDetails.StorePath - location path of certificate store
- /// - config.CertificateStoreDetails.StorePassword - password for protected stores (JKS/PKCS12)
- /// - config.CertificateStoreDetails.Properties - JSON string with custom store properties
- ///
- public JobResult ProcessJob(InventoryJobConfiguration config, SubmitInventoryUpdate submitInventory)
- {
- Logger ??= LogHandler.GetClassLogger(GetType());
- Logger.MethodEntry(MsLogLevel.Debug);
-
- try
- {
- // For K8SCert cluster-wide mode detection, we need to capture the original KubeSecretName
- // BEFORE InitializeStore modifies it (it may get set from StorePath if empty)
- string originalKubeSecretName = null;
- if (!string.IsNullOrEmpty(config.CertificateStoreDetails?.Properties))
- {
- try
- {
- var props = System.Text.Json.JsonSerializer.Deserialize>(
- config.CertificateStoreDetails.Properties);
- if (props != null && props.TryGetValue("KubeSecretName", out var val))
- {
- originalKubeSecretName = val?.ToString();
- }
- }
- catch
- {
- // Ignore JSON parsing errors - will use default behavior
- }
- }
-
- Logger.LogDebug("Initializing store for inventory job {JobId}", config.JobId);
- InitializeStore(config);
- Logger.LogTrace("Returned from InitializeStore()");
-
- // Store the original KubeSecretName for K8SCert cluster-wide mode detection
- _originalKubeSecretName = originalKubeSecretName;
-
- Logger.LogInformation("Begin INVENTORY for K8S Orchestrator Extension for job " + config.JobId);
- Logger.LogInformation($"Inventory for store type: {config.Capability}");
-
- Logger.LogTrace("KubeClient is null: {IsNull}", KubeClient == null);
- if (KubeClient == null)
- {
- throw new InvalidOperationException("KubeClient is null after InitializeStore()");
- }
-
- Logger.LogDebug("Server: {Host}", KubeClient.GetHost());
- Logger.LogDebug("Store Path: {StorePath}", StorePath);
- Logger.LogDebug("KubeSecretType: {KubeSecretType}", KubeSecretType);
- Logger.LogDebug("KubeSecretName: {KubeSecretName}", KubeSecretName);
- Logger.LogDebug("KubeNamespace: {KubeNamespace}", KubeNamespace);
- Logger.LogDebug("Host: {Host}", KubeClient.GetHost());
-
- Logger.LogTrace("Inventory entering switch based on KubeSecretType: " + KubeSecretType + "...");
-
- // Note: KubeSecretType is now derived from Capability in JobBase.DeriveSecretTypeFromCapability()
- // The following store types are handled:
- // - K8SCluster -> "cluster"
- // - K8SNS -> "namespace"
- // - K8SCert -> "certificate"
- // - K8SJKS -> "jks"
- // - K8SPKCS12 -> "pkcs12"
- // - K8SSecret -> "secret"
- // - K8STLSSecr -> "tls_secret"
-
- var allowedKeys = new List();
- if (!string.IsNullOrEmpty(CertificateDataFieldName))
- allowedKeys = CertificateDataFieldName.Split(',').ToList();
-
- // Handle null KubeSecretType gracefully
- if (string.IsNullOrEmpty(KubeSecretType))
- {
- Logger.LogWarning("KubeSecretType is null or empty, defaulting to 'secret'");
- KubeSecretType = "secret";
- }
-
- switch (KubeSecretType.ToLower())
- {
- case "secret":
- case "secrets":
- case "opaque":
- Logger.LogInformation("Inventorying opaque secrets using the following allowed keys: {Keys}",
- OpaqueAllowedKeys?.ToString());
- try
- {
- var opaqueInventory = HandleOpaqueSecretAsList(config.JobHistoryId);
- Logger.LogDebug("Returned inventory count: {Count}", opaqueInventory.Count.ToString());
- if (opaqueInventory.Count == 0)
- {
- Logger.LogInformation("No certificates found in Opaque secret {Namespace}/{Name}",
- KubeNamespace, KubeSecretName);
- submitInventory.Invoke(new List());
- return SuccessJob(config.JobHistoryId, "No certificates found in Opaque secret");
- }
- return PushInventory(opaqueInventory, config.JobHistoryId, submitInventory, true);
- }
- catch (StoreNotFoundException)
- {
- Logger.LogWarning("Unable to locate Opaque secret {Namespace}/{Name}. Sending empty inventory.",
- KubeNamespace, KubeSecretName);
- return PushInventory(new List(), config.JobHistoryId, submitInventory, false,
- "WARNING: Opaque secret not found in Kubernetes cluster. Assuming empty inventory.");
- }
- catch (Exception ex)
- {
- Logger.LogError("Inventory failed with exception: " + ex.Message);
- Logger.LogTrace(ex.Message);
- Logger.LogTrace(ex.StackTrace);
- //Status: 2=Success, 3=Warning, 4=Error
- Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + config.JobId +
- " with failure.");
- return new JobResult
- {
- Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId,
- FailureMessage = ex.Message
- };
- }
-
- case "tls_secret":
- case "tls":
- case "tlssecret":
- case "tls_secrets":
- Logger.LogInformation("Inventorying TLS secrets using the following allowed keys: {Keys}",
- TLSAllowedKeys?.ToString());
- try
- {
- var tlsCertsInv = HandleTlsSecret(config.JobHistoryId);
- Logger.LogDebug("Returned inventory count: {Count}", tlsCertsInv.Count.ToString());
- if (tlsCertsInv.Count == 0)
- {
- Logger.LogInformation("No certificates found in TLS secret {Namespace}/{Name}",
- KubeNamespace, KubeSecretName);
- submitInventory.Invoke(new List());
- return SuccessJob(config.JobHistoryId, "No certificates found in TLS secret");
- }
- return PushInventory(tlsCertsInv, config.JobHistoryId, submitInventory, true);
- }
- catch (StoreNotFoundException)
- {
- Logger.LogWarning("Unable to locate TLS secret {Namespace}/{Name}. Sending empty inventory.",
- KubeNamespace, KubeSecretName);
- return PushInventory(new List(), config.JobHistoryId, submitInventory, false,
- "WARNING: TLS secret not found in Kubernetes cluster. Assuming empty inventory.");
- }
-
- case "certificate":
- case "cert":
- case "csr":
- case "csrs":
- case "certs":
- case "certificates":
- Logger.LogInformation("Inventorying certificates using " + CertAllowedKeys);
- return HandleCertificate(config.JobHistoryId, submitInventory);
- case "pkcs12":
- case "p12":
- case "pfx":
- //combine allowed keys and CertificateDataFields into one list
- allowedKeys.AddRange(Pkcs12AllowedKeys);
- Logger.LogInformation("Inventorying PKCS12 using the following allowed keys: {Keys}", allowedKeys);
- try
- {
- var pkcs12Inventory = HandlePkcs12Secret(config, allowedKeys);
- Logger.LogDebug("Returned inventory count: {Count}", pkcs12Inventory.Count.ToString());
- if (pkcs12Inventory.Count == 0)
- {
- Logger.LogInformation("No certificates found in PKCS12 keystore {Namespace}/{Name}",
- KubeNamespace, KubeSecretName);
- submitInventory.Invoke(new List());
- return SuccessJob(config.JobHistoryId, "No certificates found in PKCS12 keystore");
- }
- return PushInventory(pkcs12Inventory, config.JobHistoryId, submitInventory, true);
- }
- catch (StoreNotFoundException)
- {
- Logger.LogWarning("Unable to locate PKCS12 secret {Namespace}/{Name}. Sending empty inventory.",
- KubeNamespace, KubeSecretName);
- return PushInventory(new List(), config.JobHistoryId, submitInventory, false,
- "WARNING: PKCS12 store not found in Kubernetes cluster. Assuming empty inventory.");
- }
- case "jks":
- allowedKeys.AddRange(JksAllowedKeys);
- Logger.LogInformation("Inventorying JKS using the following allowed keys: {Keys}", allowedKeys);
- try
- {
- var jksInventory = HandleJKSSecret(config, allowedKeys);
- Logger.LogDebug("Returned inventory count: {Count}", jksInventory.Count.ToString());
- if (jksInventory.Count == 0)
- {
- Logger.LogInformation("No certificates found in JKS keystore {Namespace}/{Name}",
- KubeNamespace, KubeSecretName);
- submitInventory.Invoke(new List());
- return SuccessJob(config.JobHistoryId, "No certificates found in JKS keystore");
- }
- return PushInventory(jksInventory, config.JobHistoryId, submitInventory, true);
- }
- catch (StoreNotFoundException)
- {
- Logger.LogWarning("Unable to locate JKS secret {Namespace}/{Name}. Sending empty inventory.",
- KubeNamespace, KubeSecretName);
- return PushInventory(new List(), config.JobHistoryId, submitInventory, false,
- "WARNING: JKS store not found in Kubernetes cluster. Assuming empty inventory.");
- }
-
- case "cluster":
- var clusterOpaqueSecrets = KubeClient.DiscoverSecrets(OpaqueAllowedKeys, "Opaque", "all");
- var clusterTlsSecrets = KubeClient.DiscoverSecrets(TLSAllowedKeys, "tls", "all");
- var errors = new List();
-
- // Use List to track per-secret private key status and full certificate chains
- var clusterInventoryEntries = new List();
- foreach (var opaqueSecret in clusterOpaqueSecrets)
- {
- KubeSecretName = "";
- KubeNamespace = "";
- KubeSecretType = "secret";
- try
- {
- // DiscoverSecrets returns format: cluster/namespace/secrets/secretname
- // Parse the path directly since ResolveStorePath doesn't handle cluster stores with 4 parts
- var pathParts = opaqueSecret.Split('/');
- if (pathParts.Length >= 4)
- {
- // Format: cluster/namespace/secrets/secretname
- KubeNamespace = pathParts[1];
- KubeSecretName = pathParts[pathParts.Length - 1];
- Logger.LogDebug("Cluster inventory: Parsed namespace={Namespace}, secretName={SecretName} from path {Path}",
- KubeNamespace, KubeSecretName, opaqueSecret);
- }
- else
- {
- // Fallback to ResolveStorePath for other formats
- ResolveStorePath(opaqueSecret);
- }
- StorePath = opaqueSecret.Replace("secrets", "secrets/opaque");
- //Split storepath by / and remove first 1 elements
- var storePathSplit = StorePath.Split('/');
- var storePathSplitList = storePathSplit.ToList();
- storePathSplitList.RemoveAt(0);
- var alias = string.Join("/", storePathSplitList);
-
- var entry = HandleOpaqueSecretAsEntry(config.JobHistoryId, alias);
- if (entry.Certificates.Count > 0)
- {
- clusterInventoryEntries.Add(entry);
- Logger.LogDebug("Cluster inventory: Added opaque secret '{Alias}' with HasPrivateKey={HasPrivateKey}, CertCount={CertCount}",
- entry.Alias, entry.HasPrivateKey, entry.Certificates.Count);
- Logger.LogTrace("Cluster inventory: Alias '{Alias}' certificate chain:\n{Chain}",
- entry.Alias, string.Join("\n---\n", entry.Certificates));
- }
- }
- catch (Exception ex)
- {
- Logger.LogError("Error processing Opaque Secret: " + opaqueSecret + " - " + ex.Message +
- "\n\t" + ex.StackTrace);
- errors.Add(ex.Message);
- }
- }
-
- foreach (var tlsSecret in clusterTlsSecrets)
- {
- KubeSecretName = "";
- KubeNamespace = "";
- KubeSecretType = "tls_secret";
- try
- {
- // DiscoverSecrets returns format: cluster/namespace/secrets/secretname
- // Parse the path directly since ResolveStorePath doesn't handle cluster stores with 4 parts
- var pathParts = tlsSecret.Split('/');
- if (pathParts.Length >= 4)
- {
- // Format: cluster/namespace/secrets/secretname
- KubeNamespace = pathParts[1];
- KubeSecretName = pathParts[pathParts.Length - 1];
- Logger.LogDebug("Cluster inventory: Parsed namespace={Namespace}, secretName={SecretName} from path {Path}",
- KubeNamespace, KubeSecretName, tlsSecret);
- }
- else
- {
- // Fallback to ResolveStorePath for other formats
- ResolveStorePath(tlsSecret);
- }
- StorePath = tlsSecret.Replace("secrets", "secrets/tls");
- //Split storepath by / and remove first 1 elements
- var storePathSplit = StorePath.Split('/');
- var storePathSplitList = storePathSplit.ToList();
- storePathSplitList.RemoveAt(0);
- var alias = string.Join("/", storePathSplitList);
-
- var entry = HandleTlsSecretAsEntry(config.JobHistoryId, alias);
- if (entry.Certificates.Count > 0)
- {
- clusterInventoryEntries.Add(entry);
- Logger.LogDebug("Cluster inventory: Added TLS secret '{Alias}' with HasPrivateKey={HasPrivateKey}, CertCount={CertCount}",
- entry.Alias, entry.HasPrivateKey, entry.Certificates.Count);
- Logger.LogTrace("Cluster inventory: Alias '{Alias}' certificate chain:\n{Chain}",
- entry.Alias, string.Join("\n---\n", entry.Certificates));
- }
- }
- catch (Exception ex)
- {
- Logger.LogError("Error processing TLS Secret: " + tlsSecret + " - " + ex.Message + "\n\t" +
- ex.StackTrace);
- errors.Add(ex.Message);
- }
- }
-
- Logger.LogDebug("Cluster inventory complete: {Count} secrets with per-item private key status", clusterInventoryEntries.Count);
- return PushInventory(clusterInventoryEntries, config.JobHistoryId, submitInventory);
- case "namespace":
- var namespaceOpaqueSecrets = KubeClient.DiscoverSecrets(OpaqueAllowedKeys, "Opaque", KubeNamespace);
- var namespaceTlsSecrets = KubeClient.DiscoverSecrets(TLSAllowedKeys, "tls", KubeNamespace);
- var namespaceErrors = new List();
-
- // Use List to track per-secret private key status and full certificate chains
- var namespaceInventoryEntries = new List();
- foreach (var opaqueSecret in namespaceOpaqueSecrets)
- {
- KubeSecretName = "";
- // KubeNamespace = "";
- KubeSecretType = "secret";
- try
- {
- // DiscoverSecrets returns format: cluster/namespace/secrets/secretname
- // Parse the path directly since ResolveStorePath doesn't handle NS stores with 4 parts
- var pathParts = opaqueSecret.Split('/');
- if (pathParts.Length >= 4)
- {
- // Format: cluster/namespace/secrets/secretname
- // KubeNamespace is already set from store config, just need secret name
- KubeSecretName = pathParts[pathParts.Length - 1];
- Logger.LogDebug("Namespace inventory: Parsed secretName={SecretName} from path {Path}",
- KubeSecretName, opaqueSecret);
- }
- else
- {
- // Fallback to ResolveStorePath for other formats
- ResolveStorePath(opaqueSecret);
- }
- StorePath = opaqueSecret.Replace("secrets", "secrets/opaque");
- //Split storepath by / and remove first 2 elements
- var storePathSplit = StorePath.Split('/');
- var storePathSplitList = storePathSplit.ToList();
- storePathSplitList.RemoveAt(0);
- storePathSplitList.RemoveAt(0);
- var alias = string.Join("/", storePathSplitList);
-
- var entry = HandleOpaqueSecretAsEntry(config.JobHistoryId, alias);
- if (entry.Certificates.Count > 0)
- {
- namespaceInventoryEntries.Add(entry);
- Logger.LogDebug("Namespace inventory: Added opaque secret '{Alias}' with HasPrivateKey={HasPrivateKey}, CertCount={CertCount}",
- entry.Alias, entry.HasPrivateKey, entry.Certificates.Count);
- Logger.LogTrace("Namespace inventory: Alias '{Alias}' certificate chain:\n{Chain}",
- entry.Alias, string.Join("\n---\n", entry.Certificates));
- }
- }
- catch (Exception ex)
- {
- Logger.LogError("Error processing Opaque Secret: " + opaqueSecret + " - " + ex.Message +
- "\n\t" + ex.StackTrace);
- namespaceErrors.Add(ex.Message);
- }
- }
-
- foreach (var tlsSecret in namespaceTlsSecrets)
- {
- KubeSecretName = "";
- // KubeNamespace = "";
- KubeSecretType = "tls_secret";
- try
- {
- // DiscoverSecrets returns format: cluster/namespace/secrets/secretname
- // Parse the path directly since ResolveStorePath doesn't handle NS stores with 4 parts
- var pathParts = tlsSecret.Split('/');
- if (pathParts.Length >= 4)
- {
- // Format: cluster/namespace/secrets/secretname
- // KubeNamespace is already set from store config, just need secret name
- KubeSecretName = pathParts[pathParts.Length - 1];
- Logger.LogDebug("Namespace inventory: Parsed secretName={SecretName} from path {Path}",
- KubeSecretName, tlsSecret);
- }
- else
- {
- // Fallback to ResolveStorePath for other formats
- ResolveStorePath(tlsSecret);
- }
- StorePath = tlsSecret.Replace("secrets", "secrets/tls");
-
- //Split storepath by / and remove first 2 elements
- var storePathSplit = StorePath.Split('/');
- var storePathSplitList = storePathSplit.ToList();
- storePathSplitList.RemoveAt(0);
- storePathSplitList.RemoveAt(0);
- var alias = string.Join("/", storePathSplitList);
-
- var entry = HandleTlsSecretAsEntry(config.JobHistoryId, alias);
- if (entry.Certificates.Count > 0)
- {
- namespaceInventoryEntries.Add(entry);
- Logger.LogDebug("Namespace inventory: Added TLS secret '{Alias}' with HasPrivateKey={HasPrivateKey}, CertCount={CertCount}",
- entry.Alias, entry.HasPrivateKey, entry.Certificates.Count);
- Logger.LogTrace("Namespace inventory: Alias '{Alias}' certificate chain:\n{Chain}",
- entry.Alias, string.Join("\n---\n", entry.Certificates));
- }
- }
- catch (Exception ex)
- {
- Logger.LogError("Error processing TLS Secret: " + tlsSecret + " - " + ex.Message + "\n\t" +
- ex.StackTrace);
- namespaceErrors.Add(ex.Message);
- }
- }
-
- Logger.LogDebug("Namespace inventory complete: {Count} secrets with per-item private key status", namespaceInventoryEntries.Count);
- return PushInventory(namespaceInventoryEntries, config.JobHistoryId, submitInventory);
-
- default:
- Logger.LogError("Inventory failed with exception: " + KubeSecretType + " not supported.");
- var errorMsg = $"{KubeSecretType} not supported.";
- Logger.LogError(errorMsg);
- Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + config.JobId +
- " with failure.");
- return new JobResult
- {
- Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId,
- FailureMessage = errorMsg
- };
- }
- }
- catch (Exception ex)
- {
- Logger.LogError("Inventory failed with exception: " + ex.Message);
- Logger.LogTrace(ex.ToString());
- Logger.LogTrace(ex.StackTrace);
- //Status: 2=Success, 3=Warning, 4=Error
- Logger.LogInformation("End INVENTORY for K8S Orchestrator Extension for job " + config.JobId +
- " with failure.");
- return new JobResult
- {
- Result = OrchestratorJobStatusJobResult.Failure,
- JobHistoryId = config.JobHistoryId,
- FailureMessage = ex.Message
- };
- }
- }
-
- ///
- /// Handles inventory of JKS (Java KeyStore) secrets stored in Kubernetes.
- /// Deserializes JKS data and extracts all certificates and their chains.
- ///
- /// Job configuration containing store properties.
- /// List of allowed secret data keys to process.
- /// Dictionary mapping certificate aliases to their PEM certificate chains.
- private Dictionary> HandleJKSSecret(JobConfiguration config, List allowedKeys)
- {
- Logger.MethodEntry(MsLogLevel.Debug);
- var hasPrivateKeyJks = false;
- Logger.LogDebug("Attempting to serialize JKS store");
- var jksStore = new JksCertificateStoreSerializer(config.JobProperties?.ToString());
- //getJksBytesFromKubeSecret
- Logger.LogDebug("Attempting to get JKS bytes from K8S secret " + KubeSecretName + " in namespace " +
- KubeNamespace);
- var k8sData = KubeClient.GetJksSecret(KubeSecretName, KubeNamespace, "", "", allowedKeys);
-
- var jksInventoryDict = new Dictionary>();
- // iterate through the keys in the secret and add them to the jks store
- Logger.LogDebug("Iterating through keys in K8S secret " + KubeSecretName + " in namespace " + KubeNamespace);
- foreach (var (keyName, keyBytes) in k8sData.Inventory)
- {
- Logger.LogDebug("Fetching store password for K8S secret " + KubeSecretName + " in namespace " +
- KubeNamespace + " and key " + keyName);
- var keyPassword = getK8SStorePassword(k8sData.Secret);
- Logger.LogTrace("Password correlation for '{Secret}/{Key}': {CorrelationId}",
- KubeSecretName, keyName, LoggingUtilities.GetPasswordCorrelationId(keyPassword));
- var keyAlias = keyName;
- Logger.LogTrace("Key alias: {Alias}", keyAlias);
- Logger.LogDebug("Attempting to deserialize JKS store '{Secret}/{Key}'", KubeSecretName, keyName);
- var sourceIsPkcs12 = false; //This refers to if the JKS store is actually a PKCS12 store
- Pkcs12Store jStoreDs;
- try
- {
- jStoreDs = jksStore.DeserializeRemoteCertificateStore(keyBytes, keyName, keyPassword);
- }
- catch (JkSisPkcs12Exception)
- {
- sourceIsPkcs12 = true;
- var pkcs12Store = new Pkcs12CertificateStoreSerializer(config.JobProperties?.ToString());
- jStoreDs = pkcs12Store.DeserializeRemoteCertificateStore(keyBytes, keyName, keyPassword);
- // return HandlePkcs12Secret(config);
- }
-
- // create a list of certificate chains in PEM format
-
- Logger.LogDebug("Iterating through aliases in JKS store '{Secret}/{Key}'", KubeSecretName, keyName);
- var certAliasLookup = new Dictionary();
- //make a copy of jStoreDs.Aliases so we can remove items from it
-
- foreach (var certAlias in jStoreDs.Aliases)
- {
- if (certAliasLookup.TryGetValue(certAlias, out var certAliasSubject))
- if (certAliasSubject == "skip")
- {
- Logger.LogTrace("Certificate alias: {Alias} already exists in lookup with subject '{Subject}'",
- certAlias, certAliasSubject);
- continue;
- }
-
- Logger.LogTrace("Certificate alias: {Alias}", certAlias);
- var certChainList = new List();
-
- Logger.LogDebug("Attempting to get certificate chain for alias '{Alias}'", certAlias);
- var certChain = jStoreDs.GetCertificateChain(certAlias);
-
- if (certChain != null)
- {
- certAliasLookup[certAlias] = certChain[0].Certificate.SubjectDN.ToString();
- if (sourceIsPkcs12 && certChain.Length > 0)
- {
- // This is a PKCS12 store that was created as a JKS so we need to check that the aliases aren't the same as the cert chain
- // If they are the same then we need to only use the chain and break out of the loop
- var certChainAliases = certChain.Select(cert => cert.Certificate.SubjectDN.ToString()).ToList();
- // Remove leaf certificate from chain
- certChainAliases.RemoveAt(0);
- var storeAliases = jStoreDs.Aliases.ToList();
- storeAliases.Remove(certAlias);
- // Iterate though the aliases and add them to the lookup as 'skip' if they are in the chain
- foreach (var alias in storeAliases.Where(alias => certChainAliases.Contains(alias)))
- certAliasLookup[alias] = "skip";
- }
- }
- else
- {
- certAliasLookup[certAlias] = "skip";
- }
-
- var fullAlias = keyAlias + "/" + certAlias;
- Logger.LogTrace("Full alias: {Alias}", fullAlias);
- //check if the alias is a private key
- if (jStoreDs.IsKeyEntry(certAlias)) hasPrivateKeyJks = true;
- var pKey = jStoreDs.GetKey(certAlias);
- if (pKey != null)
- {
- Logger.LogDebug("Found private key for alias '{Alias}'", certAlias);
- hasPrivateKeyJks = true;
- }
-
- StringBuilder certChainPem;
-
- if (certChain != null)
- {
- Logger.LogDebug("Certificate chain found for alias '{Alias}'", certAlias);
- Logger.LogDebug("Iterating through certificate chain for alias '{Alias}' to build PEM chain",
- certAlias);
- foreach (var cert in certChain)
- {
- certChainPem = new StringBuilder();
- certChainPem.AppendLine("-----BEGIN CERTIFICATE-----");
- certChainPem.AppendLine(Convert.ToBase64String(cert.Certificate.GetEncoded()));
- certChainPem.AppendLine("-----END CERTIFICATE-----");
- certChainList.Add(certChainPem.ToString());
- }
-
- Logger.LogTrace("Certificate chain for alias '{Alias}': {Chain}", certAlias, certChainList);
- }
-
- if (certChainList.Count != 0)
- {
- Logger.LogDebug("Adding certificate chain for alias '{Alias}' to inventory", certAlias);
- jksInventoryDict[fullAlias] = certChainList;
- continue;
- }
-
- Logger.LogDebug("Attempting to get leaf certificate for alias '{Alias}'", certAlias);
- var leaf = jStoreDs.GetCertificate(certAlias);
- if (leaf != null)
- {
- Logger.LogDebug("Leaf certificate found for alias '{Alias}'", certAlias);
- certChainPem = new StringBuilder();
- certChainPem.AppendLine("-----BEGIN CERTIFICATE-----");
- certChainPem.AppendLine(Convert.ToBase64String(leaf.Certificate.GetEncoded()));
- certChainPem.AppendLine("-----END CERTIFICATE-----");
- certChainList.Add(certChainPem.ToString());
- }
-
- Logger.LogDebug("Adding leaf certificate for alias '{Alias}' to inventory", certAlias);
- if (certAliasLookup[certAlias] != "skip") jksInventoryDict[fullAlias] = certChainList;
- }
- }
-
- Logger.LogDebug("JKS inventory complete with {Count} entries", jksInventoryDict.Count);
- Logger.MethodExit(MsLogLevel.Debug);
- return jksInventoryDict;
- }
-
- ///
- /// Handles inventory of Kubernetes Certificate Signing Requests (CSRs).
- /// If KubeSecretName is specified, inventories that specific CSR (legacy single-CSR mode).
- /// If KubeSecretName is empty or "*", inventories ALL issued CSRs in the cluster (cluster-wide mode).
- ///
- /// The job history ID for tracking.
- /// Callback delegate to submit discovered certificates.
- /// JobResult indicating success or failure.
- private JobResult HandleCertificate(long jobId, SubmitInventoryUpdate submitInventory)
- {
- Logger.MethodEntry(MsLogLevel.Debug);
- Logger.LogTrace("submitInventory: " + submitInventory);
-
- // Determine mode: single CSR or cluster-wide
- // Use the ORIGINAL KubeSecretName value from job config, not the potentially modified one
- // (InitializeStore may set KubeSecretName from StorePath if it was empty)
- var secretNameToCheck = _originalKubeSecretName ?? KubeSecretName;
- var isClusterWideMode = string.IsNullOrWhiteSpace(secretNameToCheck) || secretNameToCheck == "*";
-
- Logger.LogDebug("K8SCert mode detection: originalKubeSecretName='{Original}', KubeSecretName='{Current}', isClusterWideMode={IsClusterWide}",
- _originalKubeSecretName ?? "(null)", KubeSecretName, isClusterWideMode);
-
- if (isClusterWideMode)
- {
- Logger.LogDebug("Processing CSR inventory for job {JobId} - cluster-wide mode (all CSRs)", jobId);
- return HandleCertificateClusterWide(jobId, submitInventory);
- }
- else
- {
- // For single CSR mode, use the original KubeSecretName if it was explicitly set
- var csrName = !string.IsNullOrWhiteSpace(_originalKubeSecretName) ? _originalKubeSecretName : KubeSecretName;
- Logger.LogDebug("Processing CSR inventory for job {JobId} - single CSR mode (name: {CsrName})", jobId, csrName);
- return HandleCertificateSingle(jobId, submitInventory, csrName);
- }
- }
-
- ///
- /// Handles inventory of a single CSR by name (legacy behavior).
- ///
- /// The job history ID for tracking.
- /// Callback delegate to submit discovered certificates.
- /// The name of the CSR to inventory.
- private JobResult HandleCertificateSingle(long jobId, SubmitInventoryUpdate submitInventory, string csrName)
- {
- Logger.MethodEntry(MsLogLevel.Debug);
- Logger.LogTrace("Calling GetCertificateSigningRequestStatus for CSR '{CsrName}'...", csrName);
-
- try
- {
- var certificates = KubeClient.GetCertificateSigningRequestStatus(csrName);
- Logger.LogDebug("GetCertificateSigningRequestStatus returned {Count} certificates.", certificates.Count());
- Logger.LogTrace(string.Join(",", certificates));
- Logger.LogDebug("Pushing {Count} certificates to inventory", certificates.Count());
- var result = PushInventory(certificates, jobId, submitInventory);
- Logger.MethodExit(MsLogLevel.Debug);
- return result;
- }
- catch (HttpOperationException e)
- {
- Logger.LogError("HttpOperationException: {Message}", e.Message);
- Logger.LogTrace(e.ToString());
- Logger.LogTrace(e.StackTrace);
- var certDataErrorMsg =
- $"Kubernetes CSR '{csrName}' was not found on host '{KubeClient.GetHost()}'.";
- Logger.LogError(certDataErrorMsg);
- var inventoryItems = new List();
- submitInventory.Invoke(inventoryItems);
- Logger.LogTrace("Exiting HandleCertificateSingle for job id " + jobId + "...");
- return new JobResult
- {
- Result = OrchestratorJobStatusJobResult.Success,
- JobHistoryId = jobId,
- FailureMessage = certDataErrorMsg
- };
- }
- catch (Exception e)
- {
- Logger.LogError("Exception: " + e.Message);
- Logger.LogTrace(e.ToString());
- Logger.LogTrace(e.StackTrace);
- var certDataErrorMsg = $"Error querying Kubernetes CSR API: {e.Message}";
- Logger.LogError(certDataErrorMsg);
- Logger.LogTrace("Exiting HandleCertificateSingle for job id " + jobId + "...");
- return FailJob(certDataErrorMsg, jobId);
- }
- }
-
- ///
- /// Handles inventory of all CSRs in the cluster (new cluster-wide behavior).
- ///
- private JobResult HandleCertificateClusterWide(long jobId, SubmitInventoryUpdate submitInventory)
- {
- Logger.MethodEntry(MsLogLevel.Debug);
-
- try
- {
- // List all CSRs in the cluster that have issued certificates
- var csrCertificates = KubeClient.ListAllCertificateSigningRequests();
- Logger.LogDebug("Found {Count} issued certificates from CSRs", csrCertificates.Count);
-
- if (csrCertificates.Count == 0)
- {
- Logger.LogInformation("No issued CSR certificates found in cluster");
- submitInventory.Invoke(new List());
- return new JobResult
- {
- Result = OrchestratorJobStatusJobResult.Success,
- JobHistoryId = jobId,
- FailureMessage = "No issued CSR certificates found in cluster"
- };
- }
-
- var inventoryItems = new List();
- foreach (var kvp in csrCertificates)
- {
- var csrName = kvp.Key;
- var certPem = kvp.Value;
-
- Logger.LogDebug("Processing CSR {CsrName}", csrName);
- Logger.LogTrace("Certificate PEM: {CertPem}", certPem);
-
- try
- {
- // Parse the certificate chain - CSRs can contain multiple certificates if signed by a CA with intermediates
- var certChain = KubeClient.LoadCertificateChain(certPem);
- if (certChain == null || certChain.Count == 0)
- {
- Logger.LogWarning("Failed to parse certificate chain from CSR {CsrName}, skipping", csrName);
- continue;
- }
-
- // Convert each certificate in the chain to PEM format
- var certPemList = new List