Skip to content
CAMPUX Cloud Bootcamp Lab · Identity & Governance ← All labs
Hands-On Lab · Advanced
Training resources · Azure charges may apply · Bash
CLI & Bicep · torn down at the end
Identity & Governance · Lab D

Make the wrong thing impossible.

A teammate enables public blob access while creating a storage account. Put a reviewed Deny rule on one training resource group, then compare the rejected request with a compliant one. The rule protects the configuration it checks; it does not replace access control or make every storage path private.

Fig. 1 · Policy-as-code: Deny prevents
Resource group · campux-lab-policy-rgallowedCustom policyeffect: DenyCompliant resourceBad resourceblocked at create
● Screen walkthrough Not yet recorded · ~7 min
Reel · 00:00 / 07:00

Define a policy in code, deploy it, then watch it deny a non-compliant resource on camera.

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

Audit reports; Deny prevents

Azure ships hundreds of built-in policies, and you can write your own. The lever that matters is the effect. An Audit policy tells you, later, that something is wrong — useful, but the bad thing already exists. A Deny policy refuses the create outright, so the non-compliant resource never comes into being. This lab uses Deny, because it is both the stronger control and the one you can prove on the spot: you will try to create a storage account with public blob access and watch Azure reject it, then create a compliant one and watch it succeed. Guardrails, not a report you read after the incident.

A rule you cannot break beats a rule you are told to keep.

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.

Role & cost

You need permission to create policy definitions at the subscription, assign policies at the training resource group, and create/delete the group and storage accounts. Resource Policy Contributor covers policy management, but does not by itself grant resource creation. A training subscription Owner can perform these steps; use reviewed narrower grants where appropriate. Run it in Azure Cloud Shell (Bash). Storage accounts and any resources left behind can incur charges. This exercise writes no blob data; run cleanup and verify it rather than assuming a free bill. Bicep version and this guide: github.com/kloudcaptain/campux-labs.

Step 0

Choose the subscription and prepare the names

Campux wants a guardrail for one disposable group, not a policy rollout across production. This block creates that group and prints its name. The variable assignments are quiet until the final summary; stay in the same Bash terminal for the remaining steps.

export MSYS_NO_PATHCONV=1
campux_prepare_policy() {
  az account show --query '{subscription:name,tenant:tenantId}' -o table || return 1
  SUFFIX="$(date +%s)$RANDOM"
  RG="campux-policy-$SUFFIX"
  LOCATION="eastus"
  DEF_NAME="cx-deny-blob-$SUFFIX"
  ASSIGN_NAME="cxdeny$SUFFIX"
  AUDIT_NAME="cxaudit$SUFFIX"
  SUB=$(az account show --query id -o tsv) || return 1
  [ -n "$SUB" ] || { echo "STOP: no active subscription."; return 1; }
  RG_SCOPE="/subscriptions/$SUB/resourceGroups/$RG"
  BAD_SA="cxbad$SUFFIX"
  GOOD_SA="cxgood$SUFFIX"
  echo "Creating the disposable resource group..."
  az group create --name "$RG" --location "$LOCATION" --query provisioningState -o tsv || return 1
  printf 'Ready: RG=%s bad=%s good=%s
' "$RG" "$BAD_SA" "$GOOD_SA"
}
campux_prepare_policy
Checkpoint Expect the account table, Succeeded, then Ready:. If the group-create fails, stop. Check your active subscription, role and any existing policies before trying another step.
Step 1

Assign the built-in audit policy

Audit is the observer: it records non-compliance after evaluation without blocking the request. Here the assignment will cover only the group you just created. The lookup is stored in BUILTIN, so it would normally print nothing; this version shows the result and refuses to assign an empty ID.

campux_assign_audit() {
  echo "Looking up the secure-transfer audit policy..."
  BUILTIN=$(az policy definition list --query "[?displayName=='Secure transfer to storage accounts should be enabled'].name | [0]" -o tsv) || return 1
  [ -n "$BUILTIN" ] || { echo "STOP: built-in policy not found. Do not continue with an empty ID."; return 1; }
  printf 'Found: %s
' "$BUILTIN"
  echo "Assigning audit to this training group..."
  az policy assignment create --name "$AUDIT_NAME" --display-name "Campux secure transfer audit" --policy "$BUILTIN" --scope "$RG_SCOPE" --query name -o tsv || return 1
}
campux_assign_audit
Checkpoint Expect Found: with a policy ID, then your cxaudit... assignment name. Assignment success is not a completed compliance scan. Audit results arrive after evaluation; the next steps separately prove Deny enforcement.
Step 2

Write the rule file, then create the definition

The rule asks two questions: is this a storage account, and does it allow public blob access? Only when both are true does Deny apply. First write valid JSON to a local file, then pass that file to Azure CLI. Copy the whole block, including the final JSON line with no indentation; a lone > means Bash is still waiting for that delimiter. The JSON file is the policy rule, not a resource deployment.

campux_define_policy() {
  RULE_FILE="campux-policy-$SUFFIX.json"
  cat > "$RULE_FILE" <<'JSON'
{
  "if": {
    "allOf": [
      {"field": "type", "equals": "Microsoft.Storage/storageAccounts"},
      {"field": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess", "equals": true}
    ]
  },
  "then": {"effect": "deny"}
}
JSON
  echo "Rule file saved. Creating the subscription-level definition..."
  POLICY_ID=$(az policy definition create --name "$DEF_NAME" --display-name "Campux training: deny public blob access" --mode Indexed --rules "$RULE_FILE" --query id -o tsv) || return 1
  [ -n "$POLICY_ID" ] || { echo "STOP: definition ID is missing."; return 1; }
  az policy definition show --name "$DEF_NAME" --query '{name:name,effect:policyRule.then.effect}' -o json || return 1
}
campux_define_policy
Checkpoint Expect Rule file saved, then JSON showing this definition name and "effect": "deny". If the file fails to parse, check that it contains only the JSON shown above. If definition creation fails with AuthorizationFailed, the group-level resource role is not enough to create a subscription-level policy definition.
Step 3

Assign the definition to the resource group

The definition is the reusable rule; the assignment chooses where it applies. This assignment covers only this exercise’s group. A saved assignment is not evidence that a new request has already been evaluated against it.

campux_assign_deny() {
  [ -n "$POLICY_ID" ] || { echo "STOP: complete Step 2 first."; return 1; }
  echo "Assigning Deny to the disposable group..."
  az policy assignment create --name "$ASSIGN_NAME" --policy "$POLICY_ID" --scope "$RG_SCOPE" --query name -o tsv || return 1
  az policy assignment show --name "$ASSIGN_NAME" --scope "$RG_SCOPE" --query '{name:name,scope:scope,policy:policyDefinitionId}' -o json || return 1
}
campux_assign_deny
Checkpoint Expect the assignment name and JSON with your group scope and definition ID. Read both before proceeding. New assignments can take time to enforce; Step 4 checks the actual request.
Step 4

Prove the insecure request is denied

You are checking an observable result, not waiting a guaranteed number of minutes. Each attempt announces the storage request before it runs. A successful insecure create means the new guardrail has not blocked that request; delete that account before trying again. A permission, quota, naming or network error is not proof of this policy. The function stops on unrelated errors and stops after ten attempts rather than silently continuing.

campux_prove_policy() {
  DENIED=false
  for i in 1 2 3 4 5 6 7 8 9 10; do
    echo "Attempt $i of 10: submitting the insecure storage request..."
    RESULT=$(az storage account create --name "$BAD_SA" --resource-group "$RG" --location "$LOCATION" --sku Standard_LRS --allow-blob-public-access true -o none 2>&1)
    STATUS=$?
    if [ "$STATUS" -eq 0 ]; then
      echo "Request succeeded; removing that training account before retrying..."
      az storage account delete --name "$BAD_SA" --resource-group "$RG" --yes || return 1
      echo "Waiting 30 seconds before the next attempt..."
      sleep 30
    elif printf '%s' "$RESULT" | grep -qi 'RequestDisallowedByPolicy' && printf '%s' "$RESULT" | grep -Fqi "$DEF_NAME"; then
      echo "DENIED: the error identifies this exercise's policy definition."
      DENIED=true
      break
    else
      printf 'STOP: this error is not verified as our policy denial:
%s
' "$RESULT"
      return 1
    fi
  done
  [ "$DENIED" = true ] || { echo "STOP: denial was not verified. Check the assignment and retry this step later."; return 1; }
  echo "Submitting the compliant storage request..."
  az storage account create --name "$GOOD_SA" --resource-group "$RG" --location "$LOCATION" --sku Standard_LRS --allow-blob-public-access false --query provisioningState -o tsv || return 1
  echo "CREATED: the compliant request succeeded."
}
campux_prove_policy
Checkpoint You need both DENIED identifying this definition and CREATED. Keep the actual Azure error if it does not identify the expected definition; another policy may be blocking the request. A compliant-create failure is a separate problem, not a reason to remove all policies. You have tested this one configuration, not every possible way storage data can be exposed.
Step 5

Policy as code (Bicep)

Clicking policies into the portal does not scale or survive an audit. The Bicep version declares the same custom definition and assignment as code. Unlike the identity labs, policy is fully ARM-native, so the whole thing is Bicep — no Graph split. One scope note: policy definitions live at subscription (or management-group) level, so that template targets the subscription, not a resource group.

Down

Remove this exercise’s assignments, definition and resources

The definition lives at subscription scope, and the assignments must be removed explicitly. Use the unique names printed in Step 0. If you opened a new terminal, recover those exact names from the portal; do not paste cleanup with empty variables. This function stops on a failed deletion so you can check what remains.

campux_cleanup_policy() {
  [ -n "$RG" ] && [ -n "$DEF_NAME" ] && [ -n "$ASSIGN_NAME" ] && [ -n "$AUDIT_NAME" ] || { echo "STOP: exercise variables are missing."; return 1; }
  echo "Removing the Deny assignment..."
  az policy assignment delete --name "$ASSIGN_NAME" --scope "$RG_SCOPE" || return 1
  echo "Removing the audit assignment..."
  az policy assignment delete --name "$AUDIT_NAME" --scope "$RG_SCOPE" || return 1
  echo "Removing the custom definition..."
  az policy definition delete --name "$DEF_NAME" || return 1
  echo "Deleting the disposable resource group; wait for the prompt..."
  az group delete --name "$RG" --yes || return 1
  echo "Resource group still exists? Expect false:"
  az group exists --name "$RG" || return 1
  echo "Matching custom definitions? Expect []:"
  az policy definition list --query "[?name=='$DEF_NAME'].name" -o json || return 1
}
campux_cleanup_policy
Checkpoint Expect false and []. If you stopped partway through the lab, a not-found deletion can mean that object was never created; verify it in the portal and continue cleanup of the objects that do exist. The local JSON rule file can remain as portfolio evidence; it has no credentials.
End

What you can now honestly claim

You authored and assigned a custom Azure Policy with a Deny effect to enforce storage security, proved it blocks a non-compliant resource while allowing a good one, and saw how the same governance is deployed as code. Keep four things: an applicable Deny can reject a matching create or update request; it does not delete existing resources or prove all access paths are safe; built-ins cover most needs, and when none fits you write an if/then rule with an effect; effects matter — Audit reports, Deny blocks; and scope controls blast radius — definitions at subscription or management-group level, assignments at a subscription, resource group, or below. This completes the Identity & Governance track. Its companion is the Resource Locks lab: policy stops non-compliant creation, locks stop deletion.