Skip to content

Commit 5895f2d

Browse files
authored
KeyVaultSecretsRepository comparisons are now case insensitive (#9644)
* Ignore case on KeyVaultSecrets repository lookups and comparisons * Adding test to check Secret Provider reads Host Secrets properly from KeyVault * Testing WriteAsync to ensure we're not deleting secrets that should be kept * Adding tests for Reading and Writing Function Secrets * Adding log messages for set and delete keys. * Added release notes
1 parent d248676 commit 5895f2d

File tree

4 files changed

+274
-10
lines changed

4 files changed

+274
-10
lines changed

release_notes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@
2525
- Update Node.js Worker Version to [3.9.0](https://github.com/Azure/azure-functions-nodejs-worker/releases/tag/v3.9.0)
2626
- Upgrading dependent packages to latest versions. #9646
2727
- Azure.Identity (1.10.0 to 1.10.3)
28-
- Azure.Core (1.34.0 to 1.35.0)
28+
- Azure.Core (1.34.0 to 1.35.0)
29+
- Bug fix: Comparisons in the Azure Key Vault Secrets Repository are now case insensitive (#9644)
30+
- This fixes a bug where keys could be automatically recreated if they had been manually added to Key Vault with all lowercase secret names

src/WebJobs.Script.WebHost/Diagnostics/Extensions/ScriptHostServiceSecurityExtension.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,31 @@ public static class ScriptHostServiceSecurityExtension
1515
new EventId(600, nameof(BlobStorageSecretRepoError)),
1616
"There was an error performing a {operation} operation on the Blob Storage Secret Repository.");
1717

18+
private static readonly Action<ILogger, string, Exception> _keyVaultSecretRepoSetKey =
19+
LoggerMessage.Define<string>(
20+
LogLevel.Debug,
21+
new EventId(601, nameof(KeyVaultSecretRepoSetKey)),
22+
"Setting key '{key}' on the KeyVault Secret Repository.");
23+
24+
private static readonly Action<ILogger, string, Exception> _keyVaultSecretRepoDeleteKey =
25+
LoggerMessage.Define<string>(
26+
LogLevel.Debug,
27+
new EventId(602, nameof(KeyVaultSecretRepoDeleteKey)),
28+
"Deleting key '{key}' on the KeyVault Secret Repository.");
29+
1830
public static void BlobStorageSecretRepoError(this ILogger logger, string operation, Exception exception)
1931
{
2032
_blobStorageSecretRepoError(logger, operation, exception);
2133
}
34+
35+
public static void KeyVaultSecretRepoSetKey(this ILogger logger, string key)
36+
{
37+
_keyVaultSecretRepoSetKey(logger, key, null);
38+
}
39+
40+
public static void KeyVaultSecretRepoDeleteKey(this ILogger logger, string key)
41+
{
42+
_keyVaultSecretRepoDeleteKey(logger, key, null);
43+
}
2244
}
2345
}

src/WebJobs.Script.WebHost/Security/KeyManagement/KeyVaultSecretsRepository.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Azure.Core;
1010
using Azure.Identity;
1111
using Azure.Security.KeyVault.Secrets;
12+
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.Extensions;
1213
using Microsoft.Extensions.Logging;
1314

1415
namespace Microsoft.Azure.WebJobs.Script.WebHost
@@ -50,6 +51,12 @@ public KeyVaultSecretsRepository(string secretsSentinelFilePath, string vaultUri
5051
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
5152
}
5253

54+
// For testing
55+
internal KeyVaultSecretsRepository(SecretClient secretClient, string secretsSentinelFilePath, ILogger logger, IEnvironment environment) : base(secretsSentinelFilePath, logger, environment)
56+
{
57+
_secretClient = new Lazy<SecretClient>(() => secretClient);
58+
}
59+
5360
public override bool IsEncryptionSupported
5461
{
5562
get
@@ -75,11 +82,12 @@ public override async Task WriteAsync(ScriptSecretsType type, string functionNam
7582
List<Task> deleteTasks = new List<Task>();
7683
string prefix = (type == ScriptSecretsType.Host) ? HostPrefix : FunctionPrefix + Normalize(functionName);
7784

78-
foreach (SecretProperties item in await FindSecrets(secretsPages, x => x.Name.StartsWith(prefix)))
85+
foreach (SecretProperties item in await FindSecrets(secretsPages, x => x.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
7986
{
8087
// Delete only keys which no longer exist in passed-in secrets
8188
if (!dictionary.Keys.Contains(item.Name))
8289
{
90+
Logger?.KeyVaultSecretRepoDeleteKey(item.Name);
8391
deleteTasks.Add(_secretClient.Value.StartDeleteSecretAsync(item.Name));
8492
}
8593
}
@@ -93,6 +101,7 @@ public override async Task WriteAsync(ScriptSecretsType type, string functionNam
93101
List<Task> setTasks = new List<Task>();
94102
foreach (string key in dictionary.Keys)
95103
{
104+
Logger?.KeyVaultSecretRepoSetKey(key);
96105
setTasks.Add(_secretClient.Value.SetSecretAsync(key, dictionary[key]));
97106
}
98107
await Task.WhenAll(setTasks);
@@ -125,7 +134,7 @@ private async Task<ScriptSecrets> ReadHostSecrets()
125134
List<Task<Response<KeyVaultSecret>>> tasks = new List<Task<Response<KeyVaultSecret>>>();
126135

127136
// Add master key task
128-
List<SecretProperties> masterItems = await FindSecrets(secretsPages, x => x.Name.StartsWith(MasterKey));
137+
List<SecretProperties> masterItems = await FindSecrets(secretsPages, x => x.Name.StartsWith(MasterKey, StringComparison.OrdinalIgnoreCase));
129138
if (masterItems.Count > 0)
130139
{
131140
tasks.Add(_secretClient.Value.GetSecretAsync(masterItems[0].Name));
@@ -136,13 +145,13 @@ private async Task<ScriptSecrets> ReadHostSecrets()
136145
}
137146

138147
// Add functionKey tasks
139-
foreach (SecretProperties item in await FindSecrets(secretsPages, x => x.Name.StartsWith(FunctionKeyPrefix)))
148+
foreach (SecretProperties item in await FindSecrets(secretsPages, x => x.Name.StartsWith(FunctionKeyPrefix, StringComparison.OrdinalIgnoreCase)))
140149
{
141150
tasks.Add(_secretClient.Value.GetSecretAsync(item.Name));
142151
}
143152

144153
// Add systemKey tasks
145-
foreach (SecretProperties item in await FindSecrets(secretsPages, x => x.Name.StartsWith(SystemKeyPrefix)))
154+
foreach (SecretProperties item in await FindSecrets(secretsPages, x => x.Name.StartsWith(SystemKeyPrefix, StringComparison.OrdinalIgnoreCase)))
146155
{
147156
tasks.Add(_secretClient.Value.GetSecretAsync(item.Name));
148157
}
@@ -158,15 +167,15 @@ private async Task<ScriptSecrets> ReadHostSecrets()
158167
foreach (Task<Response<KeyVaultSecret>> task in tasks)
159168
{
160169
KeyVaultSecret item = task.Result;
161-
if (item.Name.StartsWith(MasterKey))
170+
if (item.Name.StartsWith(MasterKey, StringComparison.OrdinalIgnoreCase))
162171
{
163172
hostSecrets.MasterKey = KeyVaultSecretToKey(item, MasterKey);
164173
}
165-
else if (item.Name.StartsWith(FunctionKeyPrefix))
174+
else if (item.Name.StartsWith(FunctionKeyPrefix, StringComparison.OrdinalIgnoreCase))
166175
{
167176
hostSecrets.FunctionKeys.Add(KeyVaultSecretToKey(item, FunctionKeyPrefix));
168177
}
169-
else if (item.Name.StartsWith(SystemKeyPrefix))
178+
else if (item.Name.StartsWith(SystemKeyPrefix, StringComparison.OrdinalIgnoreCase))
170179
{
171180
hostSecrets.SystemKeys.Add(KeyVaultSecretToKey(item, SystemKeyPrefix));
172181
}
@@ -234,7 +243,7 @@ public static async Task<List<SecretProperties>> FindSecrets(AsyncPageable<Secre
234243

235244
public static Dictionary<string, string> GetDictionaryFromScriptSecrets(ScriptSecrets secrets, string functionName)
236245
{
237-
Dictionary<string, string> dic = new Dictionary<string, string>();
246+
Dictionary<string, string> dic = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
238247
HostSecrets hostSecrets = secrets as HostSecrets;
239248
FunctionSecrets functionSecrets = secrets as FunctionSecrets;
240249
if (hostSecrets != null)

0 commit comments

Comments
 (0)