diff --git a/README.md b/README.md index e35adc5..030ea4e 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@ Integration of pre-configurator involves 3 steps:- #### Copying binaries before Traefik packaging You can compile once and copy next to Traefik binary or you can do this using build process. Do perform copy on every build follow the sample project. In sample project, the Traefik.sfproj has a PreBuildEvent. This PreBuildEvent copies the required binaries. -``` +```xml xcopy /I /Y $(MSBuildThisFileDirectory)..\..\Src\TraefikPreConfiguratorWindows\bin\$(Configuration) $(MSBuildThisFileDirectory)ApplicationPackageRoot\TraefikPkg\Code ``` Adjust the path to copy to the correct directory. This requires the binaries to be compiled before they can be copied. To ensure that the binaries are always present before copy you can add a condition to the Validate MS Build target as shown below (just the last line is required, rest are just for completeness) -``` +```xml @@ -44,7 +44,7 @@ This requires the pre-configurator project to be present in same solution. #### Setup pre-configurator to run before the Traefik This can be setup in the Traefik service manifest. Refer to [Sample Service Manifest](/Samples/Traefik/ApplicationPackageRoot/TraefikPkg/ServiceManifest.xml) -``` +```xml TraefikPreConfiguratorWindows.exe @@ -59,8 +59,9 @@ This can be setup in the Traefik service manifest. Refer to [Sample Service Mani This will ensure that pre-configurator is run before the Traefik binary. To provide the required configuration to pre-configurator, also add the following environment variables to Service manifest -``` +```xml + @@ -73,7 +74,7 @@ To provide the required configuration to pre-configurator, also add the followin To provide values for each environment, these also need to be declared in the Application Manifest. Refer to [Sample Application Manifest](/Samples/Traefik/ApplicationPackageRoot/ApplicationManifest.xml) -``` +```xml @@ -81,16 +82,18 @@ To provide values for each environment, these also need to be declared in the Ap + ``` and override the parameters for the Traefik service -``` +```xml + @@ -106,15 +109,16 @@ and override the parameters for the Traefik service #### Configure per-environment parameters Once the Application Manifest is set to provide values to Traefik service based on the values provided to it, now we can use Application Parameters to override values for different environment. Refer to the [Sample Application Parameters](/Samples/Traefik/ApplicationParameters/Cloud.xml) to see how to configure values -``` +```xml - + + ``` @@ -129,11 +133,12 @@ The parameters are as follows Identifier is Certificate thumbprint for LocalMachine and KeyVault secret name for KeyVault. *Note the certificates MUST be uploaded to keyvault using the Certificates option and not Secrets* -- **TraefikKeyVaultUri** - Only required if you want to use KeyVault. This should be the KeyVault Uri. Start with https:// +- **TraefikKeyVaultUri** - Only required if you want to use KeyVault. This should be the comma separated KeyVault Uris. Start with https://. All KeyVaults must be allowed access from the type of authentication used (managed identity or service principal). - **TraefikKeyVaultClientId** - Only required if using KeyVault. An Application must be associated with KeyVault to access it. Refer [this](https://docs.microsoft.com/en-us/azure/key-vault/key-vault-use-from-web-application#authenticate-with-a-certificate-instead-of-a-client-secret) to setup - **TraefikKeyVaultClientSecret** or **TraefikKeyVaultClientCert** - Only required if using KeyVault. Depending on what option you used in the keyvault application setup, you need to specify the client secret or certificate thumbprint for the application certificate. If the certificate is used, it must be installed on the machine. TraefikKeyVaultClientCert is the preferred option as it ensures no secrets are present in the configuration files. Deploy the Traefik service fabric application and pre-configurator should configure the Traefik instance before running. +- **TraefikUseManagedIdentityAuth** - Allows using Managed Identity for authenticating with KeyVault(s). You can read more about Managed Identity [here](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview). To specify ClientId use the **TraefikKeyVaultClientId** parameter. ## Appendix 1 - Using HTTPS on Traefik The above process allows you to dump SSL certs onto the machine for Traefik to use. Refer to [sample toml file](/Samples/Traefik/ApplicationPackageRoot/TraefikPkg/Code/traefik.toml) on how to specify these. These allows Traefik to bind to 443 port. @@ -177,3 +182,6 @@ although it will have the same result as ```clustercert;MyLocalMachine;0efeb8fa621a4a0be2378f2b60eb2142ce846663```. This method is only supported for Local Machine certificates and not for KeyVault certificates. + +## Appendix 3 - Using Managed Identity instead of Service Principal +[Managed Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) allows developers to authenticate with resources in Azure without using any client secret or client certificate. To enable using Managed Identity set **TraefikUseManagedIdentityAuth** to `true`. If you are using [User Assigned Managed Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview#how-does-the-managed-identities-for-azure-resources-work) you can also specify the ClientId of the managed Identity using **TraefikKeyVaultClientId** option. You can get ClientId for the managed identity from the Azure Portal. diff --git a/Samples/Traefik/ApplicationPackageRoot/ApplicationManifest.xml b/Samples/Traefik/ApplicationPackageRoot/ApplicationManifest.xml index 927082a..89497e9 100644 --- a/Samples/Traefik/ApplicationPackageRoot/ApplicationManifest.xml +++ b/Samples/Traefik/ApplicationPackageRoot/ApplicationManifest.xml @@ -9,6 +9,7 @@ + + diff --git a/Samples/Traefik/ApplicationPackageRoot/TraefikPkg/ServiceManifest.xml b/Samples/Traefik/ApplicationPackageRoot/TraefikPkg/ServiceManifest.xml index 0dad7d1..d4474c4 100644 --- a/Samples/Traefik/ApplicationPackageRoot/TraefikPkg/ServiceManifest.xml +++ b/Samples/Traefik/ApplicationPackageRoot/TraefikPkg/ServiceManifest.xml @@ -32,6 +32,7 @@ + diff --git a/Samples/Traefik/ApplicationParameters/Cloud.xml b/Samples/Traefik/ApplicationParameters/Cloud.xml index 02c8525..23e7240 100644 --- a/Samples/Traefik/ApplicationParameters/Cloud.xml +++ b/Samples/Traefik/ApplicationParameters/Cloud.xml @@ -5,6 +5,6 @@ - + \ No newline at end of file diff --git a/Samples/Traefik/Traefik.sfproj b/Samples/Traefik/Traefik.sfproj index 0257467..e363131 100644 --- a/Samples/Traefik/Traefik.sfproj +++ b/Samples/Traefik/Traefik.sfproj @@ -42,13 +42,12 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Service Fabric Tools\Microsoft.VisualStudio.Azure.Fabric.ApplicationProject.targets + + xcopy /I /Y $(MSBuildThisFileDirectory)..\..\Src\TraefikPreConfiguratorWindows\bin\$(Configuration) $(MSBuildThisFileDirectory)ApplicationPackageRoot\TraefikPkg\Code + - - - xcopy /I /Y $(MSBuildThisFileDirectory)..\..\Src\TraefikPreConfiguratorWindows\bin\$(Configuration) $(MSBuildThisFileDirectory)ApplicationPackageRoot\TraefikPkg\Code - \ No newline at end of file diff --git a/Src/TraefikPreConfiguratorWindows/CertHelpers.cs b/Src/TraefikPreConfiguratorWindows/CertHelpers.cs index f8f2169..f0162a3 100644 --- a/Src/TraefikPreConfiguratorWindows/CertHelpers.cs +++ b/Src/TraefikPreConfiguratorWindows/CertHelpers.cs @@ -7,7 +7,7 @@ namespace TraefikPreConfiguratorWindows /// /// Certificate helpers. /// - public static class CertHelpers + internal static class CertHelpers { /// /// Finds the certificates with given parameters. diff --git a/Src/TraefikPreConfiguratorWindows/CertificateHandler.cs b/Src/TraefikPreConfiguratorWindows/CertificateHandler.cs index 76046df..6b03967 100644 --- a/Src/TraefikPreConfiguratorWindows/CertificateHandler.cs +++ b/Src/TraefikPreConfiguratorWindows/CertificateHandler.cs @@ -3,15 +3,19 @@ namespace TraefikPreConfiguratorWindows { using System; + using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; + using System.Linq; using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Microsoft.Azure.KeyVault; using Microsoft.Azure.KeyVault.Models; + using Microsoft.Azure.Services.AppAuthentication; using Microsoft.IdentityModel.Clients.ActiveDirectory; + using Microsoft.Rest.Azure; /// /// Performs Certificate related tasks. @@ -34,12 +38,20 @@ internal static class CertificateHandler /// Directory to put the certificatex in. /// Certificate configuration. This is a combination of comma separated values in following format /// *certFileName*;*SourceOfCert*;*CertIdentifierInSource*. - /// KeyVault uri if key vault is to be used. + /// KeyVault uris if key vault is to be used. /// Application client Id to access keyvault. /// Application client secret to access keyvault. /// Application client certificate thumbprint if the keyvault app has certificate credentials. + /// Use managed identity. /// Exit code for the operation. - internal static async Task ProcessAsync(string directoryPath, string certConfiguration, string keyVaultUri, string keyVaultClientId, string keyVaultClientSecret, string keyVaultClientCert) + internal static async Task ProcessAsync( + string directoryPath, + string certConfiguration, + List keyVaultUris, + string keyVaultClientId, + string keyVaultClientSecret, + string keyVaultClientCert, + bool useManagedIdentity) { if (string.IsNullOrEmpty(directoryPath)) { @@ -55,39 +67,77 @@ internal static async Task ProcessAsync(string directoryPath, string c // 1. Initialize KeyVault Client if params were passed. KeyVaultClient keyVaultClient = null; - if (!string.IsNullOrEmpty(keyVaultUri)) + Dictionary keyVaultSecretMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (keyVaultUris.Any()) { - if (string.IsNullOrEmpty(keyVaultClientId)) - { - Logger.LogError(CallInfo.Site(), "If KeyVaultUri is specified, KeyVault ClientId must be specified"); - return ExitCode.KeyVaultConfigurationIncomplete; - } + KeyVaultClient.AuthenticationCallback callback = null; - if (string.IsNullOrEmpty(keyVaultClientSecret) && string.IsNullOrEmpty(keyVaultClientCert)) + if (useManagedIdentity) { - Logger.LogError(CallInfo.Site(), "If KeyVaultUri is specified, KeyVault ClientSecret or KeyVault ClientCert must be specified"); - return ExitCode.KeyVaultConfigurationIncomplete; - } + string connectionString = "RunAs=App"; - if (!string.IsNullOrEmpty(keyVaultClientSecret)) - { - KeyVaultClient.AuthenticationCallback callback = - (authority, resource, scope) => GetTokenFromClientSecretAsync(authority, resource, keyVaultClientId, keyVaultClientSecret); - keyVaultClient = new KeyVaultClient(callback); + if (!string.IsNullOrEmpty(keyVaultClientId)) + { + connectionString = connectionString + ";AppId=" + keyVaultClientId; + } + + AzureServiceTokenProvider tokenProvider = new AzureServiceTokenProvider(connectionString); + callback = new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback); } else { - X509Certificate2Collection keyVaultCerts = CertHelpers.FindCertificates(keyVaultClientCert, X509FindType.FindByThumbprint); + 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 (keyVaultCerts.Count == 0) + if (string.IsNullOrEmpty(keyVaultClientSecret) && string.IsNullOrEmpty(keyVaultClientCert)) { - Logger.LogError(CallInfo.Site(), "Failed to find Client cert with thumbprint '{0}'", 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; } - KeyVaultClient.AuthenticationCallback callback = - (authority, resource, scope) => GetTokenFromClientCertificateAsync(authority, resource, keyVaultClientId, keyVaultCerts[0]); - keyVaultClient = new KeyVaultClient(callback); + 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 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; + } + } } } @@ -127,6 +177,12 @@ internal static async Task ProcessAsync(string directoryPath, string c } 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, diff --git a/Src/TraefikPreConfiguratorWindows/CommandOptionExtensions.cs b/Src/TraefikPreConfiguratorWindows/CommandOptionExtensions.cs index 8ad3526..5ff057f 100644 --- a/Src/TraefikPreConfiguratorWindows/CommandOptionExtensions.cs +++ b/Src/TraefikPreConfiguratorWindows/CommandOptionExtensions.cs @@ -3,6 +3,8 @@ namespace TraefikPreConfiguratorWindows { using System; + using System.Collections.Generic; + using System.Linq; using Microsoft.Extensions.CommandLineUtils; /// @@ -33,6 +35,29 @@ public static string GetValueExtended(this CommandOption commandOption, bool use } } + /// + /// Gets the values for the command option based if the value is to be fetched from commandline or Environment varibles. + /// + /// The command option. + /// True, if environment variable is to be used instead of command line. + /// Value for the command option. + public static List GetValuesExtended(this CommandOption commandOption, bool useEnvironmentVariable) + { + if (!commandOption.HasValueExtended(useEnvironmentVariable)) + { + return null; + } + + if (useEnvironmentVariable) + { + return Environment.GetEnvironmentVariable(commandOption.LongName).Split(',').ToList(); + } + else + { + return commandOption.Values; + } + } + /// /// Determines whether the command option has value or not based on if the value needs to be pulled from command line of environment variables. /// @@ -52,5 +77,32 @@ public static bool HasValueExtended(this CommandOption commandOption, bool useEn return commandOption.HasValue(); } } + + /// + /// Checks if a switch was specified by the user. + /// + /// Command option. + /// True if environment variable should be checked. + /// True if the switch was specified. + public static bool IsSwitchSpecified(this CommandOption commandOption, bool useEnvironmentVariable) + { + if (useEnvironmentVariable) + { + if (commandOption.HasValueExtended(useEnvironmentVariable: true)) + { + string value = commandOption.GetValueExtended(useEnvironmentVariable: true); + + return value.Equals("true", StringComparison.OrdinalIgnoreCase); + } + else + { + return false; + } + } + else + { + return commandOption.HasValue(); + } + } } } diff --git a/Src/TraefikPreConfiguratorWindows/Logger.cs b/Src/TraefikPreConfiguratorWindows/Logger.cs index 55cca35..1b75981 100644 --- a/Src/TraefikPreConfiguratorWindows/Logger.cs +++ b/Src/TraefikPreConfiguratorWindows/Logger.cs @@ -27,7 +27,7 @@ public static void ConfigureLogger(string instrumentationKey) { TelemetryConfiguration telemetryConfiguration = new TelemetryConfiguration(instrumentationKey); new DiagnosticsTelemetryModule().Initialize(telemetryConfiguration); - telemetryClient = new TelemetryClient(telemetryConfiguration); + Logger.telemetryClient = new TelemetryClient(telemetryConfiguration); } /// @@ -99,6 +99,14 @@ public static void LogError(CallInfo callInfo, Exception exp, string customMessa $"CustomMessage {GetMessage(customMessageFormat, arguments)} \n Exception {exp}"); } + /// + /// Flushes the logs to the backend diagnostics engine. + /// + public static void Flush() + { + Logger.telemetryClient.Flush(); + } + /// /// Logs message to underlying loggers. /// @@ -109,7 +117,7 @@ private static void Log(CallInfo callInfo, TraceLevel traceLevel, string message { string messageToLog = callInfo.ToString() + " -- " + traceLevel.ToString() + " -- " + message; Console.WriteLine(messageToLog); - telemetryClient.TrackTrace(messageToLog); + Logger.telemetryClient.TrackTrace(messageToLog); } /// diff --git a/Src/TraefikPreConfiguratorWindows/Program.cs b/Src/TraefikPreConfiguratorWindows/Program.cs index 1c3a9a6..f09c60c 100644 --- a/Src/TraefikPreConfiguratorWindows/Program.cs +++ b/Src/TraefikPreConfiguratorWindows/Program.cs @@ -54,7 +54,7 @@ private static void Main(string[] args) CommandOption keyVaultUriOption = commandLineApplication.Option( "--KeyVaultUri ", "Uri to use for KeyVault connection. Use --KeyVaultClientId to specify ClientId of the app to use to access Key Vault.", - CommandOptionType.SingleValue); + CommandOptionType.MultipleValue); CommandOption keyVaultClientIdOption = commandLineApplication.Option( "--KeyVaultClientId ", "Client Id to use for KeyVault connection. Specify the secret by using --KeyVaultClientSecret or --KeyVaultClientCert.", @@ -63,6 +63,10 @@ private static void Main(string[] args) "--KeyVaultClientSecret ", "Client secret to use for KeyVault connection. Specify the ClientId using --KeyVaultClientId.", CommandOptionType.SingleValue); + CommandOption useManagedIdentity = commandLineApplication.Option( + "--UseManagedIdentity", + "Uses Managed Identity for authenticating with KeyVault. You can specify the client Id if you want to use one in --KeyVaultClientId option.", + CommandOptionType.NoValue); CommandOption keyVaultClientCertThumbprintOption = commandLineApplication.Option( "--KeyVaultClientCert ", "Cert thumbprint to be used to contact key vault. The cert needs to be present on the machine. Specify the ClientId using --KeyVaultClientId.", @@ -87,10 +91,11 @@ private static void Main(string[] args) ExitCode certHandlerExitCode = await CertificateHandler.ProcessAsync( configureCertsOption.GetValueExtended(useEnvironmentVariables), certsToConfigureOption.GetValueExtended(useEnvironmentVariables), - keyVaultUriOption.GetValueExtended(useEnvironmentVariables), + keyVaultUriOption.GetValuesExtended(useEnvironmentVariables), keyVaultClientIdOption.GetValueExtended(useEnvironmentVariables), keyVaultClientSecretOption.GetValueExtended(useEnvironmentVariables), - keyVaultClientCertThumbprintOption.GetValueExtended(useEnvironmentVariables)).ConfigureAwait(false); + keyVaultClientCertThumbprintOption.GetValueExtended(useEnvironmentVariables), + useManagedIdentity.IsSwitchSpecified(useEnvironmentVariables)).ConfigureAwait(false); if (certHandlerExitCode != ExitCode.Success) { @@ -118,6 +123,7 @@ private static void Main(string[] args) }); commandLineApplication.Execute(args); + Logger.Flush(); } } } diff --git a/Src/TraefikPreConfiguratorWindows/TraefikPreConfiguratorWindows.csproj b/Src/TraefikPreConfiguratorWindows/TraefikPreConfiguratorWindows.csproj index d4c9cc7..46d2338 100644 --- a/Src/TraefikPreConfiguratorWindows/TraefikPreConfiguratorWindows.csproj +++ b/Src/TraefikPreConfiguratorWindows/TraefikPreConfiguratorWindows.csproj @@ -71,6 +71,9 @@ 3.0.0 + + 1.2.0-preview2 + 2.6.2 runtime; build; native; contentfiles; analyzers