- 10 hours ago
- 14 min read

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 minutesArchitecture
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 UserThe 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 operationControl 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 | <vault>.vault.azure.net | 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 versionSign in:
az loginList the available subscriptions:
az account list `
--query "[].{
Name:name,
IsDefault:isDefault,
State:state
}" `
--output tableSelect the target subscription:
$SubscriptionId = "<your-subscription-id>"
az account set `
--subscription $SubscriptionIdConfirm the active context without printing tenant or user identifiers:
az account show `
--query "{
Subscription:name,
State:state
}" `
--output table2. 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 noneVerify the resource group:
az group show `
--name $ResourceGroup `
--query "{
Name:name,
Location:location,
State:properties.provisioningState
}" `
--output table4. 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 noneVerify 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

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 noneInspect 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 jsonc7. 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 noneVerify 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 jsoncThe 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 noneCertificate 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


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 representationThis 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 noneResolve its identifiers:
$Identity = (
az identity show `
--name $IdentityName `
--resource-group $ResourceGroup `
--query "{
ClientId:clientId,
PrincipalId:principalId,
Id:id
}" `
--output json
) | ConvertFrom-JsonThe 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 noneCreate 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 noneCreate 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 noneCreate 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 noneNo 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 noneThe 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
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 $NegativeScriptExpected result:
PASSWORDLESS NEGATIVE TEST
HTTP status: 403
Key Vault result: Forbidden
Outcome: EXPECTED DENY
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
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:
requests a token through IMDS;
reads the demonstration secret but prints only its length and a hash prefix;
signs a digest with the RSA key;
asks Key Vault to verify the signature;
reads the certificate subject, thumbprint, expiration, and exportability policy;
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 $ValidationScriptExpected 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


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($_) }
).CountExpected result:
0Confirm that the certificate private key is non-exportable:
az keyvault certificate show `
--vault-name $VaultName `
--name $CertificateName `
--query "policy.keyProperties.exportable" `
--output tsvExpected result:
falseConfirm that the VM has the user-assigned identity:
az vm identity show `
--name $VmName `
--resource-group $ResourceGroup `
--query "contains(
keys(userAssignedIdentities),
'$($Identity.Id)'
)" `
--output tsvExpected result:
true19. 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 tableInspect the lifetime action:
az keyvault certificate show `
--vault-name $VaultName `
--name $CertificateName `
--query "policy.lifetimeActions" `
--output jsoncFor 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 foundCheck:
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 jsoncKey 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 tableCheck:
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 jsoncFor 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 jsoncProduction 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:
Use one vault per application and environment.
Separate identities by workload and permission boundary.
Prefer vault-scoped role assignments over subscription-wide assignments.
Use Privileged Identity Management for human administration.
Use Private Link, selected networks, or a Network Security Perimeter.
Enable Key Vault diagnostic logs and send them to a protected Log Analytics workspace.
Alert on authorization failures, deletion, purge, and certificate expiry events.
Use a trusted certificate authority for public or enterprise TLS certificates.
Keep certificate private keys non-exportable unless a documented consumer requires export.
Test application behavior during secret, key, and certificate version changes.
Do not log access tokens, secret values, signatures, or certificate private material.
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 tableDelete the resource group:
az group delete `
--name $ResourceGroup `
--yes `
--no-wait `
--only-show-errorsMonitor 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 tsvExpected result:
false
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 readThe most important lessons are:
Managed identity removes credentials from application configuration.
Authentication and authorization are separate controls.
Key Vault control-plane roles do not imply data-plane access.
Use object-specific roles and the smallest practical assignment scope.
Key Vault performs private-key operations without exporting the key.
A Key Vault certificate creates linked certificate, key, and secret objects.
A non-exportable certificate policy protects private material even when a caller can read certificate content.
Role assignments are eventually consistent and require retry logic.
Network access and RBAC must both permit a request.
Validate real object operations, not only the presence of a role assignment.
Comments