Skip to content
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 no stored Azure login secret.

Campux’s app needs a harmless training value from Key Vault. Give its Azure host a managed identity, grant the required data role, and prove that the reference resolves without displaying the value. Azure manages the identity’s credentials; your team still protects the host, token and permissions.

Fig. 1 · Managed identity + least privilege
Resource group · campux-lab-rbac-rgreads secret · role: Key Vault Secrets UserApp Servicesystem-assigned identityManaged identitythe app’s own principalKey VaultRBAC mode · one secretidentity authenticates; the role authorises
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 Azure client secret stored in the app.

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

Run one step, then check it

Read the step before copying. Bash commands belong in Git Bash on Windows or Azure Cloud Shell set to Bash; PowerShell uses different syntax. A JSON, YAML, Bicep or application-code example belongs in the named file, not directly at the terminal prompt. Follow any shell-specific instructions below.

Keep the same terminal open so earlier variables remain available. Copy one complete command block without the terminal's $ prompt. A backslash continues a command onto the next line. If Bash shows a lone >, it is waiting for the rest of a quote, command or file block. Finish the complete block; if the paste was incomplete, press Ctrl+C and copy it again. Do not paste the next step into an unfinished command.

A line such as RG="training-group" stores a value without printing it. Output captured with $(...) is also stored; a blank screen alone does not prove failure. A sleep deliberately waits, and -o none suppresses normal output. Wait for your normal prompt, then run the step's checkpoint. After an error, stop and keep the exact command and message; later steps cannot repair a missing resource. Never share passwords, access tokens or secret values in a screenshot.

Why

Managed identity and least privilege, in one task

Think of the app’s identity as its own badge. Enabling it issues the badge; the data role grants entry to this vault. In this lab App Service resolves a Key Vault reference using its system-assigned identity, then supplies the harmless training value to the running app. Your sign-in creates the resources; the application’s identity performs the reference lookup.

An identity still needs permission.

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 roles — Owner or User Access Administrator on the subscription. A plain Contributor cannot, and the role commands below will fail with AuthorizationFailed. The teardown's last step, az keyvault purge, needs more than that: only a subscription Owner or a principal holding Key Vault Purge Operator can purge a vault, so User Access Administrator alone is not enough to finish the teardown. On your own subscription you are Owner, which covers both. Run it all in Cloud Shell (Bash); az, zip, and curl are already there.

Cost and permission

Use the F1 plan only where supported and available in your subscription; Key Vault operations can incur charges. Check current Azure pricing and quotas. Resource creation and role-assignment permission are separate; User Access Administrator alone does not grant permission to create the app or vault. Use a disposable group and delete it when finished.

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.

Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing.

# 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-$SUFFIX-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 are 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 is 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.

Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing. This block includes a deliberate wait. Leave this terminal open until the normal prompt returns; a wait is not proof that access has propagated.

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

# If lookup fails, inspect the error and use your own user object ID from Entra.
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 id -o tsv prints the secret identifier, not its value. (If it returns Forbidden, inspect the assignment, identity and vault network rules. Propagation is one possible cause, not the only one. Retry after checking those facts.)
Check the object ID

The signed-in-user lookup can fail because of directory permissions or account context. Do not assume the cause from an empty value. You can copy your own object ID from the tenant’s user page instead. Stop if ME is empty; do not paste tokens into logs or shared files.

Step 3

App Service with a managed identity

Heads up — Check App Service quota 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 is 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.

Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing.

# 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 — the app’s identity was enabled; no client secret was generated. (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. Inspect this role’s current definition and other direct or inherited grants; this one row alone does not prove effective access. (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 "DB_CONNECTION=@Microsoft.KeyVault(SecretUri=https://$KV.vault.azure.net/secrets/$SECRET_NAME)"

Create this small app locally. It reports only whether the training reference resolved; it never returns the stored value. Do not substitute a real database credential.

mkdir -p campux-identity-demo
cd campux-identity-demo
cat > package.json <<'JSON'
{"name":"campux-identity-demo","version":"1.0.0","scripts":{"start":"node server.js"}}
JSON
cat > server.js <<'JS'
const http = require('node:http');
http.createServer((req, res) => {
  const value = process.env.DB_CONNECTION || '';
  const resolved = value.length > 0 && !value.startsWith('@Microsoft.KeyVault(');
  res.writeHead(resolved ? 200 : 503, {'Content-Type': 'application/json'});
  res.end(JSON.stringify({referenceResolved: resolved}));
}).listen(process.env.PORT || 8080);
JS
# Git Bash on Windows has no zip; use this line instead:
# powershell.exe -NoProfile -Command "Compress-Archive -Force -Path server.js,package.json -DestinationPath app.zip"
zip app.zip server.js package.json
az webapp deploy --name "$APP" --resource-group "$RG" --src-path app.zip --type zip
az webapp restart --name "$APP" --resource-group "$RG"
curl --fail-with-body "https://$APP.azurewebsites.net"
If it keeps answering 503 with referenceResolved false

Ask App Service what it thinks of the reference: az rest --method get --url "https://management.azure.com/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.Web/sites/$APP/config/configreferences/appsettings?api-version=2022-03-01" --query "value[].properties.status" -o tsv. AccessToKeyVaultDenied long after the role was granted usually means App Service cached its first failed attempt. Run the Step 5 appsettings set line again, which makes it re-read the reference, then az webapp restart. When we ran this lab in October 2026 the reference stayed denied for over ten minutes until we did exactly that; the next request returned 200.

Checkpoint — the payoff{"referenceResolved":true} with HTTP 200 proves the app received a resolved value; it does not print the value. A 503 with false means it is missing or unresolved. Check reference status, the identity object ID, its data role and vault networking. Allow for role propagation and restart before retrying; do not treat any error as proof of propagation.
Down

Tear it down

Delete everything, then purge the vault so its soft-deleted shell does not 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, lab resources removed; usage already incurred may still be billed, 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 has credentials Azure manages, while tokens and the host still need protection; 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.