/// <summary> /// Processes the certificate management. /// </summary> /// <param name="directoryPath">Directory to put the certificatex in.</param> /// <param name="certConfiguration">Certificate configuration. This is a combination of comma separated values in following format /// *certFileName*;*SourceOfCert*;*CertIdentifierInSource*.</param> /// <param name="keyVaultUris">KeyVault uris if key vault is to be used.</param> /// <param name="keyVaultClientId">Application client Id to access keyvault.</param> /// <param name="keyVaultClientSecret">Application client secret to access keyvault.</param> /// <param name="keyVaultClientCert">Application client certificate thumbprint if the keyvault app has certificate credentials.</param> /// <param name="useManagedIdentity">Use managed identity.</param> /// <returns>Exit code for the operation.</returns> internal static async Task <ExitCode> ProcessAsync( string directoryPath, string certConfiguration, List <string> keyVaultUris, string keyVaultClientId, string keyVaultClientSecret, string keyVaultClientCert, bool useManagedIdentity) { if (string.IsNullOrEmpty(directoryPath)) { Logger.LogError(CallInfo.Site(), "Directory path missing for the Certificate directory."); return(ExitCode.DirectoryPathMissing); } if (string.IsNullOrEmpty(certConfiguration)) { Logger.LogError(CallInfo.Site(), "Cert configuration missing. Please specify CertsToConfigure option"); return(ExitCode.InvalidCertConfiguration); } // 1. Initialize KeyVault Client if params were passed. KeyVaultClient keyVaultClient = null; Dictionary <string, string> keyVaultSecretMap = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); if (keyVaultUris.Any()) { KeyVaultClient.AuthenticationCallback callback = null; if (useManagedIdentity) { string connectionString = "RunAs=App"; if (!string.IsNullOrEmpty(keyVaultClientId)) { connectionString = connectionString + ";AppId=" + keyVaultClientId; } AzureServiceTokenProvider tokenProvider = new AzureServiceTokenProvider(connectionString); callback = new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback); } else { if (string.IsNullOrEmpty(keyVaultClientId)) { Logger.LogError(CallInfo.Site(), "If KeyVaultUri is specified and managed identity is not used, KeyVault ClientId must be specified"); return(ExitCode.KeyVaultConfigurationIncomplete); } if (string.IsNullOrEmpty(keyVaultClientSecret) && string.IsNullOrEmpty(keyVaultClientCert)) { Logger.LogError(CallInfo.Site(), "If KeyVaultUri is specified and managed identity is not used, KeyVault ClientSecret or KeyVault ClientCert must be specified"); return(ExitCode.KeyVaultConfigurationIncomplete); } if (!string.IsNullOrEmpty(keyVaultClientSecret)) { callback = (authority, resource, scope) => GetTokenFromClientSecretAsync(authority, resource, keyVaultClientId, keyVaultClientSecret); } else { X509Certificate2Collection keyVaultCerts = CertHelpers.FindCertificates(keyVaultClientCert, X509FindType.FindByThumbprint); if (keyVaultCerts.Count == 0) { Logger.LogError(CallInfo.Site(), "Failed to find Client cert with thumbprint '{0}'", keyVaultClientCert); return(ExitCode.KeyVaultConfigurationIncomplete); } callback = (authority, resource, scope) => GetTokenFromClientCertificateAsync(authority, resource, keyVaultClientId, keyVaultCerts[0]); } } keyVaultClient = new KeyVaultClient(callback); foreach (string keyVaultUri in keyVaultUris) { IPage <SecretItem> secrets = await keyVaultClient.GetSecretsAsync(keyVaultUri).ConfigureAwait(false); foreach (SecretItem secret in secrets) { keyVaultSecretMap[secret.Identifier.Name] = keyVaultUri; } while (!string.IsNullOrEmpty(secrets.NextPageLink)) { secrets = await keyVaultClient.GetSecretsNextAsync(secrets.NextPageLink).ConfigureAwait(false); foreach (SecretItem secret in secrets) { keyVaultSecretMap[secret.Identifier.Name] = keyVaultUri; } } } } // 2. Figure all the certs which need processing. string[] certsToConfigure = certConfiguration.Split(','); string currentExeDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string fullDirectoryPathForCerts = Path.Combine(currentExeDirectory, directoryPath); // 3. Process specified certs one by one. foreach (string certToConfigure in certsToConfigure) { // 3a. Split the cert configuration data to get actual details. This data is informat // <CertNameOnDisk>;CertSource(LocalMachine or KeyVault);<CertIdentifier(SecretName or Thumbprint)> string[] certConfigurationParams = certToConfigure.Split(';'); if (certConfigurationParams.Length != 3) { Logger.LogError(CallInfo.Site(), "Invalid certificate configuration '{0}'. Cert configuration must be in format <CertFileName>;<CertSource>;<CertIdentifier>", certToConfigure); return(ExitCode.InvalidCertConfiguration); } var certConfig = new { CertName = certConfigurationParams[0], CertSource = certConfigurationParams[1], CertIdentifier = certConfigurationParams[2] }; // 3b. Depending on the source of Cert get the PFX for the certs dropped into the directory. if (certConfig.CertSource.Equals("MyLocalMachine", StringComparison.OrdinalIgnoreCase)) { ExitCode localMachineCertHandler = await LocalMachineCertHandlerAsync( certConfig.CertName, certConfig.CertIdentifier, fullDirectoryPathForCerts).ConfigureAwait(false); if (localMachineCertHandler != ExitCode.Success) { return(localMachineCertHandler); } } else if (certConfig.CertSource.Equals("KeyVault", StringComparison.OrdinalIgnoreCase)) { if (!keyVaultSecretMap.TryGetValue(certConfig.CertIdentifier, out string keyVaultUri)) { Logger.LogError(CallInfo.Site(), "Certificate with name '{0}' missing from all specified KeyVaults", certConfig.CertIdentifier); return(ExitCode.CertificateMissingFromSource); } ExitCode keyVaultCertHandlerExitCode = await KeyVaultCertHandlerAsync( certConfig.CertName, certConfig.CertIdentifier, fullDirectoryPathForCerts, keyVaultClient, keyVaultUri).ConfigureAwait(false); if (keyVaultCertHandlerExitCode != ExitCode.Success) { return(keyVaultCertHandlerExitCode); } } else { Logger.LogError(CallInfo.Site(), "Unsupported Certificate source '{0}' for cert '{1}'", certConfig.CertSource, certConfig.CertName); return(ExitCode.UnsupportedCertSource); } // 3c. Convert PFX into .Key and .Crt. We are placing openssl next to this exe hence using current directory. ExitCode conversionExitCode = ConvertPfxIntoPemFormat(certConfig.CertName, fullDirectoryPathForCerts, currentExeDirectory, password: string.Empty); if (conversionExitCode != ExitCode.Success) { return(conversionExitCode); } // 3d. Delete the PFX as it is no longer needed. File.Delete(Path.Combine(fullDirectoryPathForCerts, certConfig.CertName + ".pfx")); } return(ExitCode.Success); }
/// <summary> /// Extracts PFX from a local cert present in LocalMachine store under My. /// </summary> /// <param name="certificateName">Name of the certificate.</param> /// <param name="certificateIdentitier">The certificate identifier.</param> /// <param name="fullDirectoryPath">The full directory path to drop PFX at.</param> /// <returns>Exit code for the operation.</returns> private static Task <ExitCode> LocalMachineCertHandlerAsync(string certificateName, string certificateIdentitier, string fullDirectoryPath) { // Split the Certificate identifier on :. If we don't find ':' then we assume the identifier is a thumbprint, // otherwise we try to figure what identifier the user gave. We support X509FindType values as string. string[] certIdentifierSplit = certificateIdentitier.Split(':'); X509FindType x509FindType = X509FindType.FindByThumbprint; if (certIdentifierSplit.Length > 1) { if (!Enum.TryParse <X509FindType>(certIdentifierSplit[1], out x509FindType)) { Logger.LogError(CallInfo.Site(), "Invalid Find type value used '{0}' in Ceritificate identifier '{1}'", certIdentifierSplit[1], certificateIdentitier); return(Task.FromResult(ExitCode.InvalidCertConfiguration)); } } X509Certificate2Collection certificateCollection = CertHelpers.FindCertificates( certIdentifierSplit[0], x509FindType, StoreName.My, StoreLocation.LocalMachine); if (certificateCollection.Count == 0) { Logger.LogError(CallInfo.Site(), "Failed to find certificate with name '{0}'", certificateName); return(Task.FromResult(ExitCode.CertificateMissingFromSource)); } X509Certificate2 selectedCertificate = certificateCollection[0]; // If more than 1 cert is found, choose the one with latest expiry. if (certificateCollection.Count > 1) { Logger.LogInfo(CallInfo.Site(), "Found multiple certificates which match the search identifier, selecting the one with latest expiry"); foreach (X509Certificate2 x509Certificate2 in certificateCollection) { // If the first selected certificate did not have a private key on it and // we found another cert with private key pick that up. if (!selectedCertificate.HasPrivateKey && x509Certificate2.HasPrivateKey) { selectedCertificate = x509Certificate2; } if (x509Certificate2.NotAfter > selectedCertificate.NotAfter && x509Certificate2.HasPrivateKey) { selectedCertificate = x509Certificate2; } } } if (!selectedCertificate.HasPrivateKey) { Logger.LogError(CallInfo.Site(), "Certificate with name '{0}' and thumbprint '{1}' has missing Private Key", certificateName, selectedCertificate.Thumbprint); return(Task.FromResult(ExitCode.PrivateKeyMissingOnCertificate)); } byte[] rawCertData = selectedCertificate.Export(X509ContentType.Pfx); return(Task.FromResult(SaveCertificatePrivateKeyToDisk(rawCertData, certificateName, fullDirectoryPath))); }
/// <summary> /// Defines the entry point of the application. /// </summary> /// <param name="args">The arguments.</param> /// <remarks> /// Possible Arguments /// 1) If you want to dump the certs from local machine only and make it work for one environment config. /// --ConfigureCerts "Certs" --ApplicationInsightsKey "AIKeyHere" --CertsToConfigure "ClusterCert;MyLocalMachine;ClusterCertThumbprint,SSLCert;MyLocalMachine;SSLCertThumbprint" /// 2) If you want to dump the certs from local machine and KeyVault and make it work for one environment config. /// --ConfigureCerts "Certs" --ApplicationInsightsKey "AIKeyHere" --CertsToConfigure "ClusterCert;MyLocalMachine;ClusterCertThumbprint,SSLCert;KeyVault;SSLSecretName" --KeyVaultUri "https://dummyvault.vault.azure.net/" --KeyVaultClientId "1dc8b8b3-be3e-482a-b56b-9092c91aa4b2" -KeyVaultClientSecret "keyvaultappsecret" /// 3) If you want to dump the certs from local machine and make it work for different environments having different configs. /// a) Set the arguments to --UseEnvironmentVariables (or -UseEnv) /// b) And set the Environment variables /// i) ConfigureCerts to Certs /// ii) ApplicationInsightsKey to AiKeyHere /// iii) CertsToConfigure to ClusterCert;MyLocalMachine;ClusterCertThumbprint,SSLCert;MyLocalMachine;SSLCertThumbprint /// Similarily other options can be set in environment variables to enable rest of the options like KeyVault. /// </remarks> private static void Main(string[] args) { CommandLineApplication commandLineApplication = new CommandLineApplication(false); CommandOption useEnvironmentVariablesOption = commandLineApplication.Option( "-UseEnv | --UseEnvironmentVariables", #pragma warning disable SA1118 // Parameter should not span multiple lines "Instead of using specified options, use Environment varibles with the same name (except the -- at start). This is to enable different integrations for different environments." + " If you use this, command line values are ignored.", #pragma warning restore SA1118 // Parameter should not span multiple lines CommandOptionType.NoValue); CommandOption configureCertsOption = commandLineApplication.Option( "--ConfigureCerts <DirectoryRelativePath>", "Configures certs for Traefik by dropping them into the specifiec directory (relative to executing assembly). Certs are dropped in .key and .crt format.", CommandOptionType.SingleValue); CommandOption certsToConfigureOption = commandLineApplication.Option( "--CertsToConfigure <FormattedCertsToConfigure>", #pragma warning disable SA1118 // Parameter should not span multiple lines "The value looks something like SSLCert;MyLocalMachine;7ce597cba5ae055fa37f222aaffc1007c3d61277,ClusterCert;KeyVault;ClusterCertSecretName. The format is" + "NameOfTheCert1;Source1;<Cert identifier1>,NameOfCert2;Source2;<Cert identifier2>." + "Possible Source values are 'MyLocalMachine' which fetches certs from Personal under LocalMachine Store and " + "'KeyVault' which fetches the cert from KeyVault. If KeyVault is specified, Specify ClientId and secret using --KeyVaultUri, --KeyVaultClientId, --KeyVaultClientSecret or --KeyVaultClientCert", #pragma warning restore SA1118 // Parameter should not span multiple lines CommandOptionType.SingleValue); CommandOption keyVaultUriOption = commandLineApplication.Option( "--KeyVaultUri <KeyVaultUri>", "Uri to use for KeyVault connection. Use --KeyVaultClientId to specify ClientId of the app to use to access Key Vault.", CommandOptionType.SingleValue); CommandOption keyVaultClientIdOption = commandLineApplication.Option( "--KeyVaultClientId <ClientId>", "Client Id to use for KeyVault connection. Specify the secret by using --KeyVaultClientSecret or --KeyVaultClientCert.", CommandOptionType.SingleValue); CommandOption keyVaultClientSecretOption = commandLineApplication.Option( "--KeyVaultClientSecret <ClientSecret>", "Client secret to use for KeyVault connection. Specify the ClientId using --KeyVaultClientId.", CommandOptionType.SingleValue); CommandOption keyVaultClientCertThumbprintOption = commandLineApplication.Option( "--KeyVaultClientCert <ClientCertThumbprint>", "Cert thumbprint to be used to contact key vault. The cert needs to be present on the machine. Specify the ClientId using --KeyVaultClientId.", CommandOptionType.SingleValue); CommandOption applicationInsightsInstrumentationKeyOption = commandLineApplication.Option( "--ApplicationInsightsKey <InstrumentationKey>", "Instrumentation key to push traces for PreConfiguration into Application Insights.", CommandOptionType.SingleValue); commandLineApplication.HelpOption("-h|--help|-?"); commandLineApplication.OnExecute(async() => { try { bool useEnvironmentVariables = useEnvironmentVariablesOption.HasValue(); if (applicationInsightsInstrumentationKeyOption.HasValueExtended(useEnvironmentVariables)) { Logger.ConfigureLogger(applicationInsightsInstrumentationKeyOption.GetValueExtended(useEnvironmentVariables)); } if (configureCertsOption.HasValueExtended(useEnvironmentVariables)) { ExitCode certHandlerExitCode = await CertificateHandler.ProcessAsync( configureCertsOption.GetValueExtended(useEnvironmentVariables), certsToConfigureOption.GetValueExtended(useEnvironmentVariables), keyVaultUriOption.GetValueExtended(useEnvironmentVariables), keyVaultClientIdOption.GetValueExtended(useEnvironmentVariables), keyVaultClientSecretOption.GetValueExtended(useEnvironmentVariables), keyVaultClientCertThumbprintOption.GetValueExtended(useEnvironmentVariables)).ConfigureAwait(false); if (certHandlerExitCode != ExitCode.Success) { return((int)certHandlerExitCode); } } return((int)ExitCode.Success); } catch (AggregateException aggrEx) { foreach (Exception innerException in aggrEx.InnerExceptions) { Logger.LogError(CallInfo.Site(), innerException); } return((int)ExitCode.UnknownFailure); } catch (Exception ex) { Logger.LogError(CallInfo.Site(), ex); return((int)ExitCode.UnknownFailure); } }); commandLineApplication.Execute(args); }
/// <summary> /// Logs the error. /// </summary> /// <param name="callInfo">The call information.</param> /// <param name="exp">The exception.</param> public static void LogError(CallInfo callInfo, Exception exp) { Log(callInfo, TraceLevel.Error, string.Format("Exception: {0}", exp)); }
/// <summary> /// Logs the error. /// </summary> /// <param name="callInfo">The call information.</param> /// <param name="messageFormat">The message format.</param> /// <param name="arguments">The arguments.</param> public static void LogError(CallInfo callInfo, string messageFormat, params object[] arguments) { Log(callInfo, TraceLevel.Error, GetMessage(messageFormat, arguments)); }
/// <summary> /// Logs the error. /// </summary> /// <param name="callInfo">The call information.</param> /// <param name="exp">The exception.</param> public static void LogError(CallInfo callInfo, Exception exp) { Log(callInfo, TraceLevel.Error, $"Exception: {exp}"); }