DEV Community

Cover image for Cloud Security and Monitoring in Azure: Defender for Cloud, Key Vault, Firewall, and Log Analytics
Raphael Gab-Momoh
Raphael Gab-Momoh

Posted on Originally published at raphaelgmomoh.pages.dev

Cloud Security and Monitoring in Azure: Defender for Cloud, Key Vault, Firewall, and Log Analytics

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}]'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>"}]'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)"
Enter fullscreen mode Exit fullscreen mode

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"}
  }'
Enter fullscreen mode Exit fullscreen mode

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}]'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
  ]'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) ─┘
Enter fullscreen mode Exit fullscreen mode

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


Originally published on my portfolio.

Top comments (0)