Skip to content
This repository has been archived by the owner on Aug 3, 2024. It is now read-only.
/ ServerCommon Public archive

Commit

Permalink
Update KeyVault to use Managed Identities (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
shishirx34 authored Feb 6, 2020
1 parent 3ba073d commit 6c8037e
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace NuGet.Services.Configuration
public class ConfigurationRootSecretReaderFactory : ISecretReaderFactory
{
private string _vaultName;
private bool _useManagedIdentity;
private string _clientId;
private string _certificateThumbprint;
private string _storeName;
Expand All @@ -20,9 +21,26 @@ public class ConfigurationRootSecretReaderFactory : ISecretReaderFactory

public ConfigurationRootSecretReaderFactory(IConfigurationRoot config)
{
if (config == null)
{
throw new ArgumentNullException($"{nameof(config)}");
}

_vaultName = config[Constants.KeyVaultVaultNameKey];

string useManagedIdentity = config[Constants.KeyVaultUseManagedIdentity];
if (!string.IsNullOrEmpty(useManagedIdentity))
{
_useManagedIdentity = bool.Parse(useManagedIdentity);
}

_clientId = config[Constants.KeyVaultClientIdKey];
_certificateThumbprint = config[Constants.KeyVaultCertificateThumbprintKey];
if (_useManagedIdentity && IsCertificateConfigurationProvided())
{
throw new ArgumentException($"The KeyVault configuration specifies usage of both, the managed identity and certificate for accessing KeyVault resource. Please specify only one configuration to be used.");
}

_storeName = config[Constants.KeyVaultStoreNameKey];
_storeLocation = config[Constants.KeyVaultStoreLocationKey];

Expand All @@ -46,21 +64,30 @@ public ISecretReader CreateSecretReader()
return new EmptySecretReader();
}

var certificate = CertificateUtility.FindCertificateByThumbprint(
!string.IsNullOrEmpty(_storeName)
? (StoreName)Enum.Parse(typeof(StoreName), _storeName)
: StoreName.My,
!string.IsNullOrEmpty(_storeLocation)
? (StoreLocation)Enum.Parse(typeof(StoreLocation), _storeLocation)
: StoreLocation.LocalMachine,
_certificateThumbprint,
_validateCertificate);
KeyVaultConfiguration keyVaultConfiguration;

var keyVaultConfiguration = new KeyVaultConfiguration(
_vaultName,
_clientId,
certificate,
_sendX5c);
if (_useManagedIdentity)
{
keyVaultConfiguration = new KeyVaultConfiguration(_vaultName);
}
else
{
var certificate = CertificateUtility.FindCertificateByThumbprint(
!string.IsNullOrEmpty(_storeName)
? (StoreName)Enum.Parse(typeof(StoreName), _storeName)
: StoreName.My,
!string.IsNullOrEmpty(_storeLocation)
? (StoreLocation)Enum.Parse(typeof(StoreLocation), _storeLocation)
: StoreLocation.LocalMachine,
_certificateThumbprint,
_validateCertificate);

keyVaultConfiguration = new KeyVaultConfiguration(
_vaultName,
_clientId,
certificate,
_sendX5c);
}

return new KeyVaultReader(keyVaultConfiguration);
}
Expand All @@ -69,5 +96,11 @@ public ISecretInjector CreateSecretInjector(ISecretReader secretReader)
{
return new SecretInjector(secretReader);
}

private bool IsCertificateConfigurationProvided()
{
return !string.IsNullOrEmpty(_clientId)
|| !string.IsNullOrEmpty(_certificateThumbprint);
}
}
}
1 change: 1 addition & 0 deletions src/NuGet.Services.Configuration/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace NuGet.Services.Configuration
public static class Constants
{
public static string KeyVaultVaultNameKey = "KeyVault_VaultName";
public static string KeyVaultUseManagedIdentity = "KeyVault_UseManagedIdentity";
public static string KeyVaultClientIdKey = "KeyVault_ClientId";
public static string KeyVaultCertificateThumbprintKey = "KeyVault_CertificateThumbprint";
public static string KeyVaultValidateCertificateKey = "KeyVault_ValidateCertificate";
Expand Down
28 changes: 26 additions & 2 deletions src/NuGet.Services.KeyVault/KeyVaultConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,33 @@ namespace NuGet.Services.KeyVault
public class KeyVaultConfiguration
{
public string VaultName { get; }
public bool UseManagedIdentity { get; }
public string ClientId { get; }
public X509Certificate2 Certificate { get; }
public bool SendX5c { get; }
public KeyVaultConfiguration(string vaultName, string clientId, X509Certificate2 certificate, bool sendX5c=false)

/// <summary>
/// The constructor for keyvault configuration when using managed identities
/// </summary>
public KeyVaultConfiguration(string vaultName)
{
if (string.IsNullOrWhiteSpace(vaultName))
{
throw new ArgumentNullException(nameof(vaultName));
}

VaultName = vaultName;
UseManagedIdentity = true;
}

/// <summary>
/// The constructor for keyvault configuration when using the certificate
/// </summary>
/// <param name="vaultName">The name of the keyvault</param>
/// <param name="clientId">Keyvault client id</param>
/// <param name="certificate">Certificate required to access the keyvault</param>
/// <param name="sendX5c">SendX5c property</param>
public KeyVaultConfiguration(string vaultName, string clientId, X509Certificate2 certificate, bool sendX5c = false)
{
if (string.IsNullOrWhiteSpace(vaultName))
{
Expand All @@ -23,7 +46,8 @@ public KeyVaultConfiguration(string vaultName, string clientId, X509Certificate2
{
throw new ArgumentNullException(nameof(clientId));
}


UseManagedIdentity = false;
VaultName = vaultName;
ClientId = clientId;
Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
Expand Down
18 changes: 13 additions & 5 deletions src/NuGet.Services.KeyVault/KeyVaultReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
using System;
using System.Threading.Tasks;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace NuGet.Services.KeyVault
{
/// <summary>
/// Reads secretes from KeyVault.
/// Authentication with KeyVault is done using a certificate in location:LocalMachine store name:My
/// Authentication with KeyVault is done using either a managed identity or a certificate in location:LocalMachine store name:My
/// </summary>
public class KeyVaultReader : ISecretReader
{
Expand Down Expand Up @@ -40,14 +41,21 @@ public async Task<string> GetSecretAsync(string secretName)
public async Task<ISecret> GetSecretObjectAsync(string secretName)
{
var secret = await _keyVaultClient.Value.GetSecretAsync(_vault, secretName);
return new KeyVaultSecret(secretName, secret.Value, secret.Attributes.Expires);
return new KeyVaultSecret(secretName, secret.Value, secret.Attributes.Expires);
}

private KeyVaultClient InitializeClient()
{
_clientAssertionCertificate = new ClientAssertionCertificate(_configuration.ClientId, _configuration.Certificate);

return new KeyVaultClient(GetTokenAsync);
if (_configuration.UseManagedIdentity)
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
return new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
}
else
{
_clientAssertionCertificate = new ClientAssertionCertificate(_configuration.ClientId, _configuration.Certificate);
return new KeyVaultClient(GetTokenAsync);
}
}

private async Task<string> GetTokenAsync(string authority, string resource, string scope)
Expand Down
3 changes: 3 additions & 0 deletions src/NuGet.Services.KeyVault/NuGet.Services.KeyVault.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@
<PackageReference Include="Microsoft.Azure.KeyVault">
<Version>1.0.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication">
<Version>1.3.1</Version>
</PackageReference>
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory">
<Version>5.2.6</Version>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Xunit;

namespace NuGet.Services.Configuration.Tests
{
public class ConfigurationRootSecretReaderFactoryFacts
{
[Fact]
public void ConstructorThrowsWhenKeyConfigIsNull()
{
Assert.Throws<ArgumentNullException>(() => new ConfigurationRootSecretReaderFactory(null));
}

public static IEnumerable<object[]> InvalidConfigs
{
get
{
yield return new object[] {
new Dictionary<string, string> {
{ Constants.KeyVaultVaultNameKey, "KeyVaultName" },
{ Constants.KeyVaultUseManagedIdentity, "true" },
{ Constants.KeyVaultClientIdKey, "KeyVaultClientId" },
{ Constants.KeyVaultCertificateThumbprintKey, "KeyVaultThumbprint" },
{ Constants.KeyVaultStoreNameKey, "StoreName"},
{ Constants.KeyVaultStoreLocationKey, "StoreLocation" },
{ Constants.KeyVaultValidateCertificateKey, "true" },
{ Constants.KeyVaultSendX5c, "false" }
}
};
}
}

public static IEnumerable<object[]> ValidConfigs
{
get
{
yield return new object[] {
new Dictionary<string, string> {
{ Constants.KeyVaultVaultNameKey, "KeyVaultName" },
{ Constants.KeyVaultUseManagedIdentity, "true" },
}
};

yield return new object[] {
new Dictionary<string, string> {
{ Constants.KeyVaultClientIdKey, "KeyVaultClientId" },
{ Constants.KeyVaultCertificateThumbprintKey, "KeyVaultThumbprint" },
{ Constants.KeyVaultStoreNameKey, "StoreName"},
{ Constants.KeyVaultStoreLocationKey, "StoreLocation" },
{ Constants.KeyVaultValidateCertificateKey, "true" },
{ Constants.KeyVaultSendX5c, "false" }
}
};

yield return new object[] {
new Dictionary<string, string> {
{ Constants.KeyVaultVaultNameKey, "KeyVaultName" },
{ Constants.KeyVaultUseManagedIdentity, "false" },
{ Constants.KeyVaultClientIdKey, "KeyVaultClientId" },
{ Constants.KeyVaultCertificateThumbprintKey, "KeyVaultThumbprint" },
{ Constants.KeyVaultStoreNameKey, "StoreName"},
{ Constants.KeyVaultStoreLocationKey, "StoreLocation" },
{ Constants.KeyVaultValidateCertificateKey, "true" },
{ Constants.KeyVaultSendX5c, "false" }
}
};

yield return new object[] {
new Dictionary<string, string> {
{ Constants.KeyVaultVaultNameKey, "KeyVaultName" },
{ Constants.KeyVaultUseManagedIdentity, "false" },
{ Constants.KeyVaultClientIdKey, "" },
{ Constants.KeyVaultCertificateThumbprintKey, "" },
{ Constants.KeyVaultStoreNameKey, ""},
{ Constants.KeyVaultStoreLocationKey, "" },
{ Constants.KeyVaultValidateCertificateKey, "" },
{ Constants.KeyVaultSendX5c, "" }
}
};
}
}

[Theory]
[MemberData(nameof(InvalidConfigs))]
public void ConstructorThrowsWhenKeyVaultConfigSpecifiesManagedIdentityAndCertificate(IDictionary<string, string> config)
{
Assert.Throws<ArgumentException>(() => new ConfigurationRootSecretReaderFactory(CreateTestConfiguration(config)));
}

[Theory]
[MemberData(nameof(ValidConfigs))]
public void CreatesSecretReaderFactoryForValidConfiguration(IDictionary<string, string> config)
{
var secretReaderFacotry = new ConfigurationRootSecretReaderFactory(CreateTestConfiguration(config));
Assert.NotNull(secretReaderFacotry);
}


private IConfigurationRoot CreateTestConfiguration(IDictionary<string, string> config)
{
return new ConfigurationBuilder()
.AddInMemoryCollection(config)
.Build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ConfigurationRootSecretReaderFactoryFacts.cs" />
<Compile Include="ConfigurationAttributeFacts.cs" />
<Compile Include="ConfigurationBuilderExtensionsFacts.cs" />
<Compile Include="ConfigurationFactoryFacts.cs" />
Expand Down

0 comments on commit 6c8037e

Please sign in to comment.