top of page
  • 10 hours ago
  • 14 min read
Neon Azure Key Vault workshop architecture showing a workload, managed identity, RBAC, secret, RSA signing key, and X.509 certificate.

Azure Key Vault protects application secrets, encryption keys, and X.509 certificates. The vault can secure these objects, but an application still needs a safe way to authenticate before it can use them.

In this workshop, you will build a complete passwordless Key Vault lab with PowerShell 7 and Azure CLI.

You will:

  • create a Key Vault that uses Azure role-based access control;

  • create a secret, an RSA signing key, and a self-signed certificate;

  • deploy a Linux virtual machine with a user-assigned managed identity;

  • prove that authentication alone does not grant authorization;

  • grant separate least-privilege Key Vault data-plane roles;

  • request a Microsoft Entra access token from the Azure Instance Metadata Service;

  • read a secret without displaying its value;

  • sign and verify a digest with a Key Vault key;

  • read X.509 certificate metadata while keeping its private key non-exportable;

  • inspect certificate policy and lifecycle settings;

  • troubleshoot identity, RBAC, network, and certificate problems;

  • remove the billable workshop resources.

No password, client secret, connection string, or certificate credential is stored in the workload.

Cost warningThe virtual machine, managed disk, and public IP address are billable. Key Vault operations can also generate small transaction charges. Complete the cleanup section after you capture the validation results.

Workshop overview

Item

Configuration

Level

Intermediate to advanced

Deployment method

PowerShell 7 and Azure CLI

Azure region

West Europe

Key Vault tier

Standard

Authorization model

Azure RBAC

Workload

Ubuntu 22.04 Linux VM

Compute size

Standard_B1s

Workload identity

User-assigned managed identity

Inbound access

Denied; no custom NSG inbound rules

Key Vault objects

Secret, RSA key, and X.509 certificate

Certificate

Self-signed, RSA-2048, six months, non-exportable private key

Validation interface

Key Vault REST API 2025-07-01

Estimated completion time:

Infrastructure deployment:  15–25 minutes
RBAC propagation:             2–10 minutes
Validation:                   10–15 minutes
Cleanup:                       5–15 minutes

Architecture

                  Microsoft Entra ID
                         │
                         │ issues OAuth token
                         ▼
              User-assigned managed identity
                         │
                         │ attached to
                         ▼
                Ubuntu workload VM
                no inbound NSG rules
                         │
                         │ IMDS token for
                         │ https://vault.azure.net
                         ▼
                 Azure Key Vault
                 Azure RBAC enabled
                         │
          ┌──────────────┼────────────────┐
          │              │                │
          ▼              ▼                ▼
   workshop-message  app-signing-key  workshop-tls
        secret          RSA key       X.509 certificate
          │              │                │
   Secrets User     Crypto User     Certificate User

The identity proves who the caller is. Azure RBAC decides what that identity can do. These are separate checks:

Authentication success + no data-plane role = HTTP 403 Forbidden
Authentication success + correct role       = authorized object operation

Control plane and data plane

Key Vault exposes two authorization surfaces.

Plane

Typical operations

Endpoint

Authorization

Control plane

Create the vault, change networking, configure diagnostics, read resource properties

Azure RBAC management actions

Data plane

Read secrets, sign with keys, retrieve certificates

Key Vault RBAC data actions

The Key Vault Contributor role can manage the vault resource but cannot read secret values, use private key operations, or retrieve certificate contents. Workloads require a Key Vault data-plane role.

System-assigned and user-assigned identities

Identity type

Lifecycle

Reuse

Best fit

System-assigned

Created and deleted with one Azure resource

One resource

Simple workload with a tightly coupled identity

User-assigned

Independent Azure resource

Multiple supported workloads

Stable identity, preauthorization, blue/green deployments, or shared access pattern

This workshop uses one user-assigned identity so its principal exists independently of the VM.

1. Verify the prerequisites

You need:

  • an Azure subscription;

  • permission to create resource groups, Key Vaults, networking, managed identities, and virtual machines;

  • permission to create role assignments, such as Owner or User Access Administrator;

  • PowerShell 7;

  • Azure CLI;

  • VM quota for Standard_B1s in West Europe.

Verify the local tools:

pwsh --version
az version

Sign in:

az login

List the available subscriptions:

az account list `
    --query "[].{
        Name:name,
        IsDefault:isDefault,
        State:state
    }" `
    --output table

Select the target subscription:

$SubscriptionId = "<your-subscription-id>"

az account set `
    --subscription $SubscriptionId

Confirm the active context without printing tenant or user identifiers:

az account show `
    --query "{
        Subscription:name,
        State:state
    }" `
    --output table

2. Define the workshop variables

Keep the same PowerShell session open throughout the workshop.

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$Location        = "westeurope"
$ResourceGroup   = "rg-kv-passwordless-weu"

# Key Vault names are globally unique.
$Suffix           = Get-Random -Minimum 100000 -Maximum 999999
$VaultName        = "kvpassless$Suffix"
$SecretName       = "workshop-message"
$KeyName          = "app-signing-key"
$CertificateName  = "workshop-tls"

# Workload identity and compute
$IdentityName = "id-kv-workload-weu"
$VmName       = "vm-kv-client-weu"

# Networking
$VnetName     = "vnet-kv-workload-weu"
$SubnetName   = "snet-workload"
$NsgName      = "nsg-kv-workload-weu"
$PublicIpName = "pip-vm-kv-client-weu"
$NicName      = "nic-vm-kv-client-weu"

# Immutable built-in role IDs
$SecretsOfficerRole      = "b86a8fe4-44ce-4948-aee5-eccb2c155cd7"
$SecretsUserRole         = "4633458b-17de-408a-b874-0445c86b69e6"
$CryptoOfficerRole       = "14b46e9e-c2b7-41b4-b07b-48a6ebf60603"
$CryptoUserRole          = "12338af0-0e69-4776-bea7-57ae8d297424"
$CertificatesOfficerRole = "a4417e6f-fecd-4de8-b567-7b0420556985"
$CertificateUserRole     = "db79e9a7-68ee-4b58-9aeb-b90e7c24fcba"

Using role IDs rather than display names keeps automation stable if a role is renamed.

3. Create the resource group

az group create `
    --name $ResourceGroup `
    --location $Location `
    --tags `
        workshop=key-vault-passwordless `
        environment=lab `
        managedBy=azure-cli `
    --only-show-errors `
    --output none

Verify the resource group:

az group show `
    --name $ResourceGroup `
    --query "{
        Name:name,
        Location:location,
        State:properties.provisioningState
    }" `
    --output table

4. Create a Key Vault with Azure RBAC

az keyvault create `
    --name $VaultName `
    --resource-group $ResourceGroup `
    --location $Location `
    --sku standard `
    --enable-rbac-authorization true `
    --enable-purge-protection true `
    --retention-days 90 `
    --public-network-access Enabled `
    --tags `
        workshop=key-vault-passwordless `
        environment=lab `
        managedBy=azure-cli `
    --only-show-errors `
    --output none

Verify the security properties:

az keyvault show `
    --name $VaultName `
    --resource-group $ResourceGroup `
    --query "{
        Name:name,
        Location:location,
        RbacEnabled:properties.enableRbacAuthorization,
        SoftDeleteRetentionDays:properties.softDeleteRetentionInDays,
        PurgeProtection:properties.enablePurgeProtection,
        PublicNetworkAccess:properties.publicNetworkAccess
    }" `
    --output table
Azure Key Vault overview with the Essentials section collapsed to protect account identifiers.
Azure Key Vault configured to use the recommended Azure RBAC permission model.
Lab network choicePublic network access is enabled so the workshop can focus on identity and authorization. A production vault should normally use selected networks, Private Link, or a Network Security Perimeter according to the workload architecture.

5. Grant the workshop operator object-management roles

Creating the vault does not automatically grant your user account data-plane access.

Resolve your Microsoft Entra object ID and the vault resource ID:

$CurrentUserObjectId = (
    az ad signed-in-user show `
        --query id `
        --output tsv
).Trim()

$VaultId = (
    az keyvault show `
        --name $VaultName `
        --resource-group $ResourceGroup `
        --query id `
        --output tsv
).Trim()

Grant only the object-management roles needed for the workshop:

foreach ($RoleId in @(
    $SecretsOfficerRole
    $CryptoOfficerRole
    $CertificatesOfficerRole
)) {
    az role assignment create `
        --assignee-object-id $CurrentUserObjectId `
        --assignee-principal-type User `
        --role $RoleId `
        --scope $VaultId `
        --only-show-errors `
        --output none
}

Role assignment changes are eventually consistent. Wait and retry if the first data-plane command returns Forbidden.

6. Create a non-sensitive demonstration secret

$SecretExpires = (
    Get-Date
).ToUniversalTime().AddMonths(3).ToString(
    "yyyy-MM-ddTHH:mm:ssZ"
)

az keyvault secret set `
    --vault-name $VaultName `
    --name $SecretName `
    --value "Passwordless access validated" `
    --expires $SecretExpires `
    --tags `
        purpose=workshop `
        sensitivity=demo-only `
    --only-show-errors `
    --output none

Inspect metadata without retrieving the value:

az keyvault secret show `
    --vault-name $VaultName `
    --name $SecretName `
    --query "{
        Name:name,
        Enabled:attributes.enabled,
        Expires:attributes.expires,
        Tags:tags
    }" `
    --output jsonc

7. Create an RSA signing key

az keyvault key create `
    --vault-name $VaultName `
    --name $KeyName `
    --kty RSA `
    --size 2048 `
    --ops sign verify `
    --protection software `
    --tags `
        purpose=workshop-signing `
        exportable=false `
    --only-show-errors `
    --output none

Verify the key properties:

az keyvault key show `
    --vault-name $VaultName `
    --name $KeyName `
    --query "{
        Name:name,
        KeyType:key.kty,
        KeySize:key.keySize,
        Operations:key.keyOps,
        Enabled:attributes.enabled
    }" `
    --output jsonc

The Key Vault service performs signing with the private portion of the key. The private key is never returned to the workload.

8. Create a certificate policy

Create cert-policy.json:

{
  "issuerParameters": {
    "certificateTransparency": null,
    "name": "Self"
  },
  "keyProperties": {
    "curve": null,
    "exportable": false,
    "keySize": 2048,
    "keyType": "RSA",
    "reuseKey": false
  },
  "lifetimeActions": [
    {
      "action": {
        "actionType": "AutoRenew"
      },
      "trigger": {
        "lifetimePercentage": 80
      }
    }
  ],
  "secretProperties": {
    "contentType": "application/x-pkcs12"
  },
  "x509CertificateProperties": {
    "keyUsage": [
      "digitalSignature",
      "keyEncipherment"
    ],
    "subject": "CN=workshop.internal",
    "subjectAlternativeNames": {
      "dnsNames": [
        "workshop.internal"
      ],
      "emails": [],
      "upns": []
    },
    "validityInMonths": 6
  }
}

Important policy choices:

  • exportable is false;

  • a new key pair is created during renewal;

  • validity is six months;

  • renewal begins at 80 percent of the certificate lifetime;

  • this is a self-signed lab certificate, not a publicly trusted TLS certificate.

9. Create the X.509 certificate

az keyvault certificate create `
    --vault-name $VaultName `
    --name $CertificateName `
    --policy "@cert-policy.json" `
    --tags `
        purpose=workshop-tls `
        privateKey=non-exportable `
    --only-show-errors `
    --output none

Certificate issuance is asynchronous. Check the completed object:

az keyvault certificate show `
    --vault-name $VaultName `
    --name $CertificateName `
    --query "{
        Name:name,
        Subject:policy.x509CertificateProperties.subject,
        ValidityMonths:policy.x509CertificateProperties.validityInMonths,
        Exportable:policy.keyProperties.exportable,
        ReuseKey:policy.keyProperties.reuseKey,
        Enabled:attributes.enabled,
        Expires:attributes.expires
    }" `
    --output jsonc
Azure Cloud Shell inventory of the workshop secret, RSA key, and certificate.
Azure Key Vault current and older certificate versions.
Six-month self-signed certificate policy with automatic renewal at 80 percent lifetime.

Certificate composition

Creating a Key Vault certificate creates three version-linked objects with the same name:

/certificates/workshop-tls/<version>  Public certificate and policy
/keys/workshop-tls/<version>          Backing key and allowed key operations
/secrets/workshop-tls/<version>       Certificate package representation

This is why certificate-oriented RBAC roles include permissions that may also touch the linked key and secret.

Key Vault Reader reads metadata only. Key Vault Certificate User can read the entire certificate contents, including access to its linked key and secret surfaces. A non-exportable certificate policy adds another protection boundary by preventing the private key from being returned.

10. Create a user-assigned managed identity

az identity create `
    --name $IdentityName `
    --resource-group $ResourceGroup `
    --location $Location `
    --tags `
        workshop=key-vault-passwordless `
        role=workload-identity `
    --only-show-errors `
    --output none

Resolve its identifiers:

$Identity = (
    az identity show `
        --name $IdentityName `
        --resource-group $ResourceGroup `
        --query "{
            ClientId:clientId,
            PrincipalId:principalId,
            Id:id
        }" `
        --output json
) | ConvertFrom-Json

The client ID selects this identity when a resource has more than one identity. The principal ID is used for Azure role assignments.

11. Create the workload network

Create the VNet and subnet:

az network vnet create `
    --name $VnetName `
    --resource-group $ResourceGroup `
    --location $Location `
    --address-prefixes 10.42.0.0/16 `
    --subnet-name $SubnetName `
    --subnet-prefixes 10.42.1.0/24 `
    --tags `
        workshop=key-vault-passwordless `
        role=workload-network `
    --only-show-errors `
    --output none

Create an NSG without a custom inbound allow rule:

az network nsg create `
    --name $NsgName `
    --resource-group $ResourceGroup `
    --location $Location `
    --tags `
        workshop=key-vault-passwordless `
        inbound=denied `
    --only-show-errors `
    --output none

Create a Standard public IP for explicit outbound connectivity:

az network public-ip create `
    --name $PublicIpName `
    --resource-group $ResourceGroup `
    --location $Location `
    --sku Standard `
    --allocation-method Static `
    --zone 1 2 3 `
    --tags `
        workshop=key-vault-passwordless `
        purpose=explicit-outbound `
    --only-show-errors `
    --output none

Create the NIC:

az network nic create `
    --name $NicName `
    --resource-group $ResourceGroup `
    --location $Location `
    --vnet-name $VnetName `
    --subnet $SubnetName `
    --network-security-group $NsgName `
    --public-ip-address $PublicIpName `
    --tags `
        workshop=key-vault-passwordless `
        role=managed-identity-client `
    --only-show-errors `
    --output none

No SSH or other inbound rule is created.

12. Create the Linux VM and attach the identity

az vm create `
    --name $VmName `
    --resource-group $ResourceGroup `
    --location $Location `
    --nics $NicName `
    --image Ubuntu2204 `
    --size Standard_B1s `
    --admin-username azureuser `
    --authentication-type ssh `
    --generate-ssh-keys `
    --assign-identity $Identity.Id `
    --os-disk-delete-option Delete `
    --tags `
        workshop=key-vault-passwordless `
        role=passwordless-client `
        inbound=denied `
    --only-show-errors `
    --output none

The generated SSH key is a VM bootstrap requirement in this example. The NSG does not permit SSH traffic, and the workshop uses Azure Run Command rather than an inbound administrative session.

Verify the identity attachment:

az vm identity show `
    --name $VmName `
    --resource-group $ResourceGroup `
    --query "{
        Type:type,
        UserAssignedIdentities:keys(userAssignedIdentities)
    }" `
    --output jsonc
User-assigned managed identity attached to the passwordless Linux virtual machine.

13. Understand the IMDS token request

Azure exposes the Instance Metadata Service to the VM at 169.254.169.254.

The workload requests a token for the Key Vault resource:

http://169.254.169.254/metadata/identity/oauth2/token
    ?api-version=2025-04-07
    &resource=https%3A%2F%2Fvault.azure.net
    &client_id=<user-assigned-identity-client-id>

The required Metadata: true header prevents ordinary web requests from accessing the endpoint accidentally.

The returned bearer token is short-lived. Never write it to a log, screenshot, source file, or application setting.

14. Prove that authentication is not authorization

The managed identity currently has no Key Vault data-plane role.

Create a remote shell script:

$NegativeScript = @"
set -eu
CLIENT_ID='$($Identity.ClientId)'
VAULT_URI='https://$VaultName.vault.azure.net'

TOKEN=`$(curl -fsS -H Metadata:true \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2025-04-07&resource=https%3A%2F%2Fvault.azure.net&client_id=`$CLIENT_ID" |
  python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')

STATUS=`$(curl -sS \
  -o /tmp/kv-denied.json \
  -w "%{http_code}" \
  -H "Authorization: Bearer `$TOKEN" \
  "`$VAULT_URI/secrets/$SecretName?api-version=2025-07-01")

ERROR_CODE=`$(python3 -c \
  'import json; print(json.load(open("/tmp/kv-denied.json")).get("error",{}).get("code","Unknown"))')

echo "PASSWORDLESS NEGATIVE TEST"
echo "HTTP status: `$STATUS"
echo "Key Vault result: `$ERROR_CODE"

if [ "`$STATUS" = "403" ]; then
  echo "Outcome: EXPECTED DENY"
else
  echo "Outcome: UNEXPECTED"
  exit 1
fi
"@

Execute it through Azure Run Command. The helper base64-encodes the multiline Linux payload before it crosses PowerShell and Azure CLI. This prevents the ampersands in the IMDS URL from being interpreted by the Windows az.cmd launcher.

function Invoke-EncodedRunCommand {
    param([Parameter(Mandatory)][string] $Script)

    $Encoded = [Convert]::ToBase64String(
        [Text.Encoding]::UTF8.GetBytes($Script)
    )
    $Wrapper = "printf '%s' '$Encoded' | base64 --decode | /bin/sh"

    az vm run-command invoke `
        --resource-group $ResourceGroup `
        --name $VmName `
        --command-id RunShellScript `
        --query "value[0].message" `
        --output tsv `
        --scripts $Wrapper
}

Invoke-EncodedRunCommand -Script $NegativeScript

Expected result:

PASSWORDLESS NEGATIVE TEST
HTTP status: 403
Key Vault result: Forbidden
Outcome: EXPECTED DENY
Expected HTTP 403 denial captured before RBAC authorization.

This proves that the managed identity authenticated successfully. The failure occurred at the authorization layer.

15. Assign least-privilege workload roles

Assign the three object-specific roles at the vault scope:

foreach ($RoleId in @(
    $SecretsUserRole
    $CryptoUserRole
    $CertificateUserRole
)) {
    az role assignment create `
        --assignee-object-id $Identity.PrincipalId `
        --assignee-principal-type ServicePrincipal `
        --role $RoleId `
        --scope $VaultId `
        --only-show-errors `
        --output none
}

List the resulting assignments:

az role assignment list `
    --assignee $Identity.PrincipalId `
    --scope $VaultId `
    --query "[].{
        Role:roleDefinitionName,
        Scope:scope
    }" `
    --output table
Vault-scoped Key Vault Secrets User, Crypto User, and Certificate User role assignments.

Role

Purpose in this lab

Key Vault Secrets User

Read secret contents and metadata

Key Vault Crypto User

Perform sign and verify operations using Key Vault keys

Key Vault Certificate User

Read certificate content and its linked certificate surfaces

In production, split these permissions across separate identities when one workload does not require every capability.

16. Build the passwordless validation script

The validation script:

  1. requests a token through IMDS;

  2. reads the demonstration secret but prints only its length and a hash prefix;

  3. signs a digest with the RSA key;

  4. asks Key Vault to verify the signature;

  5. reads the certificate subject, thumbprint, expiration, and exportability policy;

  6. never prints the token, secret value, signature, or private key material.

$ValidationScript = @"
set -eu
CLIENT_ID='$($Identity.ClientId)'
VAULT_URI='https://$VaultName.vault.azure.net'

TOKEN=`$(curl -fsS -H Metadata:true \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2025-04-07&resource=https%3A%2F%2Fvault.azure.net&client_id=`$CLIENT_ID" |
  python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')

export TOKEN VAULT_URI

python3 - <<'PY'
import base64
import datetime
import hashlib
import json
import os
import urllib.request

token = os.environ["TOKEN"]
vault_uri = os.environ["VAULT_URI"]
api_version = "2025-07-01"

def request(method, path, body=None):
    data = None if body is None else json.dumps(body).encode("utf-8")
    req = urllib.request.Request(
        f"{vault_uri}{path}",
        data=data,
        method=method,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        },
    )
    with urllib.request.urlopen(req, timeout=30) as response:
        return response.status, json.load(response)

status, secret = request(
    "GET",
    "/secrets/workshop-message?api-version=" + api_version,
)
secret_value = secret["value"]
print("PASSWORDLESS SECRET READ")
print(f"HTTP status: {status}")
print("Value exposed: no")
print(f"Length: {len(secret_value)}")
print(
    "SHA-256 prefix: "
    + hashlib.sha256(secret_value.encode()).hexdigest()[:12]
)

status, key = request(
    "GET",
    "/keys/app-signing-key?api-version=" + api_version,
)
key_id = key["key"]["kid"]
digest = hashlib.sha256(
    b"Azure Key Vault passwordless workshop"
).digest()
digest_b64url = base64.urlsafe_b64encode(
    digest
).rstrip(b"=").decode()

status, signature = request(
    "POST",
    key_id.removeprefix(vault_uri)
        + "/sign?api-version="
        + api_version,
    {
        "alg": "RS256",
        "value": digest_b64url
    },
)

status, verification = request(
    "POST",
    key_id.removeprefix(vault_uri)
        + "/verify?api-version="
        + api_version,
    {
        "alg": "RS256",
        "digest": digest_b64url,
        "value": signature["value"]
    },
)

print("")
print("PASSWORDLESS KEY OPERATION")
print("Algorithm: RS256")
print("Signature exposed: no")
print(
    "Verified: "
    + str(verification["value"]).lower()
)

status, certificate = request(
    "GET",
    "/certificates/workshop-tls?api-version="
        + api_version,
)
expires = datetime.datetime.fromtimestamp(
    certificate["attributes"]["exp"],
    tz=datetime.timezone.utc,
).strftime("%Y-%m-%d %H:%M UTC")

print("")
print("PASSWORDLESS CERTIFICATE READ")
print(f"HTTP status: {status}")
print(
    "Subject: "
    + certificate["policy"]["x509_props"]["subject"]
)
print(f"Thumbprint: {certificate['x5t']}")
print(f"Expires: {expires}")
print(
    "Private key exportable: "
    + str(
        certificate["policy"]["key_props"]["exportable"]
    ).lower()
)

print("")
print(
    "VALIDATION COMPLETE: MANAGED IDENTITY + RBAC"
)
PY
"@

17. Validate the passwordless operations

Role assignments can take several minutes to propagate. Run the validation command with the helper from section 14:

Invoke-EncodedRunCommand -Script $ValidationScript

Expected result:

PASSWORDLESS SECRET READ
HTTP status: 200
Value exposed: no
Length: 29
SHA-256 prefix: <redacted hash prefix>

PASSWORDLESS KEY OPERATION
Algorithm: RS256
Signature exposed: no
Verified: true

PASSWORDLESS CERTIFICATE READ
HTTP status: 200
Subject: CN=workshop.internal
Thumbprint: <public certificate thumbprint>
Expires: <UTC expiration>
Private key exportable: false

VALIDATION COMPLETE: MANAGED IDENTITY + RBAC
Managed identity reading a secret while masking its value.
Managed identity completing an RS256 sign and verify operation.
Managed identity reading certificate subject, thumbprint, version, expiry, and non-exportable policy.

18. Verify the security invariants

Confirm that no custom inbound NSG rule exists:

$InboundRules = az network nsg rule list `
    --resource-group $ResourceGroup `
    --nsg-name $NsgName `
    --query "[].name" `
    --output tsv

@(
    $InboundRules |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
).Count

Expected result:

0

Confirm that the certificate private key is non-exportable:

az keyvault certificate show `
    --vault-name $VaultName `
    --name $CertificateName `
    --query "policy.keyProperties.exportable" `
    --output tsv

Expected result:

false

Confirm that the VM has the user-assigned identity:

az vm identity show `
    --name $VmName `
    --resource-group $ResourceGroup `
    --query "contains(
        keys(userAssignedIdentities),
        '$($Identity.Id)'
    )" `
    --output tsv

Expected result:

true

19. Test certificate versioning

Key Vault uses immutable object versions. Creating a new certificate version does not replace the historical version.

List the versions:

az keyvault certificate list-versions `
    --vault-name $VaultName `
    --name $CertificateName `
    --query "[].{
        Version:id,
        Enabled:attributes.enabled,
        Created:attributes.created,
        Expires:attributes.expires
    }" `
    --output table

Inspect the lifetime action:

az keyvault certificate show `
    --vault-name $VaultName `
    --name $CertificateName `
    --query "policy.lifetimeActions" `
    --output jsonc

For a production certificate, test renewal in a non-production vault before relying on it operationally. Confirm that every consuming service notices and loads the new version.

20. Troubleshooting

The token request fails

Typical symptoms:

curl: (7) Failed to connect to 169.254.169.254
Identity not found

Check:

  • the command is running inside an Azure resource that supports managed identity;

  • the identity is attached to the VM;

  • the Metadata: true header is present;

  • the correct user-assigned client ID is supplied;

  • the token resource is https://vault.azure.net.

Inspect the VM identity configuration:

az vm identity show `
    --name $VmName `
    --resource-group $ResourceGroup `
    --output jsonc

Key Vault returns HTTP 403

Authentication succeeded, but authorization or networking failed.

Inspect the identity's role assignments:

az role assignment list `
    --assignee-object-id $Identity.PrincipalId `
    --scope $VaultId `
    --include-inherited `
    --query "[].{
        Role:roleDefinitionName,
        Scope:scope
    }" `
    --output table

Check:

  • the assignment uses the identity principal ID, not its client ID;

  • the role is a Key Vault data-plane role;

  • the assignment scope is the correct vault;

  • enough time has passed for propagation;

  • the vault firewall allows the network path.

The vault exists but object commands fail

Key Vault Contributor is a control-plane role. It does not grant permission to read or manage secrets, keys, and certificates.

Assign the correct object-specific role rather than using Key Vault Administrator as a default troubleshooting shortcut.

The certificate remains pending

Inspect the operation:

az keyvault certificate pending show `
    --vault-name $VaultName `
    --name $CertificateName `
    --output jsonc

For CA-issued certificates, check the issuer account, domain validation, certificate transparency behavior, and certificate-signing request workflow.

The certificate is readable as a secret

A Key Vault certificate has linked certificate, key, and secret objects. Review the complete data actions included in both Key Vault Secrets User and Key Vault Certificate User.

If a workload needs only public certificate metadata, use Key Vault Reader or a more constrained design rather than granting certificate-content access.

The VM has no outbound connectivity

Check the NIC and public IP association:

az network nic show `
    --name $NicName `
    --resource-group $ResourceGroup `
    --query "{
        ProvisioningState:provisioningState,
        PublicIp:ipConfigurations[0].publicIPAddress.id
    }" `
    --output jsonc

Production networks commonly use Azure NAT Gateway, Azure Firewall, or another controlled egress path instead of a per-VM public IP.

21. Production hardening

For a production implementation:

  1. Use one vault per application and environment.

  2. Separate identities by workload and permission boundary.

  3. Prefer vault-scoped role assignments over subscription-wide assignments.

  4. Use Privileged Identity Management for human administration.

  5. Use Private Link, selected networks, or a Network Security Perimeter.

  6. Enable Key Vault diagnostic logs and send them to a protected Log Analytics workspace.

  7. Alert on authorization failures, deletion, purge, and certificate expiry events.

  8. Use a trusted certificate authority for public or enterprise TLS certificates.

  9. Keep certificate private keys non-exportable unless a documented consumer requires export.

  10. Test application behavior during secret, key, and certificate version changes.

  11. Do not log access tokens, secret values, signatures, or certificate private material.

  12. Use Azure Policy to audit RBAC, purge protection, public access, and diagnostic settings.

22. Clean up the workshop

Review the resources before deletion:

az resource list `
    --resource-group $ResourceGroup `
    --query "[].{
        Name:name,
        Type:type
    }" `
    --output table

Delete the resource group:

az group delete `
    --name $ResourceGroup `
    --yes `
    --no-wait `
    --only-show-errors

Monitor deletion:

do {
    $Exists = az group exists `
        --name $ResourceGroup `
        --output tsv

    Write-Host "Resource group exists: $Exists"

    if ($Exists -eq "true") {
        Start-Sleep -Seconds 30
    }
}
until ($Exists -eq "false")

Verify the result:

az group exists `
    --name $ResourceGroup `
    --output tsv

Expected result:

false
Resource-group deletion verification and purge-protected vault retention notice.

Because purge protection is enabled, the deleted vault cannot be purged early. Its globally unique name remains reserved until the 90-day retention period expires. The soft-deleted Standard vault does not continue to generate a standing vault charge, but associated resources outside the deleted resource group must be reviewed separately.

Summary

This workshop created:

Azure Key Vault
    ├── Azure RBAC authorization
    ├── Soft delete: 90 days
    ├── Purge protection
    ├── Secret: workshop-message
    ├── RSA key: app-signing-key
    └── Certificate: workshop-tls
          ├── RSA-2048
          ├── six-month validity
          ├── 80% lifetime renewal action
          └── non-exportable private key

User-assigned managed identity
    ├── Key Vault Secrets User
    ├── Key Vault Crypto User
    └── Key Vault Certificate User

Ubuntu workload VM
    ├── no custom inbound NSG rules
    ├── IMDS token acquisition
    ├── passwordless secret read
    ├── passwordless sign and verify
    └── passwordless certificate read

The most important lessons are:

  1. Managed identity removes credentials from application configuration.

  2. Authentication and authorization are separate controls.

  3. Key Vault control-plane roles do not imply data-plane access.

  4. Use object-specific roles and the smallest practical assignment scope.

  5. Key Vault performs private-key operations without exporting the key.

  6. A Key Vault certificate creates linked certificate, key, and secret objects.

  7. A non-exportable certificate policy protects private material even when a caller can read certificate content.

  8. Role assignments are eventually consistent and require retry logic.

  9. Network access and RBAC must both permit a request.

  10. Validate real object operations, not only the presence of a role assignment.

Microsoft references

Comments


bottom of page