A Terraform module for managing Network Security Group (NSG) rules in Azure with intelligent priority assignment and duplicate detection.
NSGator is a Terraform module that automates Azure NSG rule management by handling priority assignment, detecting duplicates, and ensuring consistent rule deployment across network security groups.
Its input is a map of objects; each object represents a connectivity path including source / destination IPs or service tags, ports (if required), protocol, and a workload identifier. The module creates inbound and outbound rules as needed and manages priorities intelligently to avoid conflicts and ensure zero-downtime style updates.
This module simplifies managing multiple rules across Azure Network Security Groups (NSGs). It's especially useful for organizations using deny-by-default security policies that need to explicitly allow traffic for specific workloads while maintaining clear and consistent rule management.
NSGator simplifies Azure NSG rule management by providing:
- Intelligent Priority Assignment: Automatically assigns priorities to new rules within specified ranges
- Smart Duplicate Detection: Identifies and preserves existing rules to prevent conflicts
- Bidirectional Rule Support: Creates both inbound and outbound rules as needed
- Automated Tagging: Tracks Terraform-managed priority ranges with NSG tags
| Name | Version |
|---|---|
| terraform | >= 1.12 |
| azapi | >= 2.0 |
See the basic example for a full working configuration.
Steps:
- Create (or reference) your NSGs using the AzAPI or Azurerm provider.
- Configure provider aliases if you target different subscriptions (see
terraform.tf). - Add the module block with
source, priority ranges, and rules. - Run
terraform initthenterraform apply.
Minimal module usage:
module "nsgator" {
source = "../" # or registry source
source_nsg_id = azurerm_network_security_group.source.id
destination_nsg_id = azurerm_network_security_group.destination.id
priority_range = {
source_start = 1000
source_end = 1100
destination_start = 2000
destination_end = 2100
}
rules = {
web = {
source_ips = ["10.1.1.0/24"]
destination_ips = ["10.2.1.0/24"]
ports = ["80"]
protocol = "Tcp"
workload = "web"
}
}
}The module automatically prefixes rule names based on direction:
- Outbound rules (source NSG):
outbound-${workload}-${protocol}(e.g.,outbound-web-Tcp) - Inbound rules (destination NSG):
inbound-${workload}-${protocol}(e.g.,inbound-web-Tcp) - The
workloadproperty must be unique across all rules to avoid conflicts
This naming convention helps identify rule direction and purpose at a glance.
- Existing Rules: If a rule with identical properties exists, the module preserves its current priority
- New Rules: Assigned sequential priorities starting from the highest existing priority + 1
- Range Validation: New rules are only created if they fall within the specified priority range
- Error Handling: Rules that would exceed the priority range are skipped with an error message
The module enforces extensive input validation to surface errors early:
- At least one of
source_nsg_idordestination_nsg_idmust be provided. - Priority range fields required (and validated 100–4096, start < end) only for the side whose NSG ID is provided.
- Protocol must be one of:
Tcp,Udp,Icmp,Esp,Ah,*. - Workload names must be unique (used in rule naming).
- ICMP rules must NOT declare ports; non‑ICMP rules must declare at least one port.
- Exactly one of
source_ipsorsource_service_tagmust be supplied (not both, not neither) per rule. - Exactly one of
destination_ipsordestination_service_tagmust be supplied (not both, not neither) per rule. - A service tag cannot be combined with the corresponding NSG ID for that direction (e.g.
source_service_tagwithsource_nsg_id). - When service tag is provided the matching IP set must be omitted (and vice versa).
Rules that fail validation will stop planning with a clear multi‑line error message. Rules whose computed priority would fall outside the configured range are skipped via resource precondition failure.
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| source_nsg_id | The ID of the source NSG | string |
null |
no* |
| destination_nsg_id | The ID of the destination NSG | string |
null |
no* |
| create_outbound_rules | Flag to create outbound rules | bool |
true |
no |
| create_inbound_rules | Flag to create inbound rules | bool |
true |
no |
| create_tags | Whether to tag NSGs with managed priority range metadata | bool |
true |
no |
| rules | Map of rules to create (see rules object details) | map(object({ |
n/a | yes |
| priority_range | Priority ranges for source and destination NSGs | object({ |
n/a | yes |
*At least one of source_nsg_id or destination_nsg_id must be provided.
priority_range = {
source_start = optional(number, 0) # Start of priority range for source NSG (required if source_nsg_id is provided, must be 100-4096)
source_end = optional(number, 0) # End of priority range for source NSG (required if source_nsg_id is provided, must be 100-4096)
destination_start = optional(number, 0) # Start of priority range for destination NSG (required if destination_nsg_id is provided, must be 100-4096)
destination_end = optional(number, 0) # End of priority range for destination NSG (required if destination_nsg_id is provided, must be 100-4096)
}The module supports traditional IP / CIDR based rules as well as Azure Service Tags for the source or destination side of a flow (but not both sides in the same direction). A rule replaces address sets with a single service tag when a *_service_tag attribute is provided.
rules = {
# Standard TCP rule
web = {
source_ips = ["10.1.1.0/24"]
destination_ips = ["10.2.1.0/24"]
ports = ["80", "443"]
protocol = "Tcp"
workload = "web"
}
# Outbound using destination service tag
reach_aad = {
source_ips = ["10.1.1.0/24"]
destination_service_tag = "AzureActiveDirectory"
ports = ["443"]
protocol = "Tcp"
workload = "aad"
access = "Allow"
}
# Inbound using source service tag
from_storage = {
source_service_tag = "Storage"
destination_ips = ["10.2.1.10/32"]
ports = ["443"]
protocol = "Tcp"
workload = "storage-ingress"
}
# ICMP (no ports)
ping = {
source_ips = ["10.1.1.0/24"]
destination_ips = ["10.2.1.0/24"]
protocol = "Icmp"
workload = "ping"
}
}Important Notes / Validation Rules:
workloadmust be unique across all rules.- ICMP rules must NOT set
ports. All other protocols must set at least one port. - If
source_service_tagis provided:source_ipsmust NOT be set.source_ipsmust be set whensource_service_tagis NOT provided.- You must NOT provide
source_service_tagtogether with asource_nsg_id(the service tag represents an external source, not another managed NSG).
- If
destination_service_tagis provided:destination_ipsmust NOT be set.destination_ipsmust be set whendestination_service_tagis NOT provided.- You must NOT provide
destination_service_tagtogether with adestination_nsg_id.
- Service tags are mutually exclusive with address lists on the same side of a rule.
- Protocol must be one of:
Tcp,Udp,Icmp,Esp,Ah,*(case sensitive as shown). - Priorities are auto-assigned only when they fall inside the configured range; out-of-range rules are skipped with a precondition error.
| Name | Type | Description |
|---|---|---|
outbound_rules |
map(object) | Map keyed by rule key of outbound NSG rule metadata (id, name, priority, access, protocol, direction). Empty if outbound disabled or no source_nsg_id. |
inbound_rules |
map(object) | Map keyed by rule key of inbound NSG rule metadata (id, name, priority, access, protocol, direction). Empty if inbound disabled or no destination_nsg_id. |
Example: list all created inbound rule priorities
output "inbound_priorities" {
value = [for r in module.nsgator.inbound_rules : r.priority]
}Example: map of outbound rule names to protocols
output "outbound_protocols" {
value = { for k, r in module.nsgator.outbound_rules : k => r.protocol }
}If you need additional outputs (e.g., priority ranges or NSG IDs) you can expose them by adding outputs to outputs.tf.
NSGator can add tags to managed NSGs for tracking and governance (disable via create_tags = false):
managed_by_terraform_outbound_priority_range: Priority range for outbound rules (e.g., "1000-1100")managed_by_terraform_inbound_priority_range: Priority range for inbound rules (e.g., "2000-2100")
These tags help identify which priority ranges are managed by Terraform and prevent conflicts with manually created rules.
You can use Azure Service Tags instead of explicit IP/CIDR sets for EITHER the source side (inbound rules) or destination side (outbound rules) of a flow:
- Use
source_service_tagto represent the origin (e.g.Storage,AzureMonitor) when creating inbound rules to your destination NSG. - Use
destination_service_tagto represent the target (e.g.AzureActiveDirectory,KeyVault) when creating outbound rules from your source NSG.
Design constraints enforced by validation:
- A service tag replaces the corresponding
*_ipscollection. - Do not supply
source_service_tagtogether withsource_ipsor asource_nsg_id. - Do not supply
destination_service_tagtogether withdestination_ipsor adestination_nsg_id.
This pattern lets you model flows where one side is an Azure platform service and the other side is a controlled address space.
- Priority Ranges: Rules are skipped if they would exceed the configured priority range (100-4096).
- Unique Workloads: Each rule must have a unique workload identifier.
- Protocol-Specific Ports: ICMP rules don't require ports; all other protocols do.
- Service Tag Exclusivity: Service tags cannot be combined with IP sets or NSG IDs on the same side of the rule.
- Cross-Subscription: Requires proper provider configuration for cross-subscription deployments.
- NSGs can be deployed across different subscriptions, when calling the module, provide 2 providers in the module block:
source_nsg_idfor the source NSGdestination_nsg_idfor the destination NSG example:
providers {
source = azapi.source
destination = azapi.destination
}For complete examples, see the examples/ directory:
examples/basic-example/- Simple single configuration exampleexamples/hub-spoke-nsg-rules/- Multi-tier architecture example
- Feel free to open issues for bugs or feature requests
- Pull requests are welcome! Please ensure your changes include tests and documentation updates
This module is provided as-is. It is not intended for production use without thorough testing and validation in your specific environment.
