Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
30860f6
Merge 3.2.0 to main (#74)
indrora Jan 29, 2026
cf76a6b
cleaned up docs, split RBAC permissions into seperate file for brevity
joevanwanzeeleKF Feb 10, 2026
6148151
Update generated docs
Feb 10, 2026
66e6274
Merge branch 'release-3.2' into improve_docs_#77202
joevanwanzeeleKF Feb 12, 2026
8748e52
Updated changelog, nuget package references
joevanwanzeeleKF Feb 24, 2026
2ec6848
Merge branch 'improve_docs_#77202' of https://github.com/Keyfactor/az…
joevanwanzeeleKF Feb 24, 2026
930aee6
Explicit update of Newtonsoft.Json.Bson from 1.0.2 (used by Microsoft…
joevanwanzeeleKF Mar 4, 2026
36af88c
now returning the serialized certificate tags, as well as the exporta…
joevanwanzeeleKF Apr 1, 2026
538f053
added check for vault name parameter coming through as empty string. …
joevanwanzeeleKF Apr 24, 2026
3645046
Update generated docs
Apr 24, 2026
c4ed380
replaced count() with count
joevanwanzeeleKF Apr 24, 2026
7fd5d7e
Merge branch 'vaultname_empty_check_#85697' of https://github.com/Key…
joevanwanzeeleKF Apr 24, 2026
b0680b5
Added helper method to determine key sized and curve in order to pass…
joevanwanzeeleKF May 6, 2026
6e250fb
cleanup
joevanwanzeeleKF May 6, 2026
9154980
Added unit tests
joevanwanzeeleKF May 7, 2026
e7b251e
update changelog
joevanwanzeeleKF May 11, 2026
7b12190
Merge branch 'release-3.2' into vaultname_empty_check_#85697
joevanwanzeeleKF May 11, 2026
6b5e9b7
Fixed issue with Create that would throw an exception for missing ent…
joevanwanzeeleKF May 28, 2026
219ecaa
Updated documentation with alias requirements; now returning a more h…
joevanwanzeeleKF Jun 4, 2026
6d3872f
Update generated docs
Jun 4, 2026
91bcee8
Merge branch 'release-3.2' into vault_create_fix_n_docs_#87311
joevanwanzeeleKF Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion AzureKeyVault.Tests/AzureKeyVault.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
<PackageReference Include="Keyfactor.Orchestrators.Common" Version="3.3.0" />
<PackageReference Include="Keyfactor.Orchestrators.IOrchestratorJobExtensions" Version="1.0.0" />
<PackageReference Include="Keyfactor.Platform.IPAMProvider" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

Expand Down
138 changes: 138 additions & 0 deletions AzureKeyVault.Tests/ManagementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Azure.ResourceManager.KeyVault;
using Azure.Security.KeyVault.Certificates;
using FluentAssertions;
using Keyfactor.Logging;
Expand Down Expand Up @@ -42,6 +43,54 @@
private const string EmptyTags = "";
private const long JobHistoryId = 42;

// ── Create ────────────────────────────────────────────────────────────

/// <summary>
/// Regression test for: "The given key 'CertificateTags' was not present in the dictionary."
/// A Create operation does not supply entry parameters (Tags, PreserveTags, NonExportable)
/// in JobProperties. ProcessJob must not throw a KeyNotFoundException before reaching
/// PerformCreateVault.
/// </summary>
[Fact]
public void Create_EmptyJobProperties_DoesNotThrow_AndSucceeds()
{
var mockClient = new Mock<AzureClient>();
mockClient
.Setup(c => c.CreateVault())
.ReturnsAsync(BuildFakeKeyVaultResource("test-vault"));

var resolverMock = new Mock<IPAMSecretResolver>();
resolverMock.Setup(r => r.Resolve(It.IsAny<string>())).Returns<string>(s => s);

var job = new TestableManagement(resolverMock.Object)
{
AzClient = mockClient.Object,
Logger = LogHandler.GetClassLogger<Management>()
};

var config = new ManagementJobConfiguration
{
OperationType = CertStoreOperationType.Create,
JobHistoryId = JobHistoryId,
JobProperties = new Dictionary<string, object>(), // no entry parameters — intentionally empty
ServerUsername = "test-client-id",
ServerPassword = "test-client-secret",
CertificateStoreDetails = new CertificateStore
{
StorePath = "00000000-0000-0000-0000-000000000000:test-rg:test-vault",
ClientMachine = "00000000-0000-0000-0000-000000000001", // TenantId
Properties = "{\"AzureCloud\":\"\",\"PrivateEndpoint\":\"\",\"SkuType\":\"Standard\",\"VaultRegion\":\"\",\"SubscriptionId\":\"\",\"ResourceGroupName\":\"\",\"VaultName\":\"\"}"
}
};

JobResult result = null;

Check warning on line 86 in AzureKeyVault.Tests/ManagementTests.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

Converting null literal or possible null value to non-nullable type.

Check warning on line 86 in AzureKeyVault.Tests/ManagementTests.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

Converting null literal or possible null value to non-nullable type.
var act = () => { result = job.ProcessJob(config); };

act.Should().NotThrow("ProcessJob must not throw a KeyNotFoundException when entry parameters are absent for a Create operation");
result.Should().NotBeNull();
result.Result.Should().Be(OrchestratorJobStatusJobResult.Success);
}

// ── Add: success cases ────────────────────────────────────────────────

[Fact]
Expand Down Expand Up @@ -125,7 +174,7 @@
var job = BuildJob(out _);

var result = job.CallPerformAddition(
alias: null, CertificateFixtures.PfxPassword, CertificateFixtures.Rsa2048Base64,

Check warning on line 177 in AzureKeyVault.Tests/ManagementTests.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

Cannot convert null literal to non-nullable reference type.

Check warning on line 177 in AzureKeyVault.Tests/ManagementTests.cs

View workflow job for this annotation

GitHub Actions / call-starter-workflow / call-dotnet-build-and-release-workflow / dotnet-build-and-release

Cannot convert null literal to non-nullable reference type.
EmptyTags, JobHistoryId, overwrite: true, preserveTags: false, nonExportable: false);

result.Result.Should().Be(OrchestratorJobStatusJobResult.Failure);
Expand Down Expand Up @@ -238,6 +287,79 @@
result.FailureMessage.Should().Contain("vault unreachable");
}

// ── Add: AKV alias validation (regression for cryptic AKV "invalid name" error) ─────

/// <summary>
/// Regression test: when the AKV SDK rejects an invalid alias with its
/// "The request URI contains an invalid name" error, PerformAddition should
/// translate that into a friendly message listing the AKV naming rules
/// rather than surfacing the raw SDK exception.
/// </summary>
[Theory]
[InlineData("linux01.kf.baah.net")] // contains dots
[InlineData("my_cert")] // contains underscore
[InlineData("1starts-with-digit")] // starts with a digit
[InlineData("has spaces")] // contains spaces
public void Add_InvalidAlias_ReturnsFriendlyErrorMessage(string invalidAlias)
{
var job = BuildJob(out var mockClient);

// Simulate the exact AKV SDK error message we're translating
var akvError = $"One or more errors occurred. (The request URI contains an invalid name: {invalidAlias} " +
$"Status: 400 (Bad Request) ErrorCode: BadParameter Content: " +
$"{{\"error\":{{\"code\":\"BadParameter\",\"message\":\"The request URI contains an invalid name: {invalidAlias}\"}}}})";
mockClient
.Setup(c => c.ImportCertificateAsync(
invalidAlias, It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Dictionary<string, string>>(), It.IsAny<bool>()))
.ThrowsAsync(new Exception(akvError));

var result = job.CallPerformAddition(
invalidAlias, CertificateFixtures.PfxPassword, CertificateFixtures.Rsa2048Base64,
EmptyTags, JobHistoryId, overwrite: true, preserveTags: false, nonExportable: false);

result.Result.Should().Be(OrchestratorJobStatusJobResult.Failure);

// Should NOT contain the raw SDK error noise
result.FailureMessage.Should().NotContain("Status: 400");
result.FailureMessage.Should().NotContain("BadParameter");
result.FailureMessage.Should().NotContain("request URI");

// Should explain the AKV alias rules
result.FailureMessage.Should().Contain("127",
"the message should mention the 127-character length limit");
result.FailureMessage.Should().Contain("alphanumeric",
"the message should mention the alphanumeric-only rule");
result.FailureMessage.Should().Contain("letter",
"the message should mention that the alias must start with a letter");
}

/// <summary>
/// Make sure the friendly-error branch is specific to the AKV "invalid name"
/// signature - other exceptions should still fall through to the generic
/// failure path so we don't mask unrelated problems.
/// </summary>
[Fact]
public void Add_GenericException_DoesNotReturnAliasValidationMessage()
{
var job = BuildJob(out var mockClient);
mockClient
.Setup(c => c.ImportCertificateAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
It.IsAny<Dictionary<string, string>>(), It.IsAny<bool>()))
.ThrowsAsync(new Exception("unrelated network failure"));

var result = job.CallPerformAddition(
Alias, CertificateFixtures.PfxPassword, CertificateFixtures.Rsa2048Base64,
EmptyTags, JobHistoryId, overwrite: true, preserveTags: false, nonExportable: false);

result.Result.Should().Be(OrchestratorJobStatusJobResult.Failure);
result.FailureMessage.Should().Contain("unrelated network failure");
// The AKV-naming-rules text should NOT appear for unrelated errors
result.FailureMessage.Should().NotContain("127");
result.FailureMessage.Should().NotContain("alphanumeric");
}

// ── Helpers ───────────────────────────────────────────────────────────

private static TestableManagement BuildJob(out Mock<AzureClient> mockClient)
Expand Down Expand Up @@ -312,5 +434,21 @@
}
return null;
}

/// <summary>
/// Builds a minimal KeyVaultResource whose Id contains the vault name,
/// satisfying PerformCreateVault's success check.
/// </summary>
private static KeyVaultResource BuildFakeKeyVaultResource(string vaultName)
{
// KeyVaultResource cannot be newed directly; use a Mock so we can
// control the Id property that PerformCreateVault inspects.
var mock = new Mock<KeyVaultResource>();
var resourceId = Azure.Core.ResourceIdentifier.Parse(
$"/subscriptions/00000000-0000-0000-0000-000000000000" +
$"/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/{vaultName}");
mock.Setup(r => r.Id).Returns(resourceId);
return mock.Object;
}
}
}
34 changes: 28 additions & 6 deletions AzureKeyVault/Jobs/Management.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@ public JobResult ProcessJob(ManagementJobConfiguration config)

Logger.LogTrace("parsing entry parameters.. ");

tagsJSON = config.JobProperties[EntryParameters.TAGS] as string ?? string.Empty;
preserveTags = config.JobProperties[EntryParameters.PRESERVE_TAGS] as bool? ?? true;
nonExportable = config.JobProperties[EntryParameters.NON_EXPORTABLE] as bool? ?? false;
tagsJSON = config.JobProperties.ContainsKey(EntryParameters.TAGS)
? config.JobProperties[EntryParameters.TAGS] as string ?? string.Empty
: string.Empty;
preserveTags = config.JobProperties.ContainsKey(EntryParameters.PRESERVE_TAGS)
? config.JobProperties[EntryParameters.PRESERVE_TAGS] as bool? ?? true
: true;
nonExportable = config.JobProperties.ContainsKey(EntryParameters.NON_EXPORTABLE)
? config.JobProperties[EntryParameters.NON_EXPORTABLE] as bool? ?? false
: false;

switch (config.OperationType)
{
Expand Down Expand Up @@ -191,10 +197,26 @@ protected virtual JobResult PerformAddition(string alias, string pfxPassword, st
}
catch (Exception ex)
{
complete.FailureMessage = $"An error occurred while adding {alias} to {ExtensionName}: " + ex.Message;
if (ex.Message.ToLowerInvariant().Contains("the request uri contains an invalid name"))
{
// The alias is not valid; return an error explaining the AKV naming rules.
// Note: this branch is tied to AKV's current "invalid name" error string;
// if AKV changes its error format we'll fall through to the generic branch.
var errMsg = $"The alias '{alias}' was not allowed by Azure Key Vault. The alias must:\n" +
$"\t- be under 127 characters in length\n" +
$"\t- contain only alphanumeric characters and dashes\n" +
$"\t- begin with a letter\n" +
$"Please update the alias to meet these requirements.";

complete.FailureMessage = errMsg;
}
else
{
complete.FailureMessage = $"An error occurred while adding {alias} to {ExtensionName}: " + ex.Message;

if (ex.InnerException != null)
complete.FailureMessage += " - " + ex.InnerException.Message;
if (ex.InnerException != null)
complete.FailureMessage += " - " + ex.InnerException.Message;
}
}
}
else // Non-PFX
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
- 3.2.4
- Bug fix: Fix for error during create job due to missing entry parameters
- Made 'alias' officially required for enrollment
- Documentation improvements and updates

- 3.2.3
- Bug fix: there was an issue where we were not passing the Key Size to Azure, and it was causing an error when the default didn't match
- Now checking for empty vault name property to avoid overriding an existing value during Store Creation - [Issue 39](https://github.com/Keyfactor/azurekeyvault-orchestrator/issues/39#issuecomment-4298537246)
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,15 @@ Azure and clicking "Properties" in the left menu.
> :warning: The identity you are using for authentication will need to have sufficient Azure permissions to be able to
> create new Keyvaults.

### Certificate Enrollment Alias Requirements

Azure Keyvault has the following restrictions on certificate naming:
- Must be alphanumeric characters and dashes
- Must be < 127 characters
- Must start with a letter

If, during enrollment, an alias is provided that does not meet the above requirements; the job will fail with an error message indicating the reason.

---


Expand Down Expand Up @@ -477,7 +486,7 @@ the Keyfactor Command Portal
##### Advanced Tab
| Attribute | Value | Description |
| --------- | ----- | ----- |
| Supports Custom Alias | Optional | Determines if an individual entry within a store can have a custom Alias. |
| Supports Custom Alias | Required | Determines if an individual entry within a store can have a custom Alias. |
| Private Key Handling | Optional | This determines if Keyfactor can send the private key associated with a certificate to the store. Required because IIS certificates without private keys would be invalid. |
| PFX Password Style | Default | 'Default' - PFX password is randomly generated, 'Custom' - PFX password may be specified when the enrollment job is created (Requires the Allow Custom Password application setting to be enabled.) |

Expand Down
9 changes: 9 additions & 0 deletions docsource/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,4 +336,13 @@ Azure and clicking "Properties" in the left menu.
> :warning: The identity you are using for authentication will need to have sufficient Azure permissions to be able to
> create new Keyvaults.

### Certificate Enrollment Alias Requirements

Azure Keyvault has the following restrictions on certificate naming:
- Must be alphanumeric characters and dashes
- Must be < 127 characters
- Must start with a letter

If, during enrollment, an alias is provided that does not meet the above requirements; the job will fail with an error message indicating the reason.

---
2 changes: 1 addition & 1 deletion integration-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"BlueprintAllowed": false,
"Capability": "AKV",
"ClientMachineDescription": "The GUID of the tenant ID of the Azure Keyvault instance; for example, '12345678-1234-1234-1234-123456789abc'.",
"CustomAliasAllowed": "Optional",
"CustomAliasAllowed": "Required",
"EntryParameters": [
{
"Name": "CertificateTags",
Expand Down
2 changes: 1 addition & 1 deletion scripts/store_types/bash/curl_create_store_types.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ create_store_type() {
create_store_type "AKV" '{
"BlueprintAllowed": false,
"Capability": "AKV",
"CustomAliasAllowed": "Optional",
"CustomAliasAllowed": "Required",
"EntryParameters": [
{
"Name": "CertificateTags",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ New-StoreType "AKV" @'
{
"BlueprintAllowed": false,
"Capability": "AKV",
"CustomAliasAllowed": "Optional",
"CustomAliasAllowed": "Required",
"EntryParameters": [
{
"Name": "CertificateTags",
Expand Down
Loading