July 25, 2026
Turning the NIST Cybersecurity Framework Into Code with Azure Policy
Most teams treat CSF as a document to be audited against. It works far better as a set of guardrails your cloud enforces on its own.

By Joseph A. M.
5 min read
Most teams treat CSF as a document to be audited against. It works far better as a set of guardrails your cloud enforces on its own.
The NIST Cybersecurity Framework is one of the most widely adopted security frameworks in the world, and yet for most organizations it lives as a spreadsheet. Someone maps controls to evidence once a year, screenshots get collected, and the framework goes back in a drawer until the next assessment. The framework describes outcomes — "assets are inventoried," "data-in-transit is protected" — but it says nothing about how you actually make those outcomes true and keep them true.
That gap is exactly where cloud-native tooling shines. I wanted to see how much of CSF I could express not as a checklist but as living policy — code that evaluates every resource in a subscription, continuously, and tells me the moment reality drifts from the framework. This post walks through how I mapped CSF 2.0 onto Azure Policy, function by function, and what I learned about where the framework translates cleanly into code and where it stubbornly doesn't.
A quick word on CSF 2.0
NIST released CSF 2.0 in February 2024, and its headline change matters for anyone doing this kind of work. The framework now has six functions instead of five:
- Govern (GV) — the new one: organizational context, risk strategy, roles, and oversight
- Identify (ID) — understanding your assets, risks, and environment
- Protect (PR) — safeguards that limit or contain an incident
- Detect (DE) — finding events and anomalies
- Respond (RS) — acting once something is detected
- Recover (RC) — restoring what an incident affected
The addition of Govern is not cosmetic. It reframes cybersecurity as something an organization steers deliberately rather than a purely technical afterthought — and, conveniently, "governance of what's allowed to exist in your environment" is precisely what a policy engine is built to enforce. That made Govern a natural anchor for the project rather than an abstract box to check.
The core idea: one control outcome, one policy
Azure Policy evaluates resources against rules written as JSON. Each rule has an if (the condition that describes a violation) and a then (the effect). The trick to making this framework-driven is to stop thinking about individual Azure features and start thinking about CSF outcomes, then ask: what resource condition would demonstrate that outcome is — or isn't — being met?
Here's the mapping I landed on for a starter baseline, one policy per function:
CSF FunctionControl outcomeWhat the policy checksGovernAssets deploy only where the org allowsResource region is on an approved listIdentifyEvery asset has a known ownerResource carries an ownership tagProtectData in transit is encryptedStorage enforces HTTPS-only trafficDetectActivity is observableStorage has diagnostic logging configuredRecoverRecovery material can't be destroyedKey Vault has purge protection enabled
None of these is exotic on its own. What makes it framework work rather than a random pile of best practices is that each one is deliberately chosen to represent a specific CSF outcome, tagged with the function it serves, and reported under that function.
From outcome to rule
Take Identify. The CSF outcome (ID.AM-01/02) is that you maintain an inventory of assets with clear ownership. In a cloud subscription, "ownership" is expressed through tags — so the control becomes: audit any resource that doesn't carry an Owner tag.
{
"if": {
"field": "[concat('tags[', parameters('tagName'), ']')]",
"exists": "false"
},
"then": { "effect": "audit" }
}{
"if": {
"field": "[concat('tags[', parameters('tagName'), ']')]",
"exists": "false"
},
"then": { "effect": "audit" }
}That's the entire control. It's parameterized on the tag name, so the same policy enforces whatever ownership convention an org uses. Once assigned, every resource missing that tag shows up as non-compliant — a live, always-current asset-ownership gap report, which is a far more useful artifact than a point-in-time inventory export.
Protect (PR.DS-02, data in transit) is just as direct:
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
{ "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "notEquals": true }
]
},
"then": { "effect": "audit" }
}{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
{ "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "notEquals": true }
]
},
"then": { "effect": "audit" }
}The pattern repeats across the other functions. Govern compares each resource's location against an allowed-regions parameter. Detect uses an auditIfNotExists effect to flag storage accounts that have no diagnostic settings attached. Recover checks that Key Vaults — where your keys and secrets live — have purge protection so they can't be permanently wiped during an incident.
The part that makes it a framework: grouping
Individual policies are useful. But the reason this reads as CSF and not just "some Azure rules" is the initiative — Azure's term for a bundle of policies — and specifically its grouping feature.
When you compose the initiative, you define groups named after the CSF functions and assign each member policy to its group:
GOVERN → allowed-locations
IDENTIFY → require-owner-tag
PROTECT → storage-https-only
DETECT → storage-diagnostics
RECOVER → keyvault-purge-protectionGOVERN → allowed-locations
IDENTIFY → require-owner-tag
PROTECT → storage-https-only
DETECT → storage-diagnostics
RECOVER → keyvault-purge-protectionNow the compliance dashboard doesn't just say "you're 80% compliant." It says how you're doing against Govern, against Protect, against Detect — the framework's own vocabulary, rendered as a live scoreboard. That's the moment the abstraction collapses into something an auditor, a manager, and an engineer can all read from the same screen. You've turned a framework into a metric.
Assess before you enforce
Every policy in the baseline uses an audit effect rather than deny. This is a deliberate security-operations choice, not a shortcut. If you drop hard deny rules onto an existing subscription, you break deployments and teach everyone to resent the security team. The professional rollout pattern is to introduce controls in audit mode first, measure the real non-compliance, socialize it, remediate the backlog, and then graduate the controls that matter to deny once you know they won't cause an outage.
Audit-first also keeps the whole thing observational: it reports on drift without ever blocking or modifying resources, so it's safe to run against a live environment while you build confidence. Flipping a control to enforcement later is a one-line change once the data tells you it's safe.
How CSF and NIST 800–53 fit together
A question that comes up constantly: if I'm doing CSF, what about 800–53? They're not competitors. CSF is the outcome layer — what good looks like. NIST SP 800–53 is the detailed control catalog — the specific, granular controls you implement to achieve those outcomes — and CSF's official informative references map its subcategories straight into 800–53.
Azure ships an enormous built-in 800–53 Rev. 5 initiative you can assign alongside a project like this. The mental model I find useful: build a small, hand-written CSF-shaped initiative to demonstrate you understand the framework and can express controls as code, then layer the comprehensive built-in 800–53 initiative underneath for depth. The custom one shows judgment; the built-in one shows coverage.
Where the framework resists code (and that's fine)
Honesty matters more than a tidy story here, and two limitations are worth naming.
First, Respond has no policy in this baseline. The RESPOND function is about incident triage, communications, and mitigation — process and human coordination, not resource configuration. You can support it with Azure Monitor alert rules and automation, but there's no honest way to reduce "coordinate your incident response" to a resource if/then. Pretending otherwise would be security theater. Better to map the five functions that translate cleanly and be explicit that Respond is covered by runbooks and alerting elsewhere.
Second, the subcategory identifiers deserve verification. Mapping a policy to PR.DS-02 or ID.AM-01 is a claim, and NIST occasionally renumbers subcategories between revisions. For a learning project the direction of the mapping is what matters; for anything you'd put in front of an assessor, cross-check each identifier against the current CSF 2.0 reference catalog. Treating the crosswalk as "close, verify before you cite it" is the mature posture.
What this actually demonstrates
The reason I think this is a worthwhile project — and why it's a stronger portfolio piece than yet another "deploy a VM" tutorial — is that it exercises the skill security engineering is really asking for: taking a compliance framework and turning it into enforceable, version-controlled, continuously-evaluated guardrails. That's governance-as-code, and it's the daily reality of a cloud security role. You end up fluent in policy definitions, initiatives, effects, the audit-to-deny lifecycle, and the CSF-to-800–53 relationship — all through building something that runs, rather than reading about it.
If you want to take it further, the obvious next moves are: graduate a control from audit to deny and watch enforcement kick in; add a deployIfNotExists policy so you have to learn remediation and managed identities; lift the assignment up to a management group to govern many subscriptions at once; and wire the whole thing into CI/CD so your security baseline ships like any other code. Each of those is a genuine capability, and each starts from the same small idea: a framework is just a set of outcomes, and outcomes can be code.