promptus

promptus

Azure · Terraform · Bicep · FortiGate · Git · DevOps

Workflows by task

All topics (51)

Start

Azure

Sign-in Workflows6 workflows: Sign in to Azure, Set subscription (CLI and PowerShell), Unblock and run downloaded scripts …App Gateway Workflows6 workflows: App Gateway: add a certificate from Key Vault, App Gateway: renew / replace a certificate, Import a PFX into Key Vault …Permissions Workflows5 workflows: Give a service principal a role (subscription, management group, root), Create a managed identity and assign a role, Which permissions do I (or someone else) have? …CLI Basicssign-in, filters, resources, VMs, App Service, storage, monitoringPowerShell (Az)Az moduleCLI vs PowerShellthe same task in both worldsApp Service and Azure Functionsplans/SKUs, create, deploy, slots + swap, Key Vault references, VNet integration, access restrictions, domain + certificate, FunctionsAKScreate (CNI overlay, Entra RBAC), kubectl daily, node pools, upgrade, ACR, private cluster, troubleshootingVirtual Desktophost pool, app group, workspace, token, drain mode, user sessions, network, KQLArc (Servers)onboarding with service principal, azcmagent, extensions, troubleshootingBackup and Recoveryvaults, protect VMs, backup now, restore, soft delete/immutability, ASR test failoverUpdate Managerassessment, install now, maintenance configuration (patch schedule), compliance queriesStorage in Daily WorkAzCopy, SAS, restore, protection, Files, lifecycleKey Vault, Roles, IdentityKey Vault, managed identity, RBAC, Entra ID, policyEntra IDapp registrations, expiring secrets, groups, PIMDefendersecure score, recommendations, plans, alerts, JITManagement Groupslanding zone structure, hierarchy, move a subscriptionAzure Policyeffects, assign, custom definitions, initiatives, Modify/DINE + remediation, exemptions, compliance, policy as codeMonitoring and Alertsaction groups, metric/activity/log alerts, maintenance windowsResource Graphready-made queries across all subscriptions: inventory, network, security, cleanup, tags

Network and Firewall

Code and DevOps

Clients and Logs

Look up

#I want to …

#Whole tasks

Step by step, one page per area: Copy-Paste Workflows.

#Azure General

…CommandDetails
switch the subscriptionaz account set --subscription <x>Sign-in and Subscription
find a resource without knowing whereaz graph query -q "Resources | where name contains '<x>'"Resource Graph
see who changed whataz monitor activity-log list -g <rg> --offset 2hMonitoring and Diagnostic Settings
install a PowerShell module when the profile is blockedas admin: Install-Module <module> -Scope AllUsers -Repository PSGallery -ForceInstall Modules Without the User Profile
run a command in a VMaz vm run-command invoke … --scripts "<cmd>"Virtual Machines
stop VM costsaz vm deallocate -g <rg> -n <vm>Virtual Machines
check whether a SKU works in a zoneaz vm list-skus -l <region> --size <sku>Virtual Machines
accept Marketplace termsaz vm image terms accept --urn <urn>Marketplace Images
see a web app's live logaz webapp log tail -g <rg> -n <app>App Service / Functions
deploy a web app (zip)az webapp deploy -g <rg> -n <app> --src-path app.zip --type zipDeploy Code
swap staging into productionaz webapp deployment slot swap … --slot staging --target-slot productionDeployment Slots
use a Key Vault secret in an app setting@Microsoft.KeyVault(SecretUri=…)Settings, Secrets and Key Vault References
allow only Front Door to reach a web appaz webapp config access-restriction add … --service-tag AzureFrontDoor.BackendNetwork: VNet Integration, Private Endpoint, Access Restrictions
back up VMs (vault + policy + VM list)Generator → Backup → Protect VMsCreate a Vault and Protect a VM
restore a VM, its disks or single filesGenerator → Backup → Restore a VMRestore
connect to a VM via Bastion (RDP / SSH / tunnel)Generator → Virtual Machines → BastionVirtual Machines
set or temporarily remove a delete lockGenerator → Governance → Delete lockResource Groups, Resources, Locks
tag resource groups and all their resourcesGenerator → Governance → Tag resource groupsTag Standard
find orphaned disks, NICs, public IPsGenerator → Costs and Cleanup → Find orphaned …Cleanup and Costs
check in the firewall log whether traffic was allowedGenerator → Logs and Monitoring → Firewall logsLog Analytics (KQL)
connect kubectl to AKSaz aks get-credentials -g <rg> -n <aks>Connect (kubectl)
see why a pod does not startkubectl describe pod <pod> -n <ns>kubectl in Daily Work
upgrade AKSaz aks get-upgrades → az aks upgradeUpgrade

#Network

…CommandDetails
know which route appliesaz network nic show-effective-route-table -g <rg> -n <nic>Effective Routes and Connectivity Tests
check the next hopaz network watcher show-next-hop …Effective Routes and Connectivity Tests
check peering statusaz network vnet peering list -g <rg> --vnet-name <vnet>Peering
point a 0/0 route to the firewallaz network route-table route create … --next-hop-type VirtualApplianceRoute Tables (UDR)
see NSG rules incl. defaultsaz network nsg rule list … --include-defaultNSG
see VPN/BGP statusaz network vnet-gateway list-bgp-peer-statusVPN Gateway (Site-to-Site)
add a rule to the Azure Firewallaz network firewall policy rule-collection-group collection rule add …Azure Firewall
link a private DNS zone to a VNetaz network private-dns link vnet create …Private DNS and Private Endpoints
check App Gateway backendsaz network application-gateway show-backend-healthApplication Gateway and WAF
set the WAF to Preventionaz network application-gateway waf-policy policy-setting update --mode PreventionApplication Gateway and WAF
enable IP forwarding on an NVA NICaz network nic update --ip-forwarding trueVirtual Machines
look up a subnet sizetableSubnet Sizes
give subnets a fixed outbound IP (NAT gateway)Generator → Network → NAT gatewayNAT Gateway
enable DDoS protection (plan or per IP)Generator → Network → DDoS protectionDDoS Protection
see range, usable IPs in Azure and mask of a subnetbutton IP Planner at the top (key s) → Look up a networkSubnet Sizes
lay out a hub VNet (Gateway, Firewall, Bastion, Route Server subnets)button IP Planner (key s) → Hub VNet layoutSubnet Sizes
split a spoke address space into subnets, each with its own size (with CLI / PowerShell / Bicep / Terraform)button IP Planner (key s) → Spoke plan & commandsSubnet Sizes
check overlaps with on-premises rangesbutton IP Planner (key s) → Overlap checkSubnet Sizes
create privatelink DNS zones for a landing zoneGenerator → Network → Private DNS zones for a landing zonePrivate DNS and Private Endpoints
troubleshoot systematicallyDNS → route → NSG → firewall → return path → targetOrder of Checks When "It Doesn't Work"

#Permissions and Secrets

…CommandDetails
see which roles someone hasaz role assignment list --assignee <x> --all -o tableRoles (RBAC)
give a service principal Owneraz role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope <scope> --role OwnerGive a service principal a role (subscription, management group, root)
remove the rights of a deployment service principal (MGs, root, subscriptions)Generator → Identity and Access → Remove role assignmentsGive a service principal a role (subscription, management group, root)
my object IDaz ad signed-in-user show --query id -o tsvRoles (RBAC)
read a secretaz keyvault secret show --vault-name <kv> -n <name> --query value -o tsvKey Vault
recover a deleted secretaz keyvault secret recover …Key Vault
publish an app via Front DoorGenerator → Front Door → Publish an appAzure Front Door
assign a domain to an existing Front Door routeaz afd route update --custom-domains <all-ids>Create a Route or Assign to an Existing Route
query across all subscriptionsaz graph query -q "<kql>"Resource Graph – Queries Across All Subscriptions
find orphaned disks/IPsResource Graph → cleanupCleanup and Costs
create security groups per role in bulkNew-AzADGroup -DisplayName <name> -MailNickname <name> -SecurityEnabledCreate Security Groups in Bulk
find expiring app secretsaz ad app credential listFind Expiring Secrets and Certificates
build resource names after our conventionGenerator → Naming → Build resource namesNaming Components (CAF)
look up a resource abbreviationGenerator → Naming → Abbreviation lookupAzure Resource Abbreviations (CAF)
create an alertaz monitor metrics alert createMetric Alerts
rename management groups (search & replace)Generator → Governance → Rename management groupsRename Management Groups
find out which policy blocksaz policy assignment list --disable-scope-strict-match …Troubleshooting
assign a policy in test mode firstaz policy assignment create … --enforcement-mode DoNotEnforceAssign a Policy
assign a policy with a formGenerator → Governance → Assign an Azure PolicyAssign a Policy
fix existing resources with a policy (tags, settings)az policy remediation create --policy-assignment <id>Modify and DeployIfNotExists: Identity and Remediation
exempt a resource group from a policyaz policy exemption create … --expires-on <date>Exemptions
see non-compliant resources across subscriptionsResource Graph policyresourcesCompliance and Reports
manage policies as coderepo + pipeline + what-ifPolicy as Code: Repo and Pipeline

#Terraform / Bicep

…CommandDetails
plan and apply safelyterraform plan -out tfplan → terraform apply tfplanBasic Workflow
rename a resource without rebuildmoved { from = … to = … }Code Snippets
take over an existing resourceimport { to = … id = … }Code Snippets
see drift onlyterraform plan -refresh-onlyBasic Workflow
release a lockterraform force-unlock <id>Outputs and State
calculate subnetscidrsubnets("10.0.0.0/24", 2, 2, 2, 2)Useful Functions
test Bicep without riskaz deployment group what-if …Bicep / ARM
a pipeline for TerraformtemplatePipeline Template: Terraform Plan/Apply

#FortiGate

…CommandDetails
check version/licenseget system statusSystem and License
capture packetsdiagnose sniffer packet any 'host <ip>' 4 20Troubleshooting: Sniffer, Debug Flow, Sessions
know why a packet is droppeddebug flowTroubleshooting: Sniffer, Debug Flow, Sessions
understand a debug flow messagetableDebug Flow Messages
check the route for an IPget router info routing-table details <ip>Routing
check HA syncdiagnose sys ha checksum clusterHA (FGCP)
test failoverexecute ha failover set 1HA (FGCP)
check an IPsec tunnelget vpn ipsec tunnel summaryIPsec VPN
see BGP neighborsget router info bgp summaryBGP
set up port forwardingconfig firewall vipVIP (Port Forwarding)
search the configurationshow full-configuration | grep -f <text>Using the CLI

#Clients (Linux / Windows)

…LinuxWindowsDetails
test a portnc -zv <ip> <port>Test-NetConnection <ip> -Port <port>Network / Network
check DNSdig +short <name>Resolve-DnsName <name>DNS / DNS
see the public IPcurl -s ifconfig.meInvoke-RestMethod https://api.ipify.orgNetwork / Network
path to a targetip r get <target>Find-NetRoute -RemoteIPAddress <target>Network / Network
listening portsss -tlnpGet-NetTCPConnection -State ListenNetwork / Network
continuous failover testwhile true; do curl …; donewhile ($true) { … }Mini Tests / Continuous Tests
check a server's certificateopenssl s_client -connect <host>:443 …–Certificates with OpenSSL

#Git / SSH

…CommandDetails
push a folder as a repogit init -b main … git push -u origin mainNew Repo / Push a Folder
undo the last commitgit reset --soft HEAD~1 / git revert <commit>Undo
park changes brieflygit stash / git stash popUndo
search the historygit log -S "<text>"Search and History
get an existing DevOps repogit clone git@ssh.dev.azure.com:v3/<org>/<project>/<repo>Clone an existing DevOps repo
push is rejected (TF402455)git switch -c feature/<name> → git push -u origin feature/<name> → PRPush rejected: TF402455 (protected branch)
create a pull requestaz repos pr create --source-branch <from> --target-branch <to> --openChange via feature branch and pull request to Dev
resolve a merge conflict in a PRgit merge origin/Dev → fix files → git commit → git pushResolve a merge conflict in a pull request
undo a PRabandon (open) / revert (completed)Undo a pull request (abandon / revert)
publish a static site from DevOps (Static Web App)pipeline AzureStaticWebApp@0 + SWA_TOKENPublish a static website via pipeline (Static Web App)
bring Dev to mainPR Dev → main, "Delete Dev after merging" offPromote Dev to main (release)
SSH key for Azure DevOpsssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_azureSSH key for Azure DevOps and first push
find out why Azure DevOps asks for a passwordssh -vT git@ssh.dev.azure.comAzure DevOps: test and debug
push although the server is ahead (non-fast-forward)git pull --rebase origin mainPush rejected: non-fast-forward
test the connection to Azure DevOpsssh -T git@ssh.dev.azure.comConnect and Test

#Search Script

Searches all files under docs/ and shows file, line and command. Several terms = all must appear.

PowerShell
.\search.ps1 peering               # Windows.\search.ps1 sniffer 168.63        # both terms in one line
Bash
./search.sh peering                # Linux / Git Bash

#Rebuild the HTML View

After changes to the .md files (Python 3, no extra packages):

PowerShell
python tools/build-html.py

Look and features live in tools/template.html. Produces site/index.html – a single file that runs offline in the browser (fonts are loaded online, otherwise system font).

#Quick Access

WayHow
Install as appopen promptus.cloudorbis.at in Edge/Chrome → Install button in the header (or ⋯ → Apps → Install) – own window, taskbar/start menu icon, works offline
Search from the address barEdge: Settings → Privacy, search → Address bar and search → Manage search engines → Add: name promptus, shortcut promptus, URL https://promptus.cloudorbis.at/?q=%s (Chrome: Settings → Search engine → Manage → Site search → Add). Then type promptus peering sync
Search linkhttps://promptus.cloudorbis.at/?q=<term> – opens the page with the search, good for sharing
PowerShellfunction promptus in your profile (Daily Commands): promptus peering sync
Jump inside the pageCtrl+K – topic, section, workflow or generator template by name
Teamschannel → + → Website → https://promptus.cloudorbis.at

#Publish as Azure Static Web App (promptus.cloudorbis.at)

The build writes site/index.html; site/ also contains staticwebapp.config.json (security headers). Free plan is enough: custom domain + free certificate included.

Create the static web app (once)

PowerShell
$rg = "<rg>"; $swa = "<swa-name>"; $loc = "westeurope"az group create -n $rg -l $locaz staticwebapp create -n $swa -g $rg -l $loc --sku Freeaz staticwebapp show -n $swa -g $rg --query defaultHostName -o tsv        # e.g. <name>.<id>.azurestaticapps.net

Deploy from your PC (needs Node.js)

PowerShell
python tools/build-html.py$token = az staticwebapp secrets list -n $swa -g $rg --query "properties.apiKey" -o tsvnpx --yes @azure/static-web-apps-cli deploy ./site --deployment-token $token --env production

Or automatically on every push to main: pipeline pipelines/static-web-app.yml (Azure DevOps) with the secret pipeline variable SWA_TOKEN = the token above.

The deployment token is not bound to a tenant: the DevOps repo can live in a different organization/tenant than the subscription of the static web app – no service connection needed. Create the app with az login --tenant <target-tenant-id> first and create it without a source (CLI default), not via the portal's "Azure DevOps" option. Set up the pipeline: Pipelines → New pipeline → Azure Repos Git → repo → Existing Azure Pipelines YAML file → /pipelines/static-web-app.yml → Variables → SWA_TOKEN (Keep this value secret) and optionally BLOCKED_WORDS (secret, comma separated – see Contributing) → Save and run. New token if leaked: az staticwebapp secrets reset-api-key -n $swa -g $rg.

Custom domain At the DNS provider of cloudorbis.at: CNAME promptus -> <defaultHostName> (from step 1). Then:

PowerShell
az staticwebapp hostname set -n $swa -g $rg --hostname promptus.cloudorbis.ataz staticwebapp hostname show -n $swa -g $rg --hostname promptus.cloudorbis.at --query status -o tsv   # Ready = certificate issued

Optional: only your team may open it Add to site/staticwebapp.config.json:

JSON
"routes": [ { "route": "/*", "allowedRoles": [ "team" ] } ],"responseOverrides": { "401": { "redirect": "/.auth/login/aad", "statusCode": 302 } }

Then invite people: Portal → Static Web App → Role management → Invite → provider Microsoft Entra ID, role team (Free plan: up to 25 invitations). Restricting to your own tenant without invitations needs the Standard plan (custom Entra ID provider).

Placeholders and favorites are stored per browser (localStorage) – nothing is sent to a server. The content contains only placeholders <...>, no customer data.

#Contributing / Maintenance

  • Put new commands into the matching file under docs/, comment with # (or // in KQL) after it – that way the search script finds them.
  • Add frequently used tasks to "I want to …" above or to Daily Commands.
  • Then run python tools/build-html.py and commit site/index.html too (or let the pipeline build it).
  • Never check in real IPs, names, keys, tokens or licenses – placeholders <...> only.
  • Before pushing: python tools/check.py – checks links/anchors, blocked words and anything that looks like a secret (private keys, client secrets, storage keys, SAS, tokens, GUIDs). The pipeline runs the same check and stops before deploying if it finds something.
  • Smoke test: python tools/smoke-test.py (needs pip install playwright) opens the built page in a headless browser and checks start page, layout (desktop and phone), search, every topic, Ctrl+K, placeholders and every generator template. The pipeline runs it after the build – if it fails, nothing is deployed.
  • Blocked words (customer, org and subscription names) are never stored in the repo: in the pipeline as secret variable BLOCKED_WORDS (comma separated), locally in tools/blocked-words.txt (one per line, ignored by git). A harmless line (e.g. a built-in role ID) can be excluded from the secret check with the comment check:allow.
Start

#All Workflows

Ready-made command sequences for whole tasks, sorted by area. Click a task – the workflow opens on its page. Do not write real names, secrets or IDs back into this repo. Single commands live in the topic files.

Start

#Daily Commands

The commands you type every day – compact on one page.

#Azure CLI

The most frequent Azure CLI commands.

TaskCommand
Who am I, where am I?az account show --query "{user:user.name, sub:name}" -o table
Switch subscriptionaz account set --subscription "<name>"
List subscriptionsaz account list --query "[].{name:name, id:id, active:isDefault}" -o table
Set default RG/region (saves -g/-l)az config set defaults.group=<rg> defaults.location=<region>
Remove defaults againaz config unset defaults.group defaults.location
Help for a commandaz network vnet peering create --help
Find examplesaz find "vnet peering"
Interactive shell with autocompleteaz interactive
Resources in an RGaz resource list -g <rg> --query "[].{name:name, type:type}" -o table
Running VMsaz vm list -d --query "[?powerState=='VM running'].{name:name, rg:resourceGroup}" -o table
VM IPsaz vm list-ip-addresses -g <rg> -o table
Public IPsaz network public-ip list --query "[].{name:name, ip:ipAddress}" -o table
Effective routesaz network nic show-effective-route-table -g <rg> -n <nic> -o table
Recent changesaz monitor activity-log list -g <rg> --offset 4h --query "[].{t:eventTimestamp, who:caller, what:operationName.localizedValue}" -o table
Read a secretaz keyvault secret show --vault-name <kv> -n <name> --query value -o tsv
Get a resource IDaz <type> show -g <rg> -n <name> --query id -o tsv
JSON to PowerShell objectaz vm list -o json | ConvertFrom-Json
CLI version / updateaz version · az upgrade

#Azure PowerShell

The most frequent Az PowerShell commands.

TaskCommand
Sign inConnect-AzAccount · Connect-AzAccount -UseDeviceAuthentication
ContextGet-AzContext
Switch subscriptionSet-AzContext -Subscription "<name>"
Pick a subscription in a windowGet-AzSubscription | Out-GridView -PassThru | Set-AzContext
Find resourcesGet-AzResource -Name "*<part>*" | ft Name, ResourceGroupName, ResourceType
VMs with statusGet-AzVM -Status | ft Name, ResourceGroupName, PowerState
Load App Gateway$AppGW = Get-AzApplicationGateway -Name <agw> -ResourceGroupName <rg>
Secret as plain textGet-AzKeyVaultSecret -VaultName <kv> -Name <name> -AsPlainText
Update module (admin window)Update-Module Az -Scope AllUsers

#Terraform

The Terraform commands of a normal day.

TaskCommand
Start in a new folderterraform init
Format + checkterraform fmt -recursive ; terraform validate
Plan (saved)terraform plan -out tfplan
Apply exactly this planterraform apply tfplan
Show only deletions/replacements in the planterraform plan -no-color | Select-String "destroy|replace"
Find resources in the stateterraform state list | Select-String <part>
Show one resourceterraform state show '<address>'
Output for scriptsterraform output -raw <name>
Test an expressionterraform console
Release a lockterraform force-unlock <id>
Update providersterraform init -upgrade

#Git

The Git commands of a normal day.

TaskCommand
What's going on?git status -s
Get the latestgit pull
Commit everythinggit add -A ; git commit -m "<message>"
Uploadgit push
Recent commitsgit log --oneline --graph -15
What did I change?git diff · git diff --staged
New branchgit switch -c feature/<name>
Back to maingit switch main ; git pull
Reset a filegit restore <file>
Park changesgit stash -u · git stash pop
Change the last commit messagegit commit --amend -m "<new>" (only before pushing)
Which branches?git branch -a
Clean up deleted remote branchesgit fetch --prune
PR to Devaz repos pr create --source-branch feature/<name> --target-branch Dev --open
What goes from Dev to main?git log --oneline origin/main..origin/Dev
Local branch = servergit fetch origin ; git reset --hard origin/<branch>

#FortiGate

The FortiGate commands you need most.

TaskCommand
Status / version / licenseget system status
CPU / RAM / sessionsget system performance status
HA statusget system ha status
HA in sync?diagnose sys ha checksum cluster
Routing tableget router info routing-table all
Route for an IPget router info routing-table details <ip>
Interfaces, shortget system interface physical
ARPget system arp
Search the configshow full-configuration | grep -f <text>
Show a policyshow firewall policy <id>
Capture packetsdiagnose sniffer packet any 'host <ip>' 4 20
Sessions to an IPdiagnose sys session filter dst <ip> → diagnose sys session list
Ping with source IPexecute ping-options source <ip> → execute ping <target>
Test a portexecute telnet <ip> <port>
VPN overviewget vpn ipsec tunnel summary
BGP overviewget router info bgp summary
Turn all debug offdiagnose debug disable ; diagnose debug reset

#Network Tests

The same network test on Windows, Linux and macOS.

TaskWindows (PowerShell)LinuxmacOS
Own IPsipconfigip -br aifconfig · ipconfig getifaddr en0
Public IPirm https://api.ipify.orgcurl -s ifconfig.mecurl -s ifconfig.me
DNSResolve-DnsName <name>dig +short <name>dig +short <name>
Flush DNS cacheipconfig /flushdnsresolvectl flush-cachessudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
PortTest-NetConnection <ip> -Port 443nc -zv <ip> 443nc -zv <ip> 443
HTTP(S) with detailscurl.exe -v https://<target>curl -v https://<target>curl -v https://<target>
HTTP status onlycurl.exe -s -o NUL -w "%{http_code}" https://<target>curl -s -o /dev/null -w "%{http_code}\n" https://<target>curl -s -o /dev/null -w "%{http_code}\n" https://<target>
Test against a specific IPcurl.exe -k --resolve <host>:443:<ip> https://<host>curl -k --resolve <host>:443:<ip> https://<host>curl -k --resolve <host>:443:<ip> https://<host>
Pathtracert -d <ip>tracepath -n <ip>traceroute -n <ip>
Routesroute print -4ip rnetstat -rn -f inet
Listening portsnetstat -ano | findstr LISTENss -tlnplsof -nP -iTCP -sTCP:LISTEN

#PowerShell Tricks

Small PowerShell helpers: history, clipboard, export, measure.

PowerShell
Get-Command *route*                         # find a commandGet-Help Test-NetConnection -Examples       # examplesGet-History ; Invoke-History <nr>           # history(Get-PSReadLineOption).HistorySavePath      # file with the full history (old sessions too)Select-String -Path (Get-PSReadLineOption).HistorySavePath -Pattern "<text>"     # search the history<command> | Set-Clipboard                   # output to the clipboard<command> | Out-GridView                    # table with filter (Windows)<command> | Export-Csv .\out.csv -NoTypeInformation -Encoding UTF8<command> | ConvertTo-Json -Depth 5 | Set-Content .\out.json<command> | Select-Object -First 5<command> | Where-Object Name -like "*<part>*"<command> | Format-Table -AutoSizeMeasure-Command { <command> }               # measure runtimeStart-Transcript .\session.log ; Stop-Transcript     # record the whole sessioncode .                                      # open the current folder in VS Codeexplorer .                                  # open in Explorerwinget upgrade --all                        # update all tools (az, terraform, git …)

#Shell Profile: Your Own Shortcuts

Windows: open with notepad $PROFILE (create the file first if needed: New-Item -Force $PROFILE). macOS (zsh): nano ~/.zshrc. Linux (bash): nano ~/.bashrc. Paste the snippets, then start a new console (or source ~/.zshrc).

# --- Azure ---function azsub {                         # pick a subscription in a window for CLI and PowerShell    $s = az account list --query "[].{Name:name, Id:id}" -o json | ConvertFrom-Json |         Out-GridView -Title "Choose subscription" -OutputMode Single    if ($s) { az account set --subscription $s.Id; Set-AzContext -Subscription $s.Id | Out-Null; "Active: $($s.Name)" }}function azwho { az account show --query "{user:user.name, sub:name, tenant:tenantId}" -o table } # --- Terraform ---Set-Alias tf terraformfunction tfi { terraform init @args }function tfp { terraform plan -out tfplan @args }function tfa { terraform apply tfplan }function tfs { param($part) terraform state list | Select-String $part } # --- Git ---function gst { git status -s }function glg { git log --oneline --graph --decorate -15 }function gpush { param([string]$msg) git add -A; git commit -m $msg; git push } # --- Network ---function myip { Invoke-RestMethod https://api.ipify.org }function port { param($target, [int]$p) Test-NetConnection $target -Port $p -WarningAction SilentlyContinue |                Select ComputerName, RemoteAddress, RemotePort, TcpTestSucceeded }function dns  { param($name) Resolve-DnsName $name -DnsOnly | Select Name, Type, IPAddress, NameHost } # --- promptus ---function cmds  { & "<path-to-repo>\search.ps1" @args }                     # search the local repofunction promptus {                                                        # open promptus, optionally with a search    $u = "https://promptus.cloudorbis.at/"    if ($args) { $u += "?q=" + [uri]::EscapeDataString($args -join " ") }    Start-Process $u}
# --- Azure ---azsub() {                                  # pick a subscription from a numbered list  local IFS=$'\n'; local subs=($(az account list --query "[].name" -o tsv))  select s in "${subs[@]}"; do [ -n "$s" ] && az account set --subscription "$s" && echo "Active: $s"; break; done}alias azwho='az account show --query "{user:user.name, sub:name, tenant:tenantId}" -o table' # --- Terraform ---alias tf=terraformalias tfi='terraform init'alias tfp='terraform plan -out tfplan'alias tfa='terraform apply tfplan'tfs() { terraform state list | grep -i "$1"; } # --- Git ---alias gst='git status -s'alias glg='git log --oneline --graph --decorate -15'gpush() { git add -A && git commit -m "$1" && git push; } # --- Network ---alias myip='curl -s https://api.ipify.org'port() { nc -zv "$1" "$2"; }dns()  { dig +short "$1"; } # --- promptus ---cmds()  { "<path-to-repo>/search.sh" "$@"; }                               # search the local repopromptus() {                                                               # open promptus, optionally with a search  local u="https://promptus.cloudorbis.at/"  [ $# -gt 0 ] && u+="?q=$(python3 -c 'import sys,urllib.parse as p; print(p.quote(" ".join(sys.argv[1:])))' "$@")"  open "$u" 2>/dev/null || xdg-open "$u"                                   # macOS: open, Linux: xdg-open}

Then e.g.: azsub, tfp, port 10.0.1.4 443, cmds peering, promptus peering sync. Note: gl, gp, gc are already taken in PowerShell (Get-Location etc.) – hence the other names.

#Keyboard Shortcuts

Shortcuts for PowerShell, Windows Terminal, VS Code, FortiGate and the browser.

WhereKeyEffect
PowerShellCtrl+Rsearch the history backwards
PowerShellF8search the history for what you already typed
PowerShellCtrl+Spaceshow all options (parameters, values)
PowerShellTab / Shift+Tabcomplete forward/backward
PowerShellCtrl+Ccancel the running command
PowerShell` at end of linecontinue the command on the next line
Windows TerminalAlt+Shift+Dsplit pane (same profile)
Windows TerminalCtrl+Shift+T / Ctrl+Shift+Wnew / close tab
Windows TerminalCtrl+Shift+Fsearch in the window
VS CodeCtrl+Popen a file
VS CodeCtrl+Shift+Fsearch all files
VS CodeCtrl+Shift+Pcommand palette
VS CodeCtrl+`toggle terminal
VS CodeShift+Alt+Fformat the file
VS CodeCtrl+Shift+VMarkdown preview
Linux / Git BashCtrl+Rsearch the history
Linux / Git Bashsudo !!repeat the last command with sudo
FortiGateTab / ?complete / help
FortiGateCtrl+Ccancel output/debug
Browser (GitHub/DevOps)tfind a file in the repo (GitHub)
Start

#Runbook: Outage "Something Is Not Reachable"

Stay calm, go step by step, write everything down with the time.

#Minute 0–2: Clarify the Situation

Questions for the reporter (or answer them yourself):

QuestionWhy
What exactly doesn't work? URL, IP, port, error message, screenshotdefine the target
Who is affected? Everyone, one site, only internal/external?narrow down the fault
Since when? Was there a change/deployment before?most common cause
Does it work from somewhere else? (other client, VPN, phone)client vs. server

Immediately: send an outage notice to those involved (template).

#Minute 2–5: Is Azure Itself Affected?

Recent changes and the state of the most important resources.

PowerShell
# Recent changes in the affected environmentaz monitor activity-log list -g <rg> --offset 4h `  --query "[?status.value=='Succeeded'].{time:eventTimestamp, who:caller, what:operationName.localizedValue}" -o table # State of the most important resourcesaz vm list -d -g <rg> --query "[].{name:name, state:powerState}" -o tableaz network application-gateway show-backend-health -g <rg> -n <agw> `  --query "backendAddressPools[].backendHttpSettingsCollection[].servers[].{server:address, health:health}" -o table
  • Azure status: https://azure.status.microsoft – and in the portal Service Health (shows outages for your subscriptions).
  • Merged PR / pipeline in the last hours? → DevOps Pipelines → Runs.

#Minute 5–10: Check the Path

Always in this order: DNS → route → NSG → firewall → return path → target.

PowerShell
# 1. DNSResolve-DnsName <name>                                            # right IP? private or public? # 2. Port from the clientTest-NetConnection <target> -Port <port> # 3. Route from Azure's point of viewaz network watcher show-next-hop -g <rg> --vm <source-vm> --source-ip <source-ip> --dest-ip <target-ip> -o tableaz network nic show-effective-route-table -g <rg> -n <nic> -o table # 4. NSGaz network watcher test-ip-flow -g <rg> --vm <vm> --direction Inbound --protocol TCP --local <vm-ip>:<port> --remote <source>:12345

Tip: macOS / Linux: step 1 dig +short <name>, step 2 nc -zv <target> <port> (the az commands work the same).

FortiGate:

Text
get system status                                   # running, license ok?get system ha status                                # did a failover happen?diagnose sniffer packet any 'host <target-ip>' 4 20 # does traffic arrive and leave?# if it doesn't leave -> debug flow (why dropped):diagnose debug flow filter addr <target-ip>diagnose debug flow trace start 10diagnose debug enablediagnose debug disable ; diagnose debug reset

The target itself:

Bash
ss -tlnp | grep <port>                   # is the service listening?sudo systemctl status <service>sudo journalctl -u <service> -n 50

Details on messages: Error → Fix · Debug flow messages.

#Once the Cause Is Found

Fix, document, confirm with the reporter.

  • Smallest possible fix (or roll back the last change: PR revert)
  • Write down emergency changes made directly in the portal/CLI – then move them into code (Bicep/Terraform), otherwise the next deployment overwrites them
  • Have the reporter confirm with a test
  • Send the all-clear (template)

#After the Outage

Timeline, cause, missing alerts.

  • Timeline from the notes (start, detection, cause, fix)
  • Document cause and action briefly
  • Missing alert? → add it in Monitoring
  • Add the new insight to this reference (Error → Fix)
Azure

#Sign-in and Basics Workflows

Single commands: CLI Basics · PowerShell (Az) · Azure CLI ↔ Azure PowerShell

Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.

#Sign in to Azure

Has risky commands

Sign in with browser, device code, service principal or managed identity – for CLI and PowerShell.

Interactive (browser)

PowerShell
az loginaz login --tenant <tenant-id>                       # with several tenantsConnect-AzAccountConnect-AzAccount -Tenant <tenant-id>

Device code – when no browser is available (server, RDP, jump host). Enter the code at https://microsoft.com/devicelogin.

PowerShell
az login --use-device-codeaz login --use-device-code --tenant <tenant-id>az login --use-device-code --allow-no-subscriptions  # tenant without subscription (e.g. Entra ID only)Connect-AzAccount -UseDeviceAuthenticationConnect-AzAccount -UseDeviceAuthentication -Tenant <tenant-id>

Service principal (app registration with secret)

PowerShell
az login --service-principal `  --username <app-id> `  --password <client-secret> `  --tenant <tenant-id> # better: keep the secret out of the history – prompt instead of typing it$secret = Read-Host "Client secret" -AsSecureString$plain  = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret))az login --service-principal --username <app-id> --password $plain --tenant <tenant-id>Remove-Variable plain # PowerShell (Az module)$cred = Get-Credential -UserName <app-id> -Message "Client secret as password"Connect-AzAccount -ServicePrincipal -Credential $cred -Tenant <tenant-id>

Service principal with certificate

PowerShell
az login --service-principal --username <app-id> --password .\sp-cert.pem --tenant <tenant-id>   # PEM incl. private key (newer CLI: --certificate)Connect-AzAccount -ServicePrincipal -ApplicationId <app-id> -CertificateThumbprint <thumbprint> -Tenant <tenant-id>

Managed identity (on an Azure VM or agent)

PowerShell
az login --identityaz login --identity --username <client-id>          # user-assigned identity (newer CLI: --client-id)Connect-AzAccount -IdentityConnect-AzAccount -Identity -AccountId <client-id>

Check and sign out

PowerShell
az account show -o tableGet-AzContextaz logout ; az account clearDisconnect-AzAccount ; Clear-AzContext -Force

#Set subscription (CLI and PowerShell)

Read-only

CLI and PowerShell have separate sign-ins – set both if you use both.

PowerShell
$sub = "<subscription-name-or-id>"az account set --subscription $subSet-AzContext -Subscription $subaz account show --query "{name:name, id:id}" -o table(Get-AzContext).Subscription.Name

#Unblock and run downloaded scripts

Changes things

Remove the download mark and allow scripts for the current session only.

Get-ChildItem -Recurse *.ps1 | Unblock-File          # remove the download markSet-ExecutionPolicy -Scope Process Bypass            # this session only.\<script>.ps1
xattr -dr com.apple.quarantine <folder>              # macOS: remove the download mark (Linux has none)chmod +x <script>.sh                                 # make it executable./<script>.sh                                        # PowerShell script: pwsh ./<script>.ps1

Tip: Windows permanently for the user: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned. On macOS/Linux PowerShell does not enforce an execution policy.

#Find a resource without knowing where it is

Read-only

Search all subscriptions by name part, then switch to the right one.

PowerShell
az extension add -n resource-graph$part = "<name-part>"az graph query -q "Resources | where name contains '$part' | project name, type, resourceGroup, subscriptionId" -o table# then switch to the right subscriptionaz account set --subscription <subscription-id-from-result>az resource show --ids <resource-id>

#Who changed or deleted what

Read-only

Read the activity log of a resource group – all changes or deletions only.

PowerShell
$rg = "<rg>"az monitor activity-log list -g $rg --offset 24h `  --query "[?status.value=='Succeeded'].{time:eventTimestamp, who:caller, what:operationName.localizedValue, resource:resourceId}" -o table# deletions onlyaz monitor activity-log list -g $rg --offset 7d `  --query "[?contains(operationName.value,'delete')].{time:eventTimestamp, who:caller, resource:resourceId}" -o table

Longer than 90 days or across all resource groups: KQL → Azure activity.

#Create a resource group with tags and delete lock

Changes things

Create a resource group with the tag standard and protect it with a delete lock.

PowerShell
$rg = "<rg>" ; $loc = "<region>"az group create -n $rg -l $loc --tags Owner=<name> environment=<dev|test|prod> costcenter=<cost-center>az lock create -n lock-$rg -g $rg --lock-type CanNotDelete --notes "Protection against accidental deletion"az group show -n $rg --query tagsaz lock list -g $rg -o table
Azure

#Application Gateway and WAF Workflows

Single commands: Network Resources · Azure Front Door

Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.

#App Gateway: add a certificate from Key Vault

Changes things2 steps

Give the gateway an identity, add the Key Vault certificate versionless and assign it to a listener.

Prerequisite: the App Gateway has a user-assigned identity with the role Key Vault Secrets User on the vault (or access policy Get on secrets), and the vault is reachable for it.

1. No identity yet? Assign one first
PowerShell
$id    = Get-AzUserAssignedIdentity -ResourceGroupName <rg> -Name <identity>$AppGW = Get-AzApplicationGateway -Name <agw-name> -ResourceGroupName <rg-agw>Set-AzApplicationGatewayIdentity -ApplicationGateway $AppGW -UserAssignedIdentityId $id.IdSet-AzApplicationGateway -ApplicationGateway $AppGWNew-AzRoleAssignment -ObjectId $id.PrincipalId -RoleDefinitionName "Key Vault Secrets User" `  -Scope (Get-AzKeyVault -VaultName <keyvault-name>).ResourceId

Add the certificate to the App Gateway (takes a few minutes)

$sub      = "<subscription>"$rg       = "<rg-agw>"$agwName  = "<agw-name>"$kvName   = "<keyvault-name>"$certName = "<certificate-name>"           # name in Key Vault and in the App Gateway Set-AzContext -Subscription $sub $AppGW    = Get-AzApplicationGateway -Name $agwName -ResourceGroupName $rg$secret   = Get-AzKeyVaultSecret -VaultName $kvName -Name $certName$secretId = $secret.Id.Replace($secret.Version, "")   # versionless: https://<kv>.vault.azure.net/secrets/<name>/ $AppGW = Add-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name $certName -KeyVaultSecretId $secretIdSet-AzApplicationGateway -ApplicationGateway $AppGW
az account set --subscription <subscription>$secretId = "https://<keyvault-name>.vault.azure.net/secrets/<certificate-name>"     # without versionaz network application-gateway ssl-cert create -g <rg-agw> --gateway-name <agw-name> `  -n <certificate-name> --key-vault-secret-id $secretIdaz network application-gateway ssl-cert list -g <rg-agw> --gateway-name <agw-name> `  --query "[].{name:name, kv:keyVaultSecretId}" -o table

Tip: Versionless = the App Gateway picks up a renewed version automatically (checked about every 4 hours).

Assign the certificate to a listener

PowerShell
$AppGW    = Get-AzApplicationGateway -Name <agw-name> -ResourceGroupName <rg-agw>$cert     = Get-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <certificate-name>$listener = Get-AzApplicationGatewayHttpListener   -ApplicationGateway $AppGW -Name <listener-name>$listener.SslCertificate = $certSet-AzApplicationGateway -ApplicationGateway $AppGW

#App Gateway: renew / replace a certificate

Has risky commands

List the certificates, switch to a new Key Vault secret or remove an unused one.

PowerShell
# Which certificates exist and where do they come from?$AppGW = Get-AzApplicationGateway -Name <agw-name> -ResourceGroupName <rg-agw>$AppGW.SslCertificates | Select Name, KeyVaultSecretId, ProvisioningState # Switch to a new Key Vault source$AppGW = Set-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <certificate-name> -KeyVaultSecretId <new-secret-id>Set-AzApplicationGateway -ApplicationGateway $AppGW # Remove a certificate that is no longer used (detach from all listeners first)$AppGW = Remove-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <old-name>Set-AzApplicationGateway -ApplicationGateway $AppGW

#Import a PFX into Key Vault

Changes things

Import a PFX with private key into Key Vault, ready for the App Gateway.

PowerShell
az keyvault certificate import --vault-name <keyvault-name> -n <certificate-name> -f .\cert.pfx --password <pfx-password>az keyvault certificate show --vault-name <keyvault-name> -n <certificate-name> --query "{expires:attributes.expires, sid:sid}"# PowerShell$pw = Read-Host "PFX password" -AsSecureStringImport-AzKeyVaultCertificate -VaultName <keyvault-name> -Name <certificate-name> -FilePath .\cert.pfx -Password $pw

Then continue with add the certificate.

#App Gateway: backend unhealthy

Read-only

Check backend health, DNS, probe and NSG from inside the gateway VNet.

PowerShell
$rg = "<rg-agw>" ; $agw = "<agw>"az network application-gateway show-backend-health -g $rg -n $agw `  --query "backendAddressPools[].backendHttpSettingsCollection[].servers[].{server:address, health:health, log:healthProbeLog}" -o tableaz network application-gateway http-settings list -g $rg --gateway-name $agw `  --query "[].{name:name, port:port, proto:protocol, host:hostName, pickHost:pickHostNameFromBackendAddress, probe:probe.id}" -o tableaz network application-gateway probe list -g $rg --gateway-name $agw `  --query "[].{name:name, proto:protocol, host:host, path:path, pickHost:pickHostNameFromBackendHttpSettings}" -o table# Does the backend resolve privately? (from a VM in the gateway VNet)nslookup <backend-fqdn>

Interpreting messages: Troubleshooting → App Gateway.

#WAF: find a false positive and add an exclusion

Changes things4 steps

Switch to detection, find the blocking rule in the logs, add an exclusion, switch back to prevention.

Temporarily only log (if production traffic is being blocked)

PowerShell
$rg = "<rg>" ; $policy = "<waf-policy>"az network application-gateway waf-policy policy-setting update -g $rg --policy-name $policy --mode Detection --state Enabled

Which rule, which field? (Log Analytics)

KQL
AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog" and TimeGenerated > ago(1h)| where requestUri_s contains "<path>"| project TimeGenerated, ruleId_s, Message, details_message_s, details_data_s, requestUri_s

Exclusion for the affected field (e.g. form field "comment")

PowerShell
az network application-gateway waf-policy managed-rule exclusion add -g $rg --policy-name $policy `  --match-variable RequestArgNames --selector-match-operator Equals --selector commentaz network application-gateway waf-policy managed-rule exclusion list -g $rg --policy-name $policy

Switch back to blocking

PowerShell
az network application-gateway waf-policy policy-setting update -g $rg --policy-name $policy --mode Prevention --state Enabled

Tip: More --match-variable values: RequestHeaderNames, RequestCookieNames, RequestArgValues … To disable a single rule instead: Portal → WAF policy → Managed rules.

#Send diagnostic logs to Log Analytics

Changes things

Create a diagnostic setting for any resource and send logs to a workspace.

PowerShell
$resId = az network application-gateway show -g <rg> -n <agw> --query id -o tsv      # any resource$lawId = az monitor log-analytics workspace show -g <rg-law> -n <law> --query id -o tsvaz monitor diagnostic-settings create -n diag-to-law --resource $resId --workspace $lawId `  --logs '[{"categoryGroup":"allLogs","enabled":true}]' --metrics '[{"category":"AllMetrics","enabled":true}]' `  --export-to-resource-specific trueaz monitor diagnostic-settings list --resource $resId -o table

--export-to-resource-specific true = dedicated tables (e.g. AGWAccessLogs) instead of AzureDiagnostics. In PowerShell, pass JSON strings with '…' and escaped quotes if needed, or put them in a file (--logs '@logs.json').

Azure

#Permissions, Key Vault and Policy Workflows

Single commands: Key Vault, Roles, Identity · Entra ID · Azure Policy – Rules, Assignments and Policy as Code

Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.

#Give a service principal a role (subscription, management group, root)

Has risky commands3 stepsNeeds: Owner or User Access Administrator on the scope

Find the service principal, assign the role at the smallest scope, remove it again after the deployment.

Find the service principal and choose the scope – as small as possible, then assign:

PowerShell
$SPName = "<sp-display-name>"$id = az ad sp list --display-name $SPName --query "[].{id:id}" --output tsv$id                                          # exactly ONE ID? With several SPs of the same name stop and look up by app ID:# $id = az ad sp show --id <app-id> --query id -o tsv # Choose the scope – as small as possible:$scope = "/subscriptions/<subscription-id>"                                       # one subscription# $scope = "/subscriptions/<subscription-id>/resourceGroups/<rg>"                 # one resource group# $scope = "/providers/Microsoft.Management/managementGroups/<mg-id>"             # management group# $scope = "/"                                                                    # tenant root (everything!) az role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope $scope --role "Owner"az role assignment list --assignee $id --all --query "[].{role:roleDefinitionName, scope:scope}" -o table

Prefer Contributor over Owner (+ User Access Administrator only where the SP must assign roles). Remove again: az role assignment delete --assignee $id --role "Owner" --scope $scope.

Only for scope / (tenant root)

Caution: Scope / grants Owner on all subscriptions and management groups. Remove the rights again right after the deployment (step 3 below).

Elevate your own access on root (Global Admin)

You need User Access Administrator on / yourself, which a Global Admin only gets temporarily:

PowerShell
# a. Elevate your own access on root (Entra ID: "Access management for Azure resources" = Yes)az rest --method post --url "/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"# b. Assignment as above with --scope "/"az role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope "/" --role "Owner"# c. Remove your own elevation again$me = az ad signed-in-user show --query id -o tsvaz role assignment delete --assignee $me --role "User Access Administrator" --scope "/"

After the deployment – remove the rights on all management groups and root at once

PowerShell
# run as the deployment SP itself or as an admin; root "/" last$roles  = @("Owner", "User Access Administrator")$scopes = @(az account management-group list --query "[].id" -o tsv) + "/"foreach ($s in $scopes) {  az role assignment list --scope $s --assignee $id -o json --only-show-errors | ConvertFrom-Json |    Where-Object { $_.scope -eq $s -and $roles -contains $_.roleDefinitionName } |    ForEach-Object { az role assignment delete --ids $_.id --only-show-errors ; "removed: $($_.roleDefinitionName) on $s" }}

Dry run first, or for users/groups and subscriptions: web page → Generator → Identity and Access → Remove role assignments.

#Create a managed identity and assign a role

Changes things

Create a user-assigned identity and give it a role on a scope.

PowerShell
$rg = "<rg>" ; $name = "<identity>"$principalId = az identity create -g $rg -n $name --query principalId -o tsv$scope = az keyvault show -n <keyvault-name> --query id -o tsv          # or RG/subscription/resourceaz role assignment create --assignee-object-id $principalId --assignee-principal-type ServicePrincipal `  --role "Key Vault Secrets User" --scope $scopeaz role assignment list --assignee $principalId --all -o table

#Which permissions do I (or someone else) have?

Read-only

List role assignments of a user, including those received via groups.

PowerShell
$who = az ad signed-in-user show --query id -o tsv          # or: <upn> / <object-id>az role assignment list --assignee $who --all --include-inherited `  --query "[].{role:roleDefinitionName, scope:scope}" -o tableaz ad user get-member-groups --id $who --query "[].displayName" -o tsv     # permissions via groups

#Policy blocks a deployment

Changes things

Read the policy assignment from the error, make the deployment compliant or add a time-limited exemption.

PowerShell
# 1. Copy policyAssignmentId / policyDefinitionId from the error message$assignmentId = "<policyAssignmentId>"az policy assignment list --disable-scope-strict-match --scope /subscriptions/<sub>/resourceGroups/<rg> `  --query "[?id=='$assignmentId'].{name:displayName, scope:scope, def:policyDefinitionId}" -o tableaz policy definition show --name <definition-name> --query "{name:displayName, rule:policyRule}"# 2. Make it compliant (preferred) – or a time-limited exemption for the scopeaz policy exemption create -n <name> --policy-assignment $assignmentId `  --scope /subscriptions/<sub>/resourceGroups/<rg> --exemption-category Waiver `  --expires-on 2026-12-31T00:00:00Z --description "<reason, ticket>"

#Find expiring certificates

Read-only

Certificates in Key Vault and on a server that expire soon.

PowerShell
# In Key Vaultaz keyvault certificate list --vault-name <keyvault-name> `  --query "sort_by([].{name:name, expires:attributes.expires}, &expires)" -o table# What a server actually serves (Git Bash / Linux)openssl s_client -connect <host>:443 -servername <host> </dev/null 2>/dev/null | openssl x509 -noout -enddate -subject
PowerShell
# Without openssl, in PowerShell$tcp = New-Object Net.Sockets.TcpClient("<host>", 443)$ssl = New-Object Net.Security.SslStream($tcp.GetStream(), $false, { $true })$ssl.AuthenticateAsClient("<host>")$ssl.RemoteCertificate | Select Subject, @{n='Expires';e={$_.GetExpirationDateString()}}$tcp.Close()
Azure

#Azure CLI – Basics, Resources, VMs, Services

Replace placeholders <...> · PowerShell line continuation `, Bash \ · Step by step: Sign-in and Basics Workflows · VM, Linux and Windows Workflows

Top 5: az account show -o table · az account set --subscription <x> · az resource list -g <rg> -o table · az vm list -d -o table · az monitor activity-log list -g <rg> --offset 2h

#Install the Tools

Azure CLI, Terraform, Git, AzCopy and PowerShell 7 – pick your system:

winget install Microsoft.AzureCLI Hashicorp.Terraform Git.Git Microsoft.Azure.AZCopy.10 Microsoft.PowerShell
brew install azure-cli git azcopybrew install --cask powershellbrew tap hashicorp/tap && brew install hashicorp/tap/terraform
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash            # Debian / Ubuntusudo apt install -y gitwget https://aka.ms/downloadazcopy-v10-linux -O azcopy.tar.gz && tar -xf azcopy.tar.gz --strip-components=1 --wildcards '*/azcopy' && sudo mv azcopy /usr/local/bin/

Tip: Terraform and PowerShell on Linux: package repositories of HashiCorp and Microsoft – see developer.hashicorp.com/terraform/install and learn.microsoft.com/powershell (install on Linux). Check afterwards: az version · terraform version · git --version · azcopy --version.

#Sign-in and Subscription

Check the version, sign in and switch the active subscription.

PowerShell
az version                                         # installed versionaz upgrade                                         # update the CLIaz login                                           # browser sign-inaz login --tenant <tenant-id>                      # specific tenantaz login --use-device-code                         # sign in with a code (other device)az login --use-device-code --tenant <tenant-id>az login --allow-no-subscriptions                  # tenant without subscriptionaz login --identity                                # managed identity (Azure VM/pipeline)az login --service-principal --username <app-id> --password <secret> --tenant <tenant-id>   # workflow: 00-workflows.mdaz account show -o table                           # current subscriptionaz account list -o table                           # all subscriptionsaz account list --all -o table                     # incl. disabled onesaz account set --subscription <name-or-id>         # switch subscriptionaz account list --query "[?name=='<name>'].id" -o tsvaz account tenant list -o tableaz logout ; az account clear                       # sign out, clear cacheaz config set core.output=table                    # default outputaz extension list -o tableaz extension add -n <name>                         # e.g. serial-console, resource-graphaz extension update -n <name>az provider register -n Microsoft.Network          # register a resource provideraz provider show -n Microsoft.Network --query registrationState

#Output and Filters (JMESPath)

Choose the output format and filter or pick columns with --query.

PowerShell
-o table | tsv | json | jsonc | yaml | none--query "[].{name:name, rg:resourceGroup}"         # choose columns--query "[?location=='austriaeast'].name"          # filter--query "[?contains(name,'<part>')].name"          # contains--query "[?tags.environment=='prod']"              # filter by tag--query "length(@)"                                # count--query "[0].id"                                   # first element$id = az <...> --query id -o tsv                   # value into a variable (PowerShell)id=$(az <...> --query id -o tsv)                   # value into a variable (Bash)--debug                                            # show REST calls (troubleshooting)

#Resource Groups, Resources, Locks

List, create and delete resource groups and resources; protect them with locks.

PowerShell
az group list -o tableaz group create -n <rg> -l <region> --tags Owner=<x> environment=<y>az group delete -n <rg> --yes --no-waitaz resource list -g <rg> -o tableaz resource list --tag environment=<y> -o tableaz resource show --ids <resource-id>az resource tag --ids <resource-id> --tags key=value -i       # -i = keep existing tagsaz resource delete --ids <resource-id>az resource move --destination-group <rg-new> --ids <id1> <id2>az lock list -g <rg> -o tableaz lock create -n <name> -g <rg> --lock-type CanNotDeleteaz lock delete -n <name> -g <rg> # Availability / regionaz account list-locations --query "[].{name:name, display:displayName}" -o tableaz vm list-skus -l <region> --size <sku> -o table

#Resource Graph

Search across all subscriptions – ideal when you don't know where something lives.

PowerShell
az extension add -n resource-graphaz graph query -q "Resources | where name contains '<part>' | project name, type, resourceGroup, subscriptionId"az graph query -q "Resources | where type =~ 'microsoft.network/virtualnetworks' | project name, resourceGroup, properties.addressSpace.addressPrefixes"az graph query -q "Resources | where type =~ 'microsoft.network/publicipaddresses' | project name, ip=properties.ipAddress, resourceGroup"az graph query -q "Resources | summarize Count=count() by type | order by Count desc"az graph query -q "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project name, subscriptionId"

Many more queries: Resource Graph.

#Virtual Machines

List VMs with state and IP, start, stop, resize and run commands.

PowerShell
az vm list -d -o tableaz vm list -d -g <rg> --query "[].{name:name, state:powerState, zone:zones[0], ip:privateIps, pip:publicIps}" -o tableaz vm show -g <rg> -n <vm> --query "{size:hardwareProfile.vmSize, zones:zones, image:storageProfile.imageReference}"az vm get-instance-view -g <rg> -n <vm> --query "instanceView.statuses[].displayStatus"az vm start      -g <rg> -n <vm>az vm stop       -g <rg> -n <vm>                 # stopped, compute still billedaz vm deallocate -g <rg> -n <vm> --no-wait       # deallocated, no compute costsaz vm restart    -g <rg> -n <vm>az vm resize     -g <rg> -n <vm> --size <sku>az vm list-sizes -l <region> -o tableaz vm list-skus  -l <region> --size <sku> --query "[].locationInfo[].zones" -o tsv     # zonesaz vm list-usage -l <region> -o table                                                   # quotaaz vm run-command invoke -g <rg> -n <vm> --command-id RunShellScript --scripts "hostname; ip a"az vm run-command invoke -g <rg> -n <vm> --command-id RunPowerShellScript --scripts "ipconfig /all"az vm boot-diagnostics enable -g <rg> -n <vm>az vm boot-diagnostics get-boot-log -g <rg> -n <vm>az serial-console connect -g <rg> -n <vm>        # extension serial-consoleaz vm user update -g <rg> -n <vm> -u <user> -p <new-password>        # reset passwordaz vm user update -g <rg> -n <vm> -u <user> --ssh-key-value "<public-key>"az vm disk attach -g <rg> --vm-name <vm> --name <disk> --new --size-gb 32az vm extension list -g <rg> --vm-name <vm> -o tableaz vm identity assign -g <rg> -n <vm>            # system-assigned identity # NICaz network nic list -g <rg> -o tableaz network nic show -g <rg> -n <nic> --query "{ip:ipConfigurations[].privateIPAddress, fwd:enableIPForwarding, accel:enableAcceleratedNetworking}"az network nic update -g <rg> -n <nic> --ip-forwarding true          # required for NVAs

#Marketplace Images

Find publisher, offer and SKU of a Marketplace image.

PowerShell
az vm image list-publishers -l <region> --query "[?contains(name,'<part>')].name" -o tsvaz vm image list-offers -l <region> -p <publisher> -o tableaz vm image list-skus   -l <region> -p <publisher> -f <offer> -o tableaz vm image list -p <publisher> -f <offer> -s <sku> -l <region> --all -o tableaz vm image terms show   --urn <publisher>:<offer>:<sku>:<version>az vm image terms accept --urn <publisher>:<offer>:<sku>:<version>
ProductPublisherOffer
FortiGatefortinetfortinet_fortigate-vm
Ubuntu 24.04Canonicalubuntu-24_04-lts
Windows ServerMicrosoftWindowsServerWindowsServer

#App Service / Functions

In detail – slots, Key Vault references, network, Functions: App Service and Functions.

PowerShell
az webapp list --query "[].{name:name, rg:resourceGroup, state:state, host:defaultHostName}" -o tableaz webapp show -g <rg> -n <app> --query "{state:state, https:httpsOnly, pna:publicNetworkAccess, vnet:virtualNetworkSubnetId}"az webapp restart -g <rg> -n <app>az webapp log tail -g <rg> -n <app>                                  # live logaz webapp config appsettings list -g <rg> -n <app> -o tableaz webapp config appsettings set  -g <rg> -n <app> --settings KEY=valueaz webapp config access-restriction show -g <rg> -n <app>az webapp vnet-integration list -g <rg> -n <app>az webapp update -g <rg> -n <app> --set publicNetworkAccess=Disabledaz functionapp list -o table

#Storage Account

List storage accounts and check TLS, public access and network rules.

PowerShell
az storage account list --query "[].{name:name, rg:resourceGroup, sku:sku.name, pna:publicNetworkAccess}" -o tableaz storage account show -g <rg> -n <sa> --query "{tls:minimumTlsVersion, public:allowBlobPublicAccess, key:allowSharedKeyAccess}"az storage account network-rule list -g <rg> --account-name <sa>az storage account update -g <rg> -n <sa> --default-action Denyaz storage container list --account-name <sa> --auth-mode login -o tableaz storage blob list   --account-name <sa> -c <container> --auth-mode login -o tableaz storage blob upload --account-name <sa> -c <container> -f <file> -n <name> --auth-mode loginaz storage blob download --account-name <sa> -c <container> -n <name> -f <target> --auth-mode login

--auth-mode login needs a data role (e.g. Storage Blob Data Contributor); Owner alone is not enough. More: Storage.

#AKS (short)

In detail – create, node pools, upgrade, troubleshooting: AKS.

PowerShell
az aks list -o tableaz aks get-credentials -g <rg> -n <aks>              # get kubeconfigaz aks show -g <rg> -n <aks> --query "{k8s:kubernetesVersion, net:networkProfile.networkPlugin, outbound:networkProfile.outboundType}"kubectl get nodes -o wide ; kubectl get pods -A

#Monitoring and Diagnostic Settings

Read the activity log and set up diagnostic settings.

PowerShell
az monitor activity-log list -g <rg> --offset 2h `  --query "[].{time:eventTimestamp, op:operationName.localizedValue, status:status.value, by:caller}" -o tableaz monitor activity-log list --correlation-id <id>az monitor diagnostic-settings list --resource <resource-id> -o tableaz monitor diagnostic-settings categories list --resource <resource-id> -o tableaz monitor diagnostic-settings create -n <name> --resource <resource-id> --workspace <law-id> `  --logs '[{"categoryGroup":"allLogs","enabled":true}]' --metrics '[{"category":"AllMetrics","enabled":true}]'az monitor log-analytics workspace list -o tableaz monitor log-analytics workspace show -g <rg> -n <law> --query customerId -o tsv     # workspace GUIDaz monitor log-analytics query -w <workspace-guid> --analytics-query "<KQL>" -o tableaz monitor metrics list-definitions --resource <resource-id> -o tableaz monitor metrics list --resource <resource-id> --metric <name> --interval PT5M -o tableaz monitor metrics alert list -g <rg> -o table

#REST and Token

Get an access token and call any Azure REST API with az rest.

PowerShell
az account get-access-token --query accessToken -o tsvaz rest --method get --url "https://management.azure.com/subscriptions/<sub>/resourceGroups?api-version=2021-04-01"az rest --method get --url "<resource-id>?api-version=<version>"     # raw resource as JSON

Useful when a CLI command doesn't (yet) know a property.

Azure

#Azure PowerShell (Az Module)

Same tasks with the CLI: Azure CLI · Side by side: CLI ↔ PowerShell · Step by step: Sign-in and Basics Workflows

PowerShell
Install-Module Az -Scope AllUsers -Repository PSGallery -Force   # as admin – see "Install Modules Without the User Profile" belowUpdate-Module Az -Scope AllUsers                                 # as adminConnect-AzAccount [-Tenant <id>] [-UseDeviceAuthentication]Connect-AzAccount -Identity                                # managed identityConnect-AzAccount -UseDeviceAuthentication                 # device code$cred = Get-Credential -UserName <app-id> ; Connect-AzAccount -ServicePrincipal -Credential $cred -Tenant <tenant-id>Get-AzContextGet-AzSubscriptionSet-AzContext -Subscription <name-or-id>Disconnect-AzAccount

#Install Modules Without the User Profile (AllUsers)

When a company policy (software restriction policy / AppLocker) blocks the user profile, modules under Documents\... do not load:

text
... cannot be loaded because its operation is blocked by software restriction policies ...

Modules under C:\Program Files\... still work.

Caution: Always install as admin with -Scope AllUsers, never with -Scope CurrentUser.

PowerShell 7 (pwsh)Windows PowerShell 5.1 (powershell)
Install location (works)C:\Program Files\PowerShell\ModulesC:\Program Files\WindowsPowerShell\Modules
Blocked location (profile)Documents\PowerShell\ModulesDocuments\WindowsPowerShell\Modules

Which version runs: $PSVersionTable.PSVersion (7 or 5). Errors with At line:1 char:1 / CategoryInfo come from 5.1.

Open an admin window (from cmd, Win+R or any PowerShell)

powershell -Command "Start-Process pwsh -Verb RunAs"
powershell -Command "Start-Process powershell -Verb RunAs"

Rename old copies in the user profile (if there are any) – otherwise a blocked copy is loaded, or dependencies such as Az.Accounts are skipped during the install. The command finds the real location itself, so it also works when Documents is redirected (e.g. OneDrive):

PowerShell
Get-Module <module>* -ListAvailable |    Where-Object { $_.ModuleBase -like "$HOME*" } |    ForEach-Object { if ($_.ModuleBase -match '\\\d+(\.\d+)+$') { Split-Path $_.ModuleBase } else { $_.ModuleBase } } |    Select-Object -Unique |    Where-Object { (Split-Path $_ -Leaf) -notlike '*_old' } |    ForEach-Object { Rename-Item $_ -NewName ((Split-Path $_ -Leaf) + '_old') }

Tip: Where are modules searched? $env:PSModulePath -split ';'

Install in the admin window

Install-Module <module> -Scope AllUsers -Repository PSGallery -Force
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12   # 5.1 often needs TLS 1.2 for the downloadInstall-PackageProvider NuGet -ForceInstall-Module <module> -Scope AllUsers -Repository PSGallery -Force

Answer "Untrusted repository" with A (Yes to All).

Test in a normal window (not admin)

PowerShell
Get-Module <module> -ListAvailable | Select-Object Version, PathImport-Module <module>

The path must be under C:\Program Files\.... No output from the import = success.

Updates – also in an admin window of the same PowerShell version:

PowerShell
Update-Module <module> -Scope AllUsers
Examples and special cases
PowerShell
Install-Module Az -Scope AllUsers -Repository PSGallery -ForceInstall-Module EnterprisePolicyAsCode -Scope AllUsers -Repository PSGallery -ForceInstall-Module Az.Accounts -Scope AllUsers -Repository PSGallery -Force   # if Az.Accounts is missing in C:\Program Files\...
  • With Az, check that Az.Accounts is in the install folder (Get-Module Az.Accounts -ListAvailable).
  • "running scripts is disabled on this system" in 5.1: Set-ExecutionPolicy RemoteSigned -Scope CurrentUser (no admin needed, unless the company sets it via group policy) – see Unblock and run downloaded scripts.
  • The *_old folders in the profile can be deleted once everything works.

#Resources

Resource groups and resources with the Az module.

PowerShell
Get-AzResourceGroup | Format-Table ResourceGroupName, LocationGet-AzResource -ResourceGroupName <rg> | Format-Table Name, ResourceTypeGet-AzResource -Name "*<part>*" | Format-Table Name, ResourceGroupName, ResourceTypeSearch-AzGraph -Query "Resources | where name contains '<part>' | project name, type, resourceGroup"   # module Az.ResourceGraph

#VMs

List, start and stop VMs with PowerShell.

PowerShell
Get-AzVM -Status | Select Name, ResourceGroupName, PowerStateStart-AzVM -ResourceGroupName <rg> -Name <vm>Stop-AzVM  -ResourceGroupName <rg> -Name <vm> -Force          # deallocatedInvoke-AzVMRunCommand -ResourceGroupName <rg> -VMName <vm> -CommandId RunPowerShellScript -ScriptString "ipconfig /all"

#Network

Virtual networks, peerings and effective routes with PowerShell.

PowerShell
Get-AzVirtualNetwork | Select Name, @{n='Space';e={$_.AddressSpace.AddressPrefixes}}Get-AzVirtualNetworkPeering -ResourceGroupName <rg> -VirtualNetworkName <vnet> | Select Name, PeeringState, AllowForwardedTrafficGet-AzEffectiveRouteTable -ResourceGroupName <rg> -NetworkInterfaceName <nic> | Format-TableGet-AzEffectiveNetworkSecurityGroup -ResourceGroupName <rg> -NetworkInterfaceName <nic>Get-AzNetworkSecurityGroup -Name <nsg> -ResourceGroupName <rg> | Get-AzNetworkSecurityRuleConfig | Format-TableGet-AzPublicIpAddress | Select Name, IpAddressGet-AzApplicationGatewayBackendHealth -ResourceGroupName <rg> -Name <agw>

#Application Gateway

Read certificates and listeners of an Application Gateway.

PowerShell
$AppGW = Get-AzApplicationGateway -Name <agw> -ResourceGroupName <rg>$AppGW.SslCertificates | Select Name, KeyVaultSecretId$AppGW.HttpListeners   | Select Name, Protocol, HostNames$secret   = Get-AzKeyVaultSecret -VaultName <kv> -Name <name>$secretId = $secret.Id.Replace($secret.Version, "")          # versionless secret ID$AppGW = Add-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <name> -KeyVaultSecretId $secretId$AppGW = Set-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <name> -KeyVaultSecretId <new-id>$AppGW = Remove-AzApplicationGatewaySslCertificate -ApplicationGateway $AppGW -Name <name>Set-AzApplicationGateway -ApplicationGateway $AppGW         # changes only take effect hereStop-AzApplicationGateway -ApplicationGateway $AppGW ; Start-AzApplicationGateway -ApplicationGateway $AppGW

Complete with identity and listener: Workflows → App Gateway.

#Permissions and Key Vault

Role assignments and Key Vault access with PowerShell.

PowerShell
Get-AzRoleAssignment -SignInName <upn>Get-AzRoleAssignment -Scope <resource-id>New-AzRoleAssignment -ObjectId <id> -RoleDefinitionName "<role>" -Scope <resource-id>Get-AzKeyVaultSecret -VaultName <kv> -Name <name> -AsPlainText
Azure

#Azure CLI ↔ Azure PowerShell

The same task in both worlds – handy when a team script is written in the other language. · Step by step: Sign-in and Basics Workflows

#Basics

Sign-in and subscription side by side.

TaskAzure CLIAzure PowerShell
Sign inaz loginConnect-AzAccount
Device codeaz login --use-device-codeConnect-AzAccount -UseDeviceAuthentication
Managed identityaz login --identityConnect-AzAccount -Identity
Current contextaz account showGet-AzContext
Subscriptionsaz account list -o tableGet-AzSubscription
Switch subscriptionaz account set --subscription <x>Set-AzContext -Subscription <x>
Sign outaz logoutDisconnect-AzAccount
Get a tokenaz account get-access-tokenGet-AzAccessToken
Versionaz versionGet-InstalledModule Az

#Resources and VMs

Resource groups and VMs in both worlds.

TaskAzure CLIAzure PowerShell
Resource groupsaz group list -o tableGet-AzResourceGroup
Create an RGaz group create -n <rg> -l <region>New-AzResourceGroup -Name <rg> -Location <region>
Resources in an RGaz resource list -g <rg>Get-AzResource -ResourceGroupName <rg>
Resource by IDaz resource show --ids <id>Get-AzResource -ResourceId <id>
Set tags (merge)az resource tag --ids <id> --tags k=v -iUpdate-AzTag -ResourceId <id> -Tag @{k='v'} -Operation Merge
VMs with statusaz vm list -d -o tableGet-AzVM -Status
Start a VMaz vm start -g <rg> -n <vm>Start-AzVM -ResourceGroupName <rg> -Name <vm>
Deallocate a VMaz vm deallocate -g <rg> -n <vm>Stop-AzVM -ResourceGroupName <rg> -Name <vm> -Force
Command in a VMaz vm run-command invoke … --scripts "<cmd>"Invoke-AzVMRunCommand … -ScriptString "<cmd>"
Resource Graphaz graph query -q "<kql>"Search-AzGraph -Query "<kql>"

#Network

Network commands in both worlds.

TaskAzure CLIAzure PowerShell
VNetsaz network vnet listGet-AzVirtualNetwork
Subnetaz network vnet subnet show -g <rg> --vnet-name <vnet> -n <snet>Get-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name <snet>
Peeringsaz network vnet peering list -g <rg> --vnet-name <vnet>Get-AzVirtualNetworkPeering -ResourceGroupName <rg> -VirtualNetworkName <vnet>
Effective routesaz network nic show-effective-route-table -g <rg> -n <nic>Get-AzEffectiveRouteTable -ResourceGroupName <rg> -NetworkInterfaceName <nic>
Effective NSGaz network nic list-effective-nsg -g <rg> -n <nic>Get-AzEffectiveNetworkSecurityGroup -ResourceGroupName <rg> -NetworkInterfaceName <nic>
NSG rulesaz network nsg rule list -g <rg> --nsg-name <nsg>Get-AzNetworkSecurityGroup -Name <nsg> -ResourceGroupName <rg> | Get-AzNetworkSecurityRuleConfig
Public IPsaz network public-ip listGet-AzPublicIpAddress
App Gatewayaz network application-gateway show -g <rg> -n <agw>Get-AzApplicationGateway -ResourceGroupName <rg> -Name <agw>
Backend healthaz network application-gateway show-backend-health …Get-AzApplicationGatewayBackendHealth …

Key difference: PowerShell often works with objects: get → modify → write back.

PowerShell
$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg> -Name <vnet>Add-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name <snet> -AddressPrefix 10.0.5.0/24 | Out-Null$vnet | Set-AzVirtualNetwork                         # only now is it changed in Azure

The CLI changes directly: az network vnet subnet create -g <rg> --vnet-name <vnet> -n <snet> --address-prefixes 10.0.5.0/24.

#Permissions, Key Vault, Deployments

Roles, Key Vault and deployments in both worlds.

TaskAzure CLIAzure PowerShell
A user's rolesaz role assignment list --assignee <upn> --allGet-AzRoleAssignment -SignInName <upn>
Assign a roleaz role assignment create --assignee <id> --role "<role>" --scope <scope>New-AzRoleAssignment -ObjectId <id> -RoleDefinitionName "<role>" -Scope <scope>
Own object IDaz ad signed-in-user show --query id -o tsv(Get-AzADUser -SignedIn).Id
Find a service principalaz ad sp list --display-name <name>Get-AzADServicePrincipal -DisplayName <name>
Read a secretaz keyvault secret show --vault-name <kv> -n <name> --query value -o tsvGet-AzKeyVaultSecret -VaultName <kv> -Name <name> -AsPlainText
Set a secretaz keyvault secret set --vault-name <kv> -n <name> --value <v>Set-AzKeyVaultSecret -VaultName <kv> -Name <name> -SecretValue (ConvertTo-SecureString <v> -AsPlainText -Force)
Deploy Bicepaz deployment group create -g <rg> -f main.bicep -p <p>.bicepparamNew-AzResourceGroupDeployment -ResourceGroupName <rg> -TemplateFile main.bicep -TemplateParameterFile <p>.bicepparam
what-ifaz deployment group what-if …New-AzResourceGroupDeployment … -WhatIf
Policy assignmentsaz policy assignment list --scope <scope>Get-AzPolicyAssignment -Scope <scope>

#Output and Filtering

Table, JSON and filters in both worlds.

TaskAzure CLIAzure PowerShell
Table-o table| Format-Table
Single value--query id -o tsv(Get-AzVM …).Id
Filter--query "[?location=='westeurope']"| Where-Object Location -eq 'westeurope'
Choose columns--query "[].{n:name, rg:resourceGroup}"| Select-Object Name, ResourceGroupName
Count--query "length(@)"(… ).Count
As JSON-o json| ConvertTo-Json -Depth 5
CLI output as objectaz … -o json | ConvertFrom-Json–
Azure

#App Service and Azure Functions

Publish behind Front Door: Front Door · Private endpoint: Workflow · Static sites: Static Web App

Web apps, APIs and functions without managing VMs. An App Service plan = the compute (SKU, number of instances), apps run on it.

Top 5: az webapp log tail · az webapp restart · az webapp config appsettings set · az webapp deployment slot swap · az webapp deploy

#Plans and SKUs

Which plan fits which use.

SKUForNotes
F1 / B1–B3test, devno slots, no autoscale (B: manual scale up to 3)
S1–S3older standardslots (5), autoscale – prefer Premium v3
P0v3–P3v3 / P1mv3…productionslots (20), autoscale, better price/performance, zone redundancy (≥ 2–3 instances)
I1v2… (ASE v3)isolated, own VNetsingle tenant, expensive
Functions Flex Consumptionevent-driven, scale to zeroVNet integration, per-execution billing – recommended for new function apps
Functions Premium (EP1–EP3)always warm, VNetno cold start, longer runtime

Linux and Windows apps need separate plans. All apps on a plan share its CPU/RAM – one noisy app slows the others.

#Create a Web App

Plan, app, runtime and first settings.

$rg = "<rg>" ; $plan = "asp-<app>-prd-weu" ; $app = "app-<app>-prd-weu"az appservice plan create -g $rg -n $plan --sku P0v3 --is-linux -l westeuropeaz webapp list-runtimes --os linux -o table                   # valid runtime stringsaz webapp create -g $rg -n $app -p $plan --runtime "DOTNETCORE:8.0"az webapp update -g $rg -n $app --https-only trueaz webapp config set -g $rg -n $app --min-tls-version 1.2 --ftps-state Disabled --always-on true --http20-enabled trueaz webapp identity assign -g $rg -n $app                      # system-assigned managed identity
$rg = "<rg>" ; $plan = "asp-<app>-prd-weu" ; $app = "app-<app>-prd-weu"New-AzAppServicePlan -ResourceGroupName $rg -Name $plan -Location westeurope -Tier PremiumV3 -WorkerSize Small -LinuxNew-AzWebApp -ResourceGroupName $rg -Name $app -AppServicePlan $plan -Location westeuropeSet-AzWebApp -ResourceGroupName $rg -Name $app -HttpsOnly $true -MinTlsVersion 1.2 -FtpsState Disabled -AlwaysOn $true -AssignIdentity $true
resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {  name: 'asp-<app>-prd-weu'  location: location  sku: { name: 'P0v3', capacity: 1 }  kind: 'linux'  properties: { reserved: true }            // true = Linux}resource app 'Microsoft.Web/sites@2023-12-01' = {  name: 'app-<app>-prd-weu'  location: location  identity: { type: 'SystemAssigned' }  properties: {    serverFarmId: plan.id    httpsOnly: true    siteConfig: {      linuxFxVersion: 'DOTNETCORE|8.0'      minTlsVersion: '1.2'      ftpsState: 'Disabled'      alwaysOn: true    }  }}
resource "azurerm_service_plan" "this" {  name                = "asp-<app>-prd-weu"  resource_group_name = "<rg>"  location            = "westeurope"  os_type             = "Linux"  sku_name            = "P0v3"}resource "azurerm_linux_web_app" "this" {  name                = "app-<app>-prd-weu"  resource_group_name = "<rg>"  location            = "westeurope"  service_plan_id     = azurerm_service_plan.this.id  https_only          = true  identity { type = "SystemAssigned" }  site_config {    always_on           = true    minimum_tls_version = "1.2"    ftps_state          = "Disabled"    application_stack { dotnet_version = "8.0" }  }}

App names are globally unique (<app>.azurewebsites.net). Runtime strings use : in the CLI and | in linuxFxVersion.

#Deploy Code

Zip, jar and slot deployment.

PowerShell
az webapp deploy -g <rg> -n <app> --src-path .\publish.zip --type zip          # zip deploy (recommended)az webapp deploy -g <rg> -n <app> --src-path .\app.jar --type jaraz webapp deploy -g <rg> -n <app> --src-path .\publish.zip --type zip --slot stagingaz webapp config container set -g <rg> -n <app> --container-image-name <acr>.azurecr.io/<image>:<tag> `  --container-registry-url https://<acr>.azurecr.io                          # container (managed identity needs AcrPull)az webapp deployment list-publishing-profiles -g <rg> -n <app> --xml           # only if a tool needs it – contains secrets

Pipeline: task AzureWebApp@1 (appName, package, optional deployToSlotOrASE + slotName) – details in Pipelines. Kudu console / files: https://<app>.scm.azurewebsites.net.

#Deployment Slots

Deploy to staging, warm up, then swap – no downtime, roll back by swapping again.

PowerShell
az webapp deployment slot create -g <rg> -n <app> --slot staging --configuration-source <app>az webapp deployment slot list -g <rg> -n <app> -o tableaz webapp deployment slot swap -g <rg> -n <app> --slot staging --target-slot productionaz webapp deployment slot swap -g <rg> -n <app> --slot staging --action preview      # swap with preview (apply settings first)az webapp deployment slot swap -g <rg> -n <app> --slot staging --action swapaz webapp traffic-routing set -g <rg> -n <app> --distribution staging=10             # 10 % canary to stagingaz webapp traffic-routing clear -g <rg> -n <app>

Important: settings marked as deployment slot setting (--slot-settings) stay with the slot – connection strings to the test database must be slot settings, otherwise they move to production on swap.

#Settings, Secrets and Key Vault References

App settings, slot settings and secrets from Key Vault.

PowerShell
az webapp config appsettings list -g <rg> -n <app> -o tableaz webapp config appsettings set -g <rg> -n <app> --settings ASPNETCORE_ENVIRONMENT=Production FeatureX=trueaz webapp config appsettings set -g <rg> -n <app> --slot staging --slot-settings ASPNETCORE_ENVIRONMENT=Stagingaz webapp config appsettings delete -g <rg> -n <app> --setting-names FeatureXaz webapp config connection-string set -g <rg> -n <app> -t SQLAzure --settings Db="<connection-string>"# Key Vault reference instead of the secret value (identity of the app needs "Key Vault Secrets User")az webapp config appsettings set -g <rg> -n <app> --settings "DbPassword=@Microsoft.KeyVault(SecretUri=https://<keyvault-name>.vault.azure.net/secrets/<secret-name>/)"$mi = az webapp identity show -g <rg> -n <app> --query principalId -o tsvaz role assignment create --assignee-object-id $mi --assignee-principal-type ServicePrincipal `  --role "Key Vault Secrets User" --scope (az keyvault show -n <keyvault-name> --query id -o tsv)

Without a version in the SecretUri the app picks up a new secret version within 24 h (or on restart). Status of the reference: portal → app → Environment variables → green check / red error at the setting.

#Network: VNet Integration, Private Endpoint, Access Restrictions

Outbound, inbound and allow lists compared.

DirectionFeatureWhat it does
App → outbound into VNetVNet integration (delegated subnet, /26 recommended)app reaches private endpoints, on-prem, firewall
Clients → app inboundPrivate endpointapp only reachable via private IP; public access off
Clients → app inboundAccess restrictionsallow/deny by IP, service tag, VNet, Front Door ID
PowerShell
# Outbound: VNet integration – all traffic through the VNet (UDR / firewall apply)az webapp vnet-integration add -g <rg> -n <app> --vnet <vnet> --subnet <snet-appsvc-integration>az webapp config set -g <rg> -n <app> --vnet-route-all-enabled true# Inbound: only Front Door may reach the app$fdid = az afd profile show -g <rg-afd> --profile-name <afd-profile> --query frontDoorId -o tsvaz webapp config access-restriction add -g <rg> -n <app> --rule-name allow-afd --priority 100 --action Allow `  --service-tag AzureFrontDoor.Backend --http-header x-azure-fdid=$fdidaz webapp config access-restriction show -g <rg> -n <app>az webapp config access-restriction set -g <rg> -n <app> --use-same-restrictions-for-scm-site false# Inbound private: disable public access after the private endpoint worksaz webapp update -g <rg> -n <app> --set publicNetworkAccess=Disabled

Private endpoint for the web app: Workflow – DNS zone privatelink.azurewebsites.net. With VNet integration the app resolves DNS via the VNet's DNS servers.

#Custom Domain and Certificate

Domain verification, hostname and certificate.

PowerShell
# DNS: CNAME www -> <app>.azurewebsites.net and TXT asuid.www -> verification IDaz webapp show -g <rg> -n <app> --query customDomainVerificationId -o tsvaz webapp config hostname add -g <rg> --webapp-name <app> --hostname www.<domain># Free managed certificate (CNAME domains; renews automatically)az webapp config ssl create -g <rg> -n <app> --hostname www.<domain>$thumb = az webapp config ssl list -g <rg> --query "[?subjectName=='www.<domain>'].thumbprint | [0]" -o tsvaz webapp config ssl bind -g <rg> -n <app> --certificate-thumbprint $thumb --ssl-type SNI# Certificate from Key Vault (App Service resource provider needs access to the vault)az webapp config ssl import -g <rg> -n <app> --key-vault <keyvault-name> --key-vault-certificate-name <certificate-name>

Behind Front Door the certificate lives on Front Door – see Front Door.

#Logs, Restart and Scaling

Live log, restart and scale up or out.

PowerShell
az webapp log config -g <rg> -n <app> --application-logging filesystem --level information --docker-container-logging filesystem --web-server-logging filesystemaz webapp log tail -g <rg> -n <app>                           # live log streamaz webapp log download -g <rg> -n <app> --log-file logs.zipaz webapp restart -g <rg> -n <app>az webapp stop -g <rg> -n <app> ; az webapp start -g <rg> -n <app>az appservice plan update -g <rg> -n <plan> --sku P1v3          # scale upaz appservice plan update -g <rg> -n <plan> --number-of-workers 3   # scale outaz webapp show -g <rg> -n <app> --query "{state:state, host:defaultHostName, plan:appServicePlanId, outboundIps:outboundIpAddresses}"

For production: diagnostic setting → Log Analytics (AppServiceHTTPLogs, AppServiceConsoleLogs, AppServiceAppLogs) and Application Insights. KQL examples: KQL. Portal tool: Diagnose and solve problems (availability, CPU, crashes, SNAT).

#Azure Functions

Create a function app and deploy code.

$rg = "<rg>" ; $st = "st<app>func01" ; $func = "func-<app>-prd-weu"az storage account create -g $rg -n $st -l westeurope --sku Standard_LRS --allow-blob-public-access false# Flex Consumption (scale to zero, VNet capable)az functionapp create -g $rg -n $func --storage-account $st --flexconsumption-location westeurope `  --runtime python --runtime-version 3.11# Premium plan (always warm)az functionapp plan create -g $rg -n asp-<app>-func --sku EP1 --is-linux -l westeuropeaz functionapp create -g $rg -n $func --storage-account $st --plan asp-<app>-func `  --runtime dotnet-isolated --runtime-version 8 --functions-version 4 --os-type Linuxaz functionapp function list -g $rg -n $func -o tableaz functionapp keys list -g $rg -n $funcaz functionapp config appsettings set -g $rg -n $func --settings MySetting=value
func init <project> --worker-runtime python      # new project locallyfunc new --name HttpHello --template "HTTP trigger"func start                                       # run locallyfunc azure functionapp publish <func-app-name>   # deployfunc azure functionapp fetch-app-settings <func-app-name>   # settings into local.settings.json

Functions need a storage account (AzureWebJobsStorage) – with managed identity instead of a key: AzureWebJobsStorage__accountName=<storage-account> + roles Storage Blob Data Owner (and Queue/Table roles depending on triggers). Logs: Application Insights → Live metrics, traces / requests tables.

#Troubleshooting

Symptoms, causes and fixes.

SymptomCauseFix
503 after deploy / startapp crashes on start (missing setting, wrong runtime)az webapp log tail, Kudu → LogFiles, check runtime string
403 Forbiddenaccess restriction / private endpoint with public access offaccess-restriction show, test from inside the VNet
Key Vault reference shows erroridentity missing role / vault firewallKey Vault Secrets User for the app identity, allow VNet / trusted services
App cannot reach private resourcesno VNet integration / DNSVNet integration + vnetRouteAllEnabled, private DNS zone linked to the VNet
Slow first requestAlways On off / Consumption cold start--always-on true, Premium/Flex with always-ready instances
Setting moved to production on swapnot marked as slot setting--slot-settings
Custom domain not verifiedasuid TXT record missingTXT asuid.<sub> = customDomainVerificationId
SNAT port exhaustion (timeouts outbound)many new connectionsreuse HTTP clients, VNet integration + NAT gateway
Function not triggeringstorage/identity problem, app setting missingcheck AzureWebJobsStorage*, App Insights traces for errors
Azure

#AKS – Azure Kubernetes Service

Network basics: Azure Network · Monitoring: Monitoring · KQL: KQL

Managed Kubernetes: Azure runs the control plane, you run node pools (VM scale sets). Daily work = az aks for the cluster, kubectl for what runs on it.

Top 5: az aks get-credentials · kubectl get pods -A · kubectl describe pod · kubectl logs · az aks get-upgrades

#Create a Cluster

Recommended defaults: Azure CNI Overlay (pods get IPs from a private overlay range, not from the subnet), managed identity, Entra ID + Azure RBAC for access, local accounts off, tier Standard for production (uptime SLA).

$rg = "<rg>" ; $aks = "aks-<workload>-prd-weu"$subnet = az network vnet subnet show -g <rg-network> --vnet-name <vnet> -n <snet-aks> --query id -o tsvaz aks create -g $rg -n $aks -l westeurope --tier standard `  --kubernetes-version <version> --node-count 3 --node-vm-size Standard_D4ds_v5 --zones 1 2 3 `  --network-plugin azure --network-plugin-mode overlay --pod-cidr 192.168.0.0/16 --vnet-subnet-id $subnet `  --enable-managed-identity --enable-aad --enable-azure-rbac --disable-local-accounts `  --enable-oidc-issuer --enable-workload-identity `  --attach-acr <acr> --generate-ssh-keysaz aks get-versions -l westeurope -o table                    # supported versions
resource aks 'Microsoft.ContainerService/managedClusters@2024-05-01' = {  name: 'aks-<workload>-prd-weu'  location: location  sku: { name: 'Base', tier: 'Standard' }  identity: { type: 'SystemAssigned' }  properties: {    dnsPrefix: 'aks-<workload>-prd'    kubernetesVersion: '<version>'    disableLocalAccounts: true    aadProfile: { managed: true, enableAzureRBAC: true }    agentPoolProfiles: [      {        name: 'system'        mode: 'System'        count: 3        vmSize: 'Standard_D4ds_v5'        availabilityZones: [ '1', '2', '3' ]        vnetSubnetID: subnetId      }    ]    networkProfile: { networkPlugin: 'azure', networkPluginMode: 'overlay', podCidr: '192.168.0.0/16' }    oidcIssuerProfile: { enabled: true }    securityProfile: { workloadIdentity: { enabled: true } }  }}
resource "azurerm_kubernetes_cluster" "this" {  name                      = "aks-<workload>-prd-weu"  resource_group_name       = "<rg>"  location                  = "westeurope"  dns_prefix                = "aks-<workload>-prd"  kubernetes_version        = "<version>"  sku_tier                  = "Standard"  local_account_disabled    = true  oidc_issuer_enabled       = true  workload_identity_enabled = true   default_node_pool {    name           = "system"    node_count     = 3    vm_size        = "Standard_D4ds_v5"    zones          = ["1", "2", "3"]    vnet_subnet_id = "<subnet-id>"  }  identity { type = "SystemAssigned" }  network_profile {    network_plugin      = "azure"    network_plugin_mode = "overlay"    pod_cidr            = "192.168.0.0/16"  }  azure_active_directory_role_based_access_control {    azure_rbac_enabled = true  }}

Subnet size with overlay: only nodes need subnet IPs (+ surge during upgrades) – a /24 is enough for many clusters. Pod CIDR must not overlap with VNets, on-prem or the service CIDR (default 10.0.0.0/16).

#Connect (kubectl)

Get credentials and sign in with Entra ID.

PowerShell
az aks install-cli                                         # kubectl + kubeloginaz aks get-credentials -g <rg> -n <aks> --overwrite-existingkubelogin convert-kubeconfig -l azurecli                   # use your az login for Entra IDkubectl config get-contexts ; kubectl config use-context <aks>kubectl get nodes -o wide# Access with Azure RBAC: give a group a cluster roleaz role assignment create --assignee <group-object-id> --role "Azure Kubernetes Service RBAC Cluster Admin" `  --scope (az aks show -g <rg> -n <aks> --query id -o tsv)

Roles: AKS RBAC Reader / Writer / Admin (namespace scope possible: --scope <aks-id>/namespaces/<ns>), AKS RBAC Cluster Admin. To download the kubeconfig you also need Azure Kubernetes Service Cluster User Role.

#kubectl in Daily Work

Pods, logs, exec and rollouts.

PowerShell
kubectl get pods -A                                         # all namespaceskubectl get pods -n <ns> -o wide                            # with node and pod IPkubectl get deploy,svc,ingress -n <ns>kubectl describe pod <pod> -n <ns>                          # events at the bottom: why it does not startkubectl logs <pod> -n <ns> --tail 100 -f                    # follow logskubectl logs <pod> -n <ns> --previous                       # logs of the crashed containerkubectl exec -it <pod> -n <ns> -- sh                        # shell in the containerkubectl get events -n <ns> --sort-by .lastTimestampkubectl top nodes ; kubectl top pods -n <ns>                # CPU/memory (metrics server)kubectl apply -f deployment.yamlkubectl rollout status deploy/<deployment> -n <ns>kubectl rollout restart deploy/<deployment> -n <ns>         # restart all podskubectl rollout undo deploy/<deployment> -n <ns>            # back to the previous versionkubectl scale deploy/<deployment> -n <ns> --replicas 3kubectl port-forward svc/<service> -n <ns> 8080:80          # test locally on http://localhost:8080kubectl delete pod <pod> -n <ns>                            # pod is recreated by its deployment

#Node Pools and Scaling

Add, scale and remove node pools.

PowerShell
az aks nodepool list -g <rg> --cluster-name <aks> -o tableaz aks nodepool add -g <rg> --cluster-name <aks> -n user01 --mode User --node-count 2 `  --node-vm-size Standard_D8ds_v5 --zones 1 2 3 --labels workload=appsaz aks nodepool scale -g <rg> --cluster-name <aks> -n user01 --node-count 4az aks nodepool update -g <rg> --cluster-name <aks> -n user01 --enable-cluster-autoscaler --min-count 2 --max-count 6az aks nodepool update -g <rg> --cluster-name <aks> -n user01 --update-cluster-autoscaler --min-count 2 --max-count 10az aks nodepool delete -g <rg> --cluster-name <aks> -n user01

System pools run CoreDNS, metrics etc. (min. 1 node, better 3 across zones) – put applications on User pools. Node VM size of a pool cannot be changed: add a new pool, move workloads (kubectl cordon / drain), delete the old one.

#Upgrade

Check versions and upgrade control plane and nodes.

PowerShell
az aks get-upgrades -g <rg> -n <aks> -o tableaz aks upgrade -g <rg> -n <aks> --kubernetes-version <version> --control-plane-only    # first the control planeaz aks nodepool upgrade -g <rg> --cluster-name <aks> -n user01 --kubernetes-version <version>az aks nodepool upgrade -g <rg> --cluster-name <aks> -n user01 --node-image-only      # only OS image (security patches)az aks nodepool update -g <rg> --cluster-name <aks> -n user01 --max-surge 33%         # faster, needs subnet IPsaz aks update -g <rg> -n <aks> --auto-upgrade-channel patch --node-os-upgrade-channel NodeImageaz aks maintenanceconfiguration list -g <rg> --cluster-name <aks> -o table

AKS supports the last three minor versions – upgrade at least every few months. Upgrade one minor version at a time. PodDisruptionBudgets that allow 0 disruptions block node drains.

#Container Registry (ACR)

Attach a registry and test the pull.

PowerShell
az aks update -g <rg> -n <aks> --attach-acr <acr>              # AcrPull for the kubelet identityaz aks check-acr -g <rg> -n <aks> --acr <acr>.azurecr.io        # test pull from the nodesaz acr login -n <acr>az acr build -r <acr> -t <image>:<tag> .                        # build in Azure, no local Docker neededaz acr repository list -n <acr> -o tableaz acr repository show-tags -n <acr> --repository <image> --orderby time_desc -o table

#Private Cluster and Run Command

Private cluster = API server only via private endpoint. Without a jump host or VPN use run command – it runs kubectl inside Azure:

PowerShell
az aks command invoke -g <rg> -n <aks> --command "kubectl get pods -A"az aks command invoke -g <rg> -n <aks> --command "kubectl apply -f deployment.yaml" --file deployment.yaml

API server reachable only from certain IPs (public cluster): az aks update -g <rg> -n <aks> --api-server-authorized-ip-ranges <your-ip>/32.

#Costs: Stop and Start

Stop dev/test clusters and start them again.

PowerShell
az aks stop -g <rg> -n <aks>                                   # dev/test: control plane + nodes stopped, no VM costsaz aks start -g <rg> -n <aks>az aks show -g <rg> -n <aks> --query powerState.code -o tsvaz aks nodepool scale -g <rg> --cluster-name <aks> -n user01 --node-count 0    # user pools can go to 0

More savings: autoscaler with low min-count, spot node pools (--priority Spot) for interruptible jobs, right-size requests/limits. Cost view: Costs.

#Troubleshooting

Typical statuses, causes and fixes.

Status / symptomCauseFix
ImagePullBackOff / ErrImagePullwrong image/tag, no AcrPull, firewall blocks registrykubectl describe pod, az aks check-acr, allow ACR/MCR FQDNs in the firewall
CrashLoopBackOffapp crashes on start (config, secret, port)kubectl logs --previous, check env/secrets
Pendingno node with enough CPU/memory, taints, zone/PVC conflictkubectl describe pod → events, scale pool / autoscaler
OOMKilledmemory limit too lowraise resources.limits.memory, fix leak
Node NotReadynode problem, network/DNS to API serverkubectl describe node, az aks nodepool upgrade --node-image-only, restart node via VMSS
kubectl asks for device login / Unauthorizedkubeconfig not converted / no RBAC rolekubelogin convert-kubeconfig -l azurecli, assign AKS RBAC role
Upgrade hangsPodDisruptionBudget blocks drain, too few IPs for surgecheck PDBs (kubectl get pdb -A), lower max-surge or bigger subnet
Outbound from pods failsUDR to firewall without required AKS FQDNsallow AKS required FQDNs / FQDN tag AzureKubernetesService in Azure Firewall
Service type LoadBalancer gets no IPsubnet/permissions of the cluster identitycluster identity needs Network Contributor on the subnet
KQL
// Container Insights: errors in container logs of the last hourContainerLogV2| where TimeGenerated > ago(1h) and LogMessage has_any ("error", "exception", "fail")| project TimeGenerated, PodNamespace, PodName, ContainerName, LogMessage| order by TimeGenerated desc
Azure

#Azure Virtual Desktop (AVD)

Logs: KQL · Identities: Entra ID · Step by step: VM, Linux and Windows Workflows

Top 5: az desktopvirtualization hostpool list · Get-AzWvdSessionHost · Get-AzWvdUserSession · Update-AzWvdSessionHost -AllowNewSession:$false · WVDConnections

#Building Blocks

Host pool, app group, workspace and how they belong together.

ResourcePurpose
Host poolgroup of session hosts (VMs); Pooled (multi-session) or Personal (1:1)
Session hostthe VM with the AVD agent, registered to the host pool via token
Application groupDesktop or RemoteApp – users get access here
Workspacewhat the user sees in the client; contains application groups
Scaling planstarts/stops session hosts on a schedule (saves costs)
FSLogixuser profiles in an Azure Files share (profile container)

Access: role Desktop Virtualization User on the application group. With Entra-joined session hosts additionally Virtual Machine User Login (or Administrator Login) on the VMs/RG.

#Create Host Pool, App Group, Workspace

Create all three and link them.

az extension add -n desktopvirtualization$exp = (Get-Date).ToUniversalTime().AddHours(24).ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")az desktopvirtualization hostpool create -g <rg> -n vdpool-<name> -l westeurope `  --host-pool-type Pooled --load-balancer-type BreadthFirst --max-session-limit 10 `  --preferred-app-group-type Desktop --registration-info expiration-time=$exp registration-token-operation="Update"$hp = az desktopvirtualization hostpool show -g <rg> -n vdpool-<name> --query id -o tsvaz desktopvirtualization applicationgroup create -g <rg> -n vdag-<name>-desktop -l westeurope `  --host-pool-arm-path $hp --application-group-type Desktop$ag = az desktopvirtualization applicationgroup show -g <rg> -n vdag-<name>-desktop --query id -o tsvaz desktopvirtualization workspace create -g <rg> -n vdws-<name> -l westeurope --application-group-references $agaz role assignment create --assignee-object-id <group-object-id> --assignee-principal-type Group `  --role "Desktop Virtualization User" --scope $ag
$hp = New-AzWvdHostPool -ResourceGroupName <rg> -Name vdpool-<name> -Location westeurope -HostPoolType Pooled `        -LoadBalancerType BreadthFirst -MaxSessionLimit 10 -PreferredAppGroupType Desktop$ag = New-AzWvdApplicationGroup -ResourceGroupName <rg> -Name vdag-<name>-desktop -Location westeurope `        -HostPoolArmPath $hp.Id -ApplicationGroupType DesktopNew-AzWvdWorkspace -ResourceGroupName <rg> -Name vdws-<name> -Location westeurope -ApplicationGroupReference $ag.IdNew-AzRoleAssignment -ObjectId <group-object-id> -RoleDefinitionName "Desktop Virtualization User" -Scope $ag.Id

#Registration Token and Session Hosts

Create a token and add session hosts.

$exp = (Get-Date).ToUniversalTime().AddHours(24).ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ")az desktopvirtualization hostpool update -g <rg> -n vdpool-<name> `  --registration-info expiration-time=$exp registration-token-operation="Update"az desktopvirtualization hostpool retrieve-registration-token -g <rg> -n vdpool-<name> --query token -o tsv
New-AzWvdRegistrationInfo -ResourceGroupName <rg> -HostPoolName vdpool-<name> -ExpirationTime (Get-Date).ToUniversalTime().AddHours(24)(Get-AzWvdRegistrationInfo -ResourceGroupName <rg> -HostPoolName vdpool-<name>).Token

Tip: macOS / Linux (Bash) – expiry as variable: macOS exp=$(date -u -v+24H +%Y-%m-%dT%H:%M:%S.0000000Z), Linux exp=$(date -u -d '+24 hours' +%Y-%m-%dT%H:%M:%S.0000000Z). Then expiration-time=$exp.

Add session hosts: portal → host pool → Session hosts → + Add (creates VMs, joins them to Entra ID/AD and registers them with the token). The token is valid for max. 30 days – renew it for new VMs.

#Daily Operation

Session hosts, user sessions and drain mode.

PowerShell
# Session hosts: status, sessions, drain modeGet-AzWvdSessionHost -ResourceGroupName <rg> -HostPoolName vdpool-<name> |  Select-Object @{n='Host';e={($_.Name -split '/')[-1]}}, Status, Session, AllowNewSession, UpdateStateUpdate-AzWvdSessionHost -ResourceGroupName <rg> -HostPoolName vdpool-<name> -Name <host-fqdn> -AllowNewSession:$false   # drain for maintenance # User sessions: list, send message, log offGet-AzWvdUserSession -ResourceGroupName <rg> -HostPoolName vdpool-<name> |  Select-Object ActiveDirectoryUserName, SessionState, @{n='Host';e={($_.Name -split '/')[1]}}, @{n='Id';e={($_.Name -split '/')[-1]}}Send-AzWvdUserSessionMessage -ResourceGroupName <rg> -HostPoolName vdpool-<name> -SessionHostName <host-fqdn> -UserSessionId <id> `  -MessageTitle "Maintenance" -MessageBody "Please save your work – logoff in 10 minutes."Remove-AzWvdUserSession -ResourceGroupName <rg> -HostPoolName vdpool-<name> -SessionHostName <host-fqdn> -Id <id>

#Network and Firewall

URLs, ports and RDP Shortpath.

  • Session hosts need outbound access to the AVD service URLs (service tags WindowsVirtualDesktop, AzureMonitor, AzureFrontDoor.Frontend, plus login.microsoftonline.com, KMS azkms.core.windows.net:1688). Check on the VM: C:\Program Files\Microsoft RDInfra\RDAgent_*\WVDAgentUrlTool.exe.
  • No inbound ports needed – connections go through the AVD gateway (reverse connect).
  • RDP Shortpath (UDP 3390 managed networks / STUN for public networks) improves latency – allow UDP in NSG/firewall.
  • FSLogix share: SMB 445 from the session hosts to the storage account (private endpoint recommended), Storage → Azure Files.

#Logs (KQL)

Diagnostic setting on host pool and workspace → Log Analytics.

KQL
// Connections per user / host, last 24 hWVDConnections| where TimeGenerated > ago(24h) and State == "Connected"| summarize Connections = count() by UserName, SessionHostName| order by Connections desc // ErrorsWVDErrors| where TimeGenerated > ago(24h)| summarize Count = count() by CodeSymbolic, Message| order by Count desc // Session host health check failuresWVDAgentHealthStatus| where TimeGenerated > ago(1h) and Status != "Available"| project TimeGenerated, SessionHostName, Status, SessionHostHealthCheckResult
SymptomCauseFix
User sees no desktop in the clientrole missing on the app group / app group not in workspaceDesktop Virtualization User, workspace reference
Host Unavailableagent cannot reach the service URLs (firewall/proxy/DNS)URL check tool, firewall rules
Login fails on Entra-joined hostsVirtual Machine User Login missing / RDP propertyrole on the VM/RG, targetisaadjoined:i:1 in RDP properties
Profile does not loadFSLogix share not reachable / permissionsSMB 445, share and NTFS permissions
New VMs don't registerregistration token expirednew token
Azure

#Azure Arc (Servers)

Patching: Update Manager · Monitoring: Monitoring

Brings servers outside Azure (on-prem, other clouds) into Azure as resources: policy, Defender, Update Manager, monitoring, RBAC – with the Connected Machine agent (azcmagent).

Top 5: azcmagent show · azcmagent check · az connectedmachine list · az connectedmachine extension list · azcmagent logs

#Requirements

Network, permissions and supported systems.

ItemValue
OutboundHTTPS 443 to Azure (service tags AzureArcInfrastructure, AzureResourceManager, AzureActiveDirectory, GuestAndHybridManagement) – or via proxy / private link
Resource providersMicrosoft.HybridCompute, Microsoft.GuestConfiguration, Microsoft.HybridConnectivity, Microsoft.AzureArcData
Onboarding identityservice principal with role Azure Connected Machine Onboarding on the RG
OSWindows Server 2012 R2+ (ESU), current Linux distributions
PowerShell
foreach ($p in "Microsoft.HybridCompute","Microsoft.GuestConfiguration","Microsoft.HybridConnectivity","Microsoft.AzureArcData") { az provider register -n $p }az ad sp create-for-rbac -n sp-arc-onboarding --role "Azure Connected Machine Onboarding" --scopes /subscriptions/<sub-id>/resourceGroups/<rg-arc>

#Onboard a Server

Portal: Azure Arc → Machines → Add/Create → Add a machine → generates the script (single server or at scale with service principal).

# after installing the agent (AzureConnectedMachineAgent.msi)& "$env:ProgramW6432\AzureConnectedMachineAgent\azcmagent.exe" connect `  --service-principal-id <app-id> --service-principal-secret <secret> `  --tenant-id <tenant-id> --subscription-id <sub-id> --resource-group <rg-arc> --location westeurope `  --tags "Environment=prd,Owner=<team>"
wget https://aka.ms/azcmagent -O ~/install_linux_azcmagent.shsudo bash ~/install_linux_azcmagent.shsudo azcmagent connect --service-principal-id <app-id> --service-principal-secret <secret> \  --tenant-id <tenant-id> --subscription-id <sub-id> --resource-group <rg-arc> --location westeurope

Behind a proxy: azcmagent config set proxy.url "http://<proxy>:<port>" before connect. Never store the secret in scripts – short-lived secret or certificate, delete after onboarding.

#Agent on the Server

Status and checks with azcmagent.

PowerShell
azcmagent show                         # status, resource ID, last heartbeatazcmagent check                        # tests connectivity to all required endpointsazcmagent config listazcmagent logs                         # collects logs into a zip for supportazcmagent disconnect                   # remove from Azure (deletes the resource too)

#In Azure

List and manage Arc machines and their extensions.

az extension add -n connectedmachineaz connectedmachine list -g <rg-arc> --query "[].{name:name, status:status, os:osName, version:agentVersion, lastSeen:lastStatusChange}" -o tableaz connectedmachine extension list -g <rg-arc> --machine-name <server> --query "[].{name:name, state:provisioningState, version:typeHandlerVersion}" -o table# Azure Monitor Agentaz connectedmachine extension create -g <rg-arc> --machine-name <server> -n AzureMonitorWindowsAgent `  --publisher Microsoft.Azure.Monitor --type AzureMonitorWindowsAgent -l westeurope# Run a command on the serveraz connectedmachine run-command create -g <rg-arc> --machine-name <server> -n rc-hostname -l westeurope --script "hostname"
Get-AzConnectedMachine -ResourceGroupName <rg-arc> | Select-Object Name, Status, OSName, AgentVersion, LastStatusChangeGet-AzConnectedMachineExtension -ResourceGroupName <rg-arc> -MachineName <server> | Select-Object Name, ProvisioningStateNew-AzConnectedMachineExtension -ResourceGroupName <rg-arc> -MachineName <server> -Name AzureMonitorWindowsAgent `  -Publisher Microsoft.Azure.Monitor -ExtensionType AzureMonitorWindowsAgent -Location westeurope
// All Arc servers with status and OSResources| where type =~ 'microsoft.hybridcompute/machines'| project name, resourceGroup, status = tostring(properties.status), os = tostring(properties.osName),          agent = tostring(properties.agentVersion), lastSeen = todatetime(properties.lastStatusChange)| order by lastSeen asc

Linux: AzureMonitorLinuxAgent. Data collection: data collection rule (DCR) and association to the machine.

#What Arc Enables

Functions you get once a server is connected.

FunctionNote
Update Managerpatching like Azure VMs – Update Manager
Defender for Serversplan on the subscription covers Arc servers
Azure Policy / machine configurationaudit settings in the OS (e.g. baseline)
MonitoringAzure Monitor Agent + DCR → Log Analytics
RBAC / SSHaz ssh arc – SSH via Azure without open port
ESUextended security updates for Windows Server 2012 via Arc
SymptomCauseFix
Status Disconnectedno heartbeat > 15 min (network, proxy, server off)azcmagent check, proxy settings
connect fails with 403SP role missing / provider not registeredonboarding role, register providers
Extension Failedoutbound to extension endpoints blockedazcmagent check --extensions all, logs under C:\ProgramData\GuestConfig\extension_logs
Cloned VM shows wrong machineagent was installed in the imageazcmagent disconnect in the image, onboard after cloning
Azure

#Backup and Site Recovery

Storage protection: Storage · Step by step: VM, Linux and Windows Workflows

Top 5: az backup item list · az backup job list · az backup protection backup-now · az backup recoverypoint list · az backup restore restore-disks

#Which Vault for What?

Recovery Services vault versus Backup vault.

VaultWorkloads
Recovery Services vault (rsv)Azure VMs, SQL/SAP HANA in VMs, Azure Files, MARS agent (on-prem), Site Recovery
Backup vault (bvault)Azure Disks, Blobs, PostgreSQL flexible, AKS

Decide before the first backup: storage redundancy (LRS/ZRS/GRS – can't be changed later) and cross region restore (only with GRS).

#Create a Vault and Protect a VM

Form for vault + policy + list of VMs: web page → Generator → Backup → Protect VMs.

Note: az … create never creates the resource group – it must exist first (az group create), otherwise ResourceGroupNotFound.

az group create -n <rg> -l westeurope                          # only if the resource group does not exist yetaz backup vault create -g <rg> -n rsv-<name>-weu -l westeuropeaz backup vault backup-properties set -g <rg> -n rsv-<name>-weu --backup-storage-redundancy GeoRedundant --cross-region-restore-flag trueaz backup policy list -g <rg> --vault-name rsv-<name>-weu --query "[].name" -o tsvaz backup protection enable-for-vm -g <rg> --vault-name rsv-<name>-weu --vm <vm-name-or-id> --policy-name DefaultPolicy
New-AzResourceGroup -Name <rg> -Location westeurope            # only if the resource group does not exist yet$v = New-AzRecoveryServicesVault -ResourceGroupName <rg> -Name rsv-<name>-weu -Location westeuropeSet-AzRecoveryServicesBackupProperty -Vault $v -BackupStorageRedundancy GeoRedundant$pol = Get-AzRecoveryServicesBackupProtectionPolicy -VaultId $v.ID -Name DefaultPolicyEnable-AzRecoveryServicesBackupProtection -VaultId $v.ID -Policy $pol -Name <vm> -ResourceGroupName <rg-vm>
resource rsv 'Microsoft.RecoveryServices/vaults@2024-04-01' = {  name: 'rsv-<name>-weu'  location: location  sku: { name: 'RS0', tier: 'Standard' }  properties: { publicNetworkAccess: 'Enabled' }}
resource "azurerm_recovery_services_vault" "this" {  name                = "rsv-<name>-weu"  location            = "westeurope"  resource_group_name = "<rg>"  sku                 = "Standard"  storage_mode_type   = "GeoRedundant"  soft_delete_enabled = true}resource "azurerm_backup_protected_vm" "vm" {  resource_group_name = "<rg>"  recovery_vault_name = azurerm_recovery_services_vault.this.name  source_vm_id        = "<vm-id>"  backup_policy_id    = "<policy-id>"}

The VM and the vault must be in the same region. Default policy: daily, 30 days retention – create your own policy for weekly/monthly retention.

#Back Up Now, Jobs, Recovery Points

Start a backup, watch jobs, list recovery points.

PowerShell
$v = "rsv-<name>-weu"; $rg = "<rg>"az backup item list -g $rg --vault-name $v --query "[].{vm:properties.friendlyName, state:properties.protectionState, last:properties.lastBackupTime, status:properties.lastBackupStatus}" -o tableaz backup protection backup-now -g $rg --vault-name $v --container-name <vm> --item-name <vm> `  --backup-management-type AzureIaasVM --retain-until (Get-Date).AddDays(30).ToString("dd-MM-yyyy")   # macOS: $(date -v+30d +%d-%m-%Y) · Linux: $(date -d '+30 days' +%d-%m-%Y)az backup job list -g $rg --vault-name $v --query "[].{op:properties.operation, item:properties.entityFriendlyName, status:properties.status, start:properties.startTime}" -o tableaz backup recoverypoint list -g $rg --vault-name $v --container-name <vm> --item-name <vm> --backup-management-type AzureIaasVM --query "[].{name:name, time:properties.recoveryPointTime, type:properties.recoveryPointType}" -o table

#Restore

The restore options and when to use which.

WayWhen
Restore disksrestore disks, then attach them or build a new VM (most flexible)
Create new VMquick replacement in the same/another VNet (portal)
Replace existingoverwrite disks of the existing VM (VM is stopped)
File recoverysingle files: mount script from the portal on any machine
Cross region restoresecondary region, only with GRS + CRR
PowerShell
az storage account create -g $rg -n <sa-staging> -l westeurope --sku Standard_LRS     # staging for restoreaz backup restore restore-disks -g $rg --vault-name $v --container-name <vm> --item-name <vm> `  --rp-name <recovery-point-name> --storage-account <sa-staging> --target-resource-group <rg-restore>az backup job list -g $rg --vault-name $v --operation Restore -o table

#Protection Against Deletion and Ransomware

Soft delete, immutability and locks.

PowerShell
az backup vault backup-properties set -g $rg -n $v --soft-delete-feature-state AlwaysOn      # cannot be turned off againaz backup vault update -g $rg -n $v --immutability-state Unlocked                            # Locked = irreversibleaz lock create -n lock-$v -g $rg --resource-type Microsoft.RecoveryServices/vaults --resource $v --lock-type CanNotDelete

Additionally: Multi-user authorization (Resource Guard in another subscription) for critical operations, alerts for "backup disabled" and failed jobs.

#Site Recovery (DR to Another Region)

Set up in the portal: Recovery Services vault (in the target region!) → Site Recovery → Enable replication → source region/VMs → target RG, VNet, cache storage → replication policy.

StepNote
Target networkown VNet in the DR region, same address space possible (isolated)
Network mappingsource VNet → target VNet, IP addresses can be kept
Recovery planorder of VMs (DB before app), scripts/automation runbooks
Test failoverregularly into an isolated test VNet – production keeps running
Failover / commit / re-protectreal disaster: failover → commit → re-protect for the way back
PowerShell
az resource list -g <rg-asr> --resource-type Microsoft.RecoveryServices/vaults -o table# Replication status (PowerShell)$v = Get-AzRecoveryServicesVault -Name <rsv-dr> -ResourceGroupName <rg-asr>; Set-AzRecoveryServicesAsrVaultContext -Vault $vGet-AzRecoveryServicesAsrFabric | ForEach-Object { Get-AzRecoveryServicesAsrProtectionContainer -Fabric $_ } |  ForEach-Object { Get-AzRecoveryServicesAsrReplicationProtectedItem -ProtectionContainer $_ } |  Select-Object FriendlyName, ProtectionState, ReplicationHealth, TestFailoverState

#Monitoring (KQL)

Diagnostic setting on the vault → Log Analytics (resource specific).

KQL
// Failed backup jobs, last 7 daysAddonAzureBackupJobs| where TimeGenerated > ago(7d) and JobOperation == "Backup" and JobStatus == "Failed"| project TimeGenerated, BackupItemUniqueId, JobFailureCode, JobDurationInSecs| order by TimeGenerated desc

Easier: Backup center / Business continuity center in the portal – all vaults, jobs and alerts across subscriptions.

Azure

#Azure Update Manager

Servers outside Azure: Azure Arc · Maintenance notice: Templates · Step by step: VM, Linux and Windows Workflows

Patch management for Azure VMs and Arc servers (Windows and Linux) – no agent needed, successor of Update Management (Automation).

Top 5: az vm assess-patches · az vm install-patches · az maintenance configuration create · az maintenance assignment create · Resource Graph patchassessmentresources

#How It Works

Assessment, one-time install and schedules.

FunctionWhat it does
Assessmentchecks which updates are missing (on demand or every 24 h with periodic assessment)
One-time updateinstall updates now
Schedulemaintenance configuration with window + classifications; assigned to VMs or dynamic scopes (by tag, RG, OS)
Hotpatchupdates without reboot (Windows Server Azure Edition)

For schedules the VM needs patch orchestration Customer Managed Schedules (patchMode = AutomaticByPlatform + bypassPlatformSafetyChecksOnUserSchedule = true).

#Prepare VMs

Switch on periodic assessment and patch mode.

# Windows VM: periodic assessment + schedule-driven patchingaz vm update -g <rg> -n <vm> --set `  osProfile.windowsConfiguration.patchSettings.assessmentMode=AutomaticByPlatform `  osProfile.windowsConfiguration.patchSettings.patchMode=AutomaticByPlatform `  osProfile.windowsConfiguration.patchSettings.automaticByPlatformSettings.bypassPlatformSafetyChecksOnUserSchedule=true# Linux VMaz vm update -g <rg> -n <vm> --set `  osProfile.linuxConfiguration.patchSettings.assessmentMode=AutomaticByPlatform `  osProfile.linuxConfiguration.patchSettings.patchMode=AutomaticByPlatform `  osProfile.linuxConfiguration.patchSettings.automaticByPlatformSettings.bypassPlatformSafetyChecksOnUserSchedule=true
// in the VM resource (Windows)osProfile: {  windowsConfiguration: {    patchSettings: {      assessmentMode: 'AutomaticByPlatform'      patchMode: 'AutomaticByPlatform'      automaticByPlatformSettings: { bypassPlatformSafetyChecksOnUserSchedule: true }    }  }}
resource "azurerm_windows_virtual_machine" "vm" {  # ...  patch_assessment_mode                                  = "AutomaticByPlatform"  patch_mode                                             = "AutomaticByPlatform"  bypass_platform_safety_checks_on_user_schedule_enabled = true}

For many VMs: built-in policy "Configure periodic checking for missing system updates on azure virtual machines" and "Set prerequisites for scheduling recurring updates on Azure virtual machines".

#Assess and Install Now

Check missing updates and install them now.

PowerShell
az vm assess-patches -g <rg> -n <vm> --query "{status:status, critical:criticalAndSecurityPatchCount, other:otherPatchCount, reboot:rebootPending}"az vm install-patches -g <rg> -n <vm> --maximum-duration PT2H --reboot-setting IfRequired `  --classifications-to-include-win Critical Security --query "{status:status, installed:installedPatchCount, failed:failedPatchCount}"# Linux:az vm install-patches -g <rg> -n <vm> --maximum-duration PT2H --reboot-setting IfRequired --classifications-to-include-linux Critical Security

#Schedule (Maintenance Configuration)

Create a patch schedule and assign VMs.

az maintenance configuration create -g <rg> --resource-name mc-patch-<name> -l westeurope `  --maintenance-scope InGuestPatch --maintenance-window-duration "03:00" `  --maintenance-window-start-date-time "2026-11-01 22:00" --maintenance-window-time-zone "W. Europe Standard Time" `  --maintenance-window-recur-every "Month Second Tuesday Offset3" `  --install-patches-windows-parameters classifications-to-include="Critical Security UpdateRollUp" `  --install-patches-linux-parameters classifications-to-include="Critical Security" `  --reboot-setting IfRequired --extension-properties InGuestPatchMode="User"$mc = az maintenance configuration show -g <rg> --resource-name mc-patch-<name> --query id -o tsvaz maintenance assignment create -g <rg-vm> -l westeurope --resource-name <vm> --resource-type virtualMachines `  --provider-name Microsoft.Compute --configuration-assignment-name mc-patch-<name> --maintenance-configuration-id $mc
$mc = New-AzMaintenanceConfiguration -ResourceGroupName <rg> -Name mc-patch-<name> -Location westeurope `  -MaintenanceScope InGuestPatch -Duration "03:00" -StartDateTime "2026-11-01 22:00" -TimeZone "W. Europe Standard Time" `  -RecurEvery "Month Second Tuesday Offset3" -InstallPatchRebootSetting IfRequired `  -WindowParameterClassificationToInclude Critical, Security, UpdateRollUp `  -LinuxParameterClassificationToInclude Critical, Security -ExtensionProperty @{ InGuestPatchMode = "User" }New-AzConfigurationAssignment -ResourceGroupName <rg-vm> -Location westeurope -ResourceName <vm> `  -ResourceType VirtualMachines -ProviderName Microsoft.Compute -ConfigurationAssignmentName mc-patch-<name> `  -MaintenanceConfigurationId $mc.Id

Month Second Tuesday Offset3 = 3 days after Patch Tuesday. Other examples: Week Saturday, Month Last Sunday, Day. Dynamic scopes (all VMs with tag PatchGroup=A): portal → Update Manager → Machines → Schedule updates → Dynamic scopes.

#Compliance Overview (Resource Graph)

Machines with missing updates in one query.

KQL
// Machines with missing critical/security updatespatchassessmentresources| where type has "softwarepatches"| extend machine = tostring(split(id, '/')[8]), cls = tostring(properties.classifications[0])| where cls in ('Critical', 'Security')| summarize Missing = count() by machine, cls| order by Missing desc // Results of the last installation runspatchinstallationresources| where type has "patchinstallationresults"| project machine = tostring(split(id, '/')[8]), status = tostring(properties.status), start = todatetime(properties.startDateTime),          installed = toint(properties.installedPatchCount), failed = toint(properties.failedPatchCount)| order by start desc

Run with az graph query -q "<query>", see Resource Graph.

SymptomCauseFix
Schedule does not runpatch orchestration not Customer Managed Schedulesprepare the VM (above)
No assessment dataperiodic assessment off / VM stoppedassessmentMode=AutomaticByPlatform, az vm assess-patches
Linux: no updates foundrepository not reachable (proxy/firewall)OS repos outbound allowed
Window too shortmany updates + rebootlonger duration (max. 3:55 h), fewer classifications
Azure

#Storage in Daily Work

Basic commands: → Storage Account

--auth-mode login needs a data role (e.g. Storage Blob Data Contributor). Owner/Contributor alone is not enough.

#AzCopy: Upload and Download

Install: see Install the Tools (Windows winget install Microsoft.Azure.AZCopy.10, macOS brew install azcopy).

PowerShell
azcopy login --tenant-id <tenant-id>                                   # sign in with Entra IDazcopy copy ".\folder" "https://<sa>.blob.core.windows.net/<container>/" --recursiveazcopy copy "https://<sa>.blob.core.windows.net/<container>/path/*" ".\target" --recursiveazcopy sync ".\folder" "https://<sa>.blob.core.windows.net/<container>/folder" --delete-destination=false   # changes onlyazcopy copy "https://<sa-a>.blob.core.windows.net/<c>/?<sas>" "https://<sa-b>.blob.core.windows.net/<c>/?<sas>" --recursive   # account to accountazcopy list "https://<sa>.blob.core.windows.net/<container>"azcopy jobs list ; azcopy jobs resume <job-id>                         # resume an aborted transfer

#Create a SAS Token

Time-limited access without handing out the account key.

PowerShell
# User delegation SAS (with Entra sign-in, without account key – preferred)$expiry = (Get-Date).ToUniversalTime().AddHours(8).ToString("yyyy-MM-ddTHH:mmZ")$sas = az storage container generate-sas --account-name <sa> -n <container> `  --permissions rl --expiry $expiry --auth-mode login --as-user -o tsv"https://<sa>.blob.core.windows.net/<container>?$sas" # For a single blobaz storage blob generate-sas --account-name <sa> -c <container> -n <file> `  --permissions r --expiry $expiry --auth-mode login --as-user --full-uri -o tsv

Tip: macOS / Linux (Bash) – the expiry as a variable: macOS expiry=$(date -u -v+8H +%Y-%m-%dT%H:%MZ), Linux expiry=$(date -u -d '+8 hours' +%Y-%m-%dT%H:%MZ). Then --expiry "$expiry". Permissions: r read · l list · w write · c create · d delete. Always as short and as little as possible.

#Restore Deleted and Old Versions

Undelete blobs and go back to a version.

PowerShell
# Show and restore deleted blobs (soft delete must be on)az storage blob list --account-name <sa> -c <container> --include d --auth-mode login `  --query "[?deleted].{name:name, deleted:properties.deletedTime}" -o tableaz storage blob undelete --account-name <sa> -c <container> -n <file> --auth-mode login # Versions of a blob (versioning must be on)az storage blob list --account-name <sa> -c <container> --prefix <file> --include v --auth-mode login `  --query "[].{name:name, version:versionId, current:isCurrentVersion}" -o table# Make an old version the current one againaz storage blob copy start --account-name <sa> --destination-container <container> --destination-blob <file> `  --source-uri "https://<sa>.blob.core.windows.net/<container>/<file>?versionid=<version-id>" --auth-mode login # Restore a deleted containeraz storage container list --account-name <sa> --include-deleted --auth-mode login --query "[?deleted]" -o tableaz storage container restore --account-name <sa> -n <container> --deleted-version <version> --auth-mode login

#Turn On Protection

Soft delete, versioning and change feed.

PowerShell
az storage account blob-service-properties update -g <rg> --account-name <sa> `  --enable-delete-retention true --delete-retention-days 14 `  --enable-container-delete-retention true --container-delete-retention-days 14 `  --enable-versioning trueaz storage account update -g <rg> -n <sa> --allow-blob-public-access false --min-tls-version TLS1_2az storage account update -g <rg> -n <sa> --allow-shared-key-access false     # Entra sign-in only (check first who uses keys!)az lock create -n lock-<sa> -g <rg> --resource-type Microsoft.Storage/storageAccounts --resource <sa> --lock-type CanNotDelete

#Azure Files

Create a share, test port 445 and mount it.

PowerShell
az storage share-rm list -g <rg> --storage-account <sa> -o tableaz storage share-rm create -g <rg> --storage-account <sa> -n <share> --quota 100

Test the connection and mount the share (port 445 must be open):

Test-NetConnection <sa>.file.core.windows.net -Port 445net use Z: \\<sa>.file.core.windows.net\<share> /user:localhost\<sa> <storage-key>
nc -zv <sa>.file.core.windows.net 445# Finder: Go -> Connect to Server -> smb://<sa>.file.core.windows.net/<share>   (user: <sa>, password: <storage-key>)
nc -zv <sa>.file.core.windows.net 445sudo mkdir -p /mnt/<share>sudo mount -t cifs //<sa>.file.core.windows.net/<share> /mnt/<share> -o username=<sa>,password=<storage-key>,serverino,vers=3.0

Many ISPs block port 445 to the internet – then use VPN/private endpoint.

#Lifecycle (Move/Delete Old Data Automatically)

policy.json:

JSON
{  "rules": [{    "name": "cleanup-logs",    "enabled": true,    "type": "Lifecycle",    "definition": {      "filters": { "blobTypes": [ "blockBlob" ], "prefixMatch": [ "logs/" ] },      "actions": {        "baseBlob": {          "tierToCool": { "daysAfterModificationGreaterThan": 30 },          "delete":     { "daysAfterModificationGreaterThan": 365 }        }      }    }  }]}
PowerShell
az storage account management-policy create -g <rg> --account-name <sa> --policy "@policy.json"az storage account management-policy show   -g <rg> --account-name <sa>
Azure

#Azure CLI – Key Vault, Identity, Roles, Policy

Step by step: Permissions, Key Vault and Policy Workflows

Top 5: az role assignment list --assignee <x> --all -o table · az ad signed-in-user show --query id -o tsv · az keyvault secret show · az policy assignment list · az identity show

#Key Vault

List vaults, check their settings and read or set secrets.

PowerShell
az keyvault list -o tableaz keyvault show -n <kv> --query "{rbac:properties.enableRbacAuthorization, pna:properties.publicNetworkAccess, softDelete:properties.enableSoftDelete}"az keyvault secret list --vault-name <kv> -o tableaz keyvault secret show --vault-name <kv> -n <name> --query value -o tsvaz keyvault secret set  --vault-name <kv> -n <name> --value <value>az keyvault secret set  --vault-name <kv> -n <name> --file <file>           # value from a fileaz keyvault secret delete --vault-name <kv> -n <name>az keyvault secret list-deleted --vault-name <kv> -o tableaz keyvault secret recover --vault-name <kv> -n <name>                      # undo soft deleteaz keyvault certificate list --vault-name <kv> -o tableaz keyvault certificate import --vault-name <kv> -n <name> -f .\cert.pfx --password <pfx-password>az keyvault certificate show --vault-name <kv> -n <name> --query "{sid:sid, expires:attributes.expires}"# versionless secret ID: https://<kv>.vault.azure.net/secrets/<name>az keyvault certificate download --vault-name <kv> -n <name> -f cert.pem   # public part onlyaz keyvault list-deleted -o tableaz keyvault purge -n <kv>                                                   # delete permanently (careful!)az keyvault update -n <kv> --bypass AzureServices                           # allow trusted servicesaz keyvault network-rule add -n <kv> --ip-address <ip>/32az keyvault network-rule list -n <kv>

Error Forbidden … not authorized: on an RBAC vault the role Key Vault Secrets User/Officer is missing, on an access-policy vault an access policy.

#Create a Key Vault

Create a vault with RBAC, purge protection and private access.

PowerShell
az keyvault create -n <keyvault-name> -g <rg> -l <region> --sku standard `  --enable-rbac-authorization true --enable-purge-protection true --retention-days 90 --public-network-access Enabled$kvId = az keyvault show -n <keyvault-name> --query id -o tsv$me   = az ad signed-in-user show --query id -o tsvaz role assignment create --assignee-object-id $me --assignee-principal-type User --role "Key Vault Administrator" --scope $kvId

Purge protection cannot be turned off again; after deletion the name stays blocked for the retention period.

#Generate a certificate in Key Vault

Create a self-signed or CA-issued certificate inside the vault.

PowerShell
# Self-signed (test) – PowerShell$policy = New-AzKeyVaultCertificatePolicy -SubjectName "CN=<fqdn>" -DnsName "<fqdn>" -IssuerName Self `  -ValidityInMonths 12 -SecretContentType "application/x-pkcs12" -KeyType RSA -KeySize 2048Add-AzKeyVaultCertificate -VaultName <keyvault-name> -Name <certificate-name> -CertificatePolicy $policyGet-AzKeyVaultCertificateOperation -VaultName <keyvault-name> -Name <certificate-name>      # status # For your own CA: IssuerName Unknown -> get CSR -> have it signed -> mergeaz keyvault certificate pending show  --vault-name <keyvault-name> -n <certificate-name> --query csr -o tsvaz keyvault certificate pending merge --vault-name <keyvault-name> -n <certificate-name> --file .\signed.cer

Easier with a form: web page → Generator → Key Vault → Create a certificate (self-signed or CSR).

#Managed Identity

Create identities and show their principal and client ID.

PowerShell
az identity list -g <rg> -o tableaz identity create -g <rg> -n <identity>az identity show -g <rg> -n <identity> --query "{principal:principalId, client:clientId, id:id}"az vm identity show -g <rg> -n <vm>az webapp identity assign -g <rg> -n <app> --identities <identity-id>

principalId = object ID for role assignments · clientId = for sign-in in code/pipelines.

#Roles (RBAC)

List, assign and remove role assignments on a scope.

PowerShell
az role assignment list --assignee <principal-id-or-upn> --all -o tableaz role assignment list --scope <resource-id> --include-inherited -o tableaz role assignment create --assignee-object-id <principal-id> --assignee-principal-type ServicePrincipal `  --role "<role>" --scope <resource-id>az role assignment delete --assignee <principal-id> --role "<role>" --scope <resource-id>az role definition list --name "<role>" -o jsonaz role definition list --custom-role-only true -o tableaz ad signed-in-user show --query id -o tsv           # your own object ID # Service principal by display name -> object ID -> role$id = az ad sp list --display-name "<sp-name>" --query "[].{id:id}" --output tsvaz role assignment create --assignee-principal-type ServicePrincipal --assignee-object-id $id --scope "/subscriptions/<sub>" --role "Owner" # Scopes#   /subscriptions/<sub>/resourceGroups/<rg>                      resource group#   /subscriptions/<sub>                                          subscription#   /providers/Microsoft.Management/managementGroups/<mg-id>      management group#   /                                                             tenant root (everything – only with elevated access)az rest --method post --url "/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"   # Global Admin: get UAA on "/"az account management-group list -o table                    # management groups

--assignee-object-id + --assignee-principal-type avoids Graph lookups and replication errors with freshly created identities.

RoleFor
Readerread only
Contributoreverything except granting access
Ownereverything incl. access
User Access Administratorgranting access only
Network Contributornetwork resources
Private DNS Zone ContributorDNS zones/records
Key Vault Secrets Userread secrets (RBAC vault)
Key Vault Secrets Officermanage secrets
Key Vault Administratoreverything in the vault
Key Vault Certificates Officermanage certificates
Storage Blob Data Reader/Contributorblob data (e.g. Terraform state)
Monitoring Readerread metrics/logs

#Entra ID (Users, Groups, Apps)

Look up users, groups and app registrations.

PowerShell
az ad user show --id <upn> --query "{id:id, name:displayName}"az ad group list --display-name <part> -o tableaz ad group member list -g <group> --query "[].userPrincipalName" -o tsvaz ad group member check -g <group> --member-id <object-id>az ad sp list --display-name <name> -o tableaz ad sp show --id <app-id> --query "{objectId:id, appId:appId}"az ad app list --display-name <name> -o tableaz ad app credential list --id <app-id> --query "[].{end:endDateTime, name:displayName}" -o table   # secret expiryaz ad sp create-for-rbac -n <name> --role Contributor --scopes /subscriptions/<sub>

More: Entra ID.

#Azure Policy

In detail – effects, custom definitions, remediation, exemptions, policy as code: Azure Policy.

PowerShell
az policy assignment list --scope /subscriptions/<sub> -o tableaz policy assignment list --disable-scope-strict-match --scope /subscriptions/<sub>/resourceGroups/<rg> -o tableaz policy assignment show -n <assignment> --scope <scope>az policy definition show -n <definition-name> --query "{name:displayName, effect:policyRule.then.effect}"az policy state list --resource <resource-id> --query "[].{policy:policyDefinitionName, compliant:complianceState}" -o tableaz policy state summarize -g <rg>az policy state trigger-scan -g <rg> --no-wait                    # re-evaluate complianceaz policy exemption list --scope <scope> -o tableaz policy exemption create -n <name> --policy-assignment <assignment-id> --scope <scope> --exemption-category Waiver

Error RequestDisallowedByPolicy: the message contains policyAssignmentId and policyDefinitionId – use them to search.

Azure

#Entra ID – Apps, Secrets, Groups, PIM

Roles in Azure: Security · Step by step: Permissions, Key Vault and Policy Workflows

App registration = the definition of the application (app ID, secrets). Service principal = its "user account" in the tenant that roles are given to. For role assignments always use the object ID of the service principal, not the app's.

#App Registration and Service Principal

Find apps and their service principals and map the IDs.

PowerShell
az ad app list --display-name <name> --query "[].{name:displayName, appId:appId, objectId:id}" -o tableaz ad sp  list --display-name <name> --query "[].{name:displayName, appId:appId, spObjectId:id}" -o tableaz ad sp show --id <app-id> --query id -o tsv                # SP object ID from the app IDaz ad app owner list --id <app-id> --query "[].userPrincipalName" -o tsvaz ad app owner add  --id <app-id> --owner-object-id <user-object-id> # New app with SP and role (secret is shown ONCE – store it in Key Vault right away)az ad sp create-for-rbac -n <name> --role Contributor --scopes /subscriptions/<sub-id>

#Find Expiring Secrets and Certificates

Which secrets and certificates run out soon.

PowerShell
# One appaz ad app credential list --id <app-id> --query "[].{name:displayName, end:endDateTime, keyId:keyId}" -o tableaz ad app credential list --id <app-id> --cert --query "[].{name:displayName, end:endDateTime}" -o table # All apps whose secret expires within the next 30 days$limit = (Get-Date).AddDays(30)az ad app list --all --query "[].{name:displayName, appId:appId, secrets:passwordCredentials[].endDateTime}" -o json |  ConvertFrom-Json | ForEach-Object {    $app = $_    $app.secrets | Where-Object { $_ -and [datetime]$_ -lt $limit } |      ForEach-Object { [pscustomobject]@{ App = $app.name; AppId = $app.appId; Expires = ([datetime]$_).ToString('yyyy-MM-dd') } }  } | Sort-Object Expires | Format-Table

With --all this can take a while in large tenants; without --all only your own or the first 100.

#Renew a Secret

Add a new secret first, switch over, then remove the old one.

PowerShell
# Create the new secret IN ADDITION (--append), the old one stays valid for nowaz ad app credential reset --id <app-id> --append --display-name "secret-$(Get-Date -Format yyyyMM)" --years 1 --query password -o tsv   # Bash: "secret-$(date +%Y%m)"# -> enter it right away in Key Vault / the service connection / pipeline variableaz keyvault secret set --vault-name <kv> -n <secret-name> --value "<new-secret>" # Once everything is switched over: delete the old secretaz ad app credential list   --id <app-id> -o tableaz ad app credential delete --id <app-id> --key-id <old-key-id>

Where is the secret used? Typically: service connections in DevOps, pipeline variables, Key Vault, app settings.

#Groups

Create groups and manage their members.

PowerShell
az ad group list --display-name <part> --query "[].{name:displayName, id:id}" -o tableaz ad group create --display-name <name> --mail-nickname <name>az ad group member list  -g <group> --query "[].{name:displayName, upn:userPrincipalName}" -o tableaz ad group member add   -g <group> --member-id <object-id>az ad group member remove -g <group> --member-id <object-id>az ad group member check -g <group> --member-id <object-id> --query valueaz ad user get-member-groups --id <upn> --query "[].displayName" -o tsv     # a user's groups

#Create Security Groups in Bulk

One group per role, existing ones are skipped – safe to run again. With a form (incl. role assignments and Azure CLI variant): web page → Generator → Identity and Access → Create Entra ID security groups (bulk).

PowerShell
# Azure Cloud Shell (PowerShell) or locally after Connect-AzAccount$groups = @(    @{ Name = "az-<customer>-owner";                    Desc = "<Customer> - Owner" }    @{ Name = "az-<customer>-contributor";              Desc = "<Customer> - Contributor" }    @{ Name = "az-<customer>-reader";                   Desc = "<Customer> - Reader" }    @{ Name = "az-<customer>-costmanagementreader";     Desc = "<Customer> - Cost Management Reader" }    @{ Name = "az-<customer>-landingzones-owner";       Desc = "<Customer> - Landing Zones Owner" }    @{ Name = "az-<customer>-landingzones-contributor"; Desc = "<Customer> - Landing Zones Contributor" }    @{ Name = "az-<customer>-landingzones-reader";      Desc = "<Customer> - Landing Zones Reader" }) foreach ($g in $groups) {    $existing = Get-AzADGroup -DisplayName $g.Name -ErrorAction SilentlyContinue    if ($existing) {        Write-Host "Already exists: $($g.Name) (Id: $($existing.Id))" -ForegroundColor Yellow        continue    }    try {        $new = New-AzADGroup -DisplayName $g.Name -MailNickname $g.Name -Description $g.Desc -SecurityEnabled -ErrorAction Stop        Write-Host "Created: $($g.Name) (Id: $($new.Id))" -ForegroundColor Green    }    catch {        Write-Host "Error for $($g.Name): $($_.Exception.Message)" -ForegroundColor Red    }} # OverviewGet-AzADGroup -Filter "startswith(displayName,'az-<customer>')" |    Select-Object DisplayName, Id, SecurityEnabled | Format-Table -AutoSize

Then give each group its role, e.g. New-AzRoleAssignment -ObjectId <group-id> -RoleDefinitionName "Reader" -Scope /subscriptions/<sub-id> – or use the generator option Also assign Azure roles. Needs the Entra permission to create groups (e.g. Groups Administrator, or "Users can create security groups" enabled).

#Users

Look up users and guests.

PowerShell
az ad user show --id <upn> --query "{name:displayName, id:id, enabled:accountEnabled}"az ad user list --filter "startswith(displayName,'<part>')" --query "[].{name:displayName, upn:userPrincipalName}" -o tableaz ad user list --filter "userType eq 'Guest'" --query "[].{name:displayName, mail:mail}" -o table     # guests

#PIM: Activate a Role

Privileged Identity Management: the role is only "eligible" and must be activated before working.

In the portal: Microsoft Entra Privileged Identity Management → My roles → Azure resources or Microsoft Entra roles → Activate → duration + justification.

PowerShell (Azure resource roles):

PowerShell
$scope = "/subscriptions/<sub-id>"$me    = (Get-AzADUser -SignedIn).IdGet-AzRoleEligibilitySchedule -Scope $scope -Filter "asTarget()" |  Select-Object RoleDefinitionDisplayName, Scope, RoleDefinitionId          # what may I activate? $roleId = "<RoleDefinitionId from the list>"New-AzRoleAssignmentScheduleRequest -Name (New-Guid).Guid -Scope $scope `  -PrincipalId $me -RoleDefinitionId $roleId -RequestType SelfActivate `  -ScheduleInfoStartDateTime (Get-Date).ToUniversalTime().ToString("o") `  -ExpirationType AfterDuration -ExpirationDuration PT8H `  -Justification "<reason / ticket>"

After activation sign in again if needed (az account clear ; az login) so the role is in the token.

Azure

#Defender for Cloud and Security

Queries: Resource Graph → Security

#Secure Score and Recommendations

Current score and the open recommendations.

PowerShell
az security secure-scores list --query "[].{name:displayName, current:score.current, max:score.max, percent:score.percentage}" -o tableaz security secure-score-controls list --query "[].{name:displayName, current:score.current, max:score.max, open:unhealthyResourceCount}" -o tableaz security assessment list --query "[?status.code=='Unhealthy'].{recommendation:displayName, resource:resourceDetails.Id}" -o table

In the portal: Microsoft Defender for Cloud → Recommendations, sort by Risk level, per recommendation Fix or Exempt.

#Defender Plans

Which plans are on and how to switch them (costs money).

PowerShell
az security pricing list --query "value[].{plan:name, tier:pricingTier}" -o tableaz security pricing show -n VirtualMachinesaz security pricing create -n KeyVaults --tier Standard          # enable a plan (costs money)

Free = recommendations only, Standard = Defender plan with protection and alerts.

#Alerts

List and close security alerts.

PowerShell
az security alert list --query "[].{time:timeGeneratedUtc, severity:severity, title:alertDisplayName, resource:compromisedEntity, status:status}" -o tableaz security alert update --location <region> -n <alert-name> -g <rg> --status Dismiss       # close after review

In Log Analytics: table SecurityAlert.

#Just-in-Time Access to VMs

JIT opens RDP/SSH only on request, for a limited time and only for your IP.

Portal: VM → Connect → Request access, or Defender for Cloud → Workload protections → Just-in-time VM access.

PowerShell:

PowerShell
$vmId = (Get-AzVM -ResourceGroupName <rg> -Name <vm>).Id$myIp = Invoke-RestMethod https://api.ipify.org$req = @{ id = $vmId; ports = @(@{ number = 22; endTimeUtc = (Get-Date).ToUniversalTime().AddHours(3).ToString("o"); allowedSourceAddressPrefix = @($myIp) }) }Start-AzJitNetworkAccessPolicy -ResourceGroupName <rg> -Location <region> -Name default -VirtualMachine @($req)

#Quick Check of a Subscription

Score, plans and alerts of one subscription in one go.

PowerShell
$sub = "<sub-id>"az account set --subscription $subaz security secure-scores list --query "[0].score.percentage" -o tsv                   # secure score in %az security pricing list --query "value[?pricingTier=='Free'].name" -o tsv              # plans that are offaz graph query --subscriptions $sub -q "Resources | where type =~ 'microsoft.network/networksecuritygroups' | mv-expand r = properties.securityRules | where r.properties.access == 'Allow' and r.properties.direction == 'Inbound' and tostring(r.properties.sourceAddressPrefix) in ('*','Internet') | project name, rule = tostring(r.name), port = tostring(r.properties.destinationPortRange)" -o tableaz graph query --subscriptions $sub -q "Resources | where type =~ 'microsoft.storage/storageaccounts' and properties.allowBlobPublicAccess == true | project name" -o tableaz keyvault list -o json | ConvertFrom-Json | Where-Object { -not $_.properties.enablePurgeProtection } | ForEach-Object name   # vaults without purge protection
Azure

#Management Groups and Landing Zone

Policies: → Azure Policy

#Landing Zone Structure

Typical hierarchy according to the Cloud Adoption Framework (your names may differ):

text
Tenant Root Group  (/)└── <Company>    ├── Platform    │   ├── Identity        – domain controllers, Entra helper services    │   ├── Management      – Log Analytics, Automation, monitoring    │   └── Connectivity    – hub VNet, firewall/FortiGate, VPN, DNS    ├── Landing Zones    │   ├── Corp            – applications connected to the corporate network    │   └── Online          – applications directly from the internet    ├── Sandbox             – playground, no connectivity    └── Decommissioned      – retired subscriptions
RuleWhy
Assign policies and permissions on management groups where possibleapply automatically to every new subscription below
One subscription per application/environmentclean boundaries for costs, permissions, limits
Network (hub) lives in Connectivity, spokes in the landing zonesseparation platform ↔ application
Never put anything directly under the Tenant Root Grouproot policies would hit everything

#Show the Hierarchy

The management group tree with subscriptions.

PowerShell
az account management-group list -o tableaz account management-group show -n <mg-id> --expand --recurse `  --query "{name:displayName, children:children[].{name:displayName, type:type}}" -o jsonaz account management-group entities list --query "[].{name:displayName, type:type, parent:parent.id}" -o table   # everything flat

Resource Graph: which subscription sits where?

PowerShell
az graph query -q "ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project name, subscriptionId, mgChain = properties.managementGroupAncestorsChain" -o json

#Move a Subscription

Move a subscription – inherited policies and roles change at once.

PowerShell
az account management-group subscription show-sub-under-mg -n <mg-id> -o table      # subscriptions under an MGaz account management-group subscription add --name <target-mg-id> --subscription <sub-id>

Caution: moving changes inherited policies and roles immediately. Check first which policies apply at the target (deny policies can break existing deployments). You need permissions on the old and the new MG.

#Rename Management Groups

Only the display name can be changed – the ID (name) is fixed for good. Policy assignments, role assignments and subscriptions are not affected by a new display name.

Update-AzManagementGroup -GroupName <mg-id> -DisplayName "<new-display-name>"# Replace a name part in many display names – first only show old → new:$old = '<old>' ; $new = '<new>'$pattern = "(?<=^|-)$([regex]::Escape($old))(?=-|$)"      # only whole parts between hyphensGet-AzManagementGroup | Where-Object { $_.DisplayName -match $pattern } |  Select-Object Name, DisplayName, @{n='NewName'; e={ $_.DisplayName -replace $pattern, $new }}
az account management-group update -n <mg-id> --display-name "<new-display-name>"az account management-group list --query "sort_by([], &displayName)[].{id:name, name:displayName}" -o table

Many at once with a dry run and confirmation: web page → Generator → Governance → Rename management groups.

#What Applies at Which Level?

Policies and roles that apply on a management group.

PowerShell
$mg = "/providers/Microsoft.Management/managementGroups/<mg-id>"az policy assignment list --scope $mg --query "[].{name:displayName, effect:parameters.effect.value, def:policyDefinitionId}" -o tableaz role assignment list --scope $mg --query "[].{who:principalName, role:roleDefinitionName}" -o table # Everything that affects a subscription (incl. inheritance from above)az policy assignment list --disable-scope-strict-match --scope /subscriptions/<sub-id> -o tableaz role assignment list --scope /subscriptions/<sub-id> --include-inherited -o table

#Deployments at Management Group Level

what-if and deploy at management group scope.

PowerShell
az deployment mg what-if -m <mg-id> -l <region> -f main.bicep -p params/mg.bicepparamaz deployment mg create  -m <mg-id> -l <region> -f main.bicep -p params/mg.bicepparamaz deployment mg list -m <mg-id> -o tableaz deployment tenant create -l <region> -f tenant.bicep           # at the very top (needs permissions on "/")

In Bicep: targetScope = 'managementGroup' at the start of the file.

Azure

#Azure Policy – Rules, Assignments and Policy as Code

Short reference: Azure Security → Policy · Scopes: Management Groups · Error: Policy blocks a deployment · Step by step: Permissions, Key Vault and Policy Workflows

Azure Policy checks every create/update request against rules (deny, audit, modify, deploy if not exists) and shows the compliance of existing resources. Definitions, initiatives and assignments belong in a repo and are deployed via pipeline (policy as code).

Top 5: az policy assignment list · az policy state summarize · az policy state trigger-scan · az policy remediation create · az policy exemption create

#Terms in One Table

Definition, initiative, assignment, scope, exemption.

TermMeaning
Definitionone rule: if (condition) → then (effect); built-in or custom
Initiative (policy set)group of definitions, assigned together (e.g. tagging, security baseline)
Assignmentdefinition/initiative applied to a scope (management group, subscription, resource group) with parameters
Scope / exclusionassignment applies to everything below the scope, except notScopes
Exemptionsingle resource / RG exempted from an assignment – with reason and expiry date
Enforcement modeDefault = effect active · DoNotEnforce = only evaluate (ideal for testing)
ModeAll = all resource types incl. RGs · Indexed = only types with tags/location (for tag/location rules)
Aliaspath to a resource property in the rule, e.g. Microsoft.Network/publicIPAddresses/sku.name
Remediationtask that fixes existing resources (only Modify and DeployIfNotExists)

Note: Where to define: custom definitions and initiatives at the highest management group they are needed in. Assign as high as sensible, exclude with notScopes or exemptions.

#Effects

What each effect does and when to use it.

EffectWhat happensTypical use
Denyrequest is rejected (RequestDisallowedByPolicy)allowed regions, no public IPs, allowed SKUs
Auditresource is created, marked non-compliantfirst stage before Deny, reporting
Modifyadds/changes tags or properties during the request; remediation for existing onesinherit tags from the RG, disable public network access
Appendadds fields (older, mostly replaced by Modify)IP rules on storage
DeployIfNotExistsdeploys a related resource after the request (ARM template)diagnostic settings, private DNS records for private endpoints, backup
AuditIfNotExistsreports if the related resource is missing"VM without backup", "no diagnostic setting"
DenyActionblocks an action (currently DELETE)protect critical resources from deletion
Manualcompliance is set by hand (attestation)regulatory controls without technical check
Disabledrule inactiveswitch off via parameter without removing the assignment

Tip: Make the effect a parameter ([parameters('effect')] with allowedValues) – then the same definition runs as Audit in test and Deny in production.

Evaluation order and timing

Order during a request: Disabled → Append/Modify → Deny → Audit; after the deployment: AuditIfNotExists / DeployIfNotExists. Existing resources are evaluated about 30 minutes after a new assignment, then every 24 hours – or on demand with az policy state trigger-scan.

#Find Built-in Policies and Aliases

Look up built-in definitions and property aliases.

# Built-in definitions by name (displayName)az policy definition list --query "[?policyType=='BuiltIn' && contains(displayName, 'location')].{name:name, displayName:displayName}" -o tableaz policy set-definition list --query "[?policyType=='BuiltIn' && contains(displayName, 'tag')].{name:name, displayName:displayName}" -o table# Show the rule and parameters of one definitionaz policy definition show -n <definition-name> --query "{display:displayName, mode:mode, params:parameters, rule:policyRule}"# Aliases of a resource type (which properties a rule can check)az provider show --namespace Microsoft.Network --expand "resourceTypes/aliases" `  --query "resourceTypes[?resourceType=='publicIPAddresses'].aliases[].name" -o tsv
Get-AzPolicyDefinition -Builtin | Where-Object DisplayName -like "*location*" | Select-Object Name, DisplayNameGet-AzPolicySetDefinition -Builtin | Where-Object DisplayName -like "*tag*" | Select-Object Name, DisplayName(Get-AzPolicyAlias -NamespaceMatch Microsoft.Network -ResourceTypeMatch publicIPAddresses).Aliases.Name

Tip: Built-in IDs: /providers/Microsoft.Authorization/policyDefinitions/<name> – always look up the name by display name instead of copying GUIDs from blog posts. Browse/compare: portal Policy → Definitions or the AzAdvertizer website.

#Assign a Policy

Example: built-in Allowed locations on a management group, first only evaluating (DoNotEnforce), with a message for the user. Faster with a form: web page → Generator → Governance → Assign an Azure Policy (CLI or PowerShell, incl. identity, roles and remediation).

$mg  = "<mg-landingzones>"$def = az policy definition list --query "[?displayName=='Allowed locations'].name" -o tsvaz policy assignment create -n allowed-locations --display-name "Allowed locations" `  --policy $def --scope /providers/Microsoft.Management/managementGroups/$mg `  --params '{ \"listOfAllowedLocations\": { \"value\": [ \"westeurope\", \"northeurope\" ] } }' `  --enforcement-mode DoNotEnforceaz policy assignment non-compliance-message create -n allowed-locations `  --scope /providers/Microsoft.Management/managementGroups/$mg --message "Only West/North Europe allowed – see wiki <link>"# later: enforceaz policy assignment update -n allowed-locations --scope /providers/Microsoft.Management/managementGroups/$mg --enforcement-mode Default
$mg  = Get-AzManagementGroup -GroupName <mg-landingzones>$def = Get-AzPolicyDefinition -Builtin | Where-Object DisplayName -eq "Allowed locations"New-AzPolicyAssignment -Name allowed-locations -DisplayName "Allowed locations" -PolicyDefinition $def -Scope $mg.Id `  -PolicyParameterObject @{ listOfAllowedLocations = @("westeurope", "northeurope") } -EnforcementMode DoNotEnforce `  -NonComplianceMessage @{ Message = "Only West/North Europe allowed – see wiki <link>" }Update-AzPolicyAssignment -Name allowed-locations -Scope $mg.Id -EnforcementMode Default
targetScope = 'managementGroup' param allowedLocations array = [ 'westeurope', 'northeurope' ]param enforce bool = false resource assign 'Microsoft.Authorization/policyAssignments@2024-04-01' = {  name: 'allowed-locations'  properties: {    displayName: 'Allowed locations'    policyDefinitionId: tenantResourceId('Microsoft.Authorization/policyDefinitions', '<builtin-definition-name>')    enforcementMode: enforce ? 'Default' : 'DoNotEnforce'    parameters: { listOfAllowedLocations: { value: allowedLocations } }    nonComplianceMessages: [ { message: 'Only West/North Europe allowed – see wiki <link>' } ]  }}
data "azurerm_policy_definition" "allowed_locations" {  display_name = "Allowed locations"}resource "azurerm_management_group_policy_assignment" "allowed_locations" {  name                 = "allowed-locations"  display_name         = "Allowed locations"  management_group_id  = "/providers/Microsoft.Management/managementGroups/<mg-landingzones>"  policy_definition_id = data.azurerm_policy_definition.allowed_locations.id  enforce              = false  parameters = jsonencode({    listOfAllowedLocations = { value = ["westeurope", "northeurope"] }  })  non_compliance_message {    content = "Only West/North Europe allowed – see wiki <link>"  }}

Caution: Assignment names: max. 24 characters at management group scope. Also allow global in the location list for global resources (Front Door, DNS, Traffic Manager).

#Write Your Own Policy

Example: deny public IP addresses except in resource groups with the tag AllowPublicIp=true, effect as parameter.

JSON
{  "properties": {    "displayName": "Deny public IP addresses (unless RG tag AllowPublicIp=true)",    "policyType": "Custom",    "mode": "All",    "metadata": { "category": "Network", "version": "1.0.0" },    "parameters": {      "effect": { "type": "String", "allowedValues": [ "Audit", "Deny", "Disabled" ], "defaultValue": "Audit" }    },    "policyRule": {      "if": {        "allOf": [          { "field": "type", "equals": "Microsoft.Network/publicIPAddresses" },          { "value": "[resourceGroup().tags['AllowPublicIp']]", "notEquals": "true" }        ]      },      "then": { "effect": "[parameters('effect')]" }    }  }}
# rules.json = only the "policyRule" object, params.json = only the "parameters" objectaz policy definition create -n deny-public-ip --display-name "Deny public IP addresses (unless RG tag AllowPublicIp=true)" `  --mode All --rules rules.json --params params.json --metadata category=Network version=1.0.0 `  --management-group <mg-root>
New-AzPolicyDefinition -Name deny-public-ip -DisplayName "Deny public IP addresses (unless RG tag AllowPublicIp=true)" `  -Policy .\deny-public-ip.json -Mode All -ManagementGroupName <mg-root>      # whole file with "properties" works too
targetScope = 'managementGroup' resource def 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {  name: 'deny-public-ip'  properties: {    displayName: 'Deny public IP addresses (unless RG tag AllowPublicIp=true)'    policyType: 'Custom'    mode: 'All'    metadata: { category: 'Network', version: '1.0.0' }    parameters: {      effect: { type: 'String', allowedValues: [ 'Audit', 'Deny', 'Disabled' ], defaultValue: 'Audit' }    }    policyRule: loadJsonContent('definitions/deny-public-ip.rule.json')  }}
resource "azurerm_policy_definition" "deny_public_ip" {  name                = "deny-public-ip"  display_name        = "Deny public IP addresses (unless RG tag AllowPublicIp=true)"  policy_type         = "Custom"  mode                = "All"  management_group_id = "/providers/Microsoft.Management/managementGroups/<mg-root>"  metadata            = jsonencode({ category = "Network", version = "1.0.0" })  parameters          = file("${path.module}/definitions/deny-public-ip.params.json")  policy_rule         = file("${path.module}/definitions/deny-public-ip.rule.json")}

Useful conditions: equals / notEquals · in / notIn · like / notLike (wildcard *) · contains · exists · greaterOrEquals · count (for arrays, e.g. NSG rules with * source) – logic with allOf / anyOf / not.

#Initiative (Policy Set)

Bundle related rules – one assignment, one compliance view, shared parameters.

# definitions.json: [ { "policyDefinitionId": "...", "policyDefinitionReferenceId": "requireTagEnv", "parameters": { ... } }, ... ]az policy set-definition create -n tagging --display-name "Tagging baseline" `  --definitions definitions.json --params params.json --management-group <mg-root>az policy assignment create -n tagging --policy-set-definition tagging `  --scope /providers/Microsoft.Management/managementGroups/<mg-landingzones> `  --mi-system-assigned --location westeurope
resource "azurerm_policy_set_definition" "tagging" {  name                = "tagging"  display_name        = "Tagging baseline"  policy_type         = "Custom"  management_group_id = "/providers/Microsoft.Management/managementGroups/<mg-root>"  parameters          = jsonencode({ tagName = { type = "String", defaultValue = "Environment" } })   policy_definition_reference {    reference_id         = "requireTagOnRg"    policy_definition_id = data.azurerm_policy_definition.require_tag_rg.id    parameter_values     = jsonencode({ tagName = { value = "[parameters('tagName')]" } })  }  policy_definition_reference {    reference_id         = "inheritTag"    policy_definition_id = data.azurerm_policy_definition.inherit_tag.id    parameter_values     = jsonencode({ tagName = { value = "[parameters('tagName')]" } })  }}

Typical tagging initiative from built-ins: Require a tag on resource groups (Deny) + Inherit a tag from the resource group if missing (Modify) – resources then get the tag automatically. Tag standard: Naming and Tags.

#Modify and DeployIfNotExists: Identity and Remediation

Modify and DeployIfNotExists change resources – the assignment needs a managed identity with the roles from roleDefinitionIds in the rule (e.g. Tag Contributor, Contributor, Network Contributor).

JSON
"then": {  "effect": "modify",  "details": {    "roleDefinitionIds": [ "/providers/Microsoft.Authorization/roleDefinitions/4a9ae827-6dc8-4573-8ac7-8239d42aa03f" ],    "operations": [      { "operation": "addOrReplace", "field": "[concat('tags[', parameters('tagName'), ']')]",        "value": "[resourceGroup().tags[parameters('tagName')]]" }    ]  }}
$scope = "/providers/Microsoft.Management/managementGroups/<mg-landingzones>"# identity + role in one step (role from roleDefinitionIds, at the assignment scope)az policy assignment create -n inherit-env-tag --policy <definition-name> --scope $scope `  --params '{ \"tagName\": { \"value\": \"Environment\" } }' `  --mi-system-assigned --location westeurope --role "Tag Contributor" --identity-scope $scope# Fix existing resources (new and updated ones are handled automatically)$aid = az policy assignment show -n inherit-env-tag --scope $scope --query id -o tsvaz policy remediation create -n rem-inherit-env-tag --policy-assignment $aid --management-group <mg-landingzones># subscription/RG scope can re-evaluate first: --resource-discovery-mode ReEvaluateCompliance (not at management group scope)az policy remediation list --management-group <mg-landingzones> --query "[].{name:name, state:provisioningState, ok:deploymentStatus.successfulDeployments, failed:deploymentStatus.failedDeployments}" -o table# For initiatives: --definition-reference-id <referenceId>
$scope = "/providers/Microsoft.Management/managementGroups/<mg-landingzones>"$def   = Get-AzPolicyDefinition -Name <definition-name> -ManagementGroupName <mg-root>$a = New-AzPolicyAssignment -Name inherit-env-tag -PolicyDefinition $def -Scope $scope `       -PolicyParameterObject @{ tagName = "Environment" } -IdentityType SystemAssigned -Location westeuropeNew-AzRoleAssignment -ObjectId $a.IdentityPrincipalId -RoleDefinitionName "Tag Contributor" -Scope $scopeStart-AzPolicyRemediation -Name rem-inherit-env-tag -PolicyAssignmentId $a.Id -ManagementGroupName <mg-landingzones>
resource assign 'Microsoft.Authorization/policyAssignments@2024-04-01' = {  name: 'inherit-env-tag'  location: 'westeurope'  identity: { type: 'SystemAssigned' }  properties: {    policyDefinitionId: managementGroupResourceId('Microsoft.Authorization/policyDefinitions', 'inherit-tag')    parameters: { tagName: { value: 'Environment' } }  }}resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = {  name: guid(assign.id, 'tag-contributor')  properties: {    principalId: assign.identity.principalId    principalType: 'ServicePrincipal'    roleDefinitionId: tenantResourceId('Microsoft.Authorization/roleDefinitions', '4a9ae827-6dc8-4573-8ac7-8239d42aa03f')  }}

Built-in roles used most: Tag Contributor 4a9ae827-…, Contributor b24988ac-…, Network Contributor, Log Analytics Contributor, Monitoring Contributor. A remediation without the role ends with AuthorizationFailed per resource.

#Exemptions

Exempt a resource with reason and expiry date.

az policy exemption create -n exm-<rg>-public-ip --policy-assignment <assignment-id> `  --scope /subscriptions/<sub-id>/resourceGroups/<rg> --exemption-category Waiver `  --expires-on 2026-12-31T00:00:00Z --description "Ticket <ticket>: temporary public IP for vendor test"az policy exemption list --scope /subscriptions/<sub-id> --query "[].{name:name, category:exemptionCategory, expires:expiresOn}" -o table
$a = Get-AzPolicyAssignment -Name <assignment> -Scope <assignment-scope>New-AzPolicyExemption -Name exm-<rg>-public-ip -PolicyAssignment $a -Scope "/subscriptions/<sub-id>/resourceGroups/<rg>" `  -ExemptionCategory Waiver -ExpiresOn (Get-Date "2026-12-31") -Description "Ticket <ticket>: temporary public IP for vendor test"

Waiver = accepted risk · Mitigated = requirement met another way. For initiatives exempt only single rules with --policy-definition-reference-ids. Always set an expiry date and a ticket reference.

#Compliance and Reports

Compliance summary and list of non-compliant resources.

az policy state summarize --management-group <mg-landingzones> --query "value[0].results"az policy state summarize -g <rg> --query "value[0].policyAssignments[].{assignment:policyAssignmentId, nonCompliant:results.nonCompliantResources}" -o tableaz policy state list -g <rg> --filter "complianceState eq 'NonCompliant'" `  --query "[].{resource:resourceId, policy:policyDefinitionName, ref:policyDefinitionReferenceId}" -o tableaz policy state trigger-scan -g <rg> --no-wait                 # re-evaluate now (takes a few minutes)
Get-AzPolicyStateSummary -ManagementGroupName <mg-landingzones>Get-AzPolicyState -ResourceGroupName <rg> -Filter "ComplianceState eq 'NonCompliant'" | Select-Object ResourceId, PolicyDefinitionNameStart-AzPolicyComplianceScan -ResourceGroupName <rg> -AsJob
// Non-compliant resources per assignment and rule – across all subscriptionspolicyresources| where type == "microsoft.policyinsights/policystates"| where properties.complianceState == "NonCompliant"| extend assignment = tostring(properties.policyAssignmentName), rule = tostring(properties.policyDefinitionReferenceId),         definition = tostring(properties.policyDefinitionName), resource = tostring(properties.resourceId)| summarize resources = dcount(resource) by assignment, definition, rule| order by resources desc

Run with az graph query -q "<query>", see Resource Graph. Portal: Policy → Compliance – filter by scope, Non-compliant, click a rule → Resource compliance → Details shows the failing field.

#Policy as Code: Repo and Pipeline

Keep policies in a repo and deploy them by pipeline.

text
policy/  definitions/        one JSON per custom policy (name = file name)  initiatives/        initiatives with reference IDs  assignments/        per scope: mg-<org>-platform.json, mg-<org>-landingzones.json, …  exemptions/         with expiry date and ticket  main.bicep          (or Terraform / EPAC) – deploys everything at management group scopepipelines/policy.yml

Feature branch → pull request (review by a second person).

Pipeline: validate + what-if against the management group.

New rules first as DoNotEnforce or Audit – check compliance for some days.

Switch to Deny / Default in a second pull request, announced to the teams.

Caution: Do not change anything in the portal – the repo is the source of truth, portal changes are overwritten.

Pipeline file (pipelines/policy.yml)
YAML
# pipelines/policy.yml (excerpt) – service connection with "Resource Policy Contributor" on the root management groupsteps:- task: AzureCLI@2  displayName: What-if  inputs:    azureSubscription: <service-connection>    scriptType: pscore    scriptLocation: inlineScript    inlineScript: az deployment mg what-if --management-group-id <mg-root> -l westeurope -f policy/main.bicep- task: AzureCLI@2  displayName: Deploy  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))  inputs:    azureSubscription: <service-connection>    scriptType: pscore    scriptLocation: inlineScript    inlineScript: az deployment mg create --management-group-id <mg-root> -l westeurope -f policy/main.bicep
Larger estates, roles

For larger estates: EPAC (Enterprise Policy as Code, Microsoft open-source PowerShell module) – plans and deploys definitions, assignments and exemptions from folders and also removes what is no longer in the repo. Landing zone baseline policies: the Azure Landing Zones (ALZ) policy set.

Roles: Resource Policy Contributor for definitions/assignments, plus User Access Administrator (or a role with roleAssignments/write) when assignments with managed identity get their roles from the pipeline.

#Troubleshooting

Find the blocking assignment and fix common problems.

PowerShell
# Which assignment blocks? The error contains policyAssignmentId/policyDefinitionId – or search:az policy assignment list --disable-scope-strict-match --scope /subscriptions/<sub-id>/resourceGroups/<rg> `  --query "[].{name:name, display:displayName, enforce:enforcementMode, scope:scope}" -o table# Activity log: policy denials of the last 24 haz monitor activity-log list --offset 24h --query "[?category.value=='Policy' && contains(operationName.value,'deny')].{time:eventTimestamp, caller:caller, resource:resourceId}" -o table
SymptomCauseFix
RequestDisallowedByPolicya Deny rule matchesread assignment/definition from the error, adjust deployment or create an exemption
New assignment shows no compliance datafirst evaluation takes ~30 minwait or az policy state trigger-scan
Remediation Failedidentity missing the role / role at wrong scoperole from roleDefinitionIds at the assignment scope
Modify does not tag existing resourcesModify only acts on create/updatestart a remediation task
Tag rule ignores some resourcesmode Indexed skips types without tagsAll only if needed – check the target type
Custom definition not selectabledefined at a lower management group / other subscriptiondefine at the root (or higher) management group
Location policy blocks Front Door / DNSglobal resources have location globaladd global to the allowed list
Assignment name too longmanagement group scope: max. 24 charactersshorter name, use displayName for the long text
Portal change overwrittenpipeline deployed the repo state againchange in the repo via pull request
Azure

#Monitoring and Alerts

Queries: KQL · Turn on diagnostic logs: → Diagnostic logs

#Action Group (Who Gets Notified)

Create the group that sends mail or webhooks.

PowerShell
$rg = "<rg-monitoring>"az monitor action-group create -g $rg -n ag-net-team --short-name netteam `  --action email team <team@company.tld>az monitor action-group update -g $rg -n ag-net-team --add-action email oncall <oncall@company.tld>az monitor action-group update -g $rg -n ag-net-team --add-action webhook teams "<teams-workflow-url>"az monitor action-group show -g $rg -n ag-net-team --query id -o tsv

Teams: in the channel Workflows → "Post to a channel when a webhook request is received" → enter the URL as webhook (or use the channel's e-mail address). --short-name max. 12 characters (appears in SMS/mail).

#Metric Alerts

Alerts on CPU, availability and other metrics.

PowerShell
$ag = az monitor action-group show -g <rg-monitoring> -n ag-net-team --query id -o tsv # VM not available$vmId = az vm show -g <rg> -n <vm> --query id -o tsvaz monitor metrics alert create -g <rg-monitoring> -n "vm-<vm>-down" --scopes $vmId `  --condition "avg VmAvailabilityMetric < 1" --window-size 5m --evaluation-frequency 1m `  --severity 1 --action $ag --description "VM <vm> not available" # VM CPU constantly highaz monitor metrics alert create -g <rg-monitoring> -n "vm-<vm>-cpu" --scopes $vmId `  --condition "avg Percentage CPU > 90" --window-size 15m --evaluation-frequency 5m --severity 2 --action $ag # App Gateway: backend unhealthy$agwId = az network application-gateway show -g <rg> -n <agw> --query id -o tsvaz monitor metrics alert create -g <rg-monitoring> -n "agw-<agw>-unhealthy" --scopes $agwId `  --condition "max UnhealthyHostCount > 0" --window-size 5m --evaluation-frequency 1m --severity 1 --action $ag # Load balancer: health probe fails (e.g. FortiGate ILB)$lbId = az network lb show -g <rg> -n <lb> --query id -o tsvaz monitor metrics alert create -g <rg-monitoring> -n "lb-<lb>-probe" --scopes $lbId `  --condition "avg DipAvailability < 100" --window-size 5m --evaluation-frequency 1m --severity 2 --action $ag # VPN tunnel down$conId = az network vpn-connection show -g <rg> -n <conn> --query id -o tsvaz monitor metrics alert create -g <rg-monitoring> -n "vpn-<conn>-down" --scopes $conId `  --condition "max TunnelIngressBytes < 1" --window-size 15m --evaluation-frequency 5m --severity 2 --action $ag

Which metrics exist? az monitor metrics list-definitions --resource <id> --query "[].name.value" -o tsv

#Activity Log Alerts

React to changes in Azure – e.g. when the FortiGate SDN connector rewrites routes during a failover.

PowerShell
$ag = az monitor action-group show -g <rg-monitoring> -n ag-net-team --query id -o tsv$sub = "/subscriptions/<sub-id>" # Route changed (FortiGate failover via SDN connector, or someone touched a UDR)az monitor activity-log alert create -g <rg-monitoring> -n "route-changed" --scope $sub `  --condition category=Administrative and operationName=Microsoft.Network/routeTables/routes/write `  --action-group $ag # NSG rule changedaz monitor activity-log alert create -g <rg-monitoring> -n "nsg-rule-changed" --scope $sub `  --condition category=Administrative and operationName=Microsoft.Network/networkSecurityGroups/securityRules/write `  --action-group $ag # Service Health (Microsoft outages)az monitor activity-log alert create -g <rg-monitoring> -n "service-health" --scope $sub `  --condition category=ServiceHealth --action-group $ag

#Log Alerts (KQL)

Easiest in the portal: Monitor → Alerts → Create → Alert rule → scope = Log Analytics workspace → Custom log search → paste KQL.

KQL
// FortiGate reports an HA change (Syslog/CEF)CommonSecurityLog| where DeviceVendor == "Fortinet"| where Activity has "HA" and (Activity has "state" or Message has "failover") // WAF suddenly blocks a lot (> 100 in 5 min)AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog" and action_s == "Blocked"| summarize Count = count() by bin(TimeGenerated, 5m)| where Count > 100 // VM no longer sends a heartbeatHeartbeat| summarize last = max(TimeGenerated) by Computer| where last < ago(10m)

Condition: Number of results > 0, evaluated every 5 min, period 10–15 min. Check the field names of the FortiGate logs first with CommonSecurityLog | take 20.

#Resource Health

Resource health status and alerts for it.

PowerShell
az resource show --ids <resource-id>/providers/Microsoft.ResourceHealth/availabilityStatuses/current `  --api-version 2022-10-01 --query "properties.{status:availabilityState, reason:summary}"az monitor activity-log alert create -g <rg-monitoring> -n "resource-health" --scope /subscriptions/<sub-id> `  --condition category=ResourceHealth --action-group $ag

#Manage Alerts

List, disable and change existing alerts.

PowerShell
az monitor metrics alert list -g <rg-monitoring> --query "[].{name:name, enabled:enabled, severity:severity}" -o tableaz monitor activity-log alert list -g <rg-monitoring> -o tableaz monitor metrics alert update -g <rg-monitoring> -n <alert> --enabled false        # temporarily off (maintenance)az monitor alert-processing-rule create -g <rg-monitoring> -n maintenance-<date> `  --scopes /subscriptions/<sub-id>/resourceGroups/<rg> --rule-type RemoveAllActionGroups `  --schedule-start-datetime "2026-11-01 22:00:00" --schedule-end-datetime "2026-11-02 02:00:00" --schedule-time-zone "W. Europe Standard Time"

The alert processing rule suppresses notifications during a maintenance window without deleting alerts.

SeverityMeaning
0critical
1error
2warning
3informational
4verbose
Azure

#Resource Graph – Queries Across All Subscriptions

Basics: → Resource Graph

Resource Graph searches all subscriptions you have read access to – in seconds. The query language is KQL.

#How to Run

Install the extension and run queries across subscriptions.

PowerShell
az extension add -n resource-graphaz graph query -q "<query>" --first 1000 -o tableaz graph query -q "<query>" --management-groups <mg-id> -o table        # only below one management groupaz graph query -q "<query>" --subscriptions <sub-id> -o table # Long queries: into a variable (PowerShell here-string)$q = @"Resources| where type =~ 'microsoft.compute/virtualmachines'| project name, resourceGroup"@az graph query -q $q -o table # Result as CSVaz graph query -q $q --first 1000 --query data -o json | ConvertFrom-Json | Export-Csv .\result.csv -NoTypeInformation -Encoding UTF8 # PowerShell moduleSearch-AzGraph -Query $q -First 1000 | Format-Table

In the portal: Resource Graph Explorer – paste the query, run it, save as chart/dashboard. Always use single quotes inside the queries so they fit into PowerShell's "…".

#Inventory

Count and list resources by type, size and state.

KQL
// Count per resource typeResources | summarize Count = count() by type | order by Count desc // All VMs with size and stateResources| where type =~ 'microsoft.compute/virtualmachines'| project name, resourceGroup, subscriptionId, location,          size = tostring(properties.hardwareProfile.vmSize),          state = tostring(properties.extended.instanceView.powerState.code),          zone = tostring(zones[0]) // Resources per subscription with the subscription nameResources| join kind=inner (ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, subName = name) on subscriptionId| summarize Count = count() by subName // Application gateways with SKU and stateResources| where type =~ 'microsoft.network/applicationgateways'| project name, resourceGroup, sku = tostring(properties.sku.name), state = tostring(properties.operationalState) // Key vaults with network accessResources| where type =~ 'microsoft.keyvault/vaults'| project name, resourceGroup, rbac = properties.enableRbacAuthorization, publicAccess = tostring(properties.publicNetworkAccess)

#Network

VNets, peerings, subnets and public IPs across subscriptions.

KQL
// VNets with address space and number of peeringsResources| where type =~ 'microsoft.network/virtualnetworks'| project name, resourceGroup, space = properties.addressSpace.addressPrefixes,          peerings = array_length(properties.virtualNetworkPeerings) // All subnets with NSG and route tableResources| where type =~ 'microsoft.network/virtualnetworks'| mv-expand s = properties.subnets| project vnet = name, subnet = tostring(s.name), prefix = tostring(s.properties.addressPrefix),          nsg = tostring(split(s.properties.networkSecurityGroup.id, '/')[8]),          routeTable = tostring(split(s.properties.routeTable.id, '/')[8]) // Subnets WITHOUT an NSGResources| where type =~ 'microsoft.network/virtualnetworks'| mv-expand s = properties.subnets| where isnull(s.properties.networkSecurityGroup)  and tostring(s.name) !in ('GatewaySubnet', 'AzureFirewallSubnet', 'AzureBastionSubnet', 'RouteServerSubnet')| project vnet = name, subnet = tostring(s.name), resourceGroup // Peerings that are not connectedResources| where type =~ 'microsoft.network/virtualnetworks'| mv-expand p = properties.virtualNetworkPeerings| where tostring(p.properties.peeringState) != 'Connected'| project vnet = name, peering = tostring(p.name), state = tostring(p.properties.peeringState) // All public IPs and what they are attached toResources| where type =~ 'microsoft.network/publicipaddresses'| project name, ip = tostring(properties.ipAddress), sku = tostring(sku.name),          attachedTo = tostring(split(properties.ipConfiguration.id, '/')[8]), resourceGroup // Private endpoints and their targetsResources| where type =~ 'microsoft.network/privateendpoints'| mv-expand c = properties.privateLinkServiceConnections| project name, resourceGroup,          target = tostring(split(c.properties.privateLinkServiceId, '/')[8]),          groupId = tostring(c.properties.groupIds[0]),          status = tostring(c.properties.privateLinkServiceConnectionState.status) // All routes with next hopResources| where type =~ 'microsoft.network/routetables'| mv-expand r = properties.routes| project routeTable = name, route = tostring(r.name), prefix = tostring(r.properties.addressPrefix),          nextHop = tostring(r.properties.nextHopType), ip = tostring(r.properties.nextHopIpAddress) // Who owns this private IP?Resources| where type =~ 'microsoft.network/networkinterfaces'| mv-expand ipc = properties.ipConfigurations| project nic = name, ip = tostring(ipc.properties.privateIPAddress),          vm = tostring(split(properties.virtualMachine.id, '/')[8]), resourceGroup| where ip == '<ip>'

#Security

Open NSG rules, public storage and other exposed resources.

KQL
// NSG rules that allow SSH/RDP/everything from the internetResources| where type =~ 'microsoft.network/networksecuritygroups'| mv-expand r = properties.securityRules| where r.properties.direction == 'Inbound' and r.properties.access == 'Allow'| where tostring(r.properties.sourceAddressPrefix) in ('*', 'Internet', '0.0.0.0/0')| where tostring(r.properties.destinationPortRange) in ('22', '3389', '*')| project nsg = name, rule = tostring(r.name), port = tostring(r.properties.destinationPortRange), resourceGroup // Storage accounts with public accessResources| where type =~ 'microsoft.storage/storageaccounts'| project name, resourceGroup, publicNetwork = tostring(properties.publicNetworkAccess),          blobPublic = properties.allowBlobPublicAccess, minTls = tostring(properties.minimumTlsVersion) // Open Defender recommendations per topicSecurityResources| where type == 'microsoft.security/assessments'| where properties.status.code == 'Unhealthy'| summarize Count = count() by recommendation = tostring(properties.displayName)| order by Count desc

#Cleanup and Costs

Unattached disks, unused IPs and other orphans.

KQL
// Disks without a VMResources| where type =~ 'microsoft.compute/disks' and properties.diskState =~ 'Unattached'| project name, resourceGroup, sizeGB = properties.diskSizeGB, sku = tostring(sku.name) // Unassigned public IPsResources| where type =~ 'microsoft.network/publicipaddresses'| where isnull(properties.ipConfiguration) and isnull(properties.natGateway)| project name, resourceGroup, ip = tostring(properties.ipAddress) // NICs without VM and without private endpointResources| where type =~ 'microsoft.network/networkinterfaces'| where isnull(properties.virtualMachine) and isnull(properties.privateEndpoint)| project name, resourceGroup // Stopped but not deallocated VMs (still cost money)Resources| where type =~ 'microsoft.compute/virtualmachines'| where properties.extended.instanceView.powerState.code == 'PowerState/stopped'| project name, resourceGroup // Empty resource groupsResourceContainers| where type =~ 'microsoft.resources/subscriptions/resourcegroups'| join kind=leftouter (Resources | summarize n = count() by resourceGroup, subscriptionId) on resourceGroup, subscriptionId| where isnull(n)| project resourceGroup, subscriptionId

#Tags

Resources missing a required tag.

KQL
// Resources without tag "Environment"Resources| where isnull(tags['Environment']) or tostring(tags['Environment']) == ''| project name, type, resourceGroup // Resource groups without ownerResourceContainers| where type =~ 'microsoft.resources/subscriptions/resourcegroups'| where isnull(tags['Owner'])| project name, subscriptionId // Values used for a tagResources| summarize Count = count() by env = tostring(tags['Environment'])

Tag names are case-sensitive in queries – mind your spelling.

#Changes, Permissions, Policy

What changed recently, who has access, what is non-compliant.

KQL
// What changed in the last 24 h?ResourceChanges| extend time = todatetime(properties.changeAttributes.timestamp),         kind = tostring(properties.changeType),         id = tostring(properties.targetResourceId)| where time > ago(1d)| project time, kind, id| order by time desc // Role assignments at subscription levelAuthorizationResources| where type =~ 'microsoft.authorization/roleassignments'| extend scope = tostring(properties.scope), principal = tostring(properties.principalId),         type = tostring(properties.principalType), role = tostring(split(properties.roleDefinitionId, '/')[-1])| where scope startswith '/subscriptions/' and scope !contains 'resourceGroups'| project scope, principal, type, role // Non-compliant resources per policyPolicyResources| where type =~ 'microsoft.policyinsights/policystates'| where properties.complianceState =~ 'NonCompliant'| summarize Count = count() by policy = tostring(properties.policyDefinitionName)| order by Count desc // Subscriptions with their management groupResourceContainers| where type =~ 'microsoft.resources/subscriptions'| project name, subscriptionId, mg = tostring(properties.managementGroupAncestorsChain[0].displayName)

role is the role ID – its name: az role definition list --name <id> --query "[0].roleName" -o tsv.

Network and Firewall

#Network Workflows

Single commands: Network Resources · Network Basics

Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.

#Connect a new spoke to the hub

Changes things4 steps

VNet, subnet, peering in both directions and a route table via the firewall – in four steps.

Variables first – run once, then step by step:

PowerShell
$hubRg   = "<rg-hub>"   ; $hubVnet = "<vnet-hub>"$rg      = "<rg-spoke>" ; $vnet    = "<vnet-spoke>" ; $loc = "<region>"$space   = "10.1.0.0/24" ; $snet = "snet-app" ; $snetPrefix = "10.1.0.0/26"$nvaIp   = "<ilb-ip-of-firewall>"

VNet and subnet

PowerShell
az group create -n $rg -l $locaz network vnet create -g $rg -n $vnet -l $loc --address-prefixes $spaceaz network vnet subnet create -g $rg --vnet-name $vnet -n $snet --address-prefixes $snetPrefix --default-outbound false

Peering in both directions

PowerShell
$hubId   = az network vnet show -g $hubRg -n $hubVnet --query id -o tsv$spokeId = az network vnet show -g $rg    -n $vnet    --query id -o tsvaz network vnet peering create -g $rg    --vnet-name $vnet    -n "$vnet-to-$hubVnet" --remote-vnet $hubId   --allow-vnet-access --allow-forwarded-trafficaz network vnet peering create -g $hubRg --vnet-name $hubVnet -n "$hubVnet-to-$vnet" --remote-vnet $spokeId --allow-vnet-access --allow-forwarded-traffic

Tip: With a VPN/ER gateway in the hub additionally: hub side --allow-gateway-transit, spoke side --use-remote-gateways.

Route table: everything via the firewall

PowerShell
az network route-table create -g $rg -n "rt-$vnet" -l $loc --disable-bgp-route-propagation trueaz network route-table route create -g $rg --route-table-name "rt-$vnet" -n default-to-nva `  --address-prefix 0.0.0.0/0 --next-hop-type VirtualAppliance --next-hop-ip-address $nvaIpaz network vnet subnet update -g $rg --vnet-name $vnet -n $snet --route-table "rt-$vnet"

Check – both peerings must be Connected:

PowerShell
az network vnet peering list -g $rg --vnet-name $vnet --query "[].{name:name, state:peeringState, sync:peeringSyncLevel}" -o table

Note: Next on the FortiGate: route and policy for the spoke. With a VPN gateway also add a GatewaySubnet UDR for the spoke prefix → NVA.

#Extend address space and sync peering

Changes things

Add an address prefix to a VNet and bring all peerings back to FullyInSync.

PowerShell
$rg = "<rg>" ; $vnet = "<vnet>"az network vnet update -g $rg -n $vnet --address-prefixes <existing-prefix> <new-prefix>az network vnet peering list -g $rg --vnet-name $vnet --query "[].{name:name, sync:peeringSyncLevel}" -o tableaz network vnet peering sync -g $rg --vnet-name $vnet -n <peering>          # for each peering, status -> FullyInSync

Run sync on the remote side (hub) too, and add the new prefixes to UDRs/firewall routes.

#NSG: temporarily allow your own IP

Has risky commands

Open ports for your current public IP only, then remove the rule again.

$rg = "<rg>" ; $nsg = "<nsg>"$myIp = Invoke-RestMethod https://api.ipify.orgaz network nsg rule create -g $rg --nsg-name $nsg -n Temp-Allow-MyIp --priority 110 `  --direction Inbound --access Allow --protocol Tcp `  --source-address-prefixes "$myIp/32" --destination-port-ranges 22 443# … work …az network nsg rule delete -g $rg --nsg-name $nsg -n Temp-Allow-MyIp
rg="<rg>"; nsg="<nsg>"myIp=$(curl -s https://api.ipify.org)az network nsg rule create -g $rg --nsg-name $nsg -n Temp-Allow-MyIp --priority 110 \  --direction Inbound --access Allow --protocol Tcp \  --source-address-prefixes "$myIp/32" --destination-port-ranges 22 443# … work …az network nsg rule delete -g $rg --nsg-name $nsg -n Temp-Allow-MyIp

#VM cannot reach the internet

Read-only

Check effective routes, next hop, NSG and a live test from the VM – in that order.

PowerShell
$rg = "<rg>" ; $vm = "<vm>" ; $nic = "<nic>"az network nic show-effective-route-table -g $rg -n $nic -o table                     # 0.0.0.0/0 -> where?az network watcher show-next-hop -g $rg --vm $vm --source-ip <vm-ip> --dest-ip 8.8.8.8 -o tableaz network nic list-effective-nsg -g $rg -n $nic --query "value[].effectiveSecurityRules[?direction=='Outbound'].{name:name, access:access, prio:priority}"az vm run-command invoke -g $rg -n $vm --command-id RunShellScript --scripts "curl -s -m 5 ifconfig.me; echo"

Then on the firewall: trace a packet.

#Create a private endpoint for a web app

Changes things3 steps

Private endpoint, private DNS zone, VNet link and DNS zone group – then switch off public access.

PowerShell
$rg = "<rg>" ; $app = "<webapp>" ; $vnet = "<vnet>" ; $snet = "<snet-pe>"$rgDns = "<rg-dns>" ; $zone = "privatelink.azurewebsites.net"$dnsVnetId = "<vnet-id-that-resolves-dns>"

Create the private endpoint

PowerShell
$appId = az webapp show -g $rg -n $app --query id -o tsvaz network private-endpoint create -g $rg -n "pe-$app" --vnet-name $vnet --subnet $snet `  --private-connection-resource-id $appId --group-id sites --connection-name "pe-$app"

Private DNS zone and link (zone once per environment)

PowerShell
az network private-dns zone create -g $rgDns -n $zoneaz network private-dns link vnet create -g $rgDns -z $zone -n "link-$vnet" -v $dnsVnetId -e false$zoneId = az network private-dns zone show -g $rgDns -n $zone --query id -o tsvaz network private-endpoint dns-zone-group create -g $rg --endpoint-name "pe-$app" -n default `  --private-dns-zone $zoneId --zone-name websites

Disable public access and check

PowerShell
az webapp update -g $rg -n $app --set publicNetworkAccess=Disablednslookup "$app.azurewebsites.net"            # from the VNet: private IP expected

Tip: Other services: zone and --group-id from the table.

#Private endpoint does not resolve privately

Read-only

Find out why the name still resolves to a public IP: DNS server, zone link, A record.

PowerShell
nslookup <name>.privatelink.<zone>                    # or the public name, e.g. <app>.azurewebsites.netaz network vnet show -g <rg> -n <vnet> --query dhcpOptions.dnsServers      # empty = Azure DNSaz network private-dns link vnet list -g <rg-dns> -z <zone> --query "[].{name:name, vnet:virtualNetwork.id}" -o tableaz network private-dns record-set a list -g <rg-dns> -z <zone> --query "[].{name:name, ip:aRecords[0].ipv4Address}" -o tableaz network private-endpoint show -g <rg> -n <pe> --query "customDnsConfigs"

Must match: the zone is linked to the VNet whose DNS server does the resolution, and the A record points to the PE IP.

#Check a VPN connection

Read-only

Status, BGP peers, learned routes and shared key of a VPN gateway connection.

PowerShell
$rg = "<rg>" ; $gw = "<vpn-gateway>" ; $conn = "<connection>"az network vpn-connection show -g $rg -n $conn --query "{status:connectionStatus, in:ingressBytesTransferred, out:egressBytesTransferred}"az network vnet-gateway list-bgp-peer-status -g $rg -n $gw -o tableaz network vnet-gateway list-learned-routes -g $rg -n $gw -o tableaz network vpn-connection shared-key show -g $rg --connection-name $conn       # compare PSK with the remote side

Remote side FortiGate: debug an IPsec tunnel.

#Reach the FortiGate GUI or a VM without public IP via Bastion

Read-only

Tunnel through Bastion for HTTPS, SSH or RDP to a machine without public IP.

PowerShell
$rg = "<rg-bastion>" ; $bastion = "<bastion>"$vmId = az vm show -g <rg-vm> -n <vm> --query id -o tsv# GUI (HTTPS) -> then in the browser https://localhost:8443az network bastion tunnel -g $rg -n $bastion --target-resource-id $vmId --resource-port 443 --port 8443# SSH -> in a second window: ssh -p 50022 <admin>@127.0.0.1az network bastion tunnel -g $rg -n $bastion --target-resource-id $vmId --resource-port 22 --port 50022# RDP directlyaz network bastion rdp -g $rg -n $bastion --target-resource-id $vmId

Requires Bastion Standard with Native client support. Keep the tunnel window open.

Network and Firewall

#FortiGate Workflows

Single commands: FortiGate CLI

Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.

#FortiGate: apply and check the license

Changes things

Upload the license, wait for the reboot and verify the status.

Text
execute vm-license <FortiFlex-token>          # the FortiGate reboots afterwards# after the rebootget system status | grep -i -E "version|serial|license"diagnose debug vm-print-licenseexecute update-nowdiagnose autoupdate versions | grep -A3 "AV Engine"

With HA: on both units separately (secondary via execute ha manage <index> <admin>).

#FortiGate: let a spoke reach the internet

Changes things4 steps

Route, address object, firewall policy and source NAT for a new spoke.

Address object for the spoke

Text
config firewall address    edit "net-<spoke>"        set subnet 10.1.0.0 255.255.255.0    nextend

Static route to the spoke

Text
config router static    edit 0        set dst 10.1.0.0 255.255.255.0        set gateway <first-ip-of-port2-subnet>     # .1 of the internal subnet        set device "port2"    nextend

Policy with source NAT

Text
config firewall policy    edit 0        set name "<spoke>-to-internet"        set srcintf "port2"        set dstintf "port1"        set srcaddr "net-<spoke>"        set dstaddr "all"        set action accept        set schedule "always"        set service "HTTP" "HTTPS" "DNS"        set nat enable        set logtraffic all    nextend

Test from the spoke: curl -s ifconfig.me → must show the firewall's public IP.

#FortiGate: allow spoke-to-spoke

Changes things

Policy between two spokes – both need the UDR to the firewall.

Text
config firewall policy    edit 0        set name "<spoke-a>-to-<spoke-b>"        set srcintf "port2"        set dstintf "port2"        set srcaddr "net-<spoke-a>"        set dstaddr "net-<spoke-b>"        set action accept        set schedule "always"        set service "<services>"        set nat disable        set logtraffic all    nextend

Both spokes need the UDR to the firewall, otherwise routing is asymmetric.

#FortiGate: port forwarding from outside (VIP)

Changes things4 steps

Virtual IP and policy for an inbound port, plus load balancer rule and NSG in Azure.

Virtual IP

Text
config firewall vip    edit "vip-<service>"        set extip <private-ip-of-port1>       # or LB frontend IP with floating IP        set extintf "port1"        set portforward enable        set mappedip "<internal-server-ip>"        set extport 443        set mappedport 443    nextend

Inbound policy

Text
config firewall policy    edit 0        set name "in-<service>"        set srcintf "port1"        set dstintf "port2"        set srcaddr "all"        set dstaddr "vip-<service>"        set action accept        set schedule "always"        set service "HTTPS"        set logtraffic all    nextend

In Azure additionally: a load balancer rule for the port and an open NSG on port1.

Test with the sniffer: diagnose sniffer packet any 'port 443' 4 20.

#FortiGate: trace a packet

Read-only3 steps

Sniffer for arrival and exit, debug flow for the drop reason, then check route and policy.

Does it arrive and does it leave?

Text
diagnose sniffer packet any 'host <ip>' 4 20

Why is it dropped?

Text
diagnose debug resetdiagnose debug flow filter cleardiagnose debug flow filter addr <ip>diagnose debug flow show function-name enablediagnose debug flow trace start 10diagnose debug enable#    (generate traffic)diagnose debug disablediagnose debug reset

Cross-check route and policy

Text
get router info routing-table details <dest-ip>show firewall policy | grep -i -A20 <name>

Interpreting messages: debug flow table.

#FortiGate: test HA failover

Changes things

Continuous test from a spoke client, trigger the failover and measure the interruption.

Start a continuous test on a client in the spoke:

while ($true) { "$((Get-Date).ToString('HH:mm:ss')) $(curl.exe -s -m 2 https://api.ipify.org)"; Start-Sleep 1 }
while true; do echo "$(date +%T) $(curl -s -m 2 https://api.ipify.org)"; sleep 1; done

On the primary FortiGate:

Text
get system ha statusdiagnose sys ha checksum cluster          # in sync before?execute ha failover set 1                 # trigger failover# … watch the continuous test (interruption in seconds, possibly a new public IP)execute ha failover unset 1diagnose sys ha reset-uptime              # with override disable: hand back the role

#FortiGate: HA out of sync

Changes things

Compare the checksums, find the area that differs and resync.

Text
diagnose sys ha checksum cluster              # compare on bothdiagnose sys ha checksum show                 # which area differs?execute ha manage <index> <admin>             # switch to the secondaryexecute ha synchronize startdiagnose sys ha checksum recalculate          # if still differentget system ha status

#FortiGate: debug an IPsec tunnel

Read-only

Trigger the negotiation, read the IKE debug and check SAs and counters.

Text
get vpn ipsec tunnel summarydiagnose vpn ike log filter cleardiagnose vpn ike log filter rem-addr4 <peer-ip>diagnose debug application ike -1diagnose debug console timestamp enablediagnose debug enablediagnose vpn tunnel up <phase2> <phase1>      # trigger negotiation# … read the output …diagnose debug disablediagnose debug resetdiagnose vpn tunnel list name <phase1>        # SAs/counters once it is up

Messages: IPsec table.

#FortiGate: back up the configuration

Changes things

Back up the configuration via CLI or GUI – never into the repo.

Text
config system global    set admin-scp enableend
scp <admin>@<mgmt-ip>:sys_config ".\fgt-backup-$(Get-Date -Format yyyyMMdd).conf"
scp <admin>@<mgmt-ip>:sys_config "./fgt-backup-$(date +%Y%m%d).conf"

Or GUI → admin menu → Configuration → Backup. Backups contain hashes/secrets – not into the repo.

Network and Firewall

#Azure Network Resources

Background: Network basics · Step by step: Network Workflows · Application Gateway and WAF Workflows

Top 5: az network nic show-effective-route-table · az network vnet peering list · az network watcher show-next-hop · az network nsg rule list --include-default · az network application-gateway show-backend-health

#VNet and Subnets

List, inspect and create virtual networks and subnets.

PowerShell
az network vnet list --query "[].{name:name, rg:resourceGroup, space:addressSpace.addressPrefixes[0]}" -o tableaz network vnet show -g <rg> -n <vnet> --query "{space:addressSpace.addressPrefixes, dns:dhcpOptions.dnsServers}"az network vnet create -g <rg> -n <vnet> --address-prefixes 10.0.0.0/22 -l <region>az network vnet update -g <rg> -n <vnet> --address-prefixes 10.0.0.0/22 10.0.8.0/24     # extend address spaceaz network vnet update -g <rg> -n <vnet> --dns-servers <ip1> <ip2>                       # custom DNSaz network vnet update -g <rg> -n <vnet> --dns-servers ""                                # back to Azure DNSaz network vnet subnet list -g <rg> --vnet-name <vnet> `  --query "[].{name:name, prefix:addressPrefix, nsg:networkSecurityGroup.id, rt:routeTable.id}" -o tableaz network vnet subnet create -g <rg> --vnet-name <vnet> -n <subnet> --address-prefixes 10.0.1.0/24 `  --default-outbound falseaz network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --network-security-group <nsg>az network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --route-table <rt-id>az network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --remove routeTableaz network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --remove networkSecurityGroupaz network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --private-endpoint-network-policies Enabledaz network vnet subnet update -g <rg> --vnet-name <vnet> -n <subnet> --delegations Microsoft.Web/serverFarmsaz network vnet list-available-ips -g <rg> -n <vnet>az network vnet check-ip-address -g <rg> -n <vnet> --ip-address <ip>       # free?

#Peering

Create peerings and check state and sync level.

PowerShell
az network vnet peering list -g <rg> --vnet-name <vnet> `  --query "[].{name:name, state:peeringState, sync:peeringSyncLevel, access:allowVirtualNetworkAccess, fwd:allowForwardedTraffic, gwTransit:allowGatewayTransit, remoteGw:useRemoteGateways}" -o tableaz network vnet peering create -g <rg> --vnet-name <vnet-a> -n <a-to-b> --remote-vnet <vnet-b-id> `  --allow-vnet-access --allow-forwarded-trafficaz network vnet peering update -g <rg> --vnet-name <vnet> -n <peering> --set allowForwardedTraffic=trueaz network vnet peering sync -g <rg> --vnet-name <vnet> -n <peering>       # after an address space changeaz network vnet peering delete -g <rg> --vnet-name <vnet> -n <peering>
SettingHub sideSpoke side
Allow forwarded trafficyesyes
Allow gateway transityes (if VPN/ER GW in hub)–
Use remote gateways–yes (if VPN/ER GW in hub)

Peering must exist on both sides: Connected / FullyInSync.

#Route Tables (UDR)

Create route tables and routes, for example everything via the firewall.

PowerShell
az network route-table list -o tableaz network route-table create -g <rg> -n <rt> -l <region> --disable-bgp-route-propagation trueaz network route-table route create -g <rg> --route-table-name <rt> -n default-to-nva `  --address-prefix 0.0.0.0/0 --next-hop-type VirtualAppliance --next-hop-ip-address <nva-or-ilb-ip>az network route-table route list -g <rg> --route-table-name <rt> -o tableaz network route-table route update -g <rg> --route-table-name <rt> -n <route> --next-hop-ip-address <new-ip>az network route-table route delete -g <rg> --route-table-name <rt> -n <route>az network route-table show -g <rg> -n <rt> --query "subnets[].id" -o tsv        # where is it associated?

Next hop types: VirtualAppliance, Internet, VnetLocal, VirtualNetworkGateway, None.

#Effective Routes and Connectivity Tests

See the routes and NSG rules a NIC really uses and test the next hop.

PowerShell
az network nic show-effective-route-table -g <rg> -n <nic> -o tableaz network nic list-effective-nsg -g <rg> -n <nic>az network watcher show-next-hop -g <rg> --vm <vm> --source-ip <vm-ip> --dest-ip <target> -o tableaz network watcher test-connectivity -g <rg> --source-resource <vm> --dest-address <target> --dest-port 443az network watcher test-ip-flow -g <rg> --vm <vm> --direction Inbound --protocol TCP `  --local <vm-ip>:443 --remote <source>:12345az network watcher list -o table                                   # Network Watcher per region

Effective routes are only available for a running VM.

#NSG

List and change network security groups and their rules.

PowerShell
az network nsg list -o tableaz network nsg create -g <rg> -n <nsg>az network nsg rule list -g <rg> --nsg-name <nsg> --include-default -o tableaz network nsg rule create -g <rg> --nsg-name <nsg> -n Allow-Https-In --priority 100 `  --direction Inbound --access Allow --protocol Tcp `  --source-address-prefixes <source-or-ServiceTag> --destination-port-ranges 443az network nsg rule update -g <rg> --nsg-name <nsg> -n <rule> --access Denyaz network nsg rule delete -g <rg> --nsg-name <nsg> -n <rule>az network nsg show -g <rg> -n <nsg> --query "{subnets:subnets[].id, nics:networkInterfaces[].id}"

Service tags: Internet, VirtualNetwork, AzureLoadBalancer, GatewayManager, AzureCloud, Storage.<region>, AzureMonitor, AzureKeyVault … Priority: the lower number wins (100–4096).

#NAT Gateway

Fixed outbound public IP(s) for whole subnets – replaces default outbound access (retired for new VNets) and avoids SNAT port exhaustion. Form for all four tools: web page → Generator → Network → NAT gateway.

az network public-ip create -g <rg> -n pip-ng-<name>-weu -l westeurope --sku Standard --allocation-method Static --zone 1az network nat gateway create -g <rg> -n ng-<name>-weu -l westeurope --public-ip-addresses pip-ng-<name>-weu --idle-timeout 4 --zone 1$ng = az network nat gateway show -g <rg> -n ng-<name>-weu --query id -o tsvaz network vnet subnet update -g <rg-vnet> --vnet-name <vnet> -n <subnet> --nat-gateway $ngaz network nat gateway list --query "[].{name:name, rg:resourceGroup, zones:join(',', zones || ['-'])}" -o table
$pip = New-AzPublicIpAddress -ResourceGroupName <rg> -Name pip-ng-<name>-weu -Location westeurope -Sku Standard -AllocationMethod Static -Zone 1$ng  = New-AzNatGateway -ResourceGroupName <rg> -Name ng-<name>-weu -Location westeurope -Sku Standard -IdleTimeoutInMinutes 4 -PublicIpAddress $pip -Zone 1$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg-vnet> -Name <vnet>($vnet.Subnets | Where-Object Name -eq <subnet>).NatGateway = $ng$vnet | Set-AzVirtualNetwork
resource pip 'Microsoft.Network/publicIPAddresses@2024-05-01' = {  name: 'pip-ng-<name>-weu'  location: location  sku: { name: 'Standard' }  zones: [ '1' ]  properties: { publicIPAllocationMethod: 'Static' }}resource ng 'Microsoft.Network/natGateways@2024-05-01' = {  name: 'ng-<name>-weu'  location: location  sku: { name: 'Standard' }  zones: [ '1' ]  properties: { idleTimeoutInMinutes: 4, publicIpAddresses: [ { id: pip.id } ] }}// in the subnet of your VNet template:  natGateway: { id: ng.id }
resource "azurerm_public_ip" "ng" {  name                = "pip-ng-<name>-weu"  resource_group_name = "<rg>"  location            = "westeurope"  allocation_method   = "Static"  sku                 = "Standard"  zones               = ["1"]}resource "azurerm_nat_gateway" "this" {  name                    = "ng-<name>-weu"  resource_group_name     = "<rg>"  location                = "westeurope"  sku_name                = "Standard"  idle_timeout_in_minutes = 4  zones                   = ["1"]}resource "azurerm_nat_gateway_public_ip_association" "this" {  nat_gateway_id       = azurerm_nat_gateway.this.id  public_ip_address_id = azurerm_public_ip.ng.id}resource "azurerm_subnet_nat_gateway_association" "this" {  subnet_id      = "<subnet-id>"  nat_gateway_id = azurerm_nat_gateway.this.id}
NoteDetail
ZonesNAT gateway is zonal: public IP in the same zone (or both without zone)
Capacityper public IP 64,512 SNAT ports – up to 16 IPs or a public IP prefix (/31–/28)
PriorityNAT gateway beats instance public IPs and load balancer outbound rules for outbound traffic
With firewalla UDR 0/0 to the firewall wins – put the NAT gateway on AzureFirewallSubnet instead (firewall outbound via NAT IPs)
Not supportedgateway subnet, Basic SKU public IPs / load balancers in the subnet
Check the outbound IPaz vm run-command invoke -g <rg> -n <vm> --command-id RunShellScript --scripts "curl -s ifconfig.me"

#Bastion

Connect to VMs over SSH or RDP through Bastion.

PowerShell
az network bastion list -o tableaz network bastion ssh -g <rg> -n <bastion> --target-resource-id <vm-id> --auth-type ssh-key --username <user> --ssh-key <path>az network bastion rdp -g <rg> -n <bastion> --target-resource-id <vm-id>az network bastion tunnel -g <rg> -n <bastion> --target-resource-id <vm-id> --resource-port 22 --port 50022

bastion ssh/rdp/tunnel requires Bastion Standard with Native client support. Form: Generator → Virtual Machines → Bastion.

#DDoS Protection

Network protection versus IP protection – what each covers.

DDoS Network ProtectionDDoS IP Protection
Whatplan, assigned to VNets – protects all public IPs in themper single public IP
Costsfixed monthly price per plan, covers 100 public IPs (more cost extra)monthly price per public IP
Extrasrapid response team, cost protection, WAF discount–
Whenmany public IPs / production landing zonesa few public IPs

One plan is enough for the whole tenant – create it once (e.g. connectivity subscription) and link VNets from other subscriptions. Form: web page → Generator → Network → DDoS protection.

az network ddos-protection create -g <rg> -n ddos-<org>-weu -l westeurope$plan = az network ddos-protection show -g <rg> -n ddos-<org>-weu --query id -o tsvaz network vnet update -g <rg-vnet> -n <vnet> --ddos-protection true --ddos-protection-plan $planaz network vnet list --query "[].{vnet:name, ddos:enableDdosProtection, plan:ddosProtectionPlan.id}" -o table# IP Protection instead of a plan:az network public-ip update -g <rg> -n <pip> --ddos-protection-mode Enabledaz network public-ip show -g <rg> -n <pip> --query ddosSettings.protectionMode -o tsv# Alert while under attackaz monitor metrics alert create -g <rg-monitoring> -n alert-ddos-<pip> --scopes <pip-id> `  --condition "max IfUnderDDoSAttack > 0" --window-size 5m --evaluation-frequency 1m --severity 1 --action <action-group-id>
$plan = New-AzDdosProtectionPlan -ResourceGroupName <rg> -Name ddos-<org>-weu -Location westeurope$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg-vnet> -Name <vnet>$vnet.DdosProtectionPlan = New-Object Microsoft.Azure.Commands.Network.Models.PSResourceId$vnet.DdosProtectionPlan.Id = $plan.Id$vnet.EnableDdosProtection = $true$vnet | Set-AzVirtualNetwork
resource ddos 'Microsoft.Network/ddosProtectionPlans@2024-05-01' = {  name: 'ddos-<org>-weu'  location: location}// in the VNet:  properties: { enableDdosProtection: true, ddosProtectionPlan: { id: ddos.id }, ... }// IP Protection on a public IP:  properties: { ddosSettings: { protectionMode: 'Enabled' }, ... }
resource "azurerm_network_ddos_protection_plan" "this" {  name                = "ddos-<org>-weu"  resource_group_name = "<rg>"  location            = "westeurope"}# in azurerm_virtual_network:#   ddos_protection_plan {#     id     = azurerm_network_ddos_protection_plan.this.id#     enable = true#   }# IP Protection: azurerm_public_ip { ddos_protection_mode = "Enabled" }

Logs: diagnostic setting on the public IPs (DDoSProtectionNotifications, DDoSMitigationFlowLogs, DDoSMitigationReports) → Log Analytics. Test only with an approved simulation partner – never attack yourself.

#VPN Gateway (Site-to-Site)

Check gateway, BGP peers and connections of a site-to-site VPN.

PowerShell
az network vnet-gateway list -g <rg> -o tableaz network vnet-gateway show -g <rg> -n <gw> --query "{sku:sku.name, bgp:bgpSettings, ips:ipConfigurations[].publicIPAddress.id}"az network vnet-gateway list-bgp-peer-status -g <rg> -n <gw> -o tableaz network vnet-gateway list-learned-routes  -g <rg> -n <gw> -o tableaz network vnet-gateway list-advertised-routes -g <rg> -n <gw> --peer <bgp-peer-ip> -o tableaz network vnet-gateway reset -g <rg> -n <gw>                       # restart the instance(s)az network local-gateway list -g <rg> -o tableaz network vpn-connection list -g <rg> --query "[].{name:name, status:connectionStatus, type:connectionType}" -o tableaz network vpn-connection show -g <rg> -n <conn> --query "{status:connectionStatus, in:ingressBytesTransferred, out:egressBytesTransferred}"az network vpn-connection shared-key show  -g <rg> --connection-name <conn>az network vpn-connection shared-key update -g <rg> --connection-name <conn> --value <psk>

#Azure Firewall

Firewall policy structure: rule collection group (priority) → rule collection (priority, action Allow/Deny/DNAT) → rules. Evaluation order: DNAT → network rules → application rules, each by priority (lower = first). With a form: web page → Generator → Azure Firewall.

PowerShell
az extension add -n azure-firewallaz network firewall list -o tableaz network firewall show -g <rg> -n <fw> --query "{ip:ipConfigurations[0].privateIPAddress, policy:firewallPolicy.id}"az network firewall policy list -o tableaz network firewall policy rule-collection-group list -g <rg> --policy-name <policy> --query "[].{name:name, prio:priority}" -o tableaz network firewall policy rule-collection-group show -g <rg> --policy-name <policy> -n <rcg> `  --query "ruleCollections[].{name:name, prio:priority, action:action.type, rules:length(rules)}" -o table # Add a rule to an existing collectionaz network firewall policy rule-collection-group collection rule add -g <rg> --policy-name <policy> --rcg-name <rcg> `  --collection-name <collection> --name <rule> --rule-type NetworkRule `  --source-addresses 10.1.0.0/24 --destination-addresses 10.2.0.0/24 --destination-ports 443 --ip-protocols TCP # New collection with a ruleaz network firewall policy rule-collection-group collection add-filter-collection -g <rg> --policy-name <policy> --rcg-name <rcg> `  --name <collection> --collection-priority 200 --action Allow --rule-name <rule> --rule-type ApplicationRule `  --source-addresses 10.1.0.0/24 --target-fqdns "*.microsoft.com" --protocols Https=443 # Remove a ruleaz network firewall policy rule-collection-group collection rule remove -g <rg> --policy-name <policy> --rcg-name <rcg> `  --collection-name <collection> --name <rule>

Extend an existing rule (PowerShell – get, change, write back):

PowerShell
$pol  = Get-AzFirewallPolicy -Name <policy> -ResourceGroupName <rg>$rcg  = Get-AzFirewallPolicyRuleCollectionGroup -Name <rcg> -AzureFirewallPolicyName <policy> -ResourceGroupName <rg>$rule = ($rcg.Properties.RuleCollection | Where-Object Name -eq "<collection>").Rules | Where-Object Name -eq "<rule>"$rule.DestinationAddresses.Add("10.3.0.0/24")Set-AzFirewallPolicyRuleCollectionGroup -Name <rcg> -Priority $rcg.Properties.Priority `  -RuleCollection $rcg.Properties.RuleCollection -FirewallPolicyObject $pol

Logs (resource-specific): AZFWNetworkRule, AZFWApplicationRule, AZFWNatRule – column Action = Deny shows what is blocked.

#Private DNS and Private Endpoints

Private DNS zones, VNet links and private endpoints.

PowerShell
az network private-dns zone list -o tableaz network private-dns zone create -g <rg> -n privatelink.azurewebsites.netaz network private-dns link vnet list -g <rg-dns> -z <zone> -o tableaz network private-dns link vnet create -g <rg-dns> -z <zone> -n <link-name> -v <vnet-id> -e false   # -e = auto-registrationaz network private-dns record-set a list -g <rg-dns> -z <zone> -o tableaz network private-dns record-set a add-record -g <rg-dns> -z <zone> -n <name> -a <ip>az network private-dns record-set a remove-record -g <rg-dns> -z <zone> -n <name> -a <ip> az network private-endpoint list -g <rg> -o tableaz network private-endpoint show -g <rg> -n <pe> --query "customDnsConfigs"az network private-endpoint create -g <rg> -n <pe> --vnet-name <vnet> --subnet <subnet> `  --private-connection-resource-id <target-resource-id> --group-id <sites|vault|blob|...> --connection-name <name>az network private-endpoint dns-zone-group create -g <rg> --endpoint-name <pe> -n default `  --private-dns-zone <zone-id> --zone-name <zone>az network private-endpoint-connection list --id <target-resource-id> -o tableaz network private-endpoint-connection approve --id <connection-id> --description "ok" # DNS Private Resolveraz dns-resolver list -o tableaz dns-resolver inbound-endpoint list -g <rg> --dns-resolver-name <resolver> -o tableaz dns-resolver forwarding-ruleset list -g <rg> -o table
ServiceZonegroup-id
App Service / Functionsprivatelink.azurewebsites.netsites
Key Vaultprivatelink.vaultcore.azure.netvault
Storage Blobprivatelink.blob.core.windows.netblob
Storage Fileprivatelink.file.core.windows.netfile
Storage Queue / Tableprivatelink.queue.core.windows.net / privatelink.table.core.windows.netqueue / table
PostgreSQL Flexibleprivatelink.postgres.database.azure.compostgresqlServer
SQLprivatelink.database.windows.netsqlServer
Cosmos DB (SQL)privatelink.documents.azure.comSql
Container Registryprivatelink.azurecr.ioregistry
Service Bus / Event Hubprivatelink.servicebus.windows.netnamespace
Azure Monitor (AMPLS)privatelink.monitor.azure.com and othersazuremonitor

Rule: the zone must be linked to the VNet whose DNS setting does the resolution (Azure-provided DNS → client VNet; custom DNS → VNet of the DNS server/resolver). Test: nslookup <name>.azurewebsites.net must return a private IP.

#Application Gateway and WAF

Show gateway state, backend health and the WAF policy.

PowerShell
az network application-gateway list -o tableaz network application-gateway show -g <rg> -n <agw> --query "{state:operationalState, sku:sku.name}"az network application-gateway show-backend-health -g <rg> -n <agw> `  --query "backendAddressPools[].backendHttpSettingsCollection[].servers[].{server:address, health:health, log:healthProbeLog}" -o tableaz network application-gateway stop  -g <rg> -n <agw>az network application-gateway start -g <rg> -n <agw>az network application-gateway http-listener list -g <rg> --gateway-name <agw> -o tableaz network application-gateway rule list -g <rg> --gateway-name <agw> -o tableaz network application-gateway address-pool list -g <rg> --gateway-name <agw> -o tableaz network application-gateway http-settings list -g <rg> --gateway-name <agw> -o tableaz network application-gateway probe list -g <rg> --gateway-name <agw> -o tableaz network application-gateway ssl-cert list -g <rg> --gateway-name <agw> -o tableaz network application-gateway ssl-cert create -g <rg> --gateway-name <agw> -n <name> `  --key-vault-secret-id https://<kv>.vault.azure.net/secrets/<name>      # certificate from Key Vaultaz network application-gateway identity show -g <rg> --gateway-name <agw> # WAF policiesaz network application-gateway waf-policy list -g <rg> -o tableaz network application-gateway waf-policy show -g <rg> -n <policy> `  --query "{mode:policySettings.mode, state:policySettings.state, sets:managedRules.managedRuleSets[].{t:ruleSetType,v:ruleSetVersion}}"az network application-gateway waf-policy policy-setting update -g <rg> --policy-name <policy> --mode Prevention --state Enabledaz network application-gateway waf-policy managed-rule rule-set list -g <rg> --policy-name <policy>az network application-gateway waf-policy managed-rule rule-set add -g <rg> --policy-name <policy> `  --type Microsoft_BotManagerRuleSet --version 1.1az network application-gateway waf-policy managed-rule exclusion list -g <rg> --policy-name <policy>az network application-gateway waf-policy custom-rule list -g <rg> --policy-name <policy> -o table

Rules of thumb for App Gateway v2:

  • Dedicated subnet, no 0.0.0.0/0 UDR to anything other than Internet.
  • NSG on the subnet: allow inbound GatewayManager TCP 65200-65535 and AzureLoadBalancer.
  • App Service as backend: FQDN, HTTPS 443, pick host name from backend, private DNS zone linked to the App Gateway VNet.
  • Certificates: Key Vault + user-assigned identity (Key Vault Secrets User), versionless secret ID.
  • WAF: Detection = log only, Prevention = block (403); a separate policy per listener is possible.

#Load Balancer and Public IPs

Inspect load balancers, rules, probes and public IPs.

PowerShell
az network lb list -o tableaz network lb show -g <rg> -n <lb> --query "{fe:frontendIPConfigurations[].privateIPAddress, sku:sku.name}"az network lb rule list -g <rg> --lb-name <lb> -o tableaz network lb probe list -g <rg> --lb-name <lb> -o tableaz network lb address-pool list -g <rg> --lb-name <lb> -o tableaz network lb outbound-rule list -g <rg> --lb-name <lb> -o tableaz network lb inbound-nat-rule list -g <rg> --lb-name <lb> -o table az network public-ip list -g <rg> --query "[].{name:name, ip:ipAddress, sku:sku.name, zones:zones, on:ipConfiguration.id}" -o tableaz network public-ip create -g <rg> -n <pip> --sku Standard --allocation-method Static --zone 1 2 3

Rules of thumb:

  • The health probe source is always 168.63.129.16.
  • Floating IP on: destination stays the frontend IP (for VIPs on an NVA). Off: destination is rewritten to the backend IP (for services on the NVA itself, e.g. IPsec).
  • HA ports (internal Standard LB): all protocols/ports in one rule – the basis for NVA high availability.
  • Outbound rules only translate TCP/UDP, no ICMP (ping to the internet does not work).

#Flow Logs

List flow logs and where to analyse them.

PowerShell
az network watcher flow-log list -l <region> -o tableaz network watcher flow-log show -l <region> -n <flowlog>

New flow logs are created at VNet level (NSG flow logs are being retired). Analysis: KQL.

Network and Firewall

#Azure Front Door

With a form: web page → Generator → Front Door · Counterpart in the VNet: App Gateway · Step by step: Application Gateway and WAF Workflows

Top 5: az afd custom-domain list · az afd route show · az afd origin list · az afd endpoint purge · az afd custom-domain show --query domainValidationState

#Structure

Profile, endpoint, route, origin group and origin.

text
Profile (Standard / Premium)└── Endpoint                 <name>-<hash>.z01.azurefd.net    └── Route                which domains + paths -> which origin group        ├── Custom domains   app.company.tld (+ certificate)        └── Origin group     health probe, load balancing            └── Origins      e.g. <app>.azurewebsites.net (priority, weight, optional Private Link)
TermMeaning
Originbackend, e.g. App Service, storage website, own server
Origin groupgroup of origins with a shared health probe
Routeconnects domains + paths to an origin group
Custom domainown host name, needs TXT validation + CNAME
Security policybinds a WAF policy to domains
Private LinkFront Door reaches the origin privately (Premium only)

#Get an Overview

List profiles, endpoints, origins and routes.

PowerShell
$rg = "<rg-afd>" ; $afd = "<afd-profile>" ; $ep = "<afd-endpoint>"az afd profile list --query "[].{name:name, rg:resourceGroup, sku:sku.name}" -o tableaz afd endpoint list -g $rg --profile-name $afd --query "[].{name:name, host:hostName}" -o tableaz afd custom-domain list -g $rg --profile-name $afd --query "[].{name:name, host:hostName, validation:domainValidationState, cert:tlsSettings.certificateType}" -o tableaz afd origin-group list -g $rg --profile-name $afd --query "[].name" -o tsvaz afd origin list -g $rg --profile-name $afd --origin-group-name <origin-group> -o tableaz afd route list -g $rg --profile-name $afd --endpoint-name $ep --query "[].{route:name, og:originGroup.id, domains:customDomains[].id, paths:patternsToMatch}" -o json

#Origin Group and Origin

Create the origin group with health probe and add an origin.

PowerShell
az afd origin-group create -g $rg --profile-name $afd --origin-group-name og-<app> `  --probe-request-type HEAD --probe-protocol Https --probe-interval-in-seconds 60 --probe-path "/" `  --sample-size 4 --successful-samples-required 3 --additional-latency-in-milliseconds 50 $appHost = az webapp show -g <rg-app> -n <app> --query defaultHostName -o tsvaz afd origin create -g $rg --profile-name $afd --origin-group-name og-<app> --origin-name o-<app> `  --host-name $appHost --origin-host-header $appHost --http-port 80 --https-port 443 `  --priority 1 --weight 1000 --enabled-state Enabled

Private Link (Premium) additionally: --enable-private-link true --private-link-resource <app-id> --private-link-location <region> --private-link-sub-resource-type sites, then approve the request on the app:

PowerShell
az network private-endpoint-connection list --id <app-id> --query "[?properties.privateLinkServiceConnectionState.status=='Pending'].id" -o tsvaz network private-endpoint-connection approve --id <connection-id> --description "Front Door"

#Custom Domain and Certificate

Add your domain with a managed certificate.

PowerShell
# Certificate managed by Front Dooraz afd custom-domain create -g $rg --profile-name $afd --custom-domain-name app-company-tld --host-name app.company.tld `  --minimum-tls-version TLS12 --certificate-type ManagedCertificate # Or your own certificate from Key Vault (versionless = automatic renewal)$kvId = az keyvault show -n <keyvault-name> --query id -o tsvaz afd secret create -g $rg --profile-name $afd --secret-name sec-<certificate> `  --secret-source "$kvId/secrets/<certificate>" --use-latest-version trueaz afd custom-domain create -g $rg --profile-name $afd --custom-domain-name app-company-tld --host-name app.company.tld `  --minimum-tls-version TLS12 --certificate-type CustomerCertificate --secret sec-<certificate> # DNS recordsaz afd custom-domain show -g $rg --profile-name $afd --custom-domain-name app-company-tld --query "validationProperties.validationToken" -o tsv#   TXT   _dnsauth.app.company.tld  ->  <token>#   CNAME app.company.tld           ->  <endpoint-host>.azurefd.netaz afd custom-domain show -g $rg --profile-name $afd --custom-domain-name app-company-tld --query "{validation:domainValidationState, status:deploymentStatus}"

The custom domain's name in Front Door may only contain letters, digits and hyphens (app-company-tld); the host name is the real one (app.company.tld).

#Create a Route or Assign to an Existing Route

Connect domain and origin group with a route.

PowerShell
# New routeaz afd route create -g $rg --profile-name $afd --endpoint-name $ep --route-name rt-<app> `  --origin-group og-<app> --custom-domains app-company-tld --patterns-to-match "/*" `  --supported-protocols Http Https --https-redirect Enabled --forwarding-protocol HttpsOnly --link-to-default-domain Disabled # Add a domain to an EXISTING route – pass the existing domains too, otherwise they are removed!$cdId = az afd custom-domain show -g $rg --profile-name $afd --custom-domain-name app-company-tld --query id -o tsv$ids  = @(az afd route show -g $rg --profile-name $afd --endpoint-name $ep --route-name <route> --query "customDomains[].id" -o tsv) + $cdIdaz afd route update -g $rg --profile-name $afd --endpoint-name $ep --route-name <route> --custom-domains $ids

Caution: az afd route update --custom-domains replaces the list. Always pass the existing IDs.

#Lock Down the App Service

Only Front Door may reach the app – via service tag and your own Front Door ID (otherwise any Front Door would get through):

PowerShell
$fdId = az afd profile show -g $rg --profile-name $afd --query frontDoorId -o tsvaz webapp config access-restriction add -g <rg-app> -n <app> --rule-name AllowFrontDoor --priority 100 `  --action Allow --service-tag AzureFrontDoor.Backend --http-header x-azure-fdid=$fdId

With Private Link (Premium) instead: disable public access on the app (az webapp update --set publicNetworkAccess=Disabled).

#WAF

WAF policy with managed rules and its link to the endpoint.

PowerShell
az network front-door waf-policy create -g $rg -n wafafd<app> --sku Premium_AzureFrontDoor --mode Detectionaz network front-door waf-policy managed-rules add -g $rg --policy-name wafafd<app> --type Microsoft_DefaultRuleSet --version 2.1 --action Block$wafId = az network front-door waf-policy show -g $rg -n wafafd<app> --query id -o tsvaz afd security-policy create -g $rg --profile-name $afd --security-policy-name sp-<app> --waf-policy $wafId --domains $cdId

Logs: diagnostic setting of the profile → Log Analytics, table AzureDiagnostics with Category == "FrontDoorWebApplicationFirewallLog".

#Cache and Troubleshooting

Purge the cache and test with and without the custom domain.

PowerShell
az afd endpoint purge -g $rg --profile-name $afd --endpoint-name $ep --content-paths "/*"curl.exe -sI https://app.company.tld                      # X-Azure-Ref, X-Cache in the responsecurl.exe -sI https://<endpoint-host>.azurefd.net          # test without the custom domain
SymptomCauseFix
Domain stays PendingTXT _dnsauth missing/wrong, token expiredcheck TXT, az afd custom-domain regenerate-validation-token
404 "Page not found" from Front Doordomain not assigned to a route / path doesn't matchcheck route → domains and patternsToMatch
502/503/504origin unreachable, probe fails, wrong host headerorigin host + origin-host-header, probe path, app access restriction
403 from the appaccess restriction without x-azure-fdid or wrong IDrule as above, your own Front Door ID
Private Link origin doesn't workrequest on the app not approvedprivate-endpoint-connection approve
Change has no effectdeployment running (up to ~10 min) / cachecheck deploymentStatus, endpoint purge
Network and Firewall

#Azure Virtual WAN

Classic hub-and-spoke: Azure Network · ExpressRoute: ExpressRoute

Virtual WAN = Microsoft-managed hubs: routing between VNets, branches (VPN/SD-WAN), ExpressRoute and users (P2S) without your own UDRs. Code blocks with tabs: choose CLI / PowerShell / Bicep / Terraform – the choice is remembered.

Top 5: az network vhub list -o table · az network vhub connection list · az network vhub get-effective-routes · az network vhub routing-intent show · az network vpn-gateway connection list

#When Virtual WAN, When Classic Hub?

Comparison to choose between both designs.

Virtual WAN (Standard)Classic hub VNet
Routing between spokes, branches, ERautomatic (hub router), transitiveUDRs + NVA/firewall you maintain
Many branches / regionsstrong (any-to-any, hub-to-hub)lots of manual work
Own NVA (e.g. FortiGate)as NVA in the hub (supported partners) or in a spokefull control
FirewallAzure Firewall in the hub (secured hub) + routing intentAzure Firewall / NVA in the hub VNet
Control and troubleshootingless visibility, fixed address space per hubeverything visible and adjustable
Costshub hourly + data processingVNet free, pay gateways/firewall

Hub address space: at least /23 (recommended /23–/22), must not overlap with any VNet or branch.

#Create WAN and Hub

Create the WAN and the virtual hub.

az network vwan create -g <rg> -n vwan-<org>-prd -l westeurope --type Standardaz network vhub create -g <rg> -n vhub-<org>-prd-weu --vwan vwan-<org>-prd `  --address-prefix 10.100.0.0/23 -l westeurope --sku Standardaz network vhub show -g <rg> -n vhub-<org>-prd-weu --query "{state:provisioningState, router:routingState}"   # routingState: Provisioned
$wan = New-AzVirtualWan -ResourceGroupName <rg> -Name vwan-<org>-prd -Location westeurope -VirtualWANType Standard$hub = New-AzVirtualHub -ResourceGroupName <rg> -Name vhub-<org>-prd-weu -Location westeurope `         -VirtualWan $wan -AddressPrefix 10.100.0.0/23Get-AzVirtualHub -ResourceGroupName <rg> -Name vhub-<org>-prd-weu | Select-Object Name, ProvisioningState, RoutingState
resource vwan 'Microsoft.Network/virtualWans@2024-05-01' = {  name: 'vwan-<org>-prd'  location: location  properties: { type: 'Standard', allowBranchToBranchTraffic: true }}resource hub 'Microsoft.Network/virtualHubs@2024-05-01' = {  name: 'vhub-<org>-prd-weu'  location: location  properties: {    addressPrefix: '10.100.0.0/23'    virtualWan: { id: vwan.id }  }}
resource "azurerm_virtual_wan" "this" {  name                = "vwan-<org>-prd"  resource_group_name = "<rg>"  location            = "westeurope"  type                = "Standard"}resource "azurerm_virtual_hub" "weu" {  name                = "vhub-<org>-prd-weu"  resource_group_name = "<rg>"  location            = "westeurope"  virtual_wan_id      = azurerm_virtual_wan.this.id  address_prefix      = "10.100.0.0/23"}

Creating a hub takes about 30 minutes – the hub router is only ready when routingState = Provisioned.

#Connect a Spoke VNet

Connect a VNet to the hub.

az network vhub connection create -g <rg> --vhub-name vhub-<org>-prd-weu -n conn-<spoke> `  --remote-vnet /subscriptions/<sub-id>/resourceGroups/<rg-spoke>/providers/Microsoft.Network/virtualNetworks/<vnet-spoke> `  --internet-security trueaz network vhub connection list -g <rg> --vhub-name vhub-<org>-prd-weu --query "[].{name:name, state:provisioningState}" -o table
$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg-spoke> -Name <vnet-spoke>New-AzVirtualHubVnetConnection -ResourceGroupName <rg> -ParentResourceName vhub-<org>-prd-weu `  -Name conn-<spoke> -RemoteVirtualNetwork $vnet -EnableInternetSecurityGet-AzVirtualHubVnetConnection -ResourceGroupName <rg> -VirtualHubName vhub-<org>-prd-weu | Select-Object Name, ProvisioningState
resource conn 'Microsoft.Network/virtualHubs/hubVirtualNetworkConnections@2024-05-01' = {  parent: hub  name: 'conn-<spoke>'  properties: {    remoteVirtualNetwork: { id: spokeVnetId }    enableInternetSecurity: true  }}
resource "azurerm_virtual_hub_connection" "spoke" {  name                      = "conn-<spoke>"  virtual_hub_id            = azurerm_virtual_hub.weu.id  remote_virtual_network_id = "<vnet-spoke-id>"  internet_security_enabled = true}

Spoke VNets must not have their own VPN/ER gateway, and no UDR 0.0.0.0/0 to an NVA when routing intent is used – the hub programs the routes itself.

#Secured Hub: Azure Firewall and Routing Intent

Routing intent sends internet and/or private traffic (spoke ↔ spoke, spoke ↔ branch) through the firewall in the hub – without route tables.

az network firewall create -g <rg> -n afw-vhub-<org>-weu --sku AZFW_Hub --tier Standard `  --vhub vhub-<org>-prd-weu --public-ip-count 1 --firewall-policy <fw-policy-id>az network vhub routing-intent list -g <rg> --vhub vhub-<org>-prd-weu -o table# Routing intent itself: easiest in the portal (hub -> Routing intent and routing policies) or with PowerShell
$hub = Get-AzVirtualHub -ResourceGroupName <rg> -Name vhub-<org>-prd-weu$ip  = New-AzFirewallHubIpAddress -PublicIP (New-AzFirewallHubPublicIpAddress -Count 1)$fw  = New-AzFirewall -Name afw-vhub-<org>-weu -ResourceGroupName <rg> -Location westeurope -Sku AZFW_Hub `         -VirtualHubId $hub.Id -HubIPAddress $ip -FirewallPolicyId <fw-policy-id>$p1 = New-AzRoutingPolicy -Name PrivateTraffic -Destination @("PrivateTraffic") -NextHop $fw.Id$p2 = New-AzRoutingPolicy -Name PublicTraffic  -Destination @("Internet")       -NextHop $fw.IdNew-AzRoutingIntent -ResourceGroupName <rg> -VirtualHubName vhub-<org>-prd-weu -Name ri-default -RoutingPolicy @($p1, $p2)

Set-AzRoutingIntent replaces all policies – always pass the complete list. Firewall rules for spoke ↔ spoke: Generator → Azure Firewall.

#Site-to-Site VPN in the Hub

VPN gateway in the hub and a site connection.

PowerShell
az network vpn-gateway create -g <rg> -n vpng-<org>-weu --vhub vhub-<org>-prd-weu --scale-unit 1 -l westeuropeaz network vpn-site create -g <rg> -n vst-<site> --virtual-wan vwan-<org>-prd -l westeurope `  --ip-address <public-ip-site> --address-prefixes 192.168.10.0/24 --device-vendor Fortinet --link-speed 100az network vpn-gateway connection create -g <rg> --gateway-name vpng-<org>-weu -n vcn-<site> `  --remote-vpn-site vst-<site> --shared-key "<psk>" --protocol-type IKEv2 --enable-bgp falseaz network vpn-gateway show -g <rg> -n vpng-<org>-weu --query "bgpSettings.bgpPeeringAddresses[].tunnelIpAddresses" -o tsv   # public IPs for the FortiGate

On the FortiGate: two tunnels (both gateway instances), phase 1/2 like the Azure defaults – see FortiGate → IPsec.

#Troubleshooting

Effective routes and route tables of the hub.

PowerShell
az network vhub get-effective-routes -g <rg> -n vhub-<org>-prd-weu `  --resource-type HubVirtualNetworkConnection --resource-id <connection-id> -o table      # routes as the spoke sees themaz network vhub route-table list -g <rg> --vhub-name vhub-<org>-prd-weu -o tableaz network vpn-gateway connection show -g <rg> --gateway-name vpng-<org>-weu -n vcn-<site> --query "connectionStatus"az network nic show-effective-route-table -g <rg-spoke> -n <nic> -o table                 # in the spoke: next hop must be the hub
SymptomCauseFix
Spoke ↔ spoke does not workrouting intent missing / firewall blocksprivate traffic policy, firewall network rule
Spoke has no internetinternet-security off on the connection or no Internet policyconnection setting, routing intent Internet
Hub creation hangs at Updatinghub router still provisioningwait (up to 30 min), check routingState
Overlap error on connectspoke address space overlaps hub /23 or other spokeschange address space
Branch routes missingBGP off / wrong prefixes in the VPN siteVPN site address prefixes, BGP settings
Network and Firewall

#ExpressRoute

Virtual WAN: Virtual WAN · VPN as backup: → VPN Gateway

Top 5: az network express-route show · az network express-route list-route-tables · az network express-route list-arp-tables · az network express-route get-stats · az network vpn-connection show

#Terms

Circuit, peering, gateway and connection explained.

TermMeaning
Circuitthe logical connection to Microsoft, ordered via a provider (bandwidth, peering location)
Service keyID of the circuit – you give it to the provider
Private peeringyour VNets (via ExpressRoute gateway)
Microsoft peeringMicrosoft 365 / public Azure services (route filter required)
Primary / secondarytwo links for redundancy – always connect both
ER gatewayVNet gateway of type ExpressRoute in the GatewaySubnet
Global Reachconnects two circuits (site ↔ site via Microsoft backbone)
FastPathdata path bypasses the gateway (Ultra/ErGw3AZ, certain scenarios)
SKULocal (only local region, no egress costs), Standard (same geopolitical area), Premium (global)

#Create a Circuit

Choose the provider and create the circuit.

PowerShell
az network express-route list-service-providers --query "[].{name:name, locations:join(', ', peeringLocations)}" -o tableaz network express-route create -g <rg> -n erc-<site>-weu -l westeurope `  --provider "<provider>" --peering-location "<peering-location>" --bandwidth 1000 `  --sku-tier Standard --sku-family MeteredDataaz network express-route show -g <rg> -n erc-<site>-weu --query "{key:serviceKey, provider:serviceProviderProvisioningState, state:circuitProvisioningState}"

Give the service key to the provider. Continue only when serviceProviderProvisioningState = Provisioned.

#Private Peering

Only needed if you manage layer 3 yourself (with many providers they configure it):

PowerShell
az network express-route peering create -g <rg> --circuit-name erc-<site>-weu --peering-type AzurePrivatePeering `  --peer-asn 65010 --primary-peer-subnet 172.16.0.0/30 --secondary-peer-subnet 172.16.0.4/30 --vlan-id 100az network express-route peering show -g <rg> --circuit-name erc-<site>-weu -n AzurePrivatePeering --query "{state:state, asn:peerAsn}"

#Gateway and Connection

Gateway in the hub and the connection to the circuit.

az network public-ip create -g <rg> -n pip-ergw-<hub> --sku Standard --zone 1 2 3az network vnet-gateway create -g <rg> -n ergw-<hub> --vnet <vnet-hub> --gateway-type ExpressRoute `  --sku ErGw1AZ --public-ip-address pip-ergw-<hub> --no-wait          # takes 30-45 min$ercId = az network express-route show -g <rg> -n erc-<site>-weu --query id -o tsvaz network vpn-connection create -g <rg> -n con-ergw-<hub>-<site> --vnet-gateway1 ergw-<hub> --express-route-circuit2 $ercId
$vnet = Get-AzVirtualNetwork -ResourceGroupName <rg> -Name <vnet-hub>$sub  = Get-AzVirtualNetworkSubnetConfig -VirtualNetwork $vnet -Name GatewaySubnet$pip  = New-AzPublicIpAddress -ResourceGroupName <rg> -Name pip-ergw-<hub> -Location westeurope -Sku Standard -AllocationMethod Static -Zone 1,2,3$cfg  = New-AzVirtualNetworkGatewayIpConfig -Name ipconfig1 -SubnetId $sub.Id -PublicIpAddressId $pip.Id$gw   = New-AzVirtualNetworkGateway -ResourceGroupName <rg> -Name ergw-<hub> -Location westeurope `          -IpConfigurations $cfg -GatewayType ExpressRoute -GatewaySku ErGw1AZ$erc  = Get-AzExpressRouteCircuit -ResourceGroupName <rg> -Name erc-<site>-weuNew-AzVirtualNetworkGatewayConnection -ResourceGroupName <rg> -Name con-ergw-<hub>-<site> -Location westeurope `  -VirtualNetworkGateway1 $gw -PeerId $erc.Id -ConnectionType ExpressRoute
resource "azurerm_virtual_network_gateway" "er" {  name                = "ergw-<hub>"  location            = "westeurope"  resource_group_name = "<rg>"  type                = "ExpressRoute"  sku                 = "ErGw1AZ"  ip_configuration {    public_ip_address_id = azurerm_public_ip.ergw.id    subnet_id            = "<gateway-subnet-id>"  }}resource "azurerm_virtual_network_gateway_connection" "er" {  name                       = "con-ergw-<hub>-<site>"  location                   = "westeurope"  resource_group_name        = "<rg>"  type                       = "ExpressRoute"  virtual_network_gateway_id = azurerm_virtual_network_gateway.er.id  express_route_circuit_id   = "<circuit-id>"}

GatewaySubnet at least /27. With an NVA in the hub: UDR on the GatewaySubnet spoke prefixes → NVA (see Network Basics).

#Check Status and Routes

Circuit status, ARP and route tables.

PowerShell
az network express-route show -g <rg> -n erc-<site>-weu --query "{circuit:circuitProvisioningState, provider:serviceProviderProvisioningState, bw:serviceProviderProperties.bandwidthInMbps}"az network express-route list-arp-tables   -g <rg> -n erc-<site>-weu --peering-name AzurePrivatePeering --path primary -o tableaz network express-route list-route-tables -g <rg> -n erc-<site>-weu --peering-name AzurePrivatePeering --path primary -o tableaz network express-route list-route-tables -g <rg> -n erc-<site>-weu --peering-name AzurePrivatePeering --path secondary -o tableaz network express-route get-stats -g <rg> -n erc-<site>-weu -o table                       # bytes in/outaz network vnet-gateway list-learned-routes -g <rg> -n ergw-<hub> -o table                    # what the gateway learnedaz network vpn-connection show -g <rg> -n con-ergw-<hub>-<site> --query "{status:connectionStatus, ingress:ingressBytesTransferred}"
SymptomCauseFix
ARP table emptylayer 2 / VLAN at the provider not readyopen a ticket with the provider
ARP ok, no routesBGP down (ASN, MD5, subnets)check BGP config on the router/provider
Routes there, no trafficNSG/UDR/NVA in Azure, asymmetric routingeffective routes, GatewaySubnet UDR
Only one path workssecondary link not configuredalways configure primary and secondary
On-prem routes missing in the spokespoke peering without Use remote gatewayspeering settings
More than 1000 prefixes from on-prem (Standard)prefix limit of the private peeringsummarize or Premium (up to 10,000)

Metric alerts: BgpAvailability, ArpAvailability on the circuit – see Monitoring.

Network and Firewall

#Azure Network Basics

Commands: Azure Network · Step by step: Network Workflows

#Rules of Thumb

Reserved addresses, subnet sizes and planning rules.

TopicRule
Reserved IPs5 per subnet: network, .1 gateway, .2/.3 Azure DNS, broadcast → first usable IP is .4
Peeringnot transitive; create both sides; Allow forwarded traffic for NVA traffic
UDR prioritylongest prefix wins; on a tie: UDR > BGP > system
Hub-and-spoke with NVAspokes: 0.0.0.0/0 → NVA/ILB, BGP propagation off; NVA subnets without 0/0 UDR
Spoke ↔ spokeonly goes via the NVA if both spokes have the UDR and the policy allows it
Asymmetryoutbound and return path must use the same firewall – otherwise drop; fix: UDR on both sides or SNAT
IP forwardingthe NVA's NIC needs IP forwarding = on (Azure) and forwarding in the OS
GatewaySubnet UDRonly needed with an Azure VPN/ER gateway: spoke prefixes → NVA, no 0/0
Default outbound accessoff = VMs without PIP/LB outbound/NAT GW cannot reach the internet (intended with an NVA)
Outbound path (priority)NAT Gateway > public IP on the NIC > LB outbound rule > default outbound
Health probessource 168.63.129.16, must be allowed and routed on the NVA
Azure DNS168.63.129.16 – only reachable from Azure, not "passed through" via VPN/peering
Floating IPon = destination stays the frontend IP (VIPs); off = destination = backend IP (services on the NVA, IPsec)
ICMP via LB outboundnot translated – ping to the internet fails, test with curl
Private endpoint + DNSlink the zone to the VNet that does the DNS resolution
Private endpoint + UDRreturn traffic from PEs uses UDRs only with network policies on the subnet = on
Zoneslogical zones differ per subscription – check az vm list-skus
NSG on App Gatewayallow GatewayManager 65200-65535 + AzureLoadBalancer
Bastion NSGfixed mandatory rules (443 Internet/GatewayManager, 8080/5701 VNet …)
Special subnetsfixed names: GatewaySubnet, AzureFirewallSubnet, AzureBastionSubnet, RouteServerSubnet

#Subnet Sizes

Any network at a glance (range, usable IPs in Azure, mask, split) and planning a whole address space: web page → button IP Planner at the top (key s): look up a network, lay out the hub VNet, plan a spoke with its subnets and generate the commands, check overlaps with on-premises.

CIDRAddressesusable in Azuretypical for
/2983– (minimum)
/281611small NVA interfaces
/273227GatewaySubnet (recommended), mgmt
/266459AzureFirewallSubnet, AzureBastionSubnet
/25128123
/24256251App Gateway v2, workloads
/23512507
/2210241019whole spoke VNet

#Common Ports

The ports you meet most often and what they are for.

PortService
22 / 3389SSH / RDP
53DNS (UDP+TCP)
80 / 443HTTP / HTTPS
500 / 4500 UDPIPsec IKE / NAT-T
179BGP
123 UDPNTP
1433 / 5432 / 3306SQL Server / PostgreSQL / MySQL
445SMB (Azure Files)
8008default FortiGate probe port in Azure templates (HTTP)
541 / 514FortiManager / Syslog
65200–65535App Gateway v2 infrastructure (GatewayManager)

#Order of Checks When "It Doesn't Work"

1. DNS – does the name resolve correctly (private/public)? nslookup / Resolve-DnsName 2. Route – where does the packet go? effective routes, show-next-hop 3. NSG – allowed? list-effective-nsg, test-ip-flow 4. Firewall – does it arrive? sniffer; why dropped? debug flow 5. Return path – back through the same firewall? (asymmetry, SNAT) 6. Target – is the service listening? ss -tlnp / Get-NetTCPConnection, OS firewall

Network and Firewall

#FortiGate CLI

Step by step: FortiGate Workflows

Access: ssh <admin>@<mgmt-ip> · GUI https://<mgmt-ip> · in Azure also the serial console. Every config block ends with end – only then is it saved and synchronized in HA. abort discards.

Top 5: get system status · get router info routing-table all · diagnose sniffer packet any '<filter>' 4 20 · debug flow · get system ha status

#Using the CLI

Help, show, edit and the basic ways to move around the CLI.

FortiOS / Text
?                                      # help / possible input (Tab = complete)show                                   # configuration of the current section (changed values only)show full-configuration                # incl. defaultsget                                    # current values in the sectionshow | grep <text>show full-configuration | grep -f <text>         # matches with the enclosing config blockshow firewall policy | grep -i -A3 <name>config vdom ; edit root                          # with VDOMs enabledconfig global                                    # global settings with VDOMsabort                                            # leave a config block without savingdiagnose debug config-error-log read             # errors during apply/import

#System and License

Version, serial number, license and performance.

FortiOS / Text
get system statusget system status | grep -i -E "version|serial|license"get system performance statusdiagnose sys top 5 20                                      # processes (q = quit)get hardware statusexecute vm-license <FortiFlex-token>                       # license via tokenexecute restore vmlicense tftp <file.lic> <server-ip>      # license file via TFTP (otherwise GUI upload)diagnose debug vm-print-license                            # VM license detailsdiagnose autoupdate versions | grep -A3 "AV Engine"execute update-now                                         # trigger FortiGuard updatediagnose debug crashlog readexecute disk listdiagnose sys logdisk statusexecute time ; execute dateexecute rebootexecute shutdownexecute factoryreset                                       # careful!

#Interfaces, Admin, DNS, NTP

Interfaces, ARP, administrators, DNS and time settings.

FortiOS / Text
get system interface physicalshow system interface port1get system arpdiagnose hardware deviceinfo nic port1                     # link, error countersdiagnose netlink interface list port1config system interface    edit "port2"        set allowaccess ping https ssh probe-response    nextendshow system adminconfig system admin    edit "<admin>"        set trusthost1 <network> <mask>                    # restrict admin access    nextendconfig system global    set hostname "<name>"    set timezone "Europe/Vienna"    set admintimeout 30    set admin-sport 443endconfig system dns    set primary 168.63.129.16                              # Azure DNS (or your own resolver)endconfig system ntp    set ntpsync enable    set type fortiguardend

#HA (FGCP)

Cluster status, checksums, failover and sync.

FortiOS / Text
get system ha statusdiagnose sys ha checksum cluster              # configuration in sync? (identical checksums)diagnose sys ha checksum show                 # checksums per area (find the difference)diagnose sys ha checksum recalculatediagnose sys ha history readshow full-configuration system haexecute ha synchronize start                  # trigger sync (on the secondary)execute ha manage <index> <admin>             # switch from primary to secondaryexecute ha failover set 1                     # test failover (temporary)execute ha failover unset 1                   # remove the flagdiagnose sys ha reset-uptime                  # override disable: give up the primary roleconfig system ha    set session-pickup enable    set override disableend

override disable: primary = longest HA uptime (5 min tolerance), then priority. No automatic failback.

#Azure SDN Connector

Connector status and Azure daemon debug.

FortiOS / Text
diagnose sys sdn status                       # connector statusshow system sdn-connectordiagnose debug application azd -1             # debug the Azure daemondiagnose debug enable(wait / trigger failover)diagnose debug disable ; diagnose debug reset

With HA and SDN failover the FortiGate needs a managed identity with permissions on the affected public IPs/route tables.

#Routing

Routing table, route for one IP and static routes.

FortiOS / Text
get router info routing-table allget router info routing-table details <ip>    # which route applies to an IP?get router info routing-table database        # inactive routes tooget router info kernel                        # kernel routing (FIB)show router staticconfig router static    edit 0        set dst <network> <mask>        set gateway <gateway>        set device "port2"    nextend

Azure: the gateway of a subnet is always .1 of the subnet. Probe route 168.63.129.16/32 via the interface where the probe arrives.

#BGP

Summary, neighbours and advertised or received routes.

FortiOS / Text
get router info bgp summaryget router info bgp neighborsget router info bgp neighbors <ip> advertised-routesget router info bgp neighbors <ip> received-routes      # needs soft-reconfiguration enableget router info bgp networkexecute router clear bgp ip <ip> soft                   # re-exchange routesshow router bgp

#Addresses, Services, Policies

Address objects, services and firewall policies.

FortiOS / Text
config firewall address    edit "<name>"        set subnet <network> <mask>    next    edit "<fqdn-name>"        set type fqdn        set fqdn "<host.domain>"    nextendconfig firewall addrgrp    edit "<group>"        set member "<addr1>" "<addr2>"        append member "<addr3>"               # add instead of replace    nextendconfig firewall service custom    edit "TCP-8443"        set tcp-portrange 8443    nextendconfig firewall policy    edit 0        set name "<name>"        set srcintf "port2"        set dstintf "port1"        set srcaddr "<source>"        set dstaddr "all"        set action accept        set schedule "always"        set service "HTTPS" "DNS"        set nat enable        set logtraffic all        set utm-status enable        set ssl-ssh-profile "certificate-inspection"        set av-profile "default"        set webfilter-profile "default"        set ips-sensor "default"        set application-list "default"    nextendconfig firewall policy    move <id> before <id2>                    # change order    edit <id>        set status disable                    # policy off (enable = on)    next    delete <id>endshow firewall policyshow firewall policy <id>diagnose firewall iprope list 100004          # loaded policies (internal)diagnose firewall fqdn list                   # resolved FQDN addresses

Rule: policies are evaluated top to bottom, the first match wins.

#VIP (Port Forwarding)

Virtual IPs for inbound port forwarding.

FortiOS / Text
config firewall vip    edit "vip-<name>"        set extip <external-ip>        set extintf "port1"        set portforward enable        set mappedip "<internal-ip>"        set extport 443        set mappedport 443    nextendshow firewall vip

Use the VIP as dstaddr in the policy. Deleting: policy first, then VIP. In Azure extip is the private IP of port1 (the public IP sits on the LB/NIC), or the frontend IP with floating IP.

#IPsec VPN

Tunnel overview, SAs, IKE status and debug.

FortiOS / Text
get vpn ipsec tunnel summary                          # overview up/downdiagnose vpn tunnel list name <phase1>                # SAs, counters, selectorsdiagnose vpn ike gateway list name <phase1>           # phase 1 statusdiagnose vpn tunnel up <phase2> <phase1>              # bring the tunnel updiagnose vpn ike gateway clear name <phase1>          # renegotiate phase 1show vpn ipsec phase1-interfaceshow vpn ipsec phase2-interface # IKE debug (filter syntax differs slightly between FortiOS versions)diagnose vpn ike log filter cleardiagnose vpn ike log filter rem-addr4 <peer-ip>diagnose debug application ike -1diagnose debug enable(bring the tunnel up)diagnose debug disable ; diagnose debug reset
MessageCause
no SA proposal chosenencryption/hash/DH group do not match
peer SA proposal not match local policyphase 2 selectors (networks) differ
authentication failed / probable pre-shared secret mismatchwrong PSK
negotiation timeoutpeer does not answer – UDP 500/4500 blocked or wrong IP

#Troubleshooting: Sniffer, Debug Flow, Sessions

Capture packets, trace a flow and read sessions.

FortiOS / Text
# Sniffer: filter like tcpdump, verbosity 4 = with interface, packet count (Ctrl+C stops)diagnose sniffer packet any 'host <ip> and port 443' 4 20diagnose sniffer packet any 'net 10.0.0.0/24' 4 50diagnose sniffer packet port2 'icmp' 4 10diagnose sniffer packet any 'host 168.63.129.16 and port 8008' 4 20    # Azure LB probesdiagnose sniffer packet any 'host <ip>' 4 0 l                          # 0 = endless, l = with timestamp # Debug flow: why is a packet dropped/allowed?diagnose debug resetdiagnose debug flow filter cleardiagnose debug flow filter addr <ip>diagnose debug flow filter dport <port>diagnose debug flow show function-name enablediagnose debug console timestamp enablediagnose debug flow trace start 10diagnose debug enable(generate traffic)diagnose debug disablediagnose debug reset # Sessionsdiagnose sys session filter cleardiagnose sys session filter dst <ip>diagnose sys session filter dport 443diagnose sys session listdiagnose sys session clear                     # delete filtered sessionsdiagnose sys session stat                      # number of sessions # Reachabilityexecute ping <ip>execute ping-options source <own-ip>execute ping-options resetexecute traceroute <ip>execute telnet <ip> <port>execute ping <fqdn>                           # tests name resolution toodiagnose test application dnsproxy 7          # DNS cachediagnose test application dnsproxy 1          # clear DNS cache

#Debug Flow Messages

What the messages of the debug flow mean.

MessageMeaning
Denied by forward policy check (policy 0)no matching policy (implicit deny)
reverse path check fail, dropno route back to the source (RPF)
find a route: flag=... gw-<ip> via <port>selected route – wrong port = route missing
Allowed by Policy-<id>: SNATallowed, source NAT active
Allowed by Policy-<id>: (without SNAT)allowed, without NAT
iprope_in_check() check failed, dropaccess to the FortiGate itself not allowed (allowaccess/local-in)
no session matchedreturn packet without session (asymmetry)
DNAT <ip>:<port> -> <ip>:<port>VIP applies

#Admin Profiles and Access

Read-only and limited administrator profiles.

FortiOS / Text
config system accprofile    edit "readonly-network"        set netgrp read        set fwgrp read        set loggrp read        set sysgrp read        set vpngrp read    nextendconfig system admin    edit "<user>"        set accprofile "readonly-network"        set password <password>        set trusthost1 <mgmt-network> <mask>        set two-factor fortitoken               # optional, if FortiToken is available    nextendget system admin list                           # who is signed in right nowexecute disconnect-admin-session <index>show system accprofile

Built-in profiles: super_admin (everything), prof_admin (everything except system) – create your own profiles for read access.

#SD-WAN

Health checks, members and rules.

FortiOS / Text
diagnose sys sdwan health-check                 # quality per link (latency, jitter, loss)diagnose sys sdwan member                       # members and statusdiagnose sys sdwan service                      # rules and selected link (older versions: service4)get router info routing-table all               # SD-WAN routesshow system sdwanconfig system sdwan    config health-check        edit "internet"            set server "8.8.8.8" "1.1.1.1"            set protocol ping            set members 0                       # 0 = all members        next    endend

#SSL VPN and Dial-up VPN

Signed-in users and disconnecting sessions.

FortiOS / Text
get vpn ssl monitor                             # signed-in SSL VPN usersexecute vpn sslvpn listexecute vpn sslvpn del-tunnel <index>           # disconnect a sessiondiagnose debug application sslvpn -1diagnose debug enable(try to sign in)diagnose debug disable ; diagnose debug resetdiagnose vpn ike gateway list                   # IPsec dial-up clients (FortiClient)

Note: in newer FortiOS versions the SSL VPN tunnel mode has been removed on many models – the replacement is IPsec dial-up (IKEv2) with FortiClient or ZTNA. Check the release notes before upgrading.

#Firmware Upgrade (HA)

Upgrade a cluster without interruption – checks before and after.

FortiOS / Text
get system status                               # current versiondiagnose sys ha checksum cluster                # in sync before?show full-configuration system ha | grep -i uninterruptibleexecute backup full-config tftp <file> <server-ip>execute restore image tftp <image.out> <server-ip>    # or GUI: System -> Firmware -> Upload# afterwardsget system statusget system ha statusdiagnose sys ha history read

Sequence in HA with uninterruptible-upgrade enable: the secondary is upgraded first, then failover, then the old primary. Check the upgrade path beforehand with the FortiOS Upgrade Path Tool (docs.fortinet.com).

#FortiManager Connection

Connect the device to FortiManager.

FortiOS / Text
config system central-management    set type fortimanager    set fmg "<fortimanager-ip-or-fqdn>"endget system central-managementdiagnose debug application fgfmd -1             # debug the connection to FortiManagerdiagnose debug enablediagnose debug disable ; diagnose debug resetexecute ping <fortimanager-ip>execute telnet <fortimanager-ip> 541            # port 541 (FGFM) must be open

Then in FortiManager: Device Manager → authorize the device. From then on make changes only via FortiManager, otherwise the next install will conflict.

#Logs

Filter and read logs on the device.

FortiOS / Text
execute log filter resetexecute log filter category 0                 # 0 = trafficexecute log filter field dstip <ip>execute log filter view-lines 50execute log display

#Backup

Back up the configuration via CLI or GUI.

FortiOS / Text
execute backup config tftp <file> <server-ip>execute backup full-config ftp <file> <server-ip> <user> <pass>

Easier: GUI → admin menu (top right) → Configuration → Backup.

Code and DevOps

#Terraform and Bicep Workflows

Single commands: Terraform · Bicep / ARM · Bicep in a Team

Ready-made command sequences. Open a workflow, adjust the variables at the top, run block by block.

#New Terraform project with remote state

Changes things3 steps

Storage account for the state, role assignment, backend config and first init.

Storage account for the state – with versioning and your own data role:

PowerShell
$rg = "rg-tfstate" ; $loc = "<region>" ; $sa = "<sttfstateunique>"     # 3–24 chars, lowercase letters/digits onlyaz group create -n $rg -l $locaz storage account create -g $rg -n $sa -l $loc --sku Standard_ZRS --min-tls-version TLS1_2 --allow-blob-public-access falseaz storage account blob-service-properties update -g $rg --account-name $sa --enable-versioning true$saId = az storage account show -g $rg -n $sa --query id -o tsv$me   = az ad signed-in-user show --query id -o tsvaz role assignment create --assignee-object-id $me --assignee-principal-type User --role "Storage Blob Data Contributor" --scope $saIdaz storage container create --account-name $sa -n tfstate --auth-mode login     # role may take 1–2 minutes

Backend configuration in backend.tf:

HCL
terraform {  backend "azurerm" {    resource_group_name  = "rg-tfstate"    storage_account_name = "<sttfstateunique>"    container_name       = "tfstate"    key                  = "<project>.tfstate"    use_azuread_auth     = true  }}

Initialise and plan

$env:ARM_SUBSCRIPTION_ID = az account show --query id -o tsvterraform initterraform plan -out tfplan
export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv)terraform initterraform plan -out tfplan

#Terraform locally with a service principal

Changes things2 steps

Sign in as a service principal through environment variables and clean up afterwards.

Set the variables and run Terraform

$env:ARM_TENANT_ID       = "<tenant-id>"$env:ARM_SUBSCRIPTION_ID = "<subscription-id>"$env:ARM_CLIENT_ID       = "<app-id>"$env:ARM_CLIENT_SECRET   = "<client-secret>"terraform initterraform plan -out tfplanterraform apply tfplan
export ARM_TENANT_ID="<tenant-id>"export ARM_SUBSCRIPTION_ID="<subscription-id>"export ARM_CLIENT_ID="<app-id>"export ARM_CLIENT_SECRET="<client-secret>"terraform initterraform plan -out tfplanterraform apply tfplan

Remove the secret from the session

Remove-Item Env:ARM_CLIENT_SECRET, Env:ARM_CLIENT_ID
unset ARM_CLIENT_SECRET ARM_CLIENT_ID

Tip: Without ARM_CLIENT_* Terraform uses the az login session.

#Import an existing resource into Terraform

Changes things

Bring a portal-created resource under Terraform control without recreating it.

HCL
# imports.tfimport {  to = azurerm_virtual_network.hub  id = "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Network/virtualNetworks/<vnet>"}
PowerShell
az network vnet show -g <rg> -n <vnet> --query id -o tsv          # get the IDterraform plan -generate-config-out=generated.tf                   # let Terraform generate code# review generated.tf, clean up, move into the right fileterraform plan                                                     # goal: "1 to import, 0 to add, 0 to change, 0 to destroy"terraform apply# then delete the import block (or keep it as documentation)

#Rename a resource or move it into a module

Changes things

Use moved blocks so Terraform renames instead of destroying and recreating.

HCL
moved {  from = azurerm_subnet.app  to   = module.spoke.azurerm_subnet.this["app"]}
PowerShell
terraform plan            # must show "has moved to" and NO destroy/createterraform apply

#State lock is stuck

Has risky commands

Release a stuck state lock – with force-unlock, or by breaking the blob lease.

PowerShell
# The lock ID is in the error message ("ID: …")terraform force-unlock <lock-id># If that doesn't work: break the lease on the blobaz storage blob lease break --account-name <sa> -c tfstate -b <project>.tfstate --auth-mode login

Make sure no other run (pipeline, colleague) is active first.

#Check drift (changes made in the portal)

Read-only

Show what changed outside Terraform and decide: keep it in code or revert it.

PowerShell
terraform plan -refresh-only               # shows what changed outside Terraform# keep the change -> put it into code, then normal plan/apply# revert the change -> normal plan/apply overwrites itterraform plan -detailed-exitcode ; $LASTEXITCODE    # 0 = clean, 2 = differences (e.g. for a scheduled check)

#Update the provider version

Changes things

Raise the provider version safely and watch for unexpected replacements.

PowerShell
# 1. Raise the version in required_providers (e.g. "~> 4.0" -> "~> 4.20")terraform init -upgradeterraform providers lock -platform=windows_amd64 -platform=linux_amd64terraform plan                             # watch for unexpected changes/replacementsgit add .terraform.lock.hcl ; git commit -m "Update azurerm provider"

#Deploy Bicep safely

Changes things

Validate, what-if, deploy – and find the failed resource if it breaks.

PowerShell
$rg = "<rg>"az bicep lint -f main.bicepaz deployment group validate -g $rg -f main.bicep -p main.bicepparamaz deployment group what-if  -g $rg -f main.bicep -p main.bicepparamaz deployment group create   -g $rg -f main.bicep -p main.bicepparam -n "deploy-$(Get-Date -Format yyyyMMdd-HHmm)"# On failure: which resource?az deployment operation group list -g $rg -n <deployment-name> `  --query "[?properties.provisioningState=='Failed'].{res:properties.targetResource.resourceName, error:properties.statusMessage.error.message}" -o table

Tip: macOS / Linux (Bash): rg="<rg>", line continuation \, deployment name -n "deploy-$(date +%Y%m%d-%H%M)".

Code and DevOps

#Git and Azure DevOps Workflows

Single commands: Git · SSH and OpenSSL · Azure DevOps · Pipelines

From the first SSH key to releasing Dev to main. Open a workflow, adjust the variables at the top, run block by block.

#SSH key for Azure DevOps and first push

Changes things

Generate an RSA key, register it in Azure DevOps, configure SSH and push for the first time.

Caution: There is no SSH password in Azure DevOps. If you see git@ssh.dev.azure.com's password:, the key was not accepted – Ctrl+C and check steps 2 and 3. Use RSA, not ed25519.

# 1. Generate a key (-C is only a label, e.g. the device name; passphrase optional)ssh-keygen -t rsa -b 4096 -f $HOME\.ssh\id_rsa_azure -C "<device>-azure" # 2. Copy the PUBLIC key -> Azure DevOps: top right User settings -> SSH public keys -> + New Key -> pasteGet-Content $HOME\.ssh\id_rsa_azure.pub | Set-Clipboard     # must start with ssh-rsa AAAA… # 3. SSH config – this key for Azure DevOpsAdd-Content $HOME\.ssh\config @" Host ssh.dev.azure.com  HostName ssh.dev.azure.com  User git  IdentityFile ~/.ssh/id_rsa_azure  IdentitiesOnly yes"@ # 4. Test – answer "Shell access is not supported" = OK (first time: confirm the fingerprint with yes)ssh -T git@ssh.dev.azure.com # 5. Connect the repo and pushgit remote add origin git@ssh.dev.azure.com:v3/<org>/<project>/<repo>git push -u origin main
# 1. Generate a key (-C is only a label, e.g. the device name; passphrase optional)ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_azure -C "<device>-azure" # 2. Copy the PUBLIC key -> Azure DevOps: top right User settings -> SSH public keys -> + New Key -> pastepbcopy < ~/.ssh/id_rsa_azure.pub                  # macOS (no output); Linux: cat ~/.ssh/id_rsa_azure.pub # 3. SSH config – this key for Azure DevOpscat >> ~/.ssh/config <<'EOF' Host ssh.dev.azure.com  HostName ssh.dev.azure.com  User git  IdentityFile ~/.ssh/id_rsa_azure  IdentitiesOnly yesEOFchmod 600 ~/.ssh/config # 4. Test – answer "Shell access is not supported" = OK (first time: confirm the fingerprint with yes)ssh -T git@ssh.dev.azure.com # 5. Connect the repo and pushgit remote add origin git@ssh.dev.azure.com:v3/<org>/<project>/<repo>git push -u origin main

Caution: id_rsa_azure is the private key – never share or upload it. Only id_rsa_azure.pub goes into Azure DevOps.

Test fails or shows warnings?
  • Fingerprint on the first connection (RSA): SHA256:ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og – then stored in ~/.ssh/known_hosts.
  • no matching host key type / no mutual signature algorithm (old OpenSSH): add HostkeyAlgorithms +ssh-rsa and PubkeyAcceptedKeyTypes +ssh-rsa to the Host block – a legacy workaround; if ssh-rsa is about to be deprecated … throttled appears, remove both lines again.
  • post-quantum key exchange warning: harmless.
  • Old or wrong keys in Azure DevOps: delete them.
  • Password prompt or Permission denied (publickey): Azure DevOps: test and debug.

#Clone an existing DevOps repo

Read-only

Copy the clone URL and check out the right branch.

Address: DevOps → Repos → select repo → top right Clone → copy the URL.

PowerShell
cd C:\Git                                               # folder where the repo should go (macOS e.g. cd ~/Projekte)git clone git@ssh.dev.azure.com:v3/<org>/<project>/<repo>         # via SSH (key registered in DevOps)git clone https://<org>@dev.azure.com/<org>/<project>/_git/<repo> # via HTTPS – "password" = personal access tokencd <repo>git branch -a                                           # which branches exist? (e.g. main, Dev)git switch Dev                                          # switch to the working branch

Run git clone in the parent folder – inside a folder with the repo's name you end up with <repo>/<repo>. Spaces in the project name are %20 in the URL. HTTPS: the PAT is created under User settings → Personal access tokens (scope Code: Read & write). Repo name unknown:

PowerShell
az extension add -n azure-devopsaz devops configure --defaults organization=https://dev.azure.com/<org> project=<project>az repos list --query "[].{name:name, ssh:sshUrl, https:remoteUrl}" -o table

#Connect a local folder to an existing DevOps repo

Changes things

Link a local folder to the repo and push through a feature branch if main is protected.

PowerShell
cd <your-folder>git init -b main                                        # only if there is no .git folder yetgit remote add origin git@ssh.dev.azure.com:v3/<org>/<project>/<repo>git pull origin main --allow-unrelated-histories        # only needed if the repo already has commitsgit add -A ; git commit -m "<message>"git push -u origin main                                 # with protected main: feature branch + PR (see below)

#Push a folder as a new repo (HTTPS)

Changes things

Initialise a folder, check for secrets, add the remote and push over HTTPS.

PowerShell
cd <folder>git init -b maingit add .git status                                   # no tfstate, tfvars, pfx, licenses?git commit -m "Initial commit"git remote add origin https://dev.azure.com/<org>/<project>/_git/<repo>git push -u origin main                      # sign in via browser or PAT as password

Remote already has a README: before pushing run git pull origin main --allow-unrelated-histories.

#Empty repo: first push and missing main

Changes things

In an empty repo the first pushed branch becomes the default branch – if that was a feature branch, main is missing.

PowerShell
# first push into an empty repogit switch -c main                                      # or: git branch -M maingit add -Agit commit -m "Initial commit"git push -u origin main # main is missing on the server – what is there?git ls-remote --heads origingit push origin feature/<name>:main                     # create main on the server from the feature branch

Then DevOps → Repos → Branches → main → ⋯ → Set as default branch. A pull request needs a second branch with changes.

#Change via feature branch and pull request to Dev

Changes things3 steps

The normal path: own branch, push, pull request to Dev, clean up. Works the same for any repo – only the target branch differs.

Branch from the latest state, change, push

PowerShell
git switch Dev ; git pull                               # get the latest stategit switch -c feature/<name>                            # own branch# … change …git add -Agit commit -m "<what and why>"git push -u origin feature/<name>

Create the pull request – browser: after the push, Create a pull request appears at the top → target Dev. Or via CLI:

PowerShell
az repos pr create --organization https://dev.azure.com/<org> --project <project> --repository <repo> `  --source-branch feature/<name> --target-branch Dev --title "<title>" --open

After the merge – clean up

PowerShell
git switch Dev ; git pullgit branch -d feature/<name>                            # clean up the local feature branch

#Reviewers in a pull request

Required and optional reviewers – who has to approve and what to do if nobody is listed.

FieldMeaning
Required reviewersmandatory reviewers – without their Approve the PR cannot be completed
Optional reviewersget notified, approval not needed

Click Create without entries – the branch policy often adds the required reviewers (e.g. a team group) automatically.

Nobody listed? Add the colleague or team lead who approves for your team. Old PRs show who: Repos → Pull requests → Completed.

Wait for approvals and green checks → Complete.

Note: You usually cannot approve your own PR.

#Resolve a merge conflict (pull request or git pull)

Changes things3 steps

The PR shows "There are merge conflicts": the same lines were changed in Dev since your branch was created.

Merge Dev into your branch – Git reports the conflicts:

PowerShell
git switch feature/<name>git fetch origingit merge origin/Dev                                    # reports CONFLICTgit status                                              # affected files ("both modified")code .                                                  # in VS Code: Accept Current / Incoming / Both

Fix the file – keep the right version, delete the three marker lines:

Text
<<<<<<< HEADparam location = 'westeurope'        ← your version=======param location = 'austriaeast'       ← version from Dev>>>>>>> origin/Dev

Finish the merge

PowerShell
git add <file>                                          # for each resolved filegit commit                                              # merge commitgit push                                                # the PR updates automaticallygit merge --abort                                       # instead: abort everything

Tip: In parameter files keep new parameters from Dev. Who changed them: git log origin/Dev -- <file>.

Note: Depending on the policy the approval is reset after the push – the reviewer must click Approve again.

Tip: A conflict after a plain git pull (no PR) is solved the same way from step 2 on – finish with git commit (or git rebase --continue); abort everything with git merge --abort.

#Push rejected: TF402455 (protected branch)

Has risky commands3 steps

The branch is protected by a policy – move your commits to a feature branch and open a pull request instead.

Text
! [remote rejected] Dev -> Dev (TF402455: Pushes to this branch are not permitted; you must use a pull request to update this branch.)

Move your commits to a feature branch – they are already on the local Dev:

PowerShell
git switch -c feature/<name>                            # new branch from the current state – commits come alonggit push -u origin feature/<name>                       # this is allowedgit log --oneline origin/feature/<name> -3              # check: your commits are on the server

Create the pull request feature/<name> → Dev (see above).

Bring local Dev back to the server state

PowerShell
git switch Devgit fetch origingit reset --hard origin/Dev

Caution: Run reset --hard only once the feature branch is safely on the server.

#Push rejected: non-fast-forward (server is ahead)

Has risky commands2 steps

Your local branch is behind the server – rebase first, or take the safe way with a backup branch.

Text
! [rejected]        main -> main (non-fast-forward)

First attempt: rebase onto the server

PowerShell
git pull --rebase origin maingit push

Does not work (refusing to merge unrelated histories, or conflicts you do not want to resolve) – the safe way with a backup branch:

PowerShell
git rebase --abort                                      # only if a rebase is runninggit branch backup-main                                  # your state as a backupgit fetch origingit reset --hard origin/main                            # local = servergit checkout backup-main -- .                           # take your files overgit statusgit add -Agit commit -m "<message>"git pushgit branch -D backup-main                               # once everything looks good

Caution: git checkout backup-main -- . overwrites files that exist in both – files deleted only on your side come back from the server.

#Git: quickly switch branches in between

Changes things

Park the current work with stash, change something elsewhere, come back.

PowerShell
git stash -u                               # park current workgit switch main ; git pullgit switch -c fix/<name># … change …git add -A ; git commit -m "<message>" ; git push -u origin fix/<name>git switch <old-branch>git stash pop                              # continue working

#Undo a pull request (abandon / revert)

Has risky commands2 steps

Abandon an open PR or revert a completed one, then clean up locally.

PR still open → abandon it, nothing changes in Dev. Browser: in the PR, next to Complete click ▾ / ⋯ → Abandon (Reactivate revives it). Or:

PowerShell
az repos pr list --organization https://dev.azure.com/<org> --project <project> -o table      # PR numberaz repos pr update --id <pr-number> --status abandoned --organization https://dev.azure.com/<org>

PR already completed → counter-change: open the PR → top right Revert → Create pull request → new PR to Dev, needs approval again.

3. Clean up locally and on the server
PowerShell
git merge --abort                                       # if in the middle of a conflict mergegit switch Devgit fetch origingit reset --hard origin/Dev                             # local Dev = server stategit branch -D feature/<name>                            # delete feature branch locally (only if no longer needed)git push origin --delete feature/<name>                 # and on the server

To keep the changes and just start over: only Abandon, don't delete the branch.

#Git: secret committed by accident

Changes things

Not pushed: undo the commit. Pushed: revoke the secret first, then clean the history.

PowerShell
# NOT pushed yetgit reset --soft HEAD~1                    # undo the commit, files staygit restore --staged <file>Add-Content .gitignore "<file-or-pattern>"git add -A ; git commit -m "<message>" # Already pushed# 1. Change/revoke the secret IMMEDIATELY (Key Vault, app registration, PAT …)# 2. Remove the file from the repogit rm --cached <file>Add-Content .gitignore "<file-or-pattern>"git commit -m "Remove secret file" ; git push

The old version stays in the history – that is why only changing the secret counts.

#Promote Dev to main (release)

Changes things3 steps

Check what comes along, create the Dev → main PR, complete it without deleting Dev.

Check what comes along – that is everything in Dev, including colleagues' work:

PowerShell
git fetch origingit log --oneline origin/main..origin/Dev               # commits in Dev that are not in main yetgit diff --stat origin/main...origin/Dev                # changed files

Create the PR – browser: Repos → Pull requests → New pull request → source Dev, target main. Or:

PowerShell
az repos pr create --organization https://dev.azure.com/<org> --project <project> --repository <repo> `  --source-branch Dev --target-branch main --title "Release: Dev to main" --open

Complete with these settings:

Caution: Turn off "Delete Dev after merging" – otherwise the Dev branch is gone. Merge type Merge (no fast-forward), not Squash – otherwise Dev and main diverge.

Conflict in the Dev → main PR (main was changed directly, e.g. a hotfix)

Bring main into Dev first:

PowerShell
git switch Dev ; git pullgit switch -c merge/main-into-devgit merge origin/main                                   # resolve conflicts as abovegit add -A ; git commitgit push -u origin merge/main-into-dev                  # PR to Dev -> afterwards Dev -> main is conflict-free

#Publish a static website via pipeline (Static Web App)

Changes things4 steps

Works for any static site (HTML/CSS/JS), e.g. this page. The pipeline uses the deployment token of the app – no service connection needed, and the repo may live in another tenant. Generator: DevOps and Git → Static Web App + pipeline.

Create the static web app (once) – portal: Create Static Web App → Plan: Free, Source: Other, region Global → Review + create. Or CLI:

PowerShell
az login --tenant <target-tenant-id>az account set --subscription "<subscription>"$rg = "<rg>"; $swa = "<swa-name>"az group create -n $rg -l westeuropeaz staticwebapp create -n $swa -g $rg -l westeurope --sku Freeaz staticwebapp show -n $swa -g $rg --query defaultHostName -o tsv                 # URL / CNAME targetaz staticwebapp secrets list -n $swa -g $rg --query "properties.apiKey" -o tsv     # deployment token

Portal: Static Web App → Manage deployment token → copy.

Caution: Source: Other – not "Azure DevOps", that needs the same tenant. Plan Standard is only needed for tenant-only login without invitations.

Pipeline file in the repo – pipelines/static-web-app.yml:

YAML
trigger:  branches:    include: [ main ] pool:  vmImage: ubuntu-latest variables:  appRoot: '.'          # files in a subfolder of the repo (e.g. promptus/)? -> 'promptus' steps:- checkout: self - script: |    set -e    mkdir -p site    python3 tools/build-html.py -o site/index.html    echo "--- site/"; ls -la site  workingDirectory: $(appRoot)  displayName: Build site/index.html - task: AzureStaticWebApp@0  displayName: Deploy to Static Web App  inputs:    app_location: $(appRoot)/site    skip_app_build: true    skip_api_build: true    azure_static_web_apps_api_token: $(SWA_TOKEN)

Plain HTML site without build step: drop the script step and set app_location to the folder with index.html. Then commit:

PowerShell
git add pipelines/static-web-app.ymlgit commit -m "Add static web app pipeline"git push                                                # protected main -> feature branch + PR

Register the pipeline in DevOps (once) – DevOps does not pick up YAML files by itself: Pipelines → New pipeline → Azure Repos Git → repo → Existing Azure Pipelines YAML file → main, /pipelines/static-web-app.yml → Variables → New variable SWA_TOKEN = token, tick Keep this value secret → Save → Run. From now on every push to main deploys automatically (Pipelines → Runs).

Custom domain – at the DNS provider: CNAME promptus -> <defaultHostName>. Then portal: Custom domains → + Add → Custom domain on other DNS, or:

PowerShell
az staticwebapp hostname set -n $swa -g $rg --hostname promptus.cloudorbis.ataz staticwebapp hostname show -n $swa -g $rg --hostname promptus.cloudorbis.at --query status -o tsv   # Ready = certificate issued
5. Optional: restrict the site to your team

In site/staticwebapp.config.json:

JSON
"routes": [ { "route": "/*", "allowedRoles": [ "team" ] } ],"responseOverrides": { "401": { "redirect": "/.auth/login/aad", "statusCode": 302 } }

Portal → Role management → Invite → Microsoft Entra ID, role team (Free: up to 25 invitations). Tenant-only without invitations: Standard plan with a custom Entra ID provider.

If it fails: errors and fixes
ErrorCauseFix
App Directory Location: 'site' is invalid. Could not detect this directoryfolder not built / files in a subfolder / old build scriptpipeline above (mkdir -p site, -o site/index.html), set appRoot
No hosted parallelism has been purchased or grantednew DevOps organization without free agentrequest the free grant (link in the error), meanwhile deploy locally (below)
deployment token error / Unauthorizedvariable missing, wrong name or old tokenvariable exactly SWA_TOKEN, saved; new token: az staticwebapp secrets reset-api-key -n $swa -g $rg
pipeline does not start on pushbranch is master, not main / pipeline not registeredadjust include:, step 3
domain stays ValidatingCNAME missing or still propagatingResolve-DnsName promptus.cloudorbis.at -Type CNAME, wait
Deploy locally without a pipeline (needs Node.js)
PowerShell
python tools/build-html.py$token = az staticwebapp secrets list -n $swa -g $rg --query "properties.apiKey" -o tsvnpx --yes @azure/static-web-apps-cli deploy ./site --deployment-token $token --env production
Code and DevOps

#Git

Pull requests in Azure DevOps: Workflows · Step by step: Git and Azure DevOps Workflows

Top 5: git status · git pull · git add -A ; git commit -m "..." · git push · git log --oneline --graph -20

#Setup

Name, e-mail and basic settings.

PowerShell
git --versiongit config --global user.name  "First Last"git config --global user.email "<mail>"git config --global init.defaultBranch maingit config --global core.autocrlf true          # Windows: CRLF locally, LF in the repogit config --global pull.rebase falsegit config --global alias.lg "log --oneline --graph --decorate -20"    # git lggit config --list --show-origin

#New Repo / Push a Folder

Create a repo from a folder and push it.

PowerShell
cd <folder>git init -b maingit add .git status                                      # check: no secrets, states, licenses!git commit -m "Initial commit"git remote add origin <url>git push -u origin main# Remote already has commits (e.g. README):git pull origin main --allow-unrelated-historiesgit push -u origin main

#Daily Work

Clone, pull, commit, push and check status.

PowerShell
git clone <url> [<folder>]git pull                                        # fetch + mergegit fetch --all --prune                         # fetch onlygit status ; git status -sgit diff ; git diff --stagedgit add <file> ; git add -A                     # everything incl. deletionsgit add -p                                      # changes piece by piecegit commit -m "<message>"git commit --amend                              # change the last commit (only if not pushed)git commit --amend --no-edit                    # add a file, keep the messagegit push

#Branches and Merge

Create, switch, merge and delete branches.

PowerShell
git branch -agit switch -c feature/<name>                    # create a new branch and switchgit switch maingit push -u origin feature/<name>git merge feature/<name>git merge --abort                               # abort a merge with conflictsgit rebase main                                 # update your branch onto maingit rebase --abortgit cherry-pick <commit>                        # take a single commitgit branch -d feature/<name>                    # delete locallygit push origin --delete feature/<name>         # delete remotelygit tag v1.0 ; git push origin --tags

Resolve a conflict: edit the file (remove <<<<<<< … >>>>>>>) → git add <file> → git commit (or git rebase --continue).

#Search and History

Log, blame and search in the history.

PowerShell
git log --oneline --graph --decorate -20git log -p -- <file>                            # history of a file with changesgit log --follow -- <file>                      # across renames toogit log --grep "<text>"                         # search commit messagesgit log -S "<text>"                             # commits that added/removed <text>git log --since "2 weeks ago" --author "<name>"git grep -n "<text>"                            # search the current stategit blame <file>git show <commit>git show <commit>:<path>                        # show a file at an old stategit diff main...feature/<name>                  # what does the branch bring?git diff --stat <commit1> <commit2>

#Undo

Discard changes, unstage, revert and reset.

PowerShell
git restore <file>                              # discard local changegit restore --staged <file>                     # unstagegit restore --source <commit> <file>            # get a file from an old stategit reset --soft HEAD~1                         # undo last commit, keep changesgit reset --hard HEAD~1                         # discard last commit (careful!)git reset --hard origin/main                    # local = remote (careful!)git revert <commit>                             # counter-commit (safe for pushed state)git stash ; git stash list ; git stash pop      # park changesgit stash -u                                    # incl. untracked filesgit clean -n ; git clean -fd                    # show/delete untracked filesgit reflog                                      # find "lost" commits
SituationCommand
File broken, not committedgit restore <file>
Wrong file addedgit restore --staged <file>
Wrong commit message, not pushedgit commit --amend
Last commit wrong, not pushedgit reset --soft HEAD~1
Commit already pushedgit revert <commit>
Everything gone after resetgit reflog → git reset --hard <hash>

#Ignore and Secrets

.gitignore, removing files from Git and keeping secrets out.

PowerShell
git check-ignore -v <file>                      # why is it ignored?git add -f <file>                               # add despite .gitignoregit rm --cached <file>                          # remove from Git, keep locallygit rm -r --cached . ; git add .                # rebuild after a .gitignore changegit ls-files | Select-String "tfstate|tfvars|\.pfx|\.key|\.lic"     # check before pushing (macOS / Linux: grep -E "tfstate|tfvars|\.pfx|\.key|\.lic")

Secret pushed by accident: change/revoke the secret immediately – removing it from history is not enough.

#Remote

Show, change and add remotes.

PowerShell
git remote -vgit remote set-url origin <new-url>             # e.g. SSH <-> HTTPSgit remote add upstream <url>git ls-remote origin                            # branches/tags on the servergit ls-remote --heads origin                    # only branchesgit push origin <branch>:main                   # create/update main on the server from a local branch

Push rejected because the server is ahead: non-fast-forward · empty repo without main: first push and missing main.

#Rebase or Merge?

The difference and when to use which.

MergeRebase
What happensa merge commit joins both linesyour commits are "replayed" onto the new state
Historybranched, completestraight line
Risklowrewrites commits – never on shared branches (Dev, main)
Whenbranch is already pushed and others work on ittidy up your own feature branch before the PR
PowerShell
# Bring your feature branch up to the latest Dev (rebase)git switch feature/<name>git fetch origingit rebase origin/Dev# on conflict: fix the file -> git add <file> -> git rebase --continue   (abort: git rebase --abort)git push --force-with-lease               # needed after rebase; only overwrites if nobody else pushed # Squash several own commits into one before the PRgit reset --soft origin/Dev               # all changes stay, commits gonegit commit -m "<one clean message>"git push --force-with-lease

#Take Single Commits and Files

cherry-pick and files from other branches.

PowerShell
git log --oneline origin/<other-branch> -10                   # find the commit hashgit cherry-pick <commit>                                       # take exactly this commitgit cherry-pick <commit1> <commit2>git cherry-pick --abort git restore --source origin/<other-branch> -- <path/file>      # get only one file from another branchgit show origin/<branch>:<path/file>                           # just view a file from another branchgit diff Dev..feature/<name> -- <path/file>                    # compare one file between branches

#Who Changed This?

blame and the history of single lines.

PowerShell
git blame -L 10,30 <file>                      # lines 10–30 with author and commitgit log -L 10,30:<file>                        # history of exactly these linesgit log --follow -p -- <file>                  # all changes to a file, even after renamesgit log -S "<text>" --oneline                  # when was <text> added / removedgit log --author "<name>" --since "1 week ago" --onelinegit shortlog -sn --since "1 month ago"         # who has how many commits

#Aliases and Useful Settings

Shortcuts and settings worth setting once.

PowerShell
git config --global alias.st "status -s"git config --global alias.lg "log --oneline --graph --decorate -20"git config --global alias.last "log -1 --stat"git config --global alias.unstage "restore --staged"git config --global alias.amend "commit --amend --no-edit"git config --global alias.br "branch -vv"                     # branches with upstream and state git config --global fetch.prune true                          # remove deleted remote branches automaticallygit config --global rerere.enabled true                       # remember resolved conflictsgit config --global push.autoSetupRemote true                 # "git push" without -u the first timegit config --global core.editor "code --wait"                 # VS Code as editorgit config --global merge.tool vscodegit config --global mergetool.vscode.cmd 'code --wait $MERGED'

Then: git st, git lg, git last, git br.

#Work on Several Branches at Once

A second working folder with git worktree.

PowerShell
git worktree add ..\<repo>-hotfix main         # second working folder on main, without switchinggit worktree listgit worktree remove ..\<repo>-hotfix

Handy when a hotfix on main comes in while the feature branch is half done.

#Commit Messages

Short, imperative, what and why: Hub: add UDR for GatewaySubnet · FortiGate: license token as variable. Optional prefix: feat:, fix:, docs:, refactor:, chore:.

Code and DevOps

#SSH and Certificates (OpenSSL)

Step by step: Git and Azure DevOps Workflows

#Keys

-C is only a label (e-mail or device name). id_rsa = private key (never share), id_rsa.pub = public key (goes into Azure DevOps, GitHub, servers).

Bash
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_azure -C "<device>-azure"   # macOS / Linux: RSA for Azure DevOpspbcopy < ~/.ssh/id_rsa_azure.pub                                       # macOS: public key to the clipboardcat ~/.ssh/id_rsa_azure.pub                                            # show it (starts with ssh-rsa AAAA…)
PowerShell
ssh-keygen -t ed25519 -C "<comment>"                        # default (not for Azure DevOps)ssh-keygen -t rsa -b 4096 -f $HOME\.ssh\id_rsa              # RSA (Azure DevOps)Get-Content $HOME\.ssh\id_rsa.pub | Set-Clipboard           # copy the public keyGet-Content $HOME\.ssh\id_rsa -TotalCount 1                 # check format: -----BEGIN OPENSSH PRIVATE KEY-----ssh-keygen -y -f $HOME\.ssh\id_rsa                          # derive public key from private keyssh-keygen -p -f $HOME\.ssh\id_rsa                          # change passphrasessh-keygen -lf $HOME\.ssh\id_rsa.pub                        # fingerprintssh-keygen -R <host-or-ip>                                  # remove an old host keyRemove-Item $HOME\.ssh\config -ErrorAction SilentlyContinue   # remove a broken SSH configaz sshkey create -g <rg> -n <name>                          # key as an Azure resource

#Connect and Test

Connect, jump hosts, port forwarding and copying files.

PowerShell
ssh <user>@<host>ssh -i <private-key> <user>@<host>ssh -p 2222 <user>@<host>ssh -L 8443:<target>:443 <user>@<jump>        # local port forwarding -> https://localhost:8443ssh -D 1080 <user>@<jump>                     # SOCKS proxy via the jump hostssh -J <user>@<jump> <user>@<target>          # via jump hostscp <file> <user>@<host>:/tmp/scp <user>@<host>:/var/log/syslog .ssh -T git@ssh.dev.azure.com                  # Azure DevOps: "Shell access is not supported" = OKssh -T git@github.comssh -vT <host>                                # troubleshooting

#~/.ssh/config

Windows: C:\Users\<user>\.ssh\config – file without extension. macOS / Linux: ~/.ssh/config, then chmod 600 ~/.ssh/config (edit e.g. with nano ~/.ssh/config → Ctrl+O, Enter, Ctrl+X).

Text
Host ssh.dev.azure.com    HostName ssh.dev.azure.com    User git    IdentityFile ~/.ssh/id_rsa_azure    IdentitiesOnly yes    # only for "no matching host key type" / "no mutual signature algorithm" (legacy workaround):    # HostkeyAlgorithms +ssh-rsa    # PubkeyAcceptedKeyTypes +ssh-rsa Host fgt-a    HostName <mgmt-ip>    User <admin> Host vm-internal    HostName 10.0.1.4    User azureuser    ProxyJump jump

Then ssh fgt-a is enough.

#Azure DevOps: Test and Debug

Test the key against Azure DevOps and read the debug output.

Bash
ssh -T git@ssh.dev.azure.com                   # OK = "remote: Shell access is not supported."ssh -vT git@ssh.dev.azure.com 2>&1 | grep -iE "config|identity|offering|accept|denied|auth"ssh -i ~/.ssh/id_rsa_azure -T git@ssh.dev.azure.com          # use the key once, without the config

PowerShell: ssh -vT git@ssh.dev.azure.com 2>&1 | Select-String "config|identity|offering|accept|denied|auth"

You seeMeaning
git@ssh.dev.azure.com's password:key not accepted – Azure DevOps has no SSH password, Ctrl+C
Offering public key: …id_rsa_azure but no Acceptedkey is offered, but not registered in Azure DevOps (or in another account / organization)
no Offering lineconfig not applied – file name (no extension), Host ssh.dev.azure.com, IdentityFile path
no matching host key type / no mutual signature algorithmold OpenSSH or key – update; as a workaround add HostkeyAlgorithms +ssh-rsa / PubkeyAcceptedKeyTypes +ssh-rsa
ssh-rsa is about to be deprecated and your request has been throttledthe +ssh-rsa workaround is active – remove both lines from the config
fingerprint question on the first connectionanswer yes if it shows SHA256:ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og
post-quantum key exchange warningharmless

#ssh-agent

Enter the passphrase only once.

PowerShell
# once as administratorGet-Service ssh-agent | Set-Service -StartupType AutomaticStart-Service ssh-agent# as userssh-add $HOME\.ssh\id_rsassh-add -lgit config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"   # Git uses Windows SSH

#Permissions

Error "UNPROTECTED PRIVATE KEY FILE / too open":

PowerShell
icacls $HOME\.ssh\id_rsa /inheritance:r /grant:r "$($env:USERNAME):(R)"
Bash
chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_rsa

#Certificates with OpenSSL

Read certificates, convert formats, check key and certificate, create a CSR.

Bash
# Show a server's certificate (issuer, expiry, SAN)openssl s_client -connect <host>:443 -servername <host> </dev/null 2>/dev/null \  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName # Read a certificate fileopenssl x509 -in cert.pem -noout -textopenssl x509 -in cert.pem -noout -enddateopenssl x509 -in cert.cer -inform der -out cert.pem          # DER -> PEM # Split / build a PFXopenssl pkcs12 -in cert.pfx -nokeys -out cert.pemopenssl pkcs12 -in cert.pfx -nocerts -nodes -out key.pemopenssl pkcs12 -export -out cert.pfx -inkey key.pem -in cert.pem -certfile chain.pemopenssl pkcs12 -in old.pfx -legacy -nodes -out all.pem       # old PFX with OpenSSL 3 # Do key and certificate match? (outputs must be identical)openssl x509 -noout -modulus -in cert.pem | openssl sha256openssl rsa  -noout -modulus -in key.pem  | openssl sha256 # CSR and self-signed test certificateopenssl req -new -newkey rsa:2048 -nodes -keyout key.pem -out req.csr -subj "/CN=<fqdn>"openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 \  -subj "/CN=<fqdn>" -addext "subjectAltName=DNS:<fqdn>"

PowerShell:

PowerShell
Get-PfxCertificate .\cert.pfx | Format-List Subject, NotAfter, DnsNameListNew-SelfSignedCertificate -DnsName <fqdn> -CertStoreLocation Cert:\CurrentUser\My

App Gateway listeners need a PFX with private key and the full chain.

Code and DevOps

#Azure DevOps

SSH key for it: SSH · Pipelines: Pipelines · Step by step: Git and Azure DevOps Workflows

#In the Portal

Where to click: repos, keys, tokens, branch policies and pull requests.

TaskWhere
Create a repoProject → Repos → repo picker → + New repository (without README when pushing an existing folder)
Clone URLRepos → Clone (HTTPS or SSH)
SSH keytop right User settings → SSH public keys → + New Key (RSA only)
Personal access tokenUser settings → Personal access tokens → scope Code: Read & Write
Branch protectionRepos → Branches → main → Branch policies (pull request, reviewers, build)
Pull requestRepos → Pull requests → New (choose source → target)
Abandon a PRin the PR: Complete ▾ / ⋯ → Abandon
Undo a completed PRin the PR: Revert → Create pull request
ReviewersRequired = mandatory approval, Optional = info only
Service connectionProject settings → Service connections → Azure Resource Manager (workload identity federation)
Variable group / secretsPipelines → Library (optionally linked to Key Vault)
Approval before prodPipelines → Environments → Approvals and checks

Complete flow feature → Dev → main: Workflows → Azure DevOps.

URL formats:

Text
HTTPS: https://dev.azure.com/<org>/<project>/_git/<repo>SSH:   git@ssh.dev.azure.com:v3/<org>/<project>/<repo>

#CLI

Sign in and work with repos, pull requests and pipelines from the CLI.

PowerShell
az extension add -n azure-devopsaz devops login                                 # enter PATaz devops configure --defaults organization=https://dev.azure.com/<org> project=<project>az repos list -o tableaz repos create --name <repo>az repos pr list -o tableaz repos pr create --source-branch feature/<name> --target-branch Dev --title "<title>" --openaz repos pr create --source-branch Dev --target-branch main --title "Release: Dev to main" --openaz repos pr show --id <pr-number> --query "{status:status, merge:mergeStatus, reviewers:reviewers[].{name:displayName, vote:vote}}"az repos pr reviewer add --id <pr-number> --reviewers <mail-or-group>az repos pr update --id <pr-number> --status abandoned            # abandon PRaz repos pr update --id <pr-number> --status active               # reactivateaz repos policy list --branch Dev --repository-id <repo-id> -o table   # which branch policies applyaz pipelines list -o tableaz pipelines run --name <pipeline>az pipelines runs list --top 5 -o table

#Pipeline Template: Terraform Plan/Apply

azure-pipelines.yml – plan on every branch, apply only on main with approval via an environment.

YAML
trigger:  branches:    include: [ main ] pool:  vmImage: ubuntu-latest variables:  serviceConnection: '<service-connection>'  workDir: '$(System.DefaultWorkingDirectory)/terraform' stages:- stage: Plan  jobs:  - job: plan    steps:    - task: AzureCLI@2      displayName: terraform plan      inputs:        azureSubscription: $(serviceConnection)        scriptType: bash        scriptLocation: inlineScript        addSpnToEnvironment: true        workingDirectory: $(workDir)        inlineScript: |          export ARM_CLIENT_ID=$servicePrincipalId          export ARM_TENANT_ID=$tenantId          export ARM_OIDC_TOKEN=$idToken          export ARM_USE_OIDC=true          export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv)          terraform init -input=false          terraform fmt -check -recursive          terraform validate          terraform plan -input=false -out tfplan    - publish: $(workDir)/tfplan      artifact: tfplan - stage: Apply  dependsOn: Plan  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))  jobs:  - deployment: apply    environment: prod                       # configure approval in the environment    strategy:      runOnce:        deploy:          steps:          - checkout: self          - download: current            artifact: tfplan          - task: AzureCLI@2            displayName: terraform apply            inputs:              azureSubscription: $(serviceConnection)              scriptType: bash              scriptLocation: inlineScript              addSpnToEnvironment: true              workingDirectory: $(workDir)              inlineScript: |                export ARM_CLIENT_ID=$servicePrincipalId                export ARM_TENANT_ID=$tenantId                export ARM_OIDC_TOKEN=$idToken                export ARM_USE_OIDC=true                export ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv)                terraform init -input=false                terraform apply -input=false $(Pipeline.Workspace)/tfplan/tfplan

Note: the OIDC token from addSpnToEnvironment is short-lived – for long runs the azurerm provider (≥ 4.x) can refresh the token itself (ARM_OIDC_AZURE_SERVICE_CONNECTION_ID + SYSTEM_ACCESSTOKEN, see the azurerm docs).

Code and DevOps

#Azure DevOps Pipelines

Repo and PRs: Azure DevOps · Terraform template: → Pipeline template · Step by step: Git and Azure DevOps Workflows

#In the Portal

Where to click to run, watch and approve pipelines.

TaskWhere
Start a runPipelines → pipeline → Run pipeline → choose branch, set parameters
Run for another branchin Run pipeline switch Branch/tag at the top
Log of a stepopen run → job → click the step; top right View raw log
Retry a failed jobopen run → Rerun failed jobs
Grant an approvalrun shows "Waiting" → Review → Approve
Set up approvalsPipelines → Environments → environment → ⋯ → Approvals and checks
Variables/secretsPipelines → Library → variable group (optionally linked to Key Vault)
Build check in a PRRepos → Branches → Dev → Branch policies → Build validation
Pipeline permission on a service connectionProject settings → Service connections → connection → Security

#CLI

List, start and inspect pipeline runs from the CLI.

PowerShell
az devops configure --defaults organization=https://dev.azure.com/<org> project=<project>az pipelines list -o tableaz pipelines run --name <pipeline> --branch Devaz pipelines run --name <pipeline> --branch main --parameters environment=prod     # runtime parameteraz pipelines run --name <pipeline> --variables ImageTag=1.2.3az pipelines runs list --pipeline-ids <id> --top 10 `  --query "[].{id:id, branch:sourceBranch, status:status, result:result, start:startTime}" -o tableaz pipelines runs show --id <run-id> --open                                         # open in the browseraz pipelines runs list --branch Dev --result failed --top 5 -o tableaz pipelines variable-group list -o tableaz pipelines variable-group variable list --group-id <id> -o table

#Investigate a Failed Run

1. Open run → red job → red step → read the last red line, not the first. 2. Common causes:

MessageCauseFix
AuthorizationFailedservice connection has no role on the targetassign a role to the service connection's identity
No hosted parallelism has been purchased or grantedno agents free/enabledrequest parallel jobs or use a self-hosted agent
The pipeline is not valid … Could not find …wrong path/templatecheck the path relative to the repo root
Pipeline does not have permissions to use the referenced service connectionpermission missingopen run → Permit, or service connection → Security
Run stuck on "Waiting"approval in the environmentreviewer grants approval
The template … failed validationBicep/ARM errorrun az deployment group validate locally

3. More output: set variable system.debug = true and run again.

#Template: Bicep what-if in the PR, Deploy after Approval

pipelines/bicep.yml – what-if on every PR, deployment to Dev/main only after approval in the environment.

YAML
trigger:  branches:    include: [ Dev, main ]  paths:    include: [ infra/* ] pr:  branches:    include: [ Dev, main ] pool:  vmImage: ubuntu-latest variables:  serviceConnection: '<service-connection>'  rg: '<rg>'  ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:    paramFile: 'infra/params/prod.bicepparam'    envName: 'prod'  ${{ else }}:    paramFile: 'infra/params/dev.bicepparam'    envName: 'dev' stages:- stage: Check  jobs:  - job: whatif    steps:    - task: AzureCLI@2      displayName: Bicep lint + what-if      inputs:        azureSubscription: $(serviceConnection)        scriptType: bash        scriptLocation: inlineScript        inlineScript: |          az bicep lint -f infra/main.bicep          az deployment group what-if -g $(rg) -f infra/main.bicep -p $(paramFile) \            --exclude-change-types NoChange Ignore - stage: Deploy  dependsOn: Check  condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))  jobs:  - deployment: deploy    environment: ${{ variables.envName }}   # configure approval in the environment    strategy:      runOnce:        deploy:          steps:          - checkout: self          - task: AzureCLI@2            displayName: Bicep deploy            inputs:              azureSubscription: $(serviceConnection)              scriptType: bash              scriptLocation: inlineScript              inlineScript: |                az deployment group create -g $(rg) -f infra/main.bicep -p $(paramFile) \                  -n "pipeline-$(Build.BuildId)"

Note: in a PR the check uses the Dev parameters (for PR runs the source branch is refs/pull/…). For PRs to main, attach a separate pipeline with prod.bicepparam as build validation on main if needed.

For the what-if to run in the PR: branch policy of Dev → Build validation → add this pipeline.

#Useful YAML Snippets

Parameters, variable groups and other snippets to copy.

YAML
# Runtime parameter (choice at start)parameters:- name: environment  type: string  default: dev  values: [ dev, test, prod ] # Include a variable group (e.g. with Key Vault secrets)variables:- group: <variable-group> # Step only on main- script: echo "main only"  condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') # Step even if a previous one failed (e.g. save logs)- script: echo "always"  condition: always() # Template from another reporesources:  repositories:  - repository: templates    type: git    name: <project>/<template-repo>    ref: refs/heads/mainsteps:- template: steps/bicep-deploy.yml@templates
Code and DevOps

#Terraform

Step by step: Terraform and Bicep Workflows

Top 5: terraform init · terraform plan -out tfplan · terraform apply tfplan · terraform state list · terraform output

#Basic Workflow

init, plan, apply and the flags used most often.

PowerShell
terraform versionterraform init                         # initialize providers + backendterraform init -upgrade                # get newer provider versionsterraform init -reconfigure            # re-read the backendterraform init -migrate-state          # move state to a new backendterraform fmt -recursive               # formatterraform fmt -check -recursive        # check only (pipeline)terraform validate                     # check syntax/typesterraform test                         # tests in tests/*.tftest.hcl terraform plan                         # show changesterraform plan -out tfplan             # save the planterraform apply tfplan                 # apply exactly this planterraform apply                        # plan, ask, applyterraform apply -auto-approve          # without confirmation (pipeline)terraform plan -destroy                # show what a destroy would deleteterraform destroy terraform plan -var-file=<file>.tfvars -var="name=value"terraform plan -target='<address>'                 # one resource only (exception!)terraform apply -replace='<address>'               # rebuild deliberatelyterraform plan -refresh-only                       # show drift onlyterraform apply -refresh-only                      # accept drift into the stateterraform plan -detailed-exitcode                  # exit 0 = no change, 2 = changes, 1 = errorterraform plan -parallelism=5                      # fewer parallel API calls (throttling)terraform show -json tfplan > plan.json            # machine-readable planterraform -chdir=<folder> plan                     # run in another folder

In PowerShell always put addresses in single quotes: 'module.hub.azurerm_subnet.this["fw"]'.

#Reading a Plan

What +, ~, - and -/+ in a plan mean.

SymbolMeaning
+create
~update in place
-/+replace = destroy and recreate – check carefully!
+/-replace with create_before_destroy
-destroy
<=read data source
# forces replacementthis attribute triggers the rebuild

#Outputs and State

Read outputs, list and move state entries.

PowerShell
terraform outputterraform output -raw <name>terraform output -json > outputs.jsonterraform show                             # current state, readableterraform state listterraform state list | Select-String <part>         # macOS / Linux: ... | grep -i <part>terraform state show '<address>'terraform state mv '<old>' '<new>'         # rename without rebuild (better: moved block)terraform state rm '<address>'             # detach from Terraform, resource staysterraform state pull > state-backup.json   # back up the stateterraform import '<address>' <azure-id>    # take over an existing resourceterraform plan -generate-config-out=generated.tf   # generate code for import blocks (TF >= 1.5)terraform force-unlock <lock-id>terraform console                          # test expressionsterraform graph | dot -Tsvg > graph.svgterraform providersterraform providers lock -platform=windows_amd64 -platform=linux_amd64   # lock file for all platformsterraform workspace list | new <name> | select <name> | delete <name>

#Code Snippets

Import, moved and other blocks you copy again and again.

HCL
# Import in code (TF >= 1.5)import {  to = azurerm_resource_group.rg  id = "/subscriptions/<sub>/resourceGroups/<rg>"} # Rename without rebuildmoved {  from = azurerm_resource_group.old  to   = azurerm_resource_group.new} # Remove from Terraform, resource stays (TF >= 1.7)removed {  from = azurerm_resource_group.old  lifecycle {    destroy = false  }} # Ignore fields after creation / protectionlifecycle {  ignore_changes        = [tags["DateCreatedUTC"]]  prevent_destroy       = true  create_before_destroy = true} # Several instances from a mapresource "azurerm_subnet" "this" {  for_each             = var.subnets          # { fw = "10.0.0.0/26", mgmt = "10.0.0.64/27" }  name                 = each.key  address_prefixes     = [each.value]  resource_group_name  = azurerm_resource_group.rg.name  virtual_network_name = azurerm_virtual_network.vnet.name} # Optional resourceresource "azurerm_public_ip" "pip" {  count = var.create_pip ? 1 : 0  # ...} # Variable with validationvariable "environment" {  type = string  validation {    condition     = contains(["dev", "test", "prod"], var.environment)    error_message = "environment must be dev, test or prod."  }} # Secret variablevariable "admin_password" {  type      = string  sensitive = true} # Provider with pinned versionterraform {  required_version = ">= 1.6"  required_providers {    azurerm = {      source  = "hashicorp/azurerm"      version = "~> 4.0"    }  }}provider "azurerm" {  features {}  subscription_id = var.subscription_id      # required from azurerm 4.x (or ARM_SUBSCRIPTION_ID)}

#Useful Functions

Test in terraform console:

HCL
cidrsubnets("10.0.0.0/24", 2, 2, 2, 2)     # ["10.0.0.0/26","10.0.0.64/26",...]cidrsubnet("10.0.0.0/16", 8, 3)            # "10.0.3.0/24"cidrhost("10.0.1.0/24", 4)                 # "10.0.1.4"  (first usable IP in Azure)merge(local.tags, { role = "fw" })lookup(var.map, "key", "default")try(var.obj.value, null)coalesce(var.name, "${var.prefix}-vm")format("%s-%02d", "vm", 1)                 # "vm-01"jsonencode({ a = 1 }) ; yamldecode(file("x.yaml"))templatefile("${path.module}/init.tpl", { hostname = "fgt-a" })toset(["a","b"]) ; flatten([[1],[2,3]]) ; distinct(list)[for k, v in var.subnets : k if v != null]{ for s in var.list : s.name => s }

#Remote State in Azure

Backend configuration for a storage account and lock handling.

HCL
terraform {  backend "azurerm" {    resource_group_name  = "<rg-tfstate>"    storage_account_name = "<storageaccount>"    container_name       = "tfstate"    key                  = "<project>.tfstate"    use_azuread_auth     = true  }}

Create the storage:

PowerShell
az group create -n <rg-tfstate> -l <region>az storage account create -g <rg-tfstate> -n <storageaccount> --sku Standard_ZRS --min-tls-version TLS1_2 --allow-blob-public-access falseaz storage account blob-service-properties update -g <rg-tfstate> --account-name <storageaccount> --enable-versioning trueaz storage container create --account-name <storageaccount> -n tfstate --auth-mode loginaz role assignment create --assignee <user-or-sp> --role "Storage Blob Data Contributor" --scope <storage-id>

Lock stuck? Blob lease: az storage blob lease break --account-name <sa> -c tfstate -b <project>.tfstate --auth-mode login

#Environment Variables and Sign-in

ARM variables, service principal, OIDC and debug logging.

$env:ARM_SUBSCRIPTION_ID = "<sub>"$env:ARM_TENANT_ID = "<tenant>"$env:ARM_CLIENT_ID = "<app-id>" ; $env:ARM_CLIENT_SECRET = "<secret>"   # service principal$env:ARM_USE_OIDC = "true"                                             # pipeline with workload identity$env:TF_VAR_admin_password = "<secret>"       # set a variable without a file$env:TF_LOG = "DEBUG" ; $env:TF_LOG_PATH = "tf.log"Remove-Item Env:TF_VAR_admin_passwordGet-ChildItem Env:ARM_* , Env:TF_*            # what is set?
export ARM_SUBSCRIPTION_ID="<sub>"export ARM_TENANT_ID="<tenant>"export ARM_CLIENT_ID="<app-id>" ARM_CLIENT_SECRET="<secret>"           # service principalexport ARM_USE_OIDC="true"                                             # pipeline with workload identityexport TF_VAR_admin_password="<secret>"       # set a variable without a fileexport TF_LOG="DEBUG" TF_LOG_PATH="tf.log"unset TF_VAR_admin_passwordenv | grep -E '^(ARM_|TF_)'                   # what is set? (secrets are shown – do not share the output)

#Quality and Security

Linting and security scans before apply.

PowerShell
tflint --init ; tflint --recursive               # linter (provider rules)checkov -d .                                     # security checkstrivy config .                                   # alternative to checkovterraform-docs markdown table . > README.md      # generate module docs

#.gitignore

What must never end up in the repo.

Text
.terraform/*.tfstate*.tfstate.*tfplan*.tfplancrash.logcrash.*.logterraform.tfvars*.auto.tfvarsoverride.tfoverride.tf.json

Commit .terraform.lock.hcl (pinned provider versions).

Code and DevOps

#Bicep / ARM

In a team repo: Bicep in a team repo · Step by step: Terraform and Bicep Workflows

PowerShell
az bicep install ; az bicep upgrade ; az bicep versionaz bicep build -f main.bicep                          # -> ARM JSONaz bicep build-params -f main.bicepparam              # parameter file -> JSONaz bicep decompile -f template.json                   # ARM -> Bicepaz bicep lint -f main.bicep                           # linter # Check (changes nothing)az deployment group validate -g <rg> -f main.bicep -p main.bicepparamaz deployment group what-if  -g <rg> -f main.bicep -p main.bicepparamaz deployment sub   what-if  -l <region> -f main.bicep -p main.bicepparam # Deployaz deployment group create -g <rg> -f main.bicep -p main.bicepparamaz deployment group create -g <rg> -f main.bicep -p main.bicepparam -p adminPassword=<value>   # override a single parameteraz deployment sub   create -l <region> -f main.bicep -p main.bicepparamaz deployment group create -g <rg> -f main.bicep --mode Complete    # deletes everything not in the template (careful!) # History and errorsaz deployment group list -g <rg> --query "[].{name:name, state:properties.provisioningState, time:properties.timestamp}" -o tableaz deployment operation group list -g <rg> -n <deployment> --query "[?properties.provisioningState=='Failed'].properties.statusMessage"az deployment group cancel -g <rg> -n <deployment> # Exportaz group export -n <rg> > export.json

#Building Blocks

Parameters, variables, resources, modules and outputs in short form.

Bicep
param location string = resourceGroup().location@secure()param adminPassword string@allowed(['dev', 'test', 'prod'])param environment string var tags = { environment: environment, owner: 'network' } resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = {  name: 'vnet-hub'                       // reference an existing resource} module spoke 'modules/spoke.bicep' = {  name: 'spoke-deploy'  params: { location: location, tags: tags }} resource nsgs 'Microsoft.Network/networkSecurityGroups@2024-05-01' = [for n in ['a', 'b']: {  name: 'nsg-${n}'  location: location}] output vnetId string = vnet.id
Careful with subnets: subnets defined inline in the VNet → a redeployment removes subnets that are not in the list, and NSG/route table associations that are not in the template. Always run what-if first.
Code and DevOps

#Bicep in a Team Repo

Basic commands: Bicep / ARM · Pipeline: Pipelines · Step by step: Terraform and Bicep Workflows

Top 5: az bicep build · az bicep lint · az deployment group what-if · az deployment group create -p <file>.bicepparam · az deployment operation group list

#Typical Folder Structure

Where main, modules and parameter files live.

text
infra/├── main.bicep                  # entry point, calls modules├── modules/│   ├── network.bicep│   └── appgateway.bicep├── params/│   ├── dev.bicepparam          # values per environment│   └── prod.bicepparam└── bicepconfig.json            # linter rules

Rule: logic in .bicep, values in .bicepparam. A PR that only changes values only touches params/.

#Parameter File per Environment (.bicepparam)

One parameter file per environment and how secrets come in.

Bicep
// params/dev.bicepparamusing '../main.bicep' param environment = 'dev'param location = 'westeurope'param vnetAddressSpace = '10.10.0.0/22'param tags = {  Environment: 'dev'  Owner: '<team>'}param adminPassword = readEnvironmentVariable('ADMIN_PASSWORD')   // secret not in the file
$env:ADMIN_PASSWORD = "<secret>"                         # this session onlyaz bicep build-params -f params/dev.bicepparam           # checks that all required parameters are set
export ADMIN_PASSWORD="<secret>"                         # this session onlyaz bicep build-params -f params/dev.bicepparam           # checks that all required parameters are set

Compare environments:

PowerShell
git diff --no-index params/dev.bicepparam params/prod.bicepparamcode --diff params/dev.bicepparam params/prod.bicepparam    # side by side in VS Code

#Check Before the Pull Request

Format, lint and build before you open the PR.

PowerShell
az bicep format -f main.bicep                            # consistent formattingaz bicep lint   -f main.bicep                            # linter warningsaz bicep build  -f main.bicep --stdout > $null           # check syntax/types, write nothingaz deployment group validate -g <rg> -f main.bicep -p params/dev.bicepparamaz deployment group what-if  -g <rg> -f main.bicep -p params/dev.bicepparam `  --exclude-change-types NoChange Ignore                 # show real changes only

Reading what-if:

SymbolMeaning
+ Createwill be created
~ Modifywill be changed – details below
- Deletewill be deleted (only in Complete mode or with stacks)
= NoChangestays the same
* Ignorenot in the template – left untouched
! Deploycannot be predicted, will be rewritten

Pasting the what-if output into the PR helps the reviewer a lot.

#Deploy and Find Errors

Deploy with a unique name and find the failed resource.

PowerShell
$name = "deploy-$(Get-Date -Format yyyyMMdd-HHmm)"az deployment group create -g <rg> -n $name -f main.bicep -p params/dev.bicepparamaz deployment group create -g <rg> -n $name -f main.bicep -p params/dev.bicepparam -p location=northeurope   # override one value (newer CLI) # What went wrong?az deployment group show -g <rg> -n $name --query "properties.error" -o jsonaz deployment operation group list -g <rg> -n $name `  --query "[?properties.provisioningState=='Failed'].{res:properties.targetResource.resourceName, type:properties.targetResource.resourceType, error:properties.statusMessage.error.message}" -o table # Historyaz deployment group list -g <rg> --query "sort_by([].{name:name, status:properties.provisioningState, time:properties.timestamp}, &time)" -o table

Tip: macOS / Linux (Bash): name="deploy-$(date +%Y%m%d-%H%M)", variables without $ on assignment, line continuation \. Subscription or management group level: az deployment sub create -l <region> … or az deployment mg create -m <mg-id> -l <region> ….

#Modules from the Registry (AVM)

Azure Verified Modules (AVM) are reviewed Microsoft modules from the public registry:

Bicep
module vnet 'br/public:avm/res/network/virtual-network:<version>' = {  name: 'vnet'  params: {    name: 'vnet-hub-dev-weu-01'    addressPrefixes: [ '10.10.0.0/22' ]  }}
PowerShell
az bicep restore -f main.bicep                           # download modules (cache)

Current versions: https://aka.ms/avm – always pin the version.

#Deployment Stacks

A stack remembers which resources it created and can clean up removed resources or protect them from changes.

PowerShell
az stack group create -n <stack> -g <rg> -f main.bicep -p params/dev.bicepparam `  --action-on-unmanage detachAll --deny-settings-mode noneaz stack group list -g <rg> -o tableaz stack group show -n <stack> -g <rg> --query "resources[].id" -o tsvaz stack group delete -n <stack> -g <rg> --action-on-unmanage detachAll    # stack gone, resources stay

--action-on-unmanage deleteAll deletes resources removed from the template – careful in prod.

#bicepconfig.json

Linter rules and settings for the whole team.

JSON
{  "analyzers": {    "core": {      "rules": {        "no-hardcoded-location": { "level": "warning" },        "no-unused-params":      { "level": "warning" },        "secure-parameter-default": { "level": "error" },        "use-recent-api-versions":  { "level": "warning" }      }    }  }}
Clients and Logs

#VM, Linux and Windows Workflows

Single commands: CLI Basics · Linux · Windows / PowerShell · Azure Virtual Desktop (AVD) · Backup and Site Recovery · Azure Update Manager

Virtual machines first, then what to do on the machine itself. Open a workflow, adjust the variables at the top, run block by block.

#VM: run a command, reset password, console

Changes things

Fix a VM without signing in: run a command, reset the password or SSH key, read the boot log.

PowerShell
$rg = "<rg>" ; $vm = "<vm>"# run a command without signing inaz vm run-command invoke -g $rg -n $vm --command-id RunShellScript --scripts "hostname; ip a; ip r" --query "value[0].message" -o tsvaz vm run-command invoke -g $rg -n $vm --command-id RunPowerShellScript --scripts "ipconfig /all" --query "value[0].message" -o tsv# reset password / SSH keyaz vm user update -g $rg -n $vm -u <user> -p <new-password>az vm user update -g $rg -n $vm -u <user> --ssh-key-value "$(Get-Content $HOME\.ssh\id_rsa.pub)"# when nothing else works: boot log and serial consoleaz vm boot-diagnostics enable -g $rg -n $vmaz vm boot-diagnostics get-boot-log -g $rg -n $vmaz extension add -n serial-consoleaz serial-console connect -g $rg -n $vm

Tip: macOS / Linux (Bash): variables as rg="<rg>"; vm="<vm>", and the key as --ssh-key-value "$(cat ~/.ssh/id_rsa.pub)".

#Stop / start all VMs in a resource group

Changes things

Deallocate or start every VM of a resource group at once – no compute costs while stopped.

PowerShell
$rg = "<rg>"az vm deallocate --ids (az vm list -g $rg --query "[].id" -o tsv) --no-wait     # no compute costsaz vm start      --ids (az vm list -g $rg --query "[].id" -o tsv) --no-waitaz vm list -d -g $rg --query "[].{name:name, state:powerState}" -o table

Bash: az vm deallocate --ids $(az vm list -g $rg --query "[].id" -o tsv)

#Find a Marketplace image and accept the terms

Changes things

Look up publisher, offer and plan of a Marketplace image and accept its terms before the first deployment.

Example FortiGate:

PowerShell
$loc = "<region>"az vm image list-offers -l $loc -p fortinet --query "[?contains(name,'fortigate')].name" -o tsvaz vm image list-skus   -l $loc -p fortinet -f fortinet_fortigate-vm -o tableaz vm image list -p fortinet -f fortinet_fortigate-vm -s <sku> -l $loc --all --query "[].version" -o tsv$urn = "fortinet:fortinet_fortigate-vm:<sku>:latest"az vm image terms show   --urn $urn --query acceptedaz vm image terms accept --urn $urn

For Terraform: set plan { publisher, product, name } in the VM resource.

#Before deploying: check zones and quota

Read-only

Check whether a VM size is available in the region and zone, and whether the quota is enough.

PowerShell
$loc = "<region>" ; $sku = "<vm-size>"az vm list-skus -l $loc --size $sku --query "[].{sku:name, zones:locationInfo[0].zones, restriction:restrictions[0].reasonCode}" -o tableaz vm list-usage -l $loc --query "[?contains(name.value,'Total') || contains(localName,'<family>')].{name:localName, used:currentValue, limit:limit}" -o table

Empty zones or a restriction = SKU not available in this subscription/zone.

#Linux: test web server showing the hostname

Changes things

Tiny web server that answers with its hostname – ideal to test load balancers.

Bash
sudo apt update && sudo apt install -y nginxecho "ok $(hostname) $(hostname -I)" | sudo tee /var/www/html/index.htmlsudo systemctl enable --now nginxcurl -s localhostsudo ufw allow 80/tcp 2>/dev/null || true

Ideal for testing load balancers, App Gateways or VIPs – the response shows which server answered.

#Linux: service does not start

Changes things

Status, logs, restart and listening port of a failing service.

Bash
d=<service>sudo systemctl status $d --no-pagersudo journalctl -u $d -n 50 --no-pagersudo systemctl restart $d && sudo systemctl is-active $dsudo ss -tlnp | grep -i $d                  # is it listening?

#Linux: disk full

Read-only

Find the biggest directories and files, clean up logs and caches.

Bash
df -hsudo du -xh / --max-depth=1 2>/dev/null | sort -h | tail -10sudo du -xh /var/log --max-depth=1 | sort -h | tailsudo journalctl --vacuum-size=200Msudo apt clean

#Check a connection step by step

Read-only

DNS, route, port, TLS/HTTP – the four checks in order.

$target = "<host-or-ip>" ; $port = 443Resolve-DnsName $target                                        # 1. DNSFind-NetRoute -RemoteIPAddress (Resolve-DnsName $target -Type A)[0].IPAddress | Select -Last 1   # 2. routeTest-NetConnection $target -Port $port                         # 3. portcurl.exe -v -m 10 "https://$target"                            # 4. TLS/HTTP
target="<host-or-ip>"; port=443ip=$(dig +short "$target" | tail -1)                           # 1. DNSip route get "$ip"                                             # 2. route (macOS: route -n get "$ip")nc -zv "$target" "$port"                                       # 3. portcurl -v -m 10 "https://$target"                                # 4. TLS/HTTP
Clients and Logs

#Linux

Step by step: VM, Linux and Windows Workflows

Top 5: ip a · ip r get <target> · ss -tlnp · curl -v · sudo tcpdump -ni eth0 host <ip>

#Network

Addresses, routes, sockets and connections.

Bash
ip a                                   # IP addressesip -br a                               # briefip r                                   # routesip r get <target>                      # which path to a targetip neigh                               # ARP tabless -tlnp                               # listening TCP ports with processss -tunp                               # active connectionsss -s                                  # statisticscurl -s ifconfig.me; echo              # own public (egress) IPcurl -sI https://<target> | head -1    # HTTP statuscurl -v -m 10 https://<target> -o /dev/null      # error reason (TLS, timeout)curl -k --resolve <host>:443:<ip> https://<host>/    # test a host against a specific IPcurl -o /dev/null -s -w "%{http_code} %{time_total}s\n" https://<target>   # status + timenc -zv <ip> <port>                     # port reachable?ping -c 4 <ip>                         # ttl drops by 1 per routerping -M do -s 1372 <ip>                # test MTU (DF bit)tracepath <ip> ; traceroute <ip> ; mtr <ip>sudo tcpdump -ni eth0 host <ip> and port 443sudo tcpdump -ni any icmpsudo tcpdump -ni eth0 -w capture.pcap  # for Wiresharksysctl net.ipv4.ip_forward             # IP forwarding on?sudo netplan apply                     # Ubuntu: reload network

#DNS

Resolve names and check which DNS server is used.

Bash
nslookup <name> ; dig +short <name> ; dig @<dns> <name>dig <name> +traceresolvectl status                      # which DNS server per interfaceresolvectl flush-cachescat /etc/resolv.confgetent hosts <name>                    # how the system resolves (incl. /etc/hosts)

#Firewall and Services

ufw, iptables and systemd services.

Bash
sudo ufw status verbose ; sudo ufw allow 443/tcp ; sudo ufw delete allow 443/tcpsudo iptables -L -n -vsudo iptables -t nat -L -n -vsudo nft list rulesetsudo systemctl status <service> ; sudo systemctl restart <service> ; sudo systemctl enable --now <service>systemctl list-units --type=service --state=failedsudo journalctl -u <service> -fsudo journalctl -b -p errsudo journalctl --since "1 hour ago"

#System

Version, disk, memory, processes.

Bash
hostnamectl ; uname -a ; cat /etc/os-releasedf -h ; du -sh * ; free -h ; top ; htoplsblk ; sudo blkiduptime ; who ; last -n 10sudo apt update && sudo apt upgrade -ysudo apt install -y curl dnsutils traceroute mtr-tools tcpdump nginxtail -f /var/log/syslogsudo cat /var/log/cloud-init-output.log        # Azure: custom data / cloud-initsudo waagent --version                         # Azure Linux agent

#Files and Text

Search, filter and edit files and text.

Bash
grep -rin "<text>" <folder>            # search recursivelyfind . -name "*.tf" -mtime -1          # changed in the last 24 hsed -i 's/old/new/g' <file>tar czf package.tgz <folder> ; tar xzf package.tgzsha256sum <file>chmod 600 <file> ; chown <user>:<group> <file>

#Mini Tests

Small loops for continuous ping, port and web checks.

Bash
while true; do curl -s -m 2 ifconfig.me; echo " $(date +%T)"; sleep 1; done      # continuous failover testwhile true; do ping -c1 -W1 <ip> >/dev/null && echo "$(date +%T) OK" || echo "$(date +%T) FAIL"; sleep 1; donewhile true; do nc -zvw1 <ip> 443 2>&1 | tail -1; sleep 1; done                     # continuous port testsudo python3 -m http.server 80                                                   # quick test web serverecho "ok $(hostname)" | sudo tee /var/www/html/index.html                        # nginx test page with hostname
Clients and Logs

#Windows / PowerShell

Step by step: VM, Linux and Windows Workflows

Top 5: Test-NetConnection <ip> -Port 443 · Resolve-DnsName · ipconfig /all · curl.exe -v · Get-NetTCPConnection -State Listen

#Network

IP configuration, adapters, routes and connections.

PowerShell
ipconfig /allGet-NetIPConfigurationGet-NetIPAddress -AddressFamily IPv4 | Format-Table InterfaceAlias, IPAddress, PrefixLengthGet-NetRoute -AddressFamily IPv4 | Sort DestinationPrefix | Format-Tableroute printFind-NetRoute -RemoteIPAddress <target>              # which path to a targetTest-NetConnection <ip> -Port 443                   # port reachable?Test-NetConnection <ip> -Port 443 -InformationLevel DetailedTest-NetConnection <ip> -TraceRoutetracert <ip> ; pathping <ip>ping -f -l 1372 <ip>                                # test MTU (DF bit)Get-NetTCPConnection -State Listen | Sort LocalPortnetstat -ano | findstr :443                         # port -> process IDGet-Process -Id <pid>arp -acurl.exe -v https://<target>                        # real curl (not the PowerShell alias)curl.exe -k --resolve <host>:443:<ip> https://<host>/(Invoke-RestMethod https://api.ipify.org)           # own public IPInvoke-WebRequest https://<target> -UseBasicParsing | Select StatusCode

#DNS

Resolve names, flush the cache, check servers.

PowerShell
Resolve-DnsName <name> [-Server <dns>]Resolve-DnsName <name> -Type A -DnsOnly             # without hosts file/cachenslookup <name> <dns>ipconfig /flushdns ; Clear-DnsClientCacheGet-DnsClientServerAddress -AddressFamily IPv4Get-DnsClientCache | Where Entry -like "*<part>*"notepad C:\Windows\System32\drivers\etc\hosts       # as administrator

#Windows Firewall

Show profiles and create or remove rules.

PowerShell
Get-NetFirewallProfile | Select Name, EnabledNew-NetFirewallRule -DisplayName "Allow 443" -Direction Inbound -Protocol TCP -LocalPort 443 -Action AllowNew-NetFirewallRule -DisplayName "Allow ICMP" -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action AllowGet-NetFirewallRule -DisplayName "*<part>*" | Format-Table DisplayName, Enabled, Direction, ActionRemove-NetFirewallRule -DisplayName "Allow 443"

#Files and Scripts

Unblock downloads, execution policy, search and edit files.

PowerShell
Set-ExecutionPolicy -Scope Process Bypass       # allow scripts for this session onlyGet-ChildItem -Recurse | Unblock-File           # remove the download blockGet-ChildItem -Recurse *.ps1 | Unblock-File     # scripts onlyExpand-Archive .\package.zip -DestinationPath .\packageCompress-Archive .\folder -DestinationPath .\package.zipCopy-Item <source> <target> ; Move-Item ; Remove-Item -RecurseGet-FileHash <file>                             # checksum (SHA256)Get-Content <file> -Tail 50 -Wait               # like tail -fSelect-String -Path *.tf -Pattern "<text>"      # like grepGet-ChildItem -Recurse -Filter *.tf | Select-String "<text>"     # recursiveGet-ChildItem -Recurse | Where LastWriteTime -gt (Get-Date).AddDays(-1)$env:PATH -split ';'[IO.File]::WriteAllText("<path>", $text, (New-Object Text.UTF8Encoding($false)))   # UTF-8 without BOM$PSVersionTable.PSVersionwinget install Microsoft.AzureCLI ; winget install Hashicorp.Terraform ; winget install Git.Git

#System and Events

System info, services and event logs.

PowerShell
hostname ; whoami ; whoami /groupsGet-ComputerInfo | Select OsName, OsVersion, CsNameGet-Service | Where Status -eq RunningRestart-Service <name>Get-WinEvent -LogName System -MaxEvents 50 | Format-Table TimeCreated, Id, LevelDisplayName, Message -WrapGet-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=(Get-Date).AddHours(-24)}   # errors in 24 hGet-ChildItem Cert:\LocalMachine\My | Format-Table Subject, NotAfter, ThumbprintRestart-Computer -Force

#Continuous Tests

Loops for continuous web and port checks.

PowerShell
while ($true) { "$((Get-Date).ToString('HH:mm:ss')) $(curl.exe -s -m 2 http://<ip>)"; Start-Sleep 1 }while ($true) { $r = Test-NetConnection <ip> -Port 443 -WarningAction SilentlyContinue; "$((Get-Date).ToString('HH:mm:ss')) $($r.TcpTestSucceeded)"; Start-Sleep 1 }
Clients and Logs

#Log Analytics (KQL)

Run: Portal → Log Analytics workspace → Logs, or az monitor log-analytics query

#Skeleton

The basic shape of a query: table, time, filter, columns.

KQL
<Table>| where TimeGenerated > ago(1h)| where <field> == "<value>"            // exact (case-sensitive)| where <field> =~ "<value>"            // exact, case-insensitive| where <field> has "<word>"            // whole word (fast)| where <field> contains "<part>"       // substring (slower)| project TimeGenerated, <fields>| order by TimeGenerated desc| take 100 // Count / group / chart| summarize Count = count() by <field>, bin(TimeGenerated, 5m)| render timechart // Which tables have data?search * | where TimeGenerated > ago(1h) | summarize count() by $table // Columns of a table<Table> | getschema

#Application Gateway and WAF

Blocked requests, rules and access logs.

KQL
// WAF – blocked/detected requests (AzureDiagnostics mode)AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog" and action_s in ("Blocked","Matched","Detected")| project TimeGenerated, hostname_s, requestUri_s, clientIp_s, ruleGroup_s, ruleId_s, Message, details_message_s| order by TimeGenerated desc // Most frequent WAF rules (false-positive candidates)AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog"| summarize Count = count() by ruleId_s, ruleGroup_s, Message| order by Count desc // Access (AzureDiagnostics mode)AzureDiagnostics| where Category == "ApplicationGatewayAccessLog"| project TimeGenerated, host_s, requestUri_s, httpStatus_d, serverStatus_s, serverRouted_s, timeTaken_d // Requests with errors (resource-specific table)AGWAccessLogs| where TimeGenerated > ago(1h) and HttpStatus >= 400| project TimeGenerated, Host, RequestUri, HttpStatus, ServerStatus, ServerRouted, ClientIp| order by TimeGenerated desc // WAF (resource-specific table)AGWFirewallLogs| where TimeGenerated > ago(1h)| project TimeGenerated, Action, RuleId, Message, ClientIp, RequestUri, Hostname

Which variant applies is set in the diagnostic setting (Azure diagnostics vs. Resource specific).

#Azure Activity

Who changed or deleted what.

KQL
// Who changed what?AzureActivity| where TimeGenerated > ago(1d) and ActivityStatusValue == "Success"| project TimeGenerated, Caller, OperationNameValue, ResourceGroup, _ResourceId // DeletionsAzureActivity| where OperationNameValue endswith "/DELETE" and ActivityStatusValue == "Success"| project TimeGenerated, Caller, _ResourceId // Failed deployments / policy denialsAzureActivity| where ActivityStatusValue == "Failure"| project TimeGenerated, Caller, OperationNameValue, Properties

#Network

Flow logs and denied traffic.

KQL
// NSG flow logs / Traffic Analytics (old)AzureNetworkAnalytics_CL| where SubType_s == "FlowLog" and FlowStatus_s == "D"        // D = denied| project TimeGenerated, SrcIP_s, DestIP_s, DestPort_d, NSGRule_s // VNet flow logs / Traffic Analytics (new)NTANetAnalytics| where TimeGenerated > ago(1h)| take 50                                                      // check fields with getschema // Azure Firewall – denied (resource-specific tables)AZFWNetworkRule| where TimeGenerated > ago(1h) and Action == "Deny"| project TimeGenerated, SourceIp, DestinationIp, DestinationPort, Protocol

#FortiGate (Syslog/CEF)

Firewall logs in Log Analytics.

KQL
CommonSecurityLog| where DeviceVendor == "Fortinet"| where TimeGenerated > ago(1h)| project TimeGenerated, SourceIP, DestinationIP, DestinationPort, DeviceAction, Activity| order by TimeGenerated desc // Denied connections by destinationCommonSecurityLog| where DeviceVendor == "Fortinet" and DeviceAction in ("deny","block")| summarize Count = count() by DestinationIP, DestinationPort| order by Count desc

#VMs

Heartbeat, syslog errors and Windows events.

KQL
// Last heartbeat per VMHeartbeat | summarize last = max(TimeGenerated) by Computer | order by last asc // Linux syslog errorsSyslog | where SeverityLevel in ("err","crit") | project TimeGenerated, Computer, ProcessName, SyslogMessage // Windows eventsEvent | where EventLevelName == "Error" | project TimeGenerated, Computer, Source, RenderedDescription
Clients and Logs

#Microsoft Sentinel (KQL)

KQL basics: KQL · Defender: Defender

Ready-to-use hunting queries for the Sentinel workspace – paste into Sentinel → Logs or use as an analytics rule (scheduled query).

#Setup in Short

Enable Sentinel and connect data sources.

StepWhere
Enable Sentinel on the Log Analytics workspaceSentinel → + Create → choose workspace
Data connectorsContent hub → install solution (e.g. Microsoft Entra ID, Azure Activity, Microsoft Defender XDR, Fortinet FortiGate via CEF/AMA) → connector page
Analytics rulesAnalytics → Rule templates – enable templates or create Scheduled query rule with the queries below
AutomationAutomation rules (assign, close, tag) and playbooks (Logic Apps)
WatchlistsWatchlists – e.g. admin accounts, VIPs, known IPs; in KQL _GetWatchlist('<name>')

Costs: ingestion per GB – check Usage, use Basic/Auxiliary logs for bulk data (e.g. firewall logs) and set retention per table.

#Sign-ins (Entra ID)

Failed sign-ins, password spray, risky locations.

KQL
// Failed sign-ins per user and IP, last 24 h (possible password spray / brute force)SigninLogs| where TimeGenerated > ago(24h) and ResultType != "0"| summarize Failures = count(), Apps = make_set(AppDisplayName, 5) by UserPrincipalName, IPAddress, Location = tostring(LocationDetails.countryOrRegion)| where Failures > 10| order by Failures desc // Password spray: one IP, many different usersSigninLogs| where TimeGenerated > ago(1h) and ResultType in ("50126", "50053")| summarize Users = dcount(UserPrincipalName), Attempts = count() by IPAddress| where Users > 10| order by Users desc // Successful sign-in from a new country (compared to the last 14 days)let known = SigninLogs | where TimeGenerated between (ago(14d) .. ago(1d)) and ResultType == "0"  | summarize by UserPrincipalName, Country = tostring(LocationDetails.countryOrRegion);SigninLogs| where TimeGenerated > ago(1d) and ResultType == "0"| extend Country = tostring(LocationDetails.countryOrRegion)| join kind=leftanti known on UserPrincipalName, Country| project TimeGenerated, UserPrincipalName, Country, IPAddress, AppDisplayName // Sign-ins without MFA for admin accounts (watchlist "Admins" with column UPN)let admins = _GetWatchlist('Admins') | project UPN = tostring(SearchKey);SigninLogs| where TimeGenerated > ago(7d) and ResultType == "0" and AuthenticationRequirement == "singleFactorAuthentication"| where UserPrincipalName in~ (admins)| project TimeGenerated, UserPrincipalName, AppDisplayName, IPAddress

Common error codes: 50126 wrong password · 50053 account locked · 50074/50076 MFA required · 53003 blocked by conditional access.

#Privileged Changes (Entra ID and Azure)

Role assignments and admin changes.

KQL
// Role assignments in Entra ID (e.g. Global Administrator)AuditLogs| where TimeGenerated > ago(7d) and OperationName has "Add member to role"| extend Role = tostring(TargetResources[0].modifiedProperties[1].newValue), Target = tostring(TargetResources[0].userPrincipalName),         Actor = tostring(InitiatedBy.user.userPrincipalName)| project TimeGenerated, Actor, Target, Role // New app secret / certificate on an app registrationAuditLogs| where TimeGenerated > ago(7d) and OperationName has_any ("Add service principal credentials", "Update application – Certificates and secrets management")| project TimeGenerated, OperationName, App = tostring(TargetResources[0].displayName), Actor = tostring(InitiatedBy.user.userPrincipalName) // Azure role assignments (Owner / User Access Administrator)AzureActivity| where TimeGenerated > ago(7d) and OperationNameValue =~ "Microsoft.Authorization/roleAssignments/write" and ActivityStatusValue == "Success"| project TimeGenerated, Caller, _ResourceId, Properties // Deleted resources, diagnostic settings or locksAzureActivity| where TimeGenerated > ago(1d) and ActivityStatusValue == "Success"| where OperationNameValue has_any ("/delete") and OperationNameValue has_any ("diagnosticSettings", "locks", "Microsoft.KeyVault", "Microsoft.Network/networkSecurityGroups")| project TimeGenerated, Caller, OperationNameValue, _ResourceId

#Firewall and Network

Denied traffic and top sources from the firewall.

KQL
// FortiGate: top denied sources (CEF via AMA)CommonSecurityLog| where TimeGenerated > ago(1h) and DeviceVendor == "Fortinet" and DeviceAction in ("deny", "block", "dropped")| summarize Denied = count(), Ports = make_set(DestinationPort, 10) by SourceIP| order by Denied desc // Port scan: one source, many destination portsCommonSecurityLog| where TimeGenerated > ago(1h) and DeviceVendor == "Fortinet"| summarize Ports = dcount(DestinationPort) by SourceIP, DestinationIP| where Ports > 50 // Azure Firewall: threat intelligence hitsAZFWThreatIntel| where TimeGenerated > ago(24h)| project TimeGenerated, SourceIp, DestinationIp, DestinationPort, Action, ThreatDescription // One source talking to very many destinations (scan / beaconing)AZFWNetworkRule| where TimeGenerated > ago(1h) and Action == "Allow"| summarize Destinations = dcount(DestinationIp) by SourceIp| where Destinations > 100

#Servers (Windows Security Events)

Failed logons and other security events.

KQL
// Failed logons (4625) per account and hostSecurityEvent| where TimeGenerated > ago(24h) and EventID == 4625| summarize Failures = count() by TargetAccount, Computer, IpAddress| where Failures > 20| order by Failures desc // Member added to a privileged local/domain group (4728, 4732, 4756)SecurityEvent| where TimeGenerated > ago(7d) and EventID in (4728, 4732, 4756)| project TimeGenerated, Computer, SubjectAccount, MemberName, TargetAccount // Security log cleared (1102)SecurityEvent| where TimeGenerated > ago(7d) and EventID == 1102| project TimeGenerated, Computer, SubjectAccount

#Incidents and Rule Health

Open incidents and health of analytics rules.

KQL
// Open incidents by severitySecurityIncident| where TimeGenerated > ago(30d)| summarize arg_max(TimeGenerated, *) by IncidentNumber| where Status != "Closed"| summarize Count = count() by Severity, Title| order by Severity asc, Count desc // Analytics rules that failed to runSentinelHealth| where TimeGenerated > ago(1d) and SentinelResourceType == "Analytics Rule" and Status != "Success"| project TimeGenerated, SentinelResourceName, Status, Description // Ingestion per table (cost check)Usage| where TimeGenerated > ago(30d) and IsBillable| summarize GB = round(sum(Quantity) / 1024, 2) by DataType| order by GB desc

Analytics rule from a query: Analytics → + Create → Scheduled query rule → paste query → Run query every 1 h, Lookup data from the last 1 h → entity mapping (Account, IP, Host) → incident settings. SentinelHealth needs Settings → Health monitoring enabled.

Look up

#Error → Fix

Tip: search for the text of the error message with Ctrl+F.

#Azure General

Error message, cause, fix – policy, quota and permission errors.

SymptomCauseFix
RequestDisallowedByPolicyAzure Policyaz policy assignment list, exemption or policy team
ResourceGroupNotFoundresource group does not exist (CLI/PowerShell never create it) or wrong subscription selectedaz group create -n <rg> -l <region>, check az account show
AuthorizationFailedrole missing on this scopeaz role assignment list --assignee <x> --all
RoleAssignmentExists (409)role assigned twiceuse/import the existing assignment
PrincipalNotFoundidentity just created (replication)wait briefly, pass --assignee-principal-type
ResourceAvailabilityZonesCannotBeModifiedtwo resources with the same name, different zonename templates with a placeholder (unique per instance)
SKU "not available in zone X"zone not available in this subscriptionaz vm list-skus … locationInfo
OperationNotAllowed … quotavCPU quota exhaustedaz vm list-usage, request a quota increase
MarketplacePurchaseEligibilityFailed / termsMarketplace terms not acceptedaz vm image terms accept --urn …
ScopeLockedlock on RG/resourceaz lock list, remove the lock temporarily
InUseSubnetCannotBeDeletedsubnet still in use (NIC, PE, delegation)az network vnet subnet show … ipConfigurations
Forbidden on Key Vaultdata role or vault firewallrole Key Vault Secrets User, network rules
module … blocked by software restriction policiesmodule lies in the user profile (Documents\…\Modules), blocked by company policyas admin: Install-Module <module> -Scope AllUsers, rename the profile copy – Install Modules Without the User Profile

#Network

Error message, cause, fix – peering, routes, NSG, DNS.

SymptomCauseFix
Portal: DisablePeeringRoute is not supportedpreview feature in the portalcreate the route table via CLI/Terraform
VM has no internetdefault outbound off, no route/policycheck NVA/NAT GW, UDR and firewall policy
Ping to the internet fails, curl worksLB outbound does not translate ICMPtest with curl
No traffic spoke ↔ spokepolicy missing / peering without forwarded trafficfirewall policy, peering settings
Peering Disconnectedother side deleted/missingrecreate peering on both sides
Peering RemoteNotInSyncaddress space changedaz network vnet peering sync
Connection drops after secondsasymmetric routingUDR on both sides, SNAT on the NVA
Private endpoint resolves publiclyzone not linked to the DNS VNetprivate DNS link, check the VNet's DNS servers
NVA does not forwardIP forwarding on the NIC offaz network nic update --ip-forwarding true
LB does not distributeprobe failsallow probe port, route to 168.63.129.16
VPN connection NotConnectedPSK/proposal/selectors, UDP 500/4500gateway logs, IPsec on the other side

#Application Gateway / WAF

Backend health, probes, certificates and WAF blocks.

SymptomCauseFix
Backend Timeoutwrong port/protocol, network pathFQDN + HTTPS for App Service, check path
Backend 403name resolves publiclylink the private DNS zone to the gateway VNet
Backend Cannot connectpeering/NSG/NVABackend health → Troubleshoot
Backend certificate CN mismatch / not trustedhost header/certificate does not matchpick host name from backend, trusted root cert
Browser 502all backends unhealthyfix backend health
Browser 403WAF rulefirewall log (KQL), exclusion/rule override
Gateway Failed after a changeKey Vault certificate not readableidentity role, versionless secret ID, KV firewall
Deployment hangs / subnet errorUDR 0/0 or NSG blocks GatewayManagerremove UDR, add NSG rules

#FortiGate

Typical symptoms on the firewall and where to look first.

SymptomCauseFix
Sniffer shows only in, no outfirewall dropsdebug flow (policy/route)
Sniffer shows nothingtraffic does not arriveUDR/effective routes, NSG, peering
Denied by forward policy checkno matching policycreate policy / check order
reverse path check failroute to the source missingadd a static route
HA: secondary does not take the configend forgotten / out of syncend, diagnose sys ha checksum cluster, execute ha synchronize start
HA: switches back after unsetoverride disable, uptimediagnose sys ha reset-uptime
Failover: public IP/route does not moveSDN connector without permissionsmanaged identity + roles, diagnose debug application azd -1
GUI empty / no forwardingBYOL without licenseapply license or use PAYG
GUI not reachableallowaccess missing / trusthostset allowaccess https, check admin trusthost
IPsec no SA proposal chosenproposals differalign phase 1/2 on both sides

#Terraform

Plan, state, lock and provider errors.

SymptomCauseFix
must be replacedname/zone/image changedcheck the plan, moved/ignore_changes if needed
State lockedaborted runterraform force-unlock <id> or break the blob lease
already exists – to be managed via Terraform this resource needs to be importedresource already existsimport block or terraform import
Inconsistent dependency lock fileprovider version changedterraform init -upgrade
subscription ID could not be determinedazurerm 4.x without subscriptionARM_SUBSCRIPTION_ID or subscription_id in the provider
403 … AuthorizationPermissionMismatch (backend)data role on the storage account missingStorage Blob Data Contributor
Error acquiring the state lock in a pipelineparallel runserialize pipelines, then force-unlock
429 Too Many Requeststhrottling-parallelism=5, retry later

#SSH and Git

Key, host and push errors.

SymptomCauseFix
Public key authentication failedwrong/no RSA keyRSA key, ssh -vT, ~/.ssh/config
Load key …: error in libcryptoPuTTY / wrongly encoded fileregenerate or PuTTYgen "Export OpenSSH key"
UNPROTECTED PRIVATE KEY FILEfile permissions too openset permissions
REMOTE HOST IDENTIFICATION HAS CHANGEDserver rebuiltssh-keygen -R <host>
post-quantum key exchange warninginfo onlyignore
git@ssh.dev.azure.com's password:SSH key not accepted – Azure DevOps has no SSH passwordCtrl+C, test and debug: key registered? config applied?
Permission denied (publickey) (Azure DevOps)key not registered, wrong account, or config not appliedSSH key for Azure DevOps
ssh-rsa is about to be deprecated … throttledHostkeyAlgorithms +ssh-rsa in ~/.ssh/configremove the +ssh-rsa lines – test and debug
Git: rejected … fetch firstremote has newer commitsgit pull (maybe --allow-unrelated-histories), then push
Git: refusing to merge unrelated historiestwo separate startsgit pull origin main --allow-unrelated-histories
Git: ! [rejected] main -> main (non-fast-forward)local branch behind the servergit pull --rebase origin main → git push, otherwise backup-branch way
Git: nothing to commit although files changedfiles are not in the repo folderpwd, ls, git rev-parse --show-toplevel
Git: repo folder inside a folder of the same namecloned inside <repo>clone in the parent folder
DevOps: main missing, only a feature branchfirst push into an empty repo was the feature branchcreate main and set it as default
Git: CONFLICT (content)same place changedfix the file → git add → git commit
Git: fatal: not a git repositorywrong foldercd into the repo, git status
Git: Authentication failed (HTTPS)PAT expired / old credential storednew PAT; Windows Credential Manager: delete entry git:https://dev.azure.com
DevOps: TF402455: Pushes to this branch are not permittedbranch policy, PR onlyfeature branch + PR
DevOps PR: "There are merge conflicts"target branch changed the same linesresolve the conflict locally
DevOps PR: Complete not possiblerequired reviewer missing / build check redadd reviewer, check policies on the right side of the PR
DevOps PR: approval gone after pushpolicy "Reset votes on new pushes"ask the reviewer to Approve again
Dev branch deleted after PR"Delete source branch after merging" was onrecreate the branch: Repos → Branches → New branch (based on main)
Look up

#Glossary

Terms from the portal, DevOps and networking – explained briefly. On the web page: just search at the top.

#Git and Azure DevOps

Repo, branch, pull request and related terms.

TermExplanation
Repository (repo)folder with files and their complete change history
Clonecopy a repo from the server to your own machine
Commitsaved state with a message, local
Push / Pullsend commits to the server / get them from there
Fetchget the server state without changing your own files
Branchseparate line of work, e.g. main, Dev, feature/x
Mergetake the changes of one branch into another
Rebase"replay" your own commits onto the latest state of another branch
Squashcombine several commits into one
Cherry-pickcopy one specific commit into another branch
Merge conflictthe same place was changed in both branches – you have to decide
Stashput uncommitted changes aside
HEADthe commit you are currently on
Origindefault name for the repo on the server
Pull request (PR)request to take a branch into another – with review
Reviewerreviews and approves the PR (Required = mandatory, Optional = voluntary)
Approvethe reviewer agrees
Completethe PR is merged
Abandonthe PR is closed without merging
Revertcounter-change to an earlier commit/PR
Branch policyprotection of a branch: PR only, minimum number of reviewers, build must be green
Build validationpipeline that runs automatically on every PR
Pipelineautomatic build/deploy, described in YAML
Stage / Job / Stepstructure of a pipeline
Agentmachine the pipeline runs on
Environmenttarget in DevOps with approvals
Service connectiona pipeline's access to Azure (usually via service principal / workload identity)
Variable groupshared variables/secrets for pipelines
PATpersonal access token – password replacement for Git over HTTPS / CLI

#Azure General

Tenant, subscription, resource group and related terms.

TermExplanation
Tenantone Entra ID instance, e.g. your company's
Management groupfolder for subscriptions, inherits policies and permissions
Subscriptionbilling and permission boundary, contains resource groups
Resource group (RG)logical container for resources with the same lifecycle
Resource IDunique path /subscriptions/…/resourceGroups/…/providers/…
Regiondata center location, e.g. West Europe
Availability zoneseparate data centers within a region
SKUsize/performance tier of a resource
Lockprevents deletion (CanNotDelete) or changes (ReadOnly)
Tagkey/value on resources, e.g. Owner
Deallocatestop a VM and end compute costs
Providere.g. Microsoft.Network – must be registered in the subscription
Activity logwho changed what in Azure and when
Diagnostic settingsends a resource's logs/metrics e.g. to Log Analytics
Log Analytics workspacecentral log database, queried with KQL
Action groupwho is notified on an alert
Landing zoneprepared subscription with network, policies, permissions

#Permissions and Identities

Entra ID, RBAC, service principal, managed identity.

TermExplanation
Entra ID(formerly Azure AD) identity service: users, groups, apps
RBACrole-based access: who (principal) may do what (role) where (scope)
Scopelevel of a role/policy: MG, subscription, RG, resource; / = whole tenant
Role assignmentcombination of principal + role + scope
Principaluser, group, service principal or managed identity
Object IDID of an identity in Entra – needed for role assignments
App registrationdefinition of an application (app ID, secrets)
Service principal (SP)an app's "user account" in the tenant, gets roles
Client ID / App IDthe app's identifier for sign-in
Client secretan app's password – expires!
Managed identitypassword-less identity for Azure resources (system- or user-assigned)
Workload identity federationsign-in without a secret via token, e.g. from pipelines
PIMPrivileged Identity Management – activate roles only when needed and time-limited
Elevate accessGlobal Admin grants themselves User Access Administrator on /
Policyrule that allows/denies/audits resources (Deny, Audit, Modify)
Exemptiontime-limited exception from a policy
Key Vaultsecure store for secrets, keys, certificates
Soft delete / purge protectiondeleted items remain recoverable / may not be permanently deleted

#Network

VNet, peering, UDR, NSG and related terms.

TermExplanation
VNetyour own private network in Azure
Subnetpart of a VNet
Address space / CIDRe.g. 10.0.0.0/22 – /22 = 1024 addresses
Peeringdirect connection of two VNets, not transitive
Hub-and-spokecentral hub VNet (firewall, VPN) with connected spoke VNets
UDR / route tableyour own routes, e.g. 0.0.0.0/0 → firewall
Next hopwhere a packet is sent next
Effective routesall routes that actually apply to a NIC
NSGnetwork security group – simple firewall rules on subnet/NIC
Service tagplaceholder for IP ranges, e.g. AzureLoadBalancer
NVAnetwork virtual appliance, e.g. FortiGate as a VM
ILB / ELBinternal / external load balancer; ILB in front of the FortiGate for HA
Health probeLB checks whether a backend responds (source 168.63.129.16)
Floating IPdestination address stays the LB's frontend IP
HA portsone LB rule for all ports/protocols
SNAT / DNATsource / destination address is rewritten
Asymmetric routingreturn path does not use the same firewall → connection breaks
Private endpointprivate IP for a PaaS service in your own VNet
Private DNS zoneresolves e.g. *.privatelink.… to private IPs
Application Gatewaylayer 7 load balancer for HTTP(S), optionally with WAF
WAFweb application firewall (Detection = log only, Prevention = block)
Listenerport/host name on which the App Gateway accepts requests
Backend poolservers behind the App Gateway / LB
NAT Gatewayfixed outbound internet access for a subnet
Bastionsecure RDP/SSH access via the browser, without a public IP on the VM
VPN gateway / local network gatewayAzure side / representation of the remote side of an S2S VPN
BGProuting protocol for automatic exchange of routes

#FortiGate

FGCP, VIP, policy, SD-WAN and related terms.

TermExplanation
FGCPFortiGate Clustering Protocol – high availability active/passive
Primary / Secondaryactive / passive device in HA
Session pickupconnections survive a failover
SDN connectorconnection to Azure, rewrites routes/IPs on failover
Policyfirewall rule (top to bottom, first match wins)
VIPvirtual IP – port forwarding/DNAT to the inside
Address / address groupaddress object / group of addresses
Snifferpacket capture on the FortiGate
Debug flowshows why a packet is allowed or dropped
VDOMvirtual domain – separate FortiGate instances on one device
Phase 1 / Phase 2IPsec: building the management connection / the actual tunnel SAs
SD-WANdistributing traffic across several links by rules and quality
PAYG / BYOL / FortiFlexlicense models: usage-based via Azure / own license / flexible quota

#Infrastructure as Code

IaC, state, module, what-if and related terms.

TermExplanation
IaCinfrastructure as code – resources are described in files instead of clicked
Bicep / ARMMicrosoft's IaC language / the JSON format underneath
.bicepparamparameter file for Bicep, usually one per environment
what-ifpreview of what a deployment would change
Deployment stackBicep deployment that manages its resources and can clean up
AVMAzure Verified Modules – reviewed Microsoft modules
TerraformIaC tool from HashiCorp, provider azurerm
StateTerraform's memory: which resource belongs to which code
Plan / Applypreview / execute in Terraform
Driftdifference between code and the actual resource (e.g. change in the portal)
Idempotentrunning it several times gives the same result
Look up

#Text Templates

Copy, fill in <…>, send. On the web page "Copy block" copies the whole template.

#Pull Request Description

Template for the PR text.

text
## What<Short: what changes, e.g. "New parameters for spoke app-x in dev.bicepparam"> ## Why<Ticket / requirement / bug, e.g. "#1234 – connect app-x to the hub"> ## Impact- Environment: <dev / prod>- New: <resources>- Changed: <resources>- Deleted / replaced: <none / which> ## Checked- [ ] lint / validate without errors- [ ] what-if / plan reviewed (output below)- [ ] tested: <how> ## what-if / plan<paste output here>

#Change Request

Template for a change request.

text
Title: <short description> Description: <what will be done, in 2–3 sentences>Reason: <why, ticket>Affected systems: <subscriptions, resources, applications>Time: <date, from–to>Expected impact: <none / short interruption of approx. X minutes for Y>Risk: <low / medium / high> – <reason>Steps:  1. <step>  2. <step>Test afterwards: <how to check that everything works>Rollback plan: <how it is undone, how long that takes>Performed by: <name>Business contact: <name>

#Outage Notice

Template for the first message during an outage.

text
Subject: [OUTAGE] <system> – <short symptom> Since: <time>Affected: <who / what, e.g. "access to app-x from outside">Impact: <e.g. "application not reachable, internal use works">Status: under investigationNext update: <time, e.g. in 30 minutes>Contact: <name, phone>

All-clear:

text
Subject: [RESOLVED] <system> – <short symptom> Resolved since: <time>Duration: <from–to>Cause: <short, e.g. "missing route after a change to the route table">Action: <what was done>Follow-up: <if planned>

#Maintenance Announcement

Template for planned maintenance.

text
Subject: [MAINTENANCE] <system> on <date>, <from>–<to> What: <e.g. firmware update of the FortiGate cluster in the hub>When: <date>, <from>–<to>Impact: <e.g. short interruption of existing connections (< 1 minute) during failover>What you need to do: <nothing / save sessions / …>Contact during maintenance: <name, phone>

#Handover to On-Call / Colleagues

Template for a shift or colleague handover.

text
Handover <date> – from <from> to <to> Open topics:- <topic> – status: <…> – next step: <…> Running changes / maintenance:- <…> Observations (keep an eye on):- <e.g. WAF reports increased blocks on app-x> Important links:- promptus, dashboards, tickets: <…>

#Commit Messages

Format and examples.

text
<area>: <what> (<ticket>) Hub: add spoke app-x to GatewaySubnet UDR (#1234)FortiGate: add policy app-x -> internet (HTTPS)Params dev: adjust address space for spoke app-yFix: wrong probe path on the App Gateway

Imperative, short, area first – then you find it later with git log --grep "<area>".

Look up

#Naming Convention and Tags

Find missing tags: Resource Graph → Tags · All abbreviations: Abbreviations (CAF) · Build names: web page → Generator → Naming → Build resource names

Template based on the Cloud Adoption Framework (Define your naming convention) – adapt it to your standards and then make it binding in the team.

#Naming Components (CAF)

The parts a name is built from.

ComponentMeaningExamples
Resource typeabbreviation of the resource type (Abbreviations)rg, vm, st, app
Workload / application / projectwhat the resource belongs tohub, webshop, sap
Environmentstage of the lifecycleprod, dev, qa, stage, test
RegionAzure region (full or short)westeurope / weu, germanywestcentral / gwc
Instancenumber for otherwise identical names01, 001

Rules from CAF:

  • Names can't be changed after creation for most resources – only put stable information in the name, everything else into tags.
  • Fix the order of the components once and keep it (CAF examples: type → workload → environment → region → instance, e.g. vm-workload3-dev-001).
  • Hyphen as delimiter – except where the resource doesn't allow it (storage account, container registry): stworkload1data001, crworkload1prod001.
  • No personal or confidential data in names or tags.
  • Automate with the generator on the web page or the Azure Naming Tool.

#Scope of Uniqueness

Which names must be unique where.

ScopeMeaningExamples
Globalunique in all of Azure (public endpoint / DNS name)storage account, key vault, web app, function app, container registry, SQL server, Cosmos DB, Service Bus, API Management, Front Door endpoint
Resource groupunique within the RGVNet, VM, NSG, public IP, load balancer, App Gateway
Parent resourceunique within the parentsubnet (in the VNet), SQL database (in the server), NSG rule

#Length and Character Rules (Most Used)

Length and allowed characters per resource type.

ResourceLengthAllowedScope
Resource group1–90letters, digits, - _ . () – not ending in .subscription
Virtual network2–64letters, digits, - _ .RG
Subnet1–80letters, digits, - _ .VNet
NSG / route table / public IP / NIC1–80letters, digits, - _ .RG
Virtual machine1–15 (Windows) / 1–64 (Linux)letters, digits, - (Windows computer name max. 15!)RG
Storage account3–24lowercase letters and digits onlyglobal
Key vault3–24letters, digits, -; starts with a letter, no --global
Container registry5–50letters and digits onlyglobal
Web app / function app2–60letters, digits, -global
SQL server1–63lowercase letters, digits, -global
Log Analytics workspace4–63letters, digits, -RG
AKS cluster1–63letters, digits, - _RG
Managed identity3–128letters, digits, - _RG
Management group (ID)1–90letters, digits, - _ . ()tenant

Full list: Naming rules and restrictions for Azure resources.

#Structure of a Name

Pattern and examples.

text
<type>-<workload>-<environment>-<region>-<nr> rg-hub-prd-weu-01          resource groupvnet-hub-prd-weu-01        VNetsnet-fw-trust-prd-weu-01   subnetvm-fgt-prd-weu-01          FortiGate Astappprdweu01              storage account (lowercase letters/digits only, max. 24, no "-")kv-app-prd-weu-01          Key Vault (max. 24 characters, globally unique)

Rules: lowercase only, hyphen as separator, no special characters, running number always two digits.

#Abbreviations per Resource Type

Short codes for the most used resource types.

ResourceAbbr.ResourceAbbr.
Management groupmgResource grouprg
Virtual networkvnetSubnetsnet
Network security groupnsgRoute tablert
Public IPpipNAT gatewayng
Load balancer (internal/external)lbi / lbeApplication gatewayagw
WAF policywafAzure Firewall / policyafw / afwp
BastionbasVirtual network gateway (VPN)vgw
Local network gatewaylgwVPN connectioncon
Private endpointpepPrivate DNS resolverdnspr
Virtual machinevmNetwork interfacenic
OS disk / data diskosdisk / diskAvailability setavail
Key VaultkvStorage accountst
Managed identityidLog Analytics workspacelog
Application InsightsappiApp Service planasp
Web appappFunction appfunc
SQL server / DBsql / sqldbContainer registrycr
AKS clusteraksRecovery Services vaultrsv

#Abbreviations for Regions and Environments

Short codes for regions and environments.

RegionAbbr.EnvironmentAbbr.
West EuropeweuProductionprd
North EuropeneuTest / acceptancetst / uat
Germany West CentralgwcDevelopmentdev
Austria EastateSandboxsbx
Switzerland NorthsznShared (platform)shd

#Tag Standard

Required and optional tags with examples.

TagRequiredExamplePurpose
Owneryesnetwork-teamwho is responsible
Environmentyesprdenvironment
CostCenteryes4711cost allocation
Applicationyeshubwhich application
Criticalityrecommendedhigh / medium / lowpriority during incidents
DataClassificationrecommendedinternalprotection level
CreatedByoptionalbicep-pipelinehow it was created
ExpiryDatefor tests2026-12-31when to delete

Tags are not inherited automatically – assign the built-in policy "Inherit a tag from the resource group if missing", and "Require a tag on resource groups" for required tags.

#Set and Check Tags

Set tags and find resources without them.

PowerShell
# Set on a resource group (keep existing ones)az group update -n <rg> --set tags.Owner=<team> tags.Environment=<env> tags.CostCenter=<cc># Add to a resourceaz resource tag --ids <id> --tags Owner=<team> -i# Tag all resources of an RGaz resource list -g <rg> --query "[].id" -o tsv | ForEach-Object { az resource tag --ids $_ --tags Environment=<env> -i | Out-Null }# Which RGs have no owner?az group list --query "[?tags.Owner==null].name" -o tsv

Centrally in Bicep: define param tags object once and pass tags: tags to every resource.

Look up

#Azure Resource Abbreviations (CAF)

Source: Microsoft Learn – Abbreviation recommendations for Azure resources (as of 10/2026) · Build names: web page → Generator → Naming · Rules: Naming and Tags

Tip: search at the top, e.g. firewall policy or afwp. Some abbreviations are used twice (proj, afd, np) – check the namespace when you need an exact match.

#AI and Machine Learning

Abbreviations for AI and machine learning resources.

ResourceProvider namespaceAbbreviation
AI SearchMicrosoft.Search/searchServicessrch
Foundry Tools (multi-service account)Microsoft.CognitiveServices/accounts (kind: CognitiveServices)ais
Foundry accountMicrosoft.CognitiveServices/accounts (kind: AIServices)aif
Foundry account projectMicrosoft.CognitiveServices/accounts/projectsproj
Foundry hubMicrosoft.MachineLearningServices/workspaces (kind: Hub)hub
Foundry hub projectMicrosoft.MachineLearningServices/workspaces (kind: Project)proj
Azure AI Video IndexerMicrosoft.VideoIndexer/accountsavi
Azure Machine Learning workspaceMicrosoft.MachineLearningServices/workspacesmlw
Azure OpenAI ServiceMicrosoft.CognitiveServices/accounts (kind: OpenAI)oai
Bot serviceMicrosoft.BotService/botServices (kind: azurebot)bot
Computer visionMicrosoft.CognitiveServices/accounts (kind: ComputerVision)cv
Content moderatorMicrosoft.CognitiveServices/accounts (kind: ContentModerator)cm
Content safetyMicrosoft.CognitiveServices/accounts (kind: ContentSafety)cs
Custom vision (prediction)Microsoft.CognitiveServices/accounts (kind: CustomVision.Prediction)cstv
Custom vision (training)Microsoft.CognitiveServices/accounts (kind: CustomVision.Training)cstvt
Document intelligenceMicrosoft.CognitiveServices/accounts (kind: FormRecognizer)di
Face APIMicrosoft.CognitiveServices/accounts (kind: Face)face
Health InsightsMicrosoft.CognitiveServices/accounts (kind: HealthInsights)hi
Immersive readerMicrosoft.CognitiveServices/accounts (kind: ImmersiveReader)ir
Language serviceMicrosoft.CognitiveServices/accounts (kind: TextAnalytics)lang
Speech serviceMicrosoft.CognitiveServices/accounts (kind: SpeechServices)spch
TranslatorMicrosoft.CognitiveServices/accounts (kind: TextTranslation)trsl

#Analytics and IoT

Abbreviations for analytics and IoT resources.

ResourceProvider namespaceAbbreviation
Azure Analysis Services serverMicrosoft.AnalysisServices/serversas
Azure Databricks Access ConnectorMicrosoft.Databricks/workspaces/accessConnectorsdbac
Azure Databricks workspaceMicrosoft.Databricks/workspacesdbw
Azure Data Explorer clusterMicrosoft.Kusto/clustersdec
Azure Data Explorer cluster databaseMicrosoft.Kusto/clusters/databasesdedb
Azure Data FactoryMicrosoft.DataFactory/factoriesadf
Azure Digital Twin instanceMicrosoft.DigitalTwins/digitalTwinsInstancesdt
Azure Stream AnalyticsMicrosoft.StreamAnalytics/clusterasa
Synapse Analytics private link hubMicrosoft.Synapse/privateLinkHubssynplh
Synapse Analytics SQL dedicated poolMicrosoft.Synapse/workspaces/sqlPoolssyndp
Synapse Analytics Spark poolMicrosoft.Synapse/workspaces/bigDataPoolssynsp
Synapse Analytics workspaceMicrosoft.Synapse/workspacessynw
Data Lake Store accountMicrosoft.DataLakeStore/accountsdls
Event Hubs namespaceMicrosoft.EventHub/namespacesevhns
Event hubMicrosoft.EventHub/namespaces/eventHubsevh
Event hub consumer groupMicrosoft.EventHub/namespaces/eventhubs/consumerGroupsevhcg
Event Grid domainMicrosoft.EventGrid/domainsevgd
Event Grid namespaceMicrosoft.EventGrid/namespacesevgns
Event Grid subscriptionMicrosoft.EventGrid/eventSubscriptionsevgs
Event Grid topicMicrosoft.EventGrid/domains/topicsevgt
Event Grid system topicMicrosoft.EventGrid/systemTopicsegst
Fabric capacityMicrosoft.Fabric/capacitiesfc
HDInsight – Hadoop clusterMicrosoft.HDInsight/clustershadoop
HDInsight – HBase clusterMicrosoft.HDInsight/clustershbase
HDInsight – Kafka clusterMicrosoft.HDInsight/clusterskafka
HDInsight – Spark clusterMicrosoft.HDInsight/clustersspark
HDInsight – Storm clusterMicrosoft.HDInsight/clustersstorm
HDInsight – ML Services clusterMicrosoft.HDInsight/clustersmls
IoT hubMicrosoft.Devices/IotHubsiot
Provisioning servicesMicrosoft.Devices/provisioningServicesprovs
Provisioning services certificateMicrosoft.Devices/provisioningServices/certificatespcert
Power BI EmbeddedMicrosoft.PowerBIDedicated/capacitiespbi
Time Series Insights environmentMicrosoft.TimeSeriesInsights/environmentstsi

#Compute and Web

Abbreviations for compute and web resources.

ResourceProvider namespaceAbbreviation
App Service environmentMicrosoft.Web/hostingEnvironmentsase
App Service planMicrosoft.Web/serverFarmsasp
Azure Load Testing instanceMicrosoft.LoadTestService/loadTestslt
Availability setMicrosoft.Compute/availabilitySetsavail
Azure Arc enabled serverMicrosoft.HybridCompute/machinesarcs
Azure Arc enabled Kubernetes clusterMicrosoft.Kubernetes/connectedClustersarck
Azure Arc private link scopeMicrosoft.HybridCompute/privateLinkScopespls
Azure Arc gatewayMicrosoft.HybridCompute/gatewaysarcgw
Batch accountMicrosoft.Batch/batchAccountsba
Cloud serviceMicrosoft.Compute/cloudServicescld
Communication ServicesMicrosoft.Communication/communicationServicesacs
Disk encryption setMicrosoft.Compute/diskEncryptionSetsdes
Function appMicrosoft.Web/sitesfunc
GalleryMicrosoft.Compute/galleriesgal
Hosting environmentMicrosoft.Web/hostingEnvironmentshost
Image templateMicrosoft.VirtualMachineImages/imageTemplatesit
Managed disk (OS)Microsoft.Compute/disksosdisk
Managed disk (data)Microsoft.Compute/disksdisk
Notification HubsMicrosoft.NotificationHubs/namespaces/notificationHubsntf
Notification Hubs namespaceMicrosoft.NotificationHubs/namespacesntfns
Proximity placement groupMicrosoft.Compute/proximityPlacementGroupsppg
Restore point collectionMicrosoft.Compute/restorePointCollectionsrpc
SnapshotMicrosoft.Compute/snapshotssnap
Static web appMicrosoft.Web/staticSitesstapp
Virtual machineMicrosoft.Compute/virtualMachinesvm
Virtual machine scale setMicrosoft.Compute/virtualMachineScaleSetsvmss
VM maintenance configurationMicrosoft.Maintenance/maintenanceConfigurationsmc
VM storage accountMicrosoft.Storage/storageAccountsstvm
Web appMicrosoft.Web/sitesapp

#Containers

Abbreviations for container resources.

ResourceProvider namespaceAbbreviation
AKS clusterMicrosoft.ContainerService/managedClustersaks
AKS system node poolMicrosoft.ContainerService/managedClusters/agentPools (mode: System)npsystem
AKS user node poolMicrosoft.ContainerService/managedClusters/agentPools (mode: User)np
AKS Fleet managerMicrosoft.ContainerService/fleetsfleet
Container appMicrosoft.App/containerAppsca
Container apps environmentMicrosoft.App/managedEnvironmentscae
Container apps jobMicrosoft.App/jobscaj
Container registryMicrosoft.ContainerRegistry/registriescr
Container instanceMicrosoft.ContainerInstance/containerGroupsci
Service Fabric clusterMicrosoft.ServiceFabric/clusterssf
Service Fabric managed clusterMicrosoft.ServiceFabric/managedClusterssfmc

#Databases

Abbreviations for database resources.

ResourceProvider namespaceAbbreviation
Azure Cosmos DB databaseMicrosoft.DocumentDB/databaseAccounts/sqlDatabasescosmos
Cosmos DB for Apache Cassandra accountMicrosoft.DocumentDB/databaseAccountscoscas
Cosmos DB for MongoDB accountMicrosoft.DocumentDB/databaseAccountscosmon
Cosmos DB for NoSQL accountMicrosoft.DocumentDb/databaseAccountscosno
Cosmos DB for Table accountMicrosoft.DocumentDb/databaseAccountscostab
Cosmos DB for Apache Gremlin accountMicrosoft.DocumentDb/databaseAccountscosgrm
Cosmos DB PostgreSQL clusterMicrosoft.DBforPostgreSQL/serverGroupsv2cospos
Azure Managed RedisMicrosoft.Cache/RedisEnterpriseamr
Azure SQL Database serverMicrosoft.Sql/serverssql
Azure SQL databaseMicrosoft.Sql/servers/databasessqldb
Azure SQL Elastic Job agentMicrosoft.Sql/servers/jobAgentssqlja
Azure SQL Elastic PoolMicrosoft.Sql/servers/elasticpoolsqlep
MySQL databaseMicrosoft.DBforMySQL/serversmysql
PostgreSQL flexible serverMicrosoft.DBforPostgreSQL/flexibleServerspgsql
SQL Managed InstanceMicrosoft.Sql/managedInstancessqlmi

#Developer Tools and DevOps

Abbreviations for developer and DevOps resources.

ResourceProvider namespaceAbbreviation
App Configuration storeMicrosoft.AppConfiguration/configurationStoresappcs
Maps accountMicrosoft.Maps/accountsmap
SignalRMicrosoft.SignalRService/SignalRsigr
Web PubSubMicrosoft.SignalRService/webPubSubwps
Azure Managed GrafanaMicrosoft.Dashboard/grafanaamg
Managed DevOps PoolsMicrosoft.DevOpsInfrastructure/poolsmdp

#Integration

Abbreviations for integration resources.

ResourceProvider namespaceAbbreviation
API Management service instanceMicrosoft.ApiManagement/serviceapim
Integration accountMicrosoft.Logic/integrationAccountsia
Logic appMicrosoft.Logic/workflowslogic
Service Bus namespaceMicrosoft.ServiceBus/namespacessbns
Service Bus queueMicrosoft.ServiceBus/namespaces/queuessbq
Service Bus topicMicrosoft.ServiceBus/namespaces/topicssbt
Service Bus topic subscriptionMicrosoft.ServiceBus/namespaces/topics/subscriptionssbts

#Management and Governance

Abbreviations for management and governance resources.

ResourceProvider namespaceAbbreviation
Automation accountMicrosoft.Automation/automationAccountsaa
Azure Policy definitionMicrosoft.Authorization/policyDefinitionsdescriptive
Application InsightsMicrosoft.Insights/componentsappi
Azure Monitor action groupMicrosoft.Insights/actionGroupsag
Azure Monitor data collection ruleMicrosoft.Insights/dataCollectionRulesdcr
Azure Monitor alert processing ruleMicrosoft.AlertsManagement/actionRulesapr
Azure Monitor Private Link ScopeMicrosoft.Insights/privateLinkScopesampls
Data collection endpointMicrosoft.Insights/dataCollectionEndpointsdce
Deployment scriptMicrosoft.Resources/deploymentScriptsscript
Log Analytics workspaceMicrosoft.OperationalInsights/workspaceslog
Log Analytics query packMicrosoft.OperationalInsights/querypackspack
Management groupMicrosoft.Management/managementGroupsmg
Microsoft Purview instanceMicrosoft.Purview/accountspview
Resource groupMicrosoft.Resources/resourceGroupsrg
Template specMicrosoft.Resources/templateSpecsts

#Migration

Abbreviations for migration resources.

ResourceProvider namespaceAbbreviation
Azure Migrate projectMicrosoft.Migrate/assessmentProjectsmigr
Database Migration Service instanceMicrosoft.DataMigration/servicesdms
Recovery Services vaultMicrosoft.RecoveryServices/vaultsrsv

#Networking

Abbreviations for networking resources.

ResourceProvider namespaceAbbreviation
Application gatewayMicrosoft.Network/applicationGatewaysagw
Application security group (ASG)Microsoft.Network/applicationSecurityGroupsasg
Azure Enclave communityMicrosoft.Mission/communitiescmt
Azure Enclave community endpointMicrosoft.Mission/communities/communityEndpointsce
Azure Enclave dedicated hubMicrosoft.Mission/communities/dedicatedHubsdh
Azure Enclave transit hubMicrosoft.Mission/communities/transitHubsth
Azure Enclave enclaveMicrosoft.Mission/virtualEnclavesve
Azure Enclave enclave endpointMicrosoft.Mission/virtualEnclaves/enclaveEndpointsee
Azure Enclave enclave connectionMicrosoft.Mission/enclaveConnectionsec
Azure Enclave workloadMicrosoft.Mission/virtualEnclaves/workloadswl
CDN profileMicrosoft.Cdn/profilescdnp
CDN endpointMicrosoft.Cdn/profiles/endpointscdne
ConnectionMicrosoft.Network/connectionscon
DNS zone (public)Microsoft.Network/dnsZonesDNS domain name
DNS forwarding rulesetMicrosoft.Network/dnsForwardingRulesetsdnsfrs
DNS private resolverMicrosoft.Network/dnsResolversdnspr
DNS private resolver inbound endpointMicrosoft.Network/dnsResolvers/inboundEndpointsin
DNS private resolver outbound endpointMicrosoft.Network/dnsResolvers/outboundEndpointsout
Private DNS zoneMicrosoft.Network/privateDnsZonesDNS domain name
FirewallMicrosoft.Network/azureFirewallsafw
Firewall policyMicrosoft.Network/firewallPoliciesafwp
ExpressRoute circuitMicrosoft.Network/expressRouteCircuitserc
ExpressRoute DirectMicrosoft.Network/expressRoutePortserd
ExpressRoute gatewayMicrosoft.Network/virtualNetworkGatewaysergw
Front Door (Standard/Premium) profileMicrosoft.Cdn/profilesafd
Front Door (Standard/Premium) endpointMicrosoft.Cdn/profiles/afdEndpointsfde
Front Door firewall policyMicrosoft.Network/frontdoorWebApplicationFirewallPoliciesfdfp
Front Door (classic)Microsoft.Network/frontDoorsafd
IP groupMicrosoft.Network/ipGroupsipg
Load balancer (internal)Microsoft.Network/loadBalancerslbi
Load balancer (external)Microsoft.Network/loadBalancerslbe
Load balancer ruleMicrosoft.Network/loadBalancers/inboundNatRulesrule
Local network gatewayMicrosoft.Network/localNetworkGatewayslgw
NAT gatewayMicrosoft.Network/natGatewaysng
Network interface (NIC)Microsoft.Network/networkInterfacesnic
Network security perimeterMicrosoft.Network/networkSecurityPerimetersnsp
Network security group (NSG)Microsoft.Network/networkSecurityGroupsnsg
NSG security ruleMicrosoft.Network/networkSecurityGroups/securityRulesnsgsr
Network WatcherMicrosoft.Network/networkWatchersnw
Private Link serviceMicrosoft.Network/privateLinkServicespl
Private endpointMicrosoft.Network/privateEndpointspep
Public IP addressMicrosoft.Network/publicIPAddressespip
Public IP address prefixMicrosoft.Network/publicIPPrefixesippre
Route filterMicrosoft.Network/routeFiltersrf
Route serverMicrosoft.Network/virtualHubsrtserv
Route tableMicrosoft.Network/routeTablesrt
Service endpoint policyMicrosoft.Network/serviceEndPointPoliciesse
Traffic Manager profileMicrosoft.Network/trafficManagerProfilestraf
User defined route (UDR)Microsoft.Network/routeTables/routesudr
Virtual networkMicrosoft.Network/virtualNetworksvnet
Virtual network gatewayMicrosoft.Network/virtualNetworkGatewaysvgw
Virtual network managerMicrosoft.Network/networkManagersvnm
Virtual network peeringMicrosoft.Network/virtualNetworks/virtualNetworkPeeringspeer
Virtual network subnetMicrosoft.Network/virtualNetworks/subnetssnet
Virtual WANMicrosoft.Network/virtualWansvwan
Virtual WAN hubMicrosoft.Network/virtualHubsvhub

#Security

Abbreviations for security resources.

ResourceProvider namespaceAbbreviation
Azure BastionMicrosoft.Network/bastionHostsbas
Key vaultMicrosoft.KeyVault/vaultskv
Key Vault Managed HSMMicrosoft.KeyVault/managedHSMskvmhsm
Managed identityMicrosoft.ManagedIdentity/userAssignedIdentitiesid
SSH keyMicrosoft.Compute/sshPublicKeyssshkey
VPN Gateway (Virtual WAN)Microsoft.Network/vpnGatewaysvpng
VPN connection (Virtual WAN)Microsoft.Network/vpnGateways/vpnConnectionsvcn
VPN site (Virtual WAN)Microsoft.Network/vpnGateways/vpnSitesvst
Web Application Firewall (WAF) policyMicrosoft.Network/firewallPolicieswaf
WAF policy rule groupMicrosoft.Network/firewallPolicies/ruleGroupswafrg

#Storage

Abbreviations for storage resources.

ResourceProvider namespaceAbbreviation
Azure Backup Resource GuardMicrosoft.DataProtection/resourceGuardsrgd
Backup vaultMicrosoft.DataProtection/backupVaultsbvault
Backup vault policyMicrosoft.DataProtection/backupVaults/backupPoliciesbkpol
File shareMicrosoft.Storage/storageAccounts/fileServices/sharesshare
Storage accountMicrosoft.Storage/storageAccountsst
Storage Sync ServiceMicrosoft.StorageSync/storageSyncServicessss

#Virtual Desktop

Abbreviations for virtual desktop resources.

ResourceProvider namespaceAbbreviation
Virtual desktop host poolMicrosoft.DesktopVirtualization/hostPoolsvdpool
Virtual desktop application groupMicrosoft.DesktopVirtualization/applicationGroupsvdag
Virtual desktop workspaceMicrosoft.DesktopVirtualization/workspacesvdws
Virtual desktop scaling planMicrosoft.DesktopVirtualization/scalingPlansvdscaling
Look up

#Costs and Cleanup

Find orphaned resources: Resource Graph → Cleanup

#What Costs What?

Fastest in the portal: Cost Management → Cost analysis → view Resources or Resource groups, choose the period at the top, Group by → tag (e.g. CostCenter).

PowerShell
az extension add -n costmanagement# Costs of the current month per resource groupaz costmanagement query --type ActualCost --timeframe MonthToDate --scope "/subscriptions/<sub-id>" `  --dataset-aggregation '{\"totalCost\":{\"name\":\"Cost\",\"function\":\"Sum\"}}' `  --dataset-grouping name="ResourceGroupName" type="Dimension" -o json

JSON escaping in PowerShell is tedious – for regular reports better save a view in the portal or set up an export (daily CSV into a storage account): Cost Management → Exports.

#Budget with Alert

Portal: Cost Management → Budgets → Add → amount, period Monthly → alerts e.g. at 80 % Actual and 100 % Forecasted → e-mail or action group.

PowerShell
az consumption budget create --budget-name budget-<rg> --resource-group <rg> `  --amount 500 --category cost --time-grain monthly `  --start-date 2026-11-01 --end-date 2027-10-31az consumption budget list -o table

Notifications (thresholds, recipients) are easiest to add afterwards on the budget in the portal.

#Azure Advisor Recommendations

Cost recommendations from Advisor.

PowerShell
az advisor recommendation list --category Cost `  --query "[].{recommendation:shortDescription.problem, resource:impactedValue, impact:impact}" -o tableaz advisor recommendation list --category Cost --query "[?contains(shortDescription.problem,'Right-size')]" -o table

Typical: downsize oversized VMs, buy reservations/savings plans, delete unused resources.

#Clean Up Orphaned Resources

Find and delete disks, IPs and NICs nobody uses.

PowerShell
az extension add -n resource-graph# 1. Findaz graph query -q "Resources | where type =~ 'microsoft.compute/disks' and properties.diskState =~ 'Unattached' | project id, name, resourceGroup, sizeGB = properties.diskSizeGB" -o tableaz graph query -q "Resources | where type =~ 'microsoft.network/publicipaddresses' and isnull(properties.ipConfiguration) and isnull(properties.natGateway) | project id, name, resourceGroup" -o tableaz graph query -q "Resources | where type =~ 'microsoft.network/networkinterfaces' and isnull(properties.virtualMachine) and isnull(properties.privateEndpoint) | project id, name, resourceGroup" -o table # 2. Check (tags, owner, who created it?)az resource show --ids <id> --query "{tags:tags, created:createdTime}"az monitor activity-log list --resource-id <id> --offset 90d --query "[?contains(operationName.value,'write')].{time:eventTimestamp, who:caller}" -o table # 3. Mark first, delete lateraz resource tag --ids <id> --tags DeleteAfter=2026-12-01 Reason=orphaned -iaz resource delete --ids <id>                     # after consultation / when the deadline has passed

Back up disks as a snapshot before deleting if needed: az snapshot create -g <rg> -n snap-<disk> --source <disk-id>.

#Common Cost Traps

The usual ways money is wasted and the remedy for each.

TrapRemedy
VM only "stopped", not deallocatedaz vm deallocate instead of shutting down in the OS
Premium disks on test VMsswitch to Standard SSD
Disks/public IPs left over after deleting a VMset "Delete with VM" when deleting, Resource Graph query above
Application Gateway / firewall running permanently in teststop outside working hours (az network application-gateway stop)
Log Analytics collects too muchreduce diagnostic categories, check retention, Basic Logs for bulk data
Bastion Standard in every environmentone central Bastion in the hub
Old snapshots and backupsdefine retention, lifecycle rules
Traffic between regions/zonesworkloads and hub in the same region
No hits. Try fewer or different terms – all words must appear in the same line or in the title.