public ICertificateStore ParseCertificateStore(CertificateRenewalOptions cfg) { var target = ParseTargetResource(cfg); var store = cfg.CertificateStore ?? new GenericEntry { Type = "keyVault", Name = target.Name }; switch (store.Type.ToLowerInvariant()) { case "keyvault": // all optional var props = store.Properties?.ToObject <KeyVaultProperties>() ?? new KeyVaultProperties { Name = store.Name }; var certificateName = props.CertificateName; if (string.IsNullOrEmpty(certificateName)) { certificateName = cfg.HostNames.First().Replace(".", "-"); } var keyVaultName = props.Name; if (string.IsNullOrEmpty(keyVaultName)) { keyVaultName = target.Name; } var resourceGroupName = props.ResourceGroupName; if (string.IsNullOrEmpty(resourceGroupName)) { resourceGroupName = keyVaultName; } return(new KeyVaultCertificateStore(_azureHelper, _keyVaultFactory, keyVaultName, resourceGroupName, certificateName)); default: throw new NotImplementedException(store.Type); } }
public async Task <IChallengeResponder> ParseChallengeResponderAsync(CertificateRenewalOptions cfg, CancellationToken cancellationToken) { var certStore = ParseCertificateStore(cfg); var target = ParseTargetResource(cfg); var cr = cfg.ChallengeResponder ?? new GenericEntry { Type = "storageAccount", Properties = JObject.FromObject(new StorageProperties { AccountName = ConvertToValidStorageAccountName(target.Name), KeyVaultName = certStore.Name }) }; switch (cr.Type.ToLowerInvariant()) { case "storageaccount": var props = cr.Properties?.ToObject <StorageProperties>() ?? new StorageProperties { KeyVaultName = cr.Name, AccountName = ConvertToValidStorageAccountName(cr.Name) }; // try MSI first, must do check if we can read to know if we have access var accountName = props.AccountName; if (string.IsNullOrEmpty(accountName)) { accountName = ConvertToValidStorageAccountName(target.Name); } var storage = await _storageFactory.FromMsiAsync(accountName, props.ContainerName, cancellationToken); // verify that MSI access works, fallback otherwise // not ideal since it's a readonly check // -> we need Blob Contributor for challenge persist but user could set Blob Reader and this check would pass // alternative: write + delete a file from container as a check try { await storage.ExistsAsync(FileNameForPermissionCheck, cancellationToken); } catch (RequestFailedException e) when(e.Status == (int)HttpStatusCode.Forbidden) { _logger.LogWarning($"MSI access to storage {accountName} failed. Attempting fallbacks via connection string. (You can ignore this warning if you don't use MSI authentication)."); var connectionString = props.ConnectionString; if (string.IsNullOrEmpty(connectionString)) { // falback to secret in keyvault var keyVaultName = props.KeyVaultName; if (string.IsNullOrEmpty(keyVaultName)) { keyVaultName = certStore.Name; } _logger.LogInformation($"No connection string in config, checking keyvault {keyVaultName} for secret {props.SecretName}"); try { connectionString = await GetSecretAsync(keyVaultName, props.SecretName, cancellationToken); } catch (Exception ex) { throw new AggregateException($"Failed to get connectionstring in secret {props.SecretName} from keyvault {keyVaultName}. If you intended to use storage MSI access, set \"Storage Blob Data Contributor\" on the respective storage container (permissions might take more than 10 minutes to take effect)", new[] { ex }); } } if (string.IsNullOrEmpty(connectionString)) { throw new InvalidOperationException($"MSI access failed for {accountName} and could not find fallback connection string for storage access. Unable to proceed with Let's encrypt challenge"); } storage = _storageFactory.FromConnectionString(connectionString, props.ContainerName); } return(new AzureStorageHttpChallengeResponder(storage, props.Path)); default: throw new NotImplementedException(cr.Type); } }
public ITargetResource ParseTargetResource(CertificateRenewalOptions cfg) { switch (cfg.TargetResource.Type.ToLowerInvariant()) { case "cdn": { var props = cfg.TargetResource.Properties == null ? new CdnProperties { Endpoints = new[] { cfg.TargetResource.Name }, Name = cfg.TargetResource.Name, ResourceGroupName = cfg.TargetResource.Name } : cfg.TargetResource.Properties.ToObject <CdnProperties>(); if (string.IsNullOrEmpty(props.Name)) { throw new ArgumentException($"CDN section is missing required property {nameof(props.Name)}"); } var rg = props.ResourceGroupName; if (string.IsNullOrEmpty(rg)) { rg = props.Name; } var endpoints = props.Endpoints; if (endpoints.IsNullOrEmpty()) { endpoints = new[] { props.Name } } ; return(new CdnTargetResource(_azureCdnClient, rg, props.Name, endpoints, _loggerFactory.CreateLogger <CdnTargetResource>())); } case "appservice": { var props = cfg.TargetResource.Properties == null ? new AppServiceProperties { Name = cfg.TargetResource.Name, ResourceGroupName = cfg.TargetResource.Name } : cfg.TargetResource.Properties.ToObject <AppServiceProperties>(); if (string.IsNullOrEmpty(props.Name)) { throw new ArgumentException($"AppService section is missing required property {nameof(props.Name)}"); } var rg = props.ResourceGroupName; if (string.IsNullOrEmpty(rg)) { rg = props.Name; } return(new AppServiceTargetResoure(_azureAppServiceClient, rg, props.Name, _loggerFactory.CreateLogger <AppServiceTargetResoure>())); } default: throw new NotImplementedException(cfg.TargetResource.Type); } }