src/WebJobs.Script.WebHost/Security/KeyManagement/DefaultSecretManagerProvider.cs (158 lines of code) (raw):
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading;
using Microsoft.Azure.WebJobs.Host.Executors;
using Microsoft.Azure.WebJobs.Host.Storage;
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.Azure.WebJobs.Script.WebHost
{
public sealed class DefaultSecretManagerProvider : ISecretManagerProvider
{
private const string FileStorage = "Files";
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<DefaultSecretManagerProvider> _logger;
private readonly IMetricsLogger _metricsLogger;
private readonly IOptionsMonitor<ScriptApplicationHostOptions> _options;
private readonly IHostIdProvider _hostIdProvider;
private readonly IEnvironment _environment;
private readonly HostNameProvider _hostNameProvider;
private readonly StartupContextProvider _startupContextProvider;
private readonly IAzureBlobStorageProvider _azureBlobStorageProvider;
private Lazy<ISecretManager> _secretManagerLazy;
private Lazy<bool> _secretsEnabledLazy;
public DefaultSecretManagerProvider(IOptionsMonitor<ScriptApplicationHostOptions> options, IHostIdProvider hostIdProvider, IEnvironment environment,
ILoggerFactory loggerFactory, IMetricsLogger metricsLogger, HostNameProvider hostNameProvider, StartupContextProvider startupContextProvider,
IAzureBlobStorageProvider azureBlobStorageProvider)
{
ArgumentNullException.ThrowIfNull(loggerFactory);
_options = options ?? throw new ArgumentNullException(nameof(options));
_hostIdProvider = hostIdProvider ?? throw new ArgumentNullException(nameof(hostIdProvider));
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
_hostNameProvider = hostNameProvider ?? throw new ArgumentNullException(nameof(hostNameProvider));
_startupContextProvider = startupContextProvider ?? throw new ArgumentNullException(nameof(startupContextProvider));
_loggerFactory = loggerFactory;
_logger = _loggerFactory.CreateLogger<DefaultSecretManagerProvider>();
_metricsLogger = metricsLogger ?? throw new ArgumentNullException(nameof(metricsLogger));
_secretManagerLazy = new Lazy<ISecretManager>(Create);
_secretsEnabledLazy = new Lazy<bool>(GetSecretsEnabled);
// When these options change (due to specialization), we need to reset the secret manager.
options.OnChange(_ => ResetSecretManager());
_azureBlobStorageProvider = azureBlobStorageProvider ?? throw new ArgumentNullException(nameof(azureBlobStorageProvider));
}
public bool SecretsEnabled
{
get
{
if (_secretManagerLazy.IsValueCreated)
{
return true;
}
return _secretsEnabledLazy.Value;
}
}
public ISecretManager Current => _secretManagerLazy.Value;
private void ResetSecretManager()
{
Interlocked.Exchange(ref _secretsEnabledLazy, new Lazy<bool>(GetSecretsEnabled));
Interlocked.Exchange(ref _secretManagerLazy, new Lazy<ISecretManager>(Create));
_logger.LogDebug(new EventId(1, "ResetSecretManager"), "Reset SecretManager.");
}
private ISecretManager Create() => new SecretManager(CreateSecretsRepository(), _loggerFactory.CreateLogger<SecretManager>(), _metricsLogger, _hostNameProvider, _startupContextProvider);
internal ISecretsRepository CreateSecretsRepository()
{
ISecretsRepository repository = null;
if (TryGetSecretsRepositoryType(out Type repositoryType))
{
if (repositoryType == typeof(FileSystemSecretsRepository))
{
repository = new FileSystemSecretsRepository(_options.CurrentValue.SecretsPath, _loggerFactory.CreateLogger<FileSystemSecretsRepository>(), _environment);
}
else if (repositoryType == typeof(KeyVaultSecretsRepository))
{
string azureWebJobsSecretStorageKeyVaultUri = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultUri);
string azureWebJobsSecretStorageKeyVaultClientId = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultClientId);
string azureWebJobsSecretStorageKeyVaultClientSecret = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultClientSecret);
string azureWebJobsSecretStorageKeyVaultTenantId = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultTenantId);
var keyVaultLogger = _loggerFactory.CreateLogger<KeyVaultSecretsRepository>();
return new KeyVaultSecretsRepository(Path.Combine(_options.CurrentValue.SecretsPath, "Sentinels"), azureWebJobsSecretStorageKeyVaultUri, azureWebJobsSecretStorageKeyVaultClientId,
azureWebJobsSecretStorageKeyVaultClientSecret, azureWebJobsSecretStorageKeyVaultTenantId, keyVaultLogger, _environment);
}
else if (repositoryType == typeof(KubernetesSecretsRepository))
{
repository = new KubernetesSecretsRepository(_environment, new SimpleKubernetesClient(_environment, _loggerFactory.CreateLogger<SimpleKubernetesClient>()));
}
else if (repositoryType == typeof(BlobStorageSasSecretsRepository))
{
string secretStorageSas = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageSas);
string siteSlotName = _environment.GetAzureWebsiteUniqueSlotName() ?? _hostIdProvider.GetHostIdAsync(CancellationToken.None).GetAwaiter().GetResult();
repository = new BlobStorageSasSecretsRepository(Path.Combine(_options.CurrentValue.SecretsPath, "Sentinels"),
secretStorageSas,
siteSlotName,
_loggerFactory.CreateLogger<BlobStorageSasSecretsRepository>(),
_environment,
_azureBlobStorageProvider);
}
else if (repositoryType == typeof(BlobStorageSecretsRepository))
{
string siteSlotName = _environment.GetAzureWebsiteUniqueSlotName() ?? _hostIdProvider.GetHostIdAsync(CancellationToken.None).GetAwaiter().GetResult();
repository = new BlobStorageSecretsRepository(Path.Combine(_options.CurrentValue.SecretsPath, "Sentinels"),
ConnectionStringNames.Storage,
siteSlotName,
_loggerFactory.CreateLogger<BlobStorageSecretsRepository>(),
_environment,
_azureBlobStorageProvider);
}
}
if (repository == null)
{
throw new InvalidOperationException("Secret initialization from Blob storage failed due to missing both an Azure Storage connection string and a SAS connection uri. " +
$"For Blob Storage, please provide at least one of these. If you intend to use files for secrets, add an App Setting key '{EnvironmentSettingNames.AzureWebJobsSecretStorageType}' with value '{FileStorage}'.");
}
_logger.LogInformation(new EventId(3, "CreatedSecretRespository"), "Resolved secret storage provider {provider}", repository.Name);
return repository;
}
/// <summary>
/// Determines the repository Type to use based on configured settings.
/// </summary>
/// <remarks>
/// For scenarios where the app isn't configured for key storage (e.g. no AzureWebJobsSecretStorageType explicitly configured,
/// no storage connection string for default blob storage, etc.). Note that it's still possible for the creation of the repository
/// to fail due to invalid values. This method just does preliminary config checks to determine the Type.
/// </remarks>
/// <param name="repositoryType">The repository Type or null.</param>
/// <returns>True if a Type was determined, false otherwise.</returns>
internal bool TryGetSecretsRepositoryType(out Type repositoryType)
{
string secretStorageType = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageType);
string secretStorageSas = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageSas);
if (secretStorageType != null && secretStorageType.Equals(FileStorage, StringComparison.OrdinalIgnoreCase))
{
repositoryType = typeof(FileSystemSecretsRepository);
return true;
}
else if (secretStorageType != null && secretStorageType.Equals("keyvault", StringComparison.OrdinalIgnoreCase))
{
repositoryType = typeof(KeyVaultSecretsRepository);
return true;
}
else if (secretStorageType != null && secretStorageType.Equals("kubernetes", StringComparison.OrdinalIgnoreCase))
{
repositoryType = typeof(KubernetesSecretsRepository);
return true;
}
else if (secretStorageSas != null)
{
repositoryType = typeof(BlobStorageSasSecretsRepository);
return true;
}
else if (_azureBlobStorageProvider.TryCreateHostingBlobContainerClient(out _))
{
repositoryType = typeof(BlobStorageSecretsRepository);
return true;
}
else
{
repositoryType = null;
return false;
}
}
internal bool GetSecretsEnabled()
{
bool secretsEnabled = TryGetSecretsRepositoryType(out Type repositoryType);
_logger.LogDebug(new EventId(2, "GetSecretsEnabled"), "SecretsEnabled evaluated to {secretsEnabled} with type {provider}.", secretsEnabled, repositoryType?.Name);
return secretsEnabled;
}
}
}