Last active
April 2, 2026 12:09
-
-
Save mvasilenko/df8c293ce67f8f9b709cb1b0b5836c5b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Usage examples: | |
| # ./az-nsg-lockdown.sh on -g my-rg -v my-vm # VM: lock down (interactive) | |
| # ./az-nsg-lockdown.sh off -g my-rg -v my-vm # VM: unlock (interactive) | |
| # ./az-nsg-lockdown.sh on -g my-rg -s my-vmss # VMSS: lock down via NIC NSG only (interactive) | |
| # ./az-nsg-lockdown.sh off -g my-rg -s my-vmss # VMSS: unlock via NIC NSG only (interactive) | |
| # ./az-nsg-lockdown.sh on -g my-rg -v my-vm --yes # lock down (no prompt) | |
| # ./az-nsg-lockdown.sh off -g my-rg -s my-vmss -y # unlock (no prompt) | |
| set -euo pipefail | |
| RULE_NAME_INBOUND="DenyAllInbound" | |
| RULE_NAME_OUTBOUND="DenyAllOutbound" | |
| RULE_PRIORITY=100 | |
| usage() { | |
| cat <<EOF | |
| Usage: $(basename "$0") <on|off> [OPTIONS] | |
| Toggle deny-all NSG rules for an Azure VM, VMSS, or NSG. | |
| Commands: | |
| on Apply deny-all inbound/outbound rules | |
| off Remove deny-all rules | |
| Options: | |
| -g, --resource-group <name> Resource group (required) | |
| -n, --nsg-name <name> NSG name (use this OR --vm-name/--vmss-name) | |
| -v, --vm-name <name> VM name — NSG will be auto-detected | |
| -s, --vmss-name <name> VMSS name — NSG will be auto-detected | |
| -p, --priority <num> Rule priority (default: ${RULE_PRIORITY}) | |
| --inbound-only Only affect inbound rules | |
| --outbound-only Only affect outbound rules | |
| -y, --yes Skip confirmation prompt | |
| -h, --help Show this help | |
| Examples: | |
| $(basename "$0") on -g my-rg -v my-vm | |
| $(basename "$0") off -g my-rg -v my-vm | |
| $(basename "$0") on -g my-rg -s my-vmss | |
| $(basename "$0") off -g my-rg -s my-vmss | |
| $(basename "$0") on -g my-rg -n my-nsg -p 50 --yes | |
| EOF | |
| } | |
| die() { echo "ERROR: $*" >&2; exit 1; } | |
| # Parse command | |
| [[ $# -lt 1 ]] && { usage; exit 1; } | |
| ACTION="$1"; shift | |
| [[ "$ACTION" == "-h" || "$ACTION" == "--help" ]] && { usage; exit 0; } | |
| [[ "$ACTION" != "on" && "$ACTION" != "off" ]] && die "First argument must be 'on' or 'off', got: '$ACTION'" | |
| RESOURCE_GROUP="" | |
| NSG_NAME="" | |
| VM_NAME="" | |
| VMSS_NAME="" | |
| INBOUND_ONLY=false | |
| OUTBOUND_ONLY=false | |
| YES=false | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -g|--resource-group) RESOURCE_GROUP="$2"; shift 2 ;; | |
| -n|--nsg-name) NSG_NAME="$2"; shift 2 ;; | |
| -v|--vm-name) VM_NAME="$2"; shift 2 ;; | |
| -s|--vmss-name) VMSS_NAME="$2"; shift 2 ;; | |
| -p|--priority) RULE_PRIORITY="$2"; shift 2 ;; | |
| --inbound-only) INBOUND_ONLY=true; shift ;; | |
| --outbound-only) OUTBOUND_ONLY=true; shift ;; | |
| -y|--yes) YES=true; shift ;; | |
| -h|--help) usage; exit 0 ;; | |
| *) die "Unknown option: $1" ;; | |
| esac | |
| done | |
| [[ -z "$RESOURCE_GROUP" ]] && die "--resource-group is required" | |
| [[ "$INBOUND_ONLY" == true && "$OUTBOUND_ONLY" == true ]] && die "--inbound-only and --outbound-only are mutually exclusive" | |
| [[ -n "$VM_NAME" && -n "$VMSS_NAME" ]] && die "--vm-name and --vmss-name are mutually exclusive" | |
| # Resolve NSG name from VM or VMSS if needed | |
| if [[ -z "$NSG_NAME" ]]; then | |
| if [[ -n "$VM_NAME" ]]; then | |
| echo "Looking up NSG for VM '$VM_NAME'..." | |
| NIC_ID=$(az vm show -g "$RESOURCE_GROUP" -n "$VM_NAME" \ | |
| --query "networkProfile.networkInterfaces[0].id" -o tsv) | |
| [[ -z "$NIC_ID" ]] && die "No NIC found for VM '$VM_NAME'" | |
| NSG_ID=$(az network nic show --ids "$NIC_ID" \ | |
| --query "networkSecurityGroup.id" -o tsv 2>/dev/null || true) | |
| [[ -z "$NSG_ID" ]] && die "No NSG attached to NIC of VM '$VM_NAME'" | |
| NSG_NAME=$(basename "$NSG_ID") | |
| elif [[ -n "$VMSS_NAME" ]]; then | |
| echo "Looking up NSG for VMSS '$VMSS_NAME'..." | |
| NSG_ID=$(az vmss show -g "$RESOURCE_GROUP" -n "$VMSS_NAME" \ | |
| --query "virtualMachineProfile.networkProfile.networkInterfaceConfigurations[0].networkSecurityGroup.id" \ | |
| -o tsv 2>/dev/null || true) | |
| [[ -z "$NSG_ID" ]] && die "No NSG attached to NIC config of VMSS '$VMSS_NAME'" | |
| NSG_NAME=$(basename "$NSG_ID") | |
| else | |
| die "Either --nsg-name, --vm-name, or --vmss-name is required" | |
| fi | |
| echo "Resolved NSG: $NSG_NAME" | |
| fi | |
| apply_rule() { | |
| local direction="$1" | |
| local rule_name="$2" | |
| echo " Creating $direction deny-all rule '$rule_name' (priority $RULE_PRIORITY)..." | |
| az network nsg rule create \ | |
| --resource-group "$RESOURCE_GROUP" \ | |
| --nsg-name "$NSG_NAME" \ | |
| --name "$rule_name" \ | |
| --priority "$RULE_PRIORITY" \ | |
| --direction "$direction" \ | |
| --access Deny \ | |
| --protocol '*' \ | |
| --source-address-prefixes '*' \ | |
| --source-port-ranges '*' \ | |
| --destination-address-prefixes '*' \ | |
| --destination-port-ranges '*' \ | |
| --output none | |
| echo " Done." | |
| } | |
| remove_rule() { | |
| local direction="$1" | |
| local rule_name="$2" | |
| if az network nsg rule show \ | |
| --resource-group "$RESOURCE_GROUP" \ | |
| --nsg-name "$NSG_NAME" \ | |
| --name "$rule_name" &>/dev/null; then | |
| echo " Removing $direction deny-all rule '$rule_name'..." | |
| az network nsg rule delete \ | |
| --resource-group "$RESOURCE_GROUP" \ | |
| --nsg-name "$NSG_NAME" \ | |
| --name "$rule_name" \ | |
| --output none | |
| echo " Done." | |
| else | |
| echo " Rule '$rule_name' not found — skipping." | |
| fi | |
| } | |
| echo "NSG: $NSG_NAME | Resource group: $RESOURCE_GROUP | Action: $ACTION" | |
| echo "" | |
| if [[ "$YES" == false ]]; then | |
| read -r -p "Proceed with lockdown $ACTION? [y/N] " confirm | |
| [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } | |
| echo "" | |
| fi | |
| if [[ "$ACTION" == "on" ]]; then | |
| [[ "$OUTBOUND_ONLY" == false ]] && apply_rule Inbound "$RULE_NAME_INBOUND" | |
| [[ "$INBOUND_ONLY" == false ]] && apply_rule Outbound "$RULE_NAME_OUTBOUND" | |
| echo "Lockdown ON — all traffic denied." | |
| else | |
| [[ "$OUTBOUND_ONLY" == false ]] && remove_rule Inbound "$RULE_NAME_INBOUND" | |
| [[ "$INBOUND_ONLY" == false ]] && remove_rule Outbound "$RULE_NAME_OUTBOUND" | |
| echo "Lockdown OFF — deny-all rules removed." | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment