Security & Monitoring · Aug 2026 · 26 min read
Cloud Security and Monitoring in Azure: Defender for Cloud, Key Vault, Firewall, and Log Analytics
A granular, hands-on walkthrough of the four pillars a security engineer sets up first in Azure — workload protection with Defender for Cloud, key management with Key Vault, network filtering with Azure Firewall, and centralized monitoring with a Log Analytics workspace.
Why These Four Services, Together
Cloud security work rarely starts with an exotic architecture — it starts with four unglamorous basics done correctly: know what's vulnerable (workload protection), control who can read a secret (key management), control what network traffic is allowed (firewalling), and know what happened after the fact (centralized logging). Get these four wrong and no amount of Zero Trust diagramming elsewhere compensates. Get them right and most other security work builds cleanly on top.
This is a granular, step-by-step walkthrough of setting up all four in a real Azure subscription: Microsoft Defender for Cloud for cloud workload protection, Azure Key Vault for key/secret/certificate management, Azure Firewall for centralized network filtering, and a Log Analytics workspace to give the other three somewhere to send their signal.
Before the how, the what — four terms this walkthrough leans on:
- CWPP (Cloud Workload Protection Platform) — actively watches your running resources (VMs, containers, databases) for signs of an actual attack in progress, the way antivirus/EDR software does on a laptop. This is different from a configuration checklist — it's real-time threat detection on live workloads.
- CSPM (Cloud Security Posture Management) — continuously checks your resource configuration against best practice (is this storage account public when it shouldn't be, is disk encryption on) and scores it. CSPM finds misconfigurations before they're exploited; CWPP catches exploitation already happening — together they're "prevent" and "detect."
- RBAC (Role-Based Access Control) — granting permissions by assigning a predefined role (like "Key Vault Secrets User") to an identity, rather than listing out individual permissions one by one. The role defines a bundle of permissions scoped to a specific task, which is both easier to audit and harder to over-grant by accident.
- KQL (Kusto Query Language) — the query language used to search and aggregate log data in a Log Analytics workspace, structurally similar to SQL but built for filtering and summarizing large volumes of time-series log events. Every "find me all X in the last 7 days" query in Section 04 is written in KQL.
01 — Microsoft Defender for Cloud: Cloud Workload Protection
Defender for Cloud is Azure's Cloud Workload Protection Platform (CWPP) plus its Cloud Security Posture Management (CSPM) layer — CSPM continuously assesses configuration against best practice (the Secure Score), CWPP actively watches running workloads for threats.
1.1 Enable Defender for Cloud on the subscription
az account set --subscription "<subscription-id>"
# Free tier (CSPM/Secure Score only) is on by default. Enable the paid
# Defender plans per resource type you actually run:
az security pricing create --name VirtualMachines --tier Standard
az security pricing create --name SqlServers --tier Standard
az security pricing create --name StorageAccounts --tier Standard
az security pricing create --name KeyVaults --tier Standard
az security pricing create --name AppServices --tier Standard
Enable plans per workload type you actually run — enabling Defender for every resource type in a subscription that only runs VMs and Storage is pure spend with zero protection benefit. Check what's actually deployed first:
az resource list --query "[].type" -o tsv | sort -u
1.2 Onboard non-Azure and multicloud resources (if applicable)
Defender for Cloud can protect AWS and GCP workloads too, via a connector:
az security connector create \
--name aws-connector \
--resource-group rg-security \
--hierarchy-identifier "<aws-account-id>" \
--environment-name AWS \
--offerings '[{"offeringType":"DefenderForServersAws"}]'
1.3 Review the Secure Score and recommendations
The Secure Score is a percentage derived from how many security recommendations are resolved, weighted by impact. In the portal: Defender for Cloud → Overview → Secure Score. Via CLI, pull the raw recommendation list to triage programmatically:
az security assessment list --query "[?status.code=='Unhealthy'].{name:displayName, severity:metadata.severity}" -o table
Triage order that actually matters: fix High severity findings on internet-facing resources first (an open management port on a public VM), then High severity on internal resources, then Medium. Don't chase 100% Secure Score for its own sake — some recommendations (e.g., "enable disk encryption" on a dev sandbox VM with no real data) are legitimately low-priority for a given environment.
1.4 Configure workload protection alerts to route somewhere
An alert nobody sees might as well not fire. Wire Defender's alerts into an action group and, in section 04 below, into the Log Analytics workspace so they're queryable alongside everything else.
az monitor action-group create \
--name ag-security-oncall \
--resource-group rg-security \
--short-name secops \
--email-receiver name=oncall email=security-oncall@example.com
az security automation create \
--resource-group rg-security \
--automation-name defender-alerts-to-oncall \
--scopes '[{"description":"subscription scope","scope-path":"/subscriptions/<subscription-id>"}]' \
--sources '[{"eventSource":"Alerts","ruleSets":[{"rules":[{"propertyJPath":"properties.metadata.severity","propertyType":"String","expectedValue":"High","operator":"Equals"}]}]}]' \
--actions '[{"actionType":"ActionGroup","actionGroupResourceId":"<action-group-resource-id>"}]'
This automation rule fires the action group specifically for High severity alerts — filtering at the automation-rule level, not funneling every Low-severity informational alert to an on-call pager, is what keeps the alert channel trustworthy instead of noise the team learns to ignore.
02 — Azure Key Vault: Key and Secret Management
2.1 Create the vault with the correct protection settings from the start
az keyvault create \
--name kv-payments-prod \
--resource-group rg-security \
--location eastus \
--enable-rbac-authorization true \
--enable-soft-delete true \
--retention-days 90 \
--enable-purge-protection true
Four flags that matter, in order of how often they're skipped:
--enable-purge-protection true— without this, a deleted vault (or a deleted secret inside it) can be permanently purged before the retention window ends, by anyone with delete rights. This is the single setting standing between "an admin fat-fingered a delete" and "the encryption key is unrecoverable, forever."--enable-soft-delete true— is actually the default on new vaults now, but verify it explicitly; a soft-deleted secret is recoverable within the retention window instead of gone immediately.--enable-rbac-authorization true— use Azure RBAC for vault permissions, not the legacy vault access policy model. RBAC integrates with the same role assignments as everything else in the subscription and supports conditional access; access policies are a parallel, harder-to-audit permission system that Microsoft itself now steers new vaults away from.--retention-days 90— the maximum. A shorter window is a shorter recovery grace period for no benefit.
2.2 Grant access via RBAC, scoped to the specific vault
az role assignment create \
--role "Key Vault Secrets User" \
--assignee "<app-or-user-object-id>" \
--scope "/subscriptions/<sub-id>/resourceGroups/rg-security/providers/Microsoft.KeyVault/vaults/kv-payments-prod"
Use the narrowest built-in role that does the job:
| Role | Grants |
|---|---|
| Key Vault Reader | Read metadata, not secret values |
| Key Vault Secrets User | Read secret values (not manage them) |
| Key Vault Secrets Officer | Create/update/delete secrets |
| Key Vault Crypto User | Use keys for encrypt/decrypt/sign, not manage them |
| Key Vault Administrator | Full control of the vault's data plane |
An application that only needs to read a connection string at startup should get Key Vault Secrets User, never Secrets Officer and certainly never Administrator — the blast radius of a compromised app identity should be "can read what it needs," not "can delete every secret in the vault."
2.3 Store a secret, a key, and a certificate
# Secret (a connection string, API key, or password)
az keyvault secret set \
--vault-name kv-payments-prod \
--name "sql-connection-string" \
--value "Server=tcp:...;Database=...;"
# Key (for application-level encrypt/decrypt or signing — RSA or EC)
az keyvault key create \
--vault-name kv-payments-prod \
--name "payments-encryption-key" \
--kty RSA \
--size 2048
# Certificate (for TLS or client-cert auth), auto-renewing
az keyvault certificate create \
--vault-name kv-payments-prod \
--name "api-tls-cert" \
--policy "$(az keyvault certificate get-default-policy)"
2.4 Set an expiration and a rotation policy — don't leave secrets to live forever
az keyvault secret set-attributes \
--vault-name kv-payments-prod \
--name "sql-connection-string" \
--expires "2027-08-01T00:00:00Z"
az keyvault key rotation-policy update \
--vault-name kv-payments-prod \
--name "payments-encryption-key" \
--value '{
"lifetimeActions": [{"trigger": {"timeAfterCreate": "P9M"}, "action": {"type": "Rotate"}}],
"attributes": {"expiryTime": "P1Y"}
}'
A secret with no expiration date is a secret nobody is forced to ever revisit — set one even if it's a year out, so an expiration alert (not a breach) is what eventually prompts rotation.
2.5 Enable diagnostic logging to the Log Analytics workspace
az monitor diagnostic-settings create \
--name kv-diagnostics \
--resource "/subscriptions/<sub-id>/resourceGroups/rg-security/providers/Microsoft.KeyVault/vaults/kv-payments-prod" \
--workspace "<log-analytics-workspace-resource-id>" \
--logs '[{"category": "AuditEvent", "enabled": true}]'
Every GET on a secret is now a queryable audit record — see section 04 for the KQL to actually use it.
03 — Azure Firewall: Centralized Network Filtering
Azure Firewall is a stateful, managed firewall that sits in a hub VNet and inspects traffic between spokes, and between the VNet and the internet — the centralized enforcement point in a hub-and-spoke topology, rather than per-VM NSGs trying to do the same job in a scattered way.
3.1 Deploy the firewall into a dedicated subnet
Azure Firewall requires its own subnet, named exactly AzureFirewallSubnet, minimum /26:
az network vnet subnet create \
--resource-group rg-network \
--vnet-name vnet-hub \
--name AzureFirewallSubnet \
--address-prefixes 10.0.0.0/26
az network public-ip create \
--resource-group rg-network \
--name pip-fw-hub \
--sku Standard \
--allocation-method Static
az network firewall create \
--resource-group rg-network \
--name fw-hub \
--vnet-name vnet-hub
az network firewall ip-config create \
--resource-group rg-network \
--firewall-name fw-hub \
--name fw-hub-ipconfig \
--public-ip-address pip-fw-hub \
--vnet-name vnet-hub
3.2 Create a firewall policy with the three rule collection types
Azure Firewall evaluates three categories of rules, in a fixed order: DNAT rules → Network rules → Application rules.
az network firewall policy create \
--resource-group rg-network \
--name fw-policy-hub
az network firewall policy rule-collection-group create \
--resource-group rg-network \
--policy-name fw-policy-hub \
--name rcg-core \
--priority 200
Network rule (L3/L4 — allow a specific spoke to reach a specific destination on a specific port):
az network firewall policy rule-collection-group collection add-filter-collection \
--resource-group rg-network \
--policy-name fw-policy-hub \
--rule-collection-group-name rcg-core \
--name allow-spoke-to-sql \
--collection-priority 210 \
--action Allow \
--rule-name allow-sql-1433 \
--rule-type NetworkRule \
--description "App spoke to SQL spoke, port 1433" \
--ip-protocols TCP \
--source-addresses 10.1.0.0/24 \
--destination-addresses 10.2.0.0/24 \
--destination-ports 1433
Application rule (L7 — allow outbound HTTPS to a specific FQDN, inspected at the application layer):
az network firewall policy rule-collection-group collection add-filter-collection \
--resource-group rg-network \
--policy-name fw-policy-hub \
--rule-collection-group-name rcg-core \
--name allow-windows-update \
--collection-priority 220 \
--action Allow \
--rule-name allow-wu \
--rule-type ApplicationRule \
--description "Allow outbound Windows Update only" \
--target-fqdns "*.windowsupdate.com" "*.update.microsoft.com" \
--source-addresses 10.1.0.0/24 \
--protocols Http=80 Https=443
DNAT rule (inbound — expose one internal service through the firewall's public IP, on a specific port, rather than giving that service its own public IP):
az network firewall policy rule-collection-group collection add-nat-collection \
--resource-group rg-network \
--policy-name fw-policy-hub \
--rule-collection-group-name rcg-nat \
--name dnat-web \
--collection-priority 100 \
--action DNAT \
--rule-name web-inbound \
--description "Inbound HTTPS to internal web tier" \
--source-addresses "*" \
--destination-addresses "<firewall-public-ip>" \
--destination-ports 443 \
--translated-address 10.1.0.10 \
--translated-port 443 \
--ip-protocols TCP
3.3 Default-deny, then allow explicitly
Azure Firewall's implicit behavior is deny-all for anything not explicitly matched by a rule — the discipline that matters is resisting the temptation to add a broad 0.0.0.0/0 allow rule "temporarily" to unblock something during testing. Every application rule should name specific FQDNs; every network rule should name specific source/destination CIDRs and ports — never wildcard-and-forget.
3.4 Route spoke traffic through the firewall
The firewall only inspects traffic that's actually routed to it — creating it doesn't retroactively capture anything. Force spoke subnets' default route through the firewall's private IP via a User-Defined Route:
az network route-table create --resource-group rg-network --name rt-spoke-app
az network route-table route create \
--resource-group rg-network \
--route-table-name rt-spoke-app \
--name route-to-firewall \
--address-prefix 0.0.0.0/0 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address "<firewall-private-ip>"
az network vnet subnet update \
--resource-group rg-network \
--vnet-name vnet-spoke-app \
--name snet-app \
--route-table rt-spoke-app
This is the step most "why isn't the firewall doing anything" support tickets turn out to be missing — the firewall is live and correctly configured, but nothing is actually routed through it.
3.5 Enable firewall diagnostic logs
az monitor diagnostic-settings create \
--name fw-diagnostics \
--resource "/subscriptions/<sub-id>/resourceGroups/rg-network/providers/Microsoft.Network/azureFirewalls/fw-hub" \
--workspace "<log-analytics-workspace-resource-id>" \
--logs '[
{"category": "AzureFirewallApplicationRule", "enabled": true},
{"category": "AzureFirewallNetworkRule", "enabled": true},
{"category": "AzureFirewallDnsProxy", "enabled": true}
]'
04 — Log Analytics Workspace: Where All of This Becomes Queryable
A Log Analytics workspace is the destination every service above sends its logs to, and the query engine (Kusto Query Language, KQL) that turns raw log rows into an actual answer.
4.1 Create the workspace
az monitor log-analytics workspace create \
--resource-group rg-security \
--workspace-name log-security-hub \
--location eastus \
--retention-time 90 \
--sku PerGB2018
Retention is a real cost/compliance tradeoff, not a default to leave alone — 90 days covers most incident-investigation windows; regulated environments (PCI-DSS, HIPAA) often require 365+ days, which needs an explicit longer retention setting or an export to cheaper long-term storage (Azure Storage) via a data export rule.
4.2 One workspace, or several?
The default answer for most organizations: one central workspace per environment tier (one for production, one for non-production), not one per resource and not one giant workspace spanning every environment. One workspace per resource makes cross-resource correlation impossible; one giant workspace spanning prod and dev makes RBAC scoping (who can query what) much harder, since Log Analytics RBAC is workspace-level by default.
4.3 Query Defender for Cloud alerts
SecurityAlert
| where TimeGenerated > ago(7d)
| where AlertSeverity == "High"
| project TimeGenerated, AlertName, CompromisedEntity, Description
| order by TimeGenerated desc
4.4 Query Key Vault access — who read which secret, and when
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where OperationName == "SecretGet"
| project TimeGenerated, CallerIPAddress, identity_claim_upn_s, requestUri_s, ResultSignature
| order by TimeGenerated desc
This is the query that answers "who accessed the production database credential last week" during an incident — without vault diagnostic logging turned on (section 2.5), that question is unanswerable after the fact.
4.5 Query firewall-denied traffic — find what's actually trying to get through
AzureDiagnostics
| where Category == "AzureFirewallNetworkRule"
| where msg_s contains "Deny"
| parse msg_s with * "from " SourceIP " to " DestinationIP ":" DestinationPort " " *
| summarize DeniedAttempts = count() by SourceIP, DestinationIP, DestinationPort
| order by DeniedAttempts desc
A source IP racking up hundreds of denied attempts against different destination ports in a short window is a scanning pattern — this query surfaces it directly, rather than eyeballing a raw log stream.
4.6 Build one workbook that answers "are we okay right now"
Azure Monitor Workbooks combine multiple KQL queries into a single dashboard. A minimal security workbook worth building on day one: Secure Score trend, High-severity Defender alerts (last 7 days), Key Vault access anomalies, and firewall deny-rate over time — four panels, one place to look before assuming everything's fine.
How the Four Pieces Actually Connect
Microsoft Defender for Cloud ──┐
│
Azure Key Vault (audit logs) ──┼──▶ Log Analytics Workspace ──▶ KQL queries / Workbooks / Alerts
│
Azure Firewall (traffic logs) ─┘
None of these three services are useful in isolation for long — Defender for Cloud without a destination for its alerts is a dashboard nobody checks, Key Vault without audit logging is unauditable after an incident, and a firewall without traffic logs is a black box you can't tune. The Log Analytics workspace is what turns three separate security tools into one coherent security posture you can actually query, alert on, and improve over time.
Closing Thoughts
None of this is exotic — a workload protection plan, a properly RBAC'd vault, a default-deny firewall, and one workspace to query all of it. What separates "configured" from "actually secure" is the granular detail: purge protection turned on before the first real secret goes in, RBAC scoped to the narrowest role that works, rule collections naming specific FQDNs instead of wildcards, and diagnostic logging wired in before the incident that makes you wish you'd done it.
Microsoft Defender for Cloud · Azure Key Vault · Azure Firewall · Azure Monitor · Log Analytics · KQL · Security