Skip to content
This repository was archived by the owner on Aug 3, 2024. It is now read-only.

Commit 6c8037e

Browse files
authored
Update KeyVault to use Managed Identities (#334)
1 parent 3ba073d commit 6c8037e

7 files changed

Lines changed: 202 additions & 21 deletions

File tree

src/NuGet.Services.Configuration/ConfigurationRootSecretReaderFactory.cs

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace NuGet.Services.Configuration
1111
public class ConfigurationRootSecretReaderFactory : ISecretReaderFactory
1212
{
1313
private string _vaultName;
14+
private bool _useManagedIdentity;
1415
private string _clientId;
1516
private string _certificateThumbprint;
1617
private string _storeName;
@@ -20,9 +21,26 @@ public class ConfigurationRootSecretReaderFactory : ISecretReaderFactory
2021

2122
public ConfigurationRootSecretReaderFactory(IConfigurationRoot config)
2223
{
24+
if (config == null)
25+
{
26+
throw new ArgumentNullException($"{nameof(config)}");
27+
}
28+
2329
_vaultName = config[Constants.KeyVaultVaultNameKey];
30+
31+
string useManagedIdentity = config[Constants.KeyVaultUseManagedIdentity];
32+
if (!string.IsNullOrEmpty(useManagedIdentity))
33+
{
34+
_useManagedIdentity = bool.Parse(useManagedIdentity);
35+
}
36+
2437
_clientId = config[Constants.KeyVaultClientIdKey];
2538
_certificateThumbprint = config[Constants.KeyVaultCertificateThumbprintKey];
39+
if (_useManagedIdentity && IsCertificateConfigurationProvided())
40+
{
41+
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.");
42+
}
43+
2644
_storeName = config[Constants.KeyVaultStoreNameKey];
2745
_storeLocation = config[Constants.KeyVaultStoreLocationKey];
2846

@@ -46,21 +64,30 @@ public ISecretReader CreateSecretReader()
4664
return new EmptySecretReader();
4765
}
4866

49-
var certificate = CertificateUtility.FindCertificateByThumbprint(
50-
!string.IsNullOrEmpty(_storeName)
51-
? (StoreName)Enum.Parse(typeof(StoreName), _storeName)
52-
: StoreName.My,
53-
!string.IsNullOrEmpty(_storeLocation)
54-
? (StoreLocation)Enum.Parse(typeof(StoreLocation), _storeLocation)
55-
: StoreLocation.LocalMachine,
56-
_certificateThumbprint,
57-
_validateCertificate);
67+
KeyVaultConfiguration keyVaultConfiguration;
5868

59-
var keyVaultConfiguration = new KeyVaultConfiguration(
60-
_vaultName,
61-
_clientId,
62-
certificate,
63-
_sendX5c);
69+
if (_useManagedIdentity)
70+
{
71+
keyVaultConfiguration = new KeyVaultConfiguration(_vaultName);
72+
}
73+
else
74+
{
75+
var certificate = CertificateUtility.FindCertificateByThumbprint(
76+
!string.IsNullOrEmpty(_storeName)
77+
? (StoreName)Enum.Parse(typeof(StoreName), _storeName)
78+
: StoreName.My,
79+
!string.IsNullOrEmpty(_storeLocation)
80+
? (StoreLocation)Enum.Parse(typeof(StoreLocation), _storeLocation)
81+
: StoreLocation.LocalMachine,
82+
_certificateThumbprint,
83+
_validateCertificate);
84+
85+
keyVaultConfiguration = new KeyVaultConfiguration(
86+
_vaultName,
87+
_clientId,
88+
certificate,
89+
_sendX5c);
90+
}
6491

6592
return new KeyVaultReader(keyVaultConfiguration);
6693
}
@@ -69,5 +96,11 @@ public ISecretInjector CreateSecretInjector(ISecretReader secretReader)
6996
{
7097
return new SecretInjector(secretReader);
7198
}
99+
100+
private bool IsCertificateConfigurationProvided()
101+
{
102+
return !string.IsNullOrEmpty(_clientId)
103+
|| !string.IsNullOrEmpty(_certificateThumbprint);
104+
}
72105
}
73106
}

src/NuGet.Services.Configuration/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace NuGet.Services.Configuration
66
public static class Constants
77
{
88
public static string KeyVaultVaultNameKey = "KeyVault_VaultName";
9+
public static string KeyVaultUseManagedIdentity = "KeyVault_UseManagedIdentity";
910
public static string KeyVaultClientIdKey = "KeyVault_ClientId";
1011
public static string KeyVaultCertificateThumbprintKey = "KeyVault_CertificateThumbprint";
1112
public static string KeyVaultValidateCertificateKey = "KeyVault_ValidateCertificate";

src/NuGet.Services.KeyVault/KeyVaultConfiguration.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,33 @@ namespace NuGet.Services.KeyVault
99
public class KeyVaultConfiguration
1010
{
1111
public string VaultName { get; }
12+
public bool UseManagedIdentity { get; }
1213
public string ClientId { get; }
1314
public X509Certificate2 Certificate { get; }
1415
public bool SendX5c { get; }
15-
public KeyVaultConfiguration(string vaultName, string clientId, X509Certificate2 certificate, bool sendX5c=false)
16+
17+
/// <summary>
18+
/// The constructor for keyvault configuration when using managed identities
19+
/// </summary>
20+
public KeyVaultConfiguration(string vaultName)
21+
{
22+
if (string.IsNullOrWhiteSpace(vaultName))
23+
{
24+
throw new ArgumentNullException(nameof(vaultName));
25+
}
26+
27+
VaultName = vaultName;
28+
UseManagedIdentity = true;
29+
}
30+
31+
/// <summary>
32+
/// The constructor for keyvault configuration when using the certificate
33+
/// </summary>
34+
/// <param name="vaultName">The name of the keyvault</param>
35+
/// <param name="clientId">Keyvault client id</param>
36+
/// <param name="certificate">Certificate required to access the keyvault</param>
37+
/// <param name="sendX5c">SendX5c property</param>
38+
public KeyVaultConfiguration(string vaultName, string clientId, X509Certificate2 certificate, bool sendX5c = false)
1639
{
1740
if (string.IsNullOrWhiteSpace(vaultName))
1841
{
@@ -23,7 +46,8 @@ public KeyVaultConfiguration(string vaultName, string clientId, X509Certificate2
2346
{
2447
throw new ArgumentNullException(nameof(clientId));
2548
}
26-
49+
50+
UseManagedIdentity = false;
2751
VaultName = vaultName;
2852
ClientId = clientId;
2953
Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));

src/NuGet.Services.KeyVault/KeyVaultReader.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
using System;
55
using System.Threading.Tasks;
66
using Microsoft.Azure.KeyVault;
7+
using Microsoft.Azure.Services.AppAuthentication;
78
using Microsoft.IdentityModel.Clients.ActiveDirectory;
89

910
namespace NuGet.Services.KeyVault
1011
{
1112
/// <summary>
1213
/// Reads secretes from KeyVault.
13-
/// Authentication with KeyVault is done using a certificate in location:LocalMachine store name:My
14+
/// Authentication with KeyVault is done using either a managed identity or a certificate in location:LocalMachine store name:My
1415
/// </summary>
1516
public class KeyVaultReader : ISecretReader
1617
{
@@ -40,14 +41,21 @@ public async Task<string> GetSecretAsync(string secretName)
4041
public async Task<ISecret> GetSecretObjectAsync(string secretName)
4142
{
4243
var secret = await _keyVaultClient.Value.GetSecretAsync(_vault, secretName);
43-
return new KeyVaultSecret(secretName, secret.Value, secret.Attributes.Expires);
44+
return new KeyVaultSecret(secretName, secret.Value, secret.Attributes.Expires);
4445
}
4546

4647
private KeyVaultClient InitializeClient()
4748
{
48-
_clientAssertionCertificate = new ClientAssertionCertificate(_configuration.ClientId, _configuration.Certificate);
49-
50-
return new KeyVaultClient(GetTokenAsync);
49+
if (_configuration.UseManagedIdentity)
50+
{
51+
var azureServiceTokenProvider = new AzureServiceTokenProvider();
52+
return new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
53+
}
54+
else
55+
{
56+
_clientAssertionCertificate = new ClientAssertionCertificate(_configuration.ClientId, _configuration.Certificate);
57+
return new KeyVaultClient(GetTokenAsync);
58+
}
5159
}
5260

5361
private async Task<string> GetTokenAsync(string authority, string resource, string scope)

src/NuGet.Services.KeyVault/NuGet.Services.KeyVault.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@
7878
<PackageReference Include="Microsoft.Azure.KeyVault">
7979
<Version>1.0.0</Version>
8080
</PackageReference>
81+
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication">
82+
<Version>1.3.1</Version>
83+
</PackageReference>
8184
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory">
8285
<Version>5.2.6</Version>
8386
</PackageReference>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.Extensions.Configuration;
7+
using Xunit;
8+
9+
namespace NuGet.Services.Configuration.Tests
10+
{
11+
public class ConfigurationRootSecretReaderFactoryFacts
12+
{
13+
[Fact]
14+
public void ConstructorThrowsWhenKeyConfigIsNull()
15+
{
16+
Assert.Throws<ArgumentNullException>(() => new ConfigurationRootSecretReaderFactory(null));
17+
}
18+
19+
public static IEnumerable<object[]> InvalidConfigs
20+
{
21+
get
22+
{
23+
yield return new object[] {
24+
new Dictionary<string, string> {
25+
{ Constants.KeyVaultVaultNameKey, "KeyVaultName" },
26+
{ Constants.KeyVaultUseManagedIdentity, "true" },
27+
{ Constants.KeyVaultClientIdKey, "KeyVaultClientId" },
28+
{ Constants.KeyVaultCertificateThumbprintKey, "KeyVaultThumbprint" },
29+
{ Constants.KeyVaultStoreNameKey, "StoreName"},
30+
{ Constants.KeyVaultStoreLocationKey, "StoreLocation" },
31+
{ Constants.KeyVaultValidateCertificateKey, "true" },
32+
{ Constants.KeyVaultSendX5c, "false" }
33+
}
34+
};
35+
}
36+
}
37+
38+
public static IEnumerable<object[]> ValidConfigs
39+
{
40+
get
41+
{
42+
yield return new object[] {
43+
new Dictionary<string, string> {
44+
{ Constants.KeyVaultVaultNameKey, "KeyVaultName" },
45+
{ Constants.KeyVaultUseManagedIdentity, "true" },
46+
}
47+
};
48+
49+
yield return new object[] {
50+
new Dictionary<string, string> {
51+
{ Constants.KeyVaultClientIdKey, "KeyVaultClientId" },
52+
{ Constants.KeyVaultCertificateThumbprintKey, "KeyVaultThumbprint" },
53+
{ Constants.KeyVaultStoreNameKey, "StoreName"},
54+
{ Constants.KeyVaultStoreLocationKey, "StoreLocation" },
55+
{ Constants.KeyVaultValidateCertificateKey, "true" },
56+
{ Constants.KeyVaultSendX5c, "false" }
57+
}
58+
};
59+
60+
yield return new object[] {
61+
new Dictionary<string, string> {
62+
{ Constants.KeyVaultVaultNameKey, "KeyVaultName" },
63+
{ Constants.KeyVaultUseManagedIdentity, "false" },
64+
{ Constants.KeyVaultClientIdKey, "KeyVaultClientId" },
65+
{ Constants.KeyVaultCertificateThumbprintKey, "KeyVaultThumbprint" },
66+
{ Constants.KeyVaultStoreNameKey, "StoreName"},
67+
{ Constants.KeyVaultStoreLocationKey, "StoreLocation" },
68+
{ Constants.KeyVaultValidateCertificateKey, "true" },
69+
{ Constants.KeyVaultSendX5c, "false" }
70+
}
71+
};
72+
73+
yield return new object[] {
74+
new Dictionary<string, string> {
75+
{ Constants.KeyVaultVaultNameKey, "KeyVaultName" },
76+
{ Constants.KeyVaultUseManagedIdentity, "false" },
77+
{ Constants.KeyVaultClientIdKey, "" },
78+
{ Constants.KeyVaultCertificateThumbprintKey, "" },
79+
{ Constants.KeyVaultStoreNameKey, ""},
80+
{ Constants.KeyVaultStoreLocationKey, "" },
81+
{ Constants.KeyVaultValidateCertificateKey, "" },
82+
{ Constants.KeyVaultSendX5c, "" }
83+
}
84+
};
85+
}
86+
}
87+
88+
[Theory]
89+
[MemberData(nameof(InvalidConfigs))]
90+
public void ConstructorThrowsWhenKeyVaultConfigSpecifiesManagedIdentityAndCertificate(IDictionary<string, string> config)
91+
{
92+
Assert.Throws<ArgumentException>(() => new ConfigurationRootSecretReaderFactory(CreateTestConfiguration(config)));
93+
}
94+
95+
[Theory]
96+
[MemberData(nameof(ValidConfigs))]
97+
public void CreatesSecretReaderFactoryForValidConfiguration(IDictionary<string, string> config)
98+
{
99+
var secretReaderFacotry = new ConfigurationRootSecretReaderFactory(CreateTestConfiguration(config));
100+
Assert.NotNull(secretReaderFacotry);
101+
}
102+
103+
104+
private IConfigurationRoot CreateTestConfiguration(IDictionary<string, string> config)
105+
{
106+
return new ConfigurationBuilder()
107+
.AddInMemoryCollection(config)
108+
.Build();
109+
}
110+
}
111+
}

tests/NuGet.Services.Configuration.Tests/NuGet.Services.Configuration.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<Reference Include="System.Xml" />
4040
</ItemGroup>
4141
<ItemGroup>
42+
<Compile Include="ConfigurationRootSecretReaderFactoryFacts.cs" />
4243
<Compile Include="ConfigurationAttributeFacts.cs" />
4344
<Compile Include="ConfigurationBuilderExtensionsFacts.cs" />
4445
<Compile Include="ConfigurationFactoryFacts.cs" />

0 commit comments

Comments
 (0)