Testing secrets and RBAC pipelines with Topaz
In this tutorial, we will test a complete secrets-access pipeline: create a Key Vault, store a secret, assign a Managed Identity with the Key Vault Secrets User role, and verify the identity can read the secret. We will also write a negative test that confirms an unassigned identity is denied access.
This pattern is valuable for validating least-privilege IAM configurations before they are deployed to a real Azure environment.
A complete runnable example is available in Examples/Topaz.Example.SecretsRbac.
What you will build
- A Key Vault and secret created via the Azure CLI
- A Managed Identity with a scoped RBAC role assignment
- A .NET snippet that reads the secret using
DefaultAzureCredential - A negative test confirming access is denied without the role
Prerequisites
- Topaz installed and running (see Getting started)
- DNS setup completed and Topaz certificate trusted
- Azure CLI installed (
az --version) - Topaz cloud registered in Azure CLI (see Azure CLI integration)
- .NET 10 SDK installed
Step 1: Start Topaz
topaz-host \
--default-subscription 00000000-0000-0000-0000-000000000001 \
--log-level Information
Leave this terminal open for the rest of the tutorial.
Step 2: Set the active cloud to Topaz
az cloud set -n Topaz
export AZURE_CORE_INSTANCE_DISCOVERY=false
az login
az account set --subscription 00000000-0000-0000-0000-000000000001
Step 3: Provision the infrastructure
Create a resource group, Key Vault, and Managed Identity:
az group create \
--name rg-rbac-test \
--location westeurope
az keyvault create \
--name kv-rbac-test \
--resource-group rg-rbac-test \
--location westeurope
az keyvault secret set \
--vault-name kv-rbac-test \
--name DatabasePassword \
--value "super-secret-value"
az identity create \
--name id-app \
--resource-group rg-rbac-test \
--location westeurope
Step 4: Assign the RBAC role
Retrieve the Managed Identity's principal ID and assign Key Vault Secrets User (built-in role 4633458b-17de-408a-b874-0445c86b69e6):
PRINCIPAL_ID=$(az identity show \
--name id-app \
--resource-group rg-rbac-test \
--query principalId \
--output tsv)
KV_ID=$(az keyvault show \
--name kv-rbac-test \
--resource-group rg-rbac-test \
--query id \
--output tsv)
az role assignment create \
--assignee-object-id "$PRINCIPAL_ID" \
--assignee-principal-type ServicePrincipal \
--role "4633458b-17de-408a-b874-0445c86b69e6" \
--scope "$KV_ID"
Step 5: Read the secret using the SDK
Install the required packages:
dotnet add package Azure.Security.KeyVault.Secrets
dotnet add package Azure.Identity
dotnet add package TheCloudTheory.Topaz.Identity
Read the secret as the assigned Managed Identity using AzureLocalCredential, which maps to the principal you specify:
using Azure.Security.KeyVault.Secrets;
using Topaz.Identity;
// Retrieve the principal ID of the Managed Identity
var principalId = "<principal-id-from-az-identity-show>";
var vaultUri = new Uri("https://kv-rbac-test.vault.topaz.local.dev:8898");
var credential = new AzureLocalCredential(principalId);
var client = new SecretClient(vaultUri, credential);
// This should succeed — the role assignment grants read access
KeyVaultSecret secret = await client.GetSecretAsync("DatabasePassword");
Console.WriteLine(secret.Value); // super-secret-value
Step 6: Write the negative test
An identity without a role assignment should receive a 403 Forbidden response:
using Azure.RequestFailedException;
using Azure.Security.KeyVault.Secrets;
using Topaz.Identity;
using Xunit;
public class SecretsRbacTests
{
private const string VaultUri = "https://kv-rbac-test.vault.topaz.local.dev:8898";
[Fact]
public async Task AssignedIdentity_CanReadSecret()
{
var assignedPrincipalId = "<principal-id-with-role>";
var client = new SecretClient(
new Uri(VaultUri),
new AzureLocalCredential(assignedPrincipalId));
var secret = await client.GetSecretAsync("DatabasePassword");
Assert.Equal("super-secret-value", secret.Value.Value);
}
[Fact]
public async Task UnassignedIdentity_IsDenieAccess()
{
// A different principal ID that has no role assignment on this vault
var unassignedPrincipalId = Guid.NewGuid().ToString();
var client = new SecretClient(
new Uri(VaultUri),
new AzureLocalCredential(unassignedPrincipalId));
var exception = await Assert.ThrowsAsync<RequestFailedException>(
() => client.GetSecretAsync("DatabasePassword").AsTask());
Assert.Equal(403, exception.Status);
}
}
Step 7: Test from the .NET SDK with role assignment via ARM
If you prefer to create the role assignment programmatically rather than via the CLI, use Azure.ResourceManager.Authorization:
using Azure.ResourceManager;
using Azure.ResourceManager.Authorization;
using Azure.ResourceManager.Authorization.Models;
using Topaz.Identity;
var arm = new ArmClient(
new AzureLocalCredential(),
"00000000-0000-0000-0000-000000000001");
var subscription = await arm.GetDefaultSubscriptionAsync();
var rg = (await subscription.GetResourceGroupAsync("rg-rbac-test")).Value;
// Key Vault Secrets User built-in role
const string keyVaultSecretsUserRoleId = "4633458b-17de-408a-b874-0445c86b69e6";
var kvResource = arm.GetKeyVaultResource(
new Azure.Core.ResourceIdentifier(
$"/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/rg-rbac-test/providers/Microsoft.KeyVault/vaults/kv-rbac-test"));
var principalId = Guid.Parse("<managed-identity-principal-id>");
var roleDefinitionId = new Azure.Core.ResourceIdentifier(
$"/providers/Microsoft.Authorization/roleDefinitions/{keyVaultSecretsUserRoleId}");
await kvResource.GetRoleAssignments().CreateOrUpdateAsync(
Azure.WaitUntil.Completed,
Guid.NewGuid().ToString(),
new RoleAssignmentCreateOrUpdateContent(roleDefinitionId, principalId)
{
PrincipalType = RoleManagementPrincipalType.ServicePrincipal
});
The SDK calls, credential pattern, and role assignment IDs are identical to real Azure. The only change is replacing AzureLocalCredential with DefaultAzureCredential and pointing the vault URI at https://kv-rbac-test.vault.azure.net.
RBAC in Topaz is partially implemented. Role assignments on Key Vault scopes and resource group scopes are supported. See RBAC API coverage for the full status.