NIS2 Article 21 in Azure: Implementing Network Security Controls with Terraform
Tags: terraform, azure, security, devops
Canonical URL: https://woitzik.dev/blog/nis2-article-21-azure-terraform
Most NIS2 content is written by lawyers for lawyers. This article maps Article 21 network security requirements to concrete Azure resources — with Terraform code, not legal theory.
Disclaimer: This covers technical network infrastructure controls relevant to NIS2 Article 21. Full compliance requires organizational measures beyond infrastructure code. Nothing here constitutes legal advice.
What NIS2 Article 21 Actually Requires (For Engineers)
Four concrete network security areas:
- Network Segmentation — isolate systems by criticality
- Access Control — Default-Deny at network and identity layer
- Minimize Attack Surface — eliminate unnecessary public endpoints
- Auditability — traceable, version-controlled infrastructure changes
Here's how each maps to Azure Terraform resources.
Control 1: Network Segmentation via Hub & Spoke
Spoke-to-Spoke traffic blocked by default — all cross-workload communication routes through the Hub:
resource "azurerm_virtual_network_peering" "hub_to_spoke1" {
name = "peer-hub-to-spoke1"
virtual_network_name = azurerm_virtual_network.hub.name
remote_virtual_network_id = azurerm_virtual_network.spoke1.id
allow_forwarded_traffic = true
}
Spokes only peer with the Hub — never with each other. Cross-Spoke traffic must be explicitly routed through the Firewall.
Control 2: Default-Deny NSG Baseline
security_rule {
name = "Allow-VNet-Inbound"
priority = 100
access = "Allow"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "VirtualNetwork"
}
security_rule {
name = "Deny-Internet-Inbound"
priority = 4096
access = "Deny"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
Priority gap (100 → 4096) leaves room for application rules without restructuring the baseline.
Control 3: Egress Control via Forced Tunneling
All outbound Spoke traffic through Azure Firewall — logged, inspectable, FQDN-controlled:
route {
name = "route-to-firewall"
address_prefix = "0.0.0.0/0"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = azurerm_firewall.fw.ip_configuration[0].private_ip_address
}
# Critical — without these, Windows VMs lose activation
route {
name = "bypass-azure-kms"
address_prefix = "23.102.135.246/32"
next_hop_type = "Internet"
}
The Firewall log is your audit trail for NIS2 Article 21(2)(b) — monitoring and incident detection.
Control 4: Zero Public PaaS Endpoints
resource "azurerm_key_vault" "kv" {
public_network_access_enabled = false
network_acls {
default_action = "Deny"
bypass = "AzureServices"
}
}
An external scanner gets no TCP connection — the endpoint doesn't resolve to a public IP. Directly satisfies NIS2 Article 21(2)(h).
Control 5: No Static Credentials
Managed Identity + scoped RBAC — no connection strings, no access keys:
resource "azurerm_role_assignment" "app_kv_secrets" {
scope = azurerm_key_vault.kv.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_function_app.app.identity[0].principal_id
}
When the resource is deleted, identity and permissions are automatically destroyed.
Control 6: Auditability via IaC
Every change is a Git commit. terraform plan is an audit artifact. No ClickOps, no undocumented Portal changes.
What This Covers — and What It Doesn't
This addresses technical network controls only. Full NIS2 compliance also requires incident response procedures, supply chain risk management, business continuity measures, and organizational governance.
Infrastructure code is the foundation. Compliance is the full building.
Full article with complete code for all six controls on my blog.
Top comments (0)