CAMPUX Cloud Bootcamp Lab · Identity & Governance ← All labs
Hands-On Lab · Intermediate
~35 min · Free tier · Cloud Shell
CLI & Bicep · torn down at the end
Identity & Governance · Lab A

Read a secret with zero credentials.

Campux Retail's product API needs a database connection string. Juniors paste it into code and it leaks. You'll do it the way that doesn't: the secret in Key Vault, the app holding a managed identity with no password to steal, and a role scoped so tightly it can read that one thing and nothing else.

Fig. 1 · Managed identity + least privilege
Resource group · rg-lab-areads secret · role: Key Vault Secrets UserApp Servicesystem-assigned identityManaged identitythe app’s own principalKey VaultRBAC mode · one secretno secret in the app — the identity is the secret
A line diagram: an App Service box with a managed identity presents a token to Microsoft Entra ID, then reads a secret from an Azure Key Vault. No password appears anywhere. App Service Campux API managed identity Microsoft Entra ID Key Vault (RBAC) secret: ProductDb… Secrets User only 1 · prove who I am token, no password 2 · read the secret — with the token, not a key
The whole idea The app authenticates as itself with a token Entra issues to its managed identity; nothing in your code holds a password. The role on the vault is read-only secrets, scoped to one vault.
● Screen walkthrough Not yet recorded · ~8 min
Reel · 00:00 / 08:00

Assign a role to a managed identity and prove access works with no secret stored anywhere.

Placeholder — the page below stands alone until the reel lands
Why

Managed identity and least privilege, in one task

A managed identity is a credential Azure creates and rotates for you — there is no secret to store, and therefore none to leak. You give it to the app, grant it a role, and the app authenticates as itself. The second half of the lesson is the role: not "can touch the vault", but exactly Key Vault Secrets User — read secret contents, nothing more. No write, no keys, no certificates. That pairing — an identity with no password, holding the narrowest role that does the job — is what "secure by default" actually looks like in practice, and it is the thing an interviewer is checking when they ask how your app gets its secrets.

The secret nobody can paste is the secret nobody can leak.

Before you begin — one-time setup

You need a free Azure account, the Azure CLI (az), then be signed in with az login. First time? The 15-minute Set up your machine page covers the account, the installs (winget / brew / apt), and sign-in. Prefer zero installs? Run everything in Azure Cloud Shell (Bash), preinstalled and already signed in.

The companion repository

Run everything in Azure Cloud Shell (Bash). The tiny app and a Bicep version of this whole setup are at github.com/kloudcaptain/campux-labs.

Before

What you need

You need permission to assign rolesOwner or User Access Administrator on the subscription. A plain Contributor cannot, and the role commands below will fail with AuthorizationFailed. On your own subscription you are Owner. Run it all in Cloud Shell (Bash); az, zip, and curl are already there.

Cost

App Service F1 (Free), a Key Vault (about $0.03 per 10,000 operations), one secret. A fraction of a cent, and the teardown at the end removes all of it.

Step 0

Variables

Paste this once. $RANDOM gives every resource a unique suffix so global names never collide. The first two lines are one-time guards explained in the box below.

# Windows/Git Bash: stop it rewriting /subscriptions/... arguments (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1

# if you have more than one subscription, make the intended one active
az account set --subscription "$(az account show --query id -o tsv)"

SUFFIX=$RANDOM
RG="campux-lab-rbac-rg"
LOCATION="eastus"
KV="campux-kv-$SUFFIX"        # 3-24 chars, starts with a letter
APP="campux-api-$SUFFIX"      # globally unique across *.azurewebsites.net
PLAN="campux-plan-$SUFFIX"
SECRET_NAME="ProductDbConnection"

SUB=$(az account show --query id -o tsv)
VAULT_SCOPE="/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.KeyVault/vaults/$KV"
echo "Suffix=$SUFFIX  KV=$KV  APP=$APP  SUB=$SUB"
Checkpoint The echo prints your names and a non-empty SUB. If $SUB is empty you're not signed in — run az login.
Two guards that save Windows and multi-subscription users

export MSYS_NO_PATHCONV=1 — on Windows, Git Bash silently rewrites any argument starting with / into a Windows path before handing it to az, so --scope /subscriptions/… arrives mangled and Azure replies MissingSubscription. This turns that off for the session. It does nothing on macOS/Linux, so it's safe to always run. az account set — if your login has more than one subscription (a personal Azure account often does), this makes sure commands act on the one you mean. Both are one-liners you set once per terminal.

Step 1

Resource group and an RBAC-mode Key Vault

az group create --name "$RG" --location "$LOCATION"
az keyvault create --name "$KV" --resource-group "$RG" \
  --location "$LOCATION" --enable-rbac-authorization true
Checkpoint az keyvault show --name "$KV" --query "properties.enableRbacAuthorization" -o tsv prints true. That flag makes the vault use Azure roles instead of legacy access policies — the whole premise of the lab.
Step 2

Grant yourself, then create the secret

An RBAC vault does not let its creator read or write secret data automatically. Grant yourself the data-plane role first — this surprises almost everyone.

# your user object id — the normal way (work/school accounts)
ME=$(az ad signed-in-user show --query id -o tsv 2>/dev/null)

# personal accounts (Gmail/Outlook) return nothing above — read the oid from your token instead:
if [ -z "$ME" ]; then
  PAYLOAD=$(az account get-access-token --query accessToken -o tsv | cut -d. -f2 | tr '_-' '/+')
  case $(( ${#PAYLOAD} % 4 )) in 2) PAYLOAD="$PAYLOAD==";; 3) PAYLOAD="$PAYLOAD=";; esac
  ME=$(printf '%s' "$PAYLOAD" | base64 -d 2>/dev/null | grep -o '"oid":"[^"]*"' | cut -d'"' -f4)
fi
echo "ME=$ME"      # must be a GUID before you continue

az role assignment create --role "Key Vault Secrets Officer" \
  --assignee-object-id "$ME" --assignee-principal-type User --scope "$VAULT_SCOPE"

# role assignments take a minute to propagate
sleep 60
az keyvault secret set --vault-name "$KV" --name "$SECRET_NAME" \
  --value "Server=tcp:campux-sql.database.windows.net,1433;Database=products;Authentication=Active Directory Default;"
Checkpoint az keyvault secret show --vault-name "$KV" --name "$SECRET_NAME" --query value -o tsv prints the string. (If it returns Forbidden, the role is still propagating — wait 60s and re-run the secret set line. Not a mistake in your commands.)
Why the object-id has a fallback

az ad signed-in-user show asks Entra "who am I?" — but a personal Microsoft account (a Gmail or Outlook address on a free subscription) is a guest in its own directory and that call comes back empty. Rather than chase a mangled guest username, the fallback reads the oid (object id) claim already inside your sign-in token: same GUID, works for every account type. If echo "$ME" ever shows blank, stop — the role grant can't work without it.

Step 3

App Service with a managed identity

Heads up — App Service quota can be 0 in your region

Many new/free subscriptions start with an App Service quota of 0 "Total VMs" in some regions, including eastus. If az appservice plan create fails with "Current Limit (Total VMs): 0", that's a subscription limit, not a mistake. The loop below creates the plan in the first region that has free-tier quota — the app can live in a different region from the vault and still works.

# Create the Free (F1) plan in the first region that has quota.
for LOC in "$LOCATION" eastus2 westus2 centralus westus3 northeurope westeurope; do
  echo "Trying $LOC ..."
  if az appservice plan create --name "$PLAN" --resource-group "$RG" --sku F1 --is-linux --location "$LOC" 1>/dev/null 2>&1; then
    echo ">>> Plan created in $LOC"; break
  else
    echo "    no F1 quota in $LOC — trying the next region"
  fi
done

# Auto-pick a currently-supported Node LTS. Azure retires versions over time, and
# list-runtimes now returns objects, so query .config and swap the | for a : that --runtime wants.
RT=$(az webapp list-runtimes --os-type linux --query "[?runtime=='Node'].config" -o tsv | grep -i lts | head -1 | tr '|' ':')
echo "runtime: $RT"
az webapp create --name "$APP" --resource-group "$RG" --plan "$PLAN" --runtime "$RT"
az webapp identity assign --name "$APP" --resource-group "$RG"
APP_PID=$(az webapp identity show --name "$APP" --resource-group "$RG" --query principalId -o tsv)
echo "App identity object id: $APP_PID"
Checkpoint $APP_PID is a GUID. That GUID is the app's identity in Entra ID — no password anywhere. (If every region reports "no F1 quota," request an increase in the portal under Quotas → App Service, then re-run.)
Step 4

Grant the app least privilege

The app only needs to read secrets. Give it exactly that — Key Vault Secrets User — scoped to this one vault. Not the resource group, not the subscription, not Officer (which could write).

az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee-object-id "$APP_PID" \
  --assignee-principal-type ServicePrincipal \
  --scope "$VAULT_SCOPE"
Checkpoint — least privilege by inspection az role assignment list --scope "$VAULT_SCOPE" --query "[?principalId=='$APP_PID'].{role:roleDefinitionName, type:principalType}" -o table shows exactly one role: Key Vault Secrets User. Read of secret contents only — no write, no keys, no certificates. (We filter by principalId at the vault scope rather than --assignee "$APP_PID" on purpose: a brand-new managed identity takes a minute to appear in the Entra graph, and --assignee would error with "Cannot find … in graph database" until it does — even though the role is already granted.)
Step 5

Wire the secret in, deploy, and prove it

Point an app setting at the secret with a Key Vault reference. App Service resolves it using the managed identity before your code runs.

az webapp config appsettings set \
  --name "$APP" --resource-group "$RG" \
  --settings "[email protected](SecretUri=https://$KV.vault.azure.net/secrets/$SECRET_NAME)"

Get the tiny zero-dependency app and deploy it, then restart once — the restart forces App Service to re-fetch the reference with the role you just set, and warms the free-tier container so your first request isn't a cold-start 502.

git clone https://github.com/kloudcaptain/campux-labs.git
cd campux-labs/lab-a-rbac-managed-identity/app
# Git Bash on Windows often has no 'zip' — fall back to PowerShell's Compress-Archive
zip app.zip server.js package.json 2>/dev/null \
  || powershell -NoProfile -Command "Compress-Archive -Path server.js,package.json -DestinationPath app.zip -Force"
az webapp deploy --name "$APP" --resource-group "$RG" --src-path app.zip --type zip

az webapp restart --name "$APP" --resource-group "$RG"
sleep 45
for i in 1 2 3 4 5; do
  echo "--- attempt $i ---"
  curl -sS "https://$APP.azurewebsites.net" && break
  sleep 20
done
Checkpoint — the payoff The response prints the real connection string on the "Value App Service resolved" line. That string came from Key Vault, fetched by the app's managed identity, with zero credentials in the code. That is the entire lab, proven. (If it still shows @Microsoft.KeyVault(...), the role is propagating — wait a minute, az webapp restart, and curl again.)
Down

Tear it down

Delete everything, then purge the vault so its soft-deleted shell doesn't linger (Key Vault keeps deleted vaults recoverable for 90 days).

az group delete --name "$RG" --yes
az keyvault purge --name "$KV" --location "$LOCATION"

az group exists --name "$RG"                                # -> false
az keyvault list-deleted --query "[?name=='$KV']" -o tsv    # -> empty
Checkpoint Both false / empty — back to zero, no surprise bill, no reserved names.
End

What you can now honestly claim

You configured an App Service with a system-assigned managed identity and a least-privilege Key Vault Secrets User role, and read a secret from Key Vault with no credentials in the code. Four things worth keeping: a managed identity is a credential Azure manages so there is nothing to leak; RBAC grants Key Vault data access through Azure roles, and the creator is not automatically among them; least privilege is the narrowest role at the narrowest scope; and Key Vault references inject secrets as app settings with no code change. The same repo has a Bicep version that declares all of this as code.