Automate NSG Rule Expiration

Build a Python Azure Function that auto-disables expired NSG firewall rules.

Introduction

30 Second Summary

Imagine giving a contractor temporary access to your network for a week. If nobody remembers to revoke that access on day eight, your network stays exposed indefinitely.

In this project, you will build a serverless function that checks firewall rules for expiration dates and disables any that have expired. It runs on a daily schedule, so temporary access gets cleaned up without anyone lifting a finger.

What You'll Build

A Python Azure Function that runs on a daily timer trigger, scans all security rules in an Azure Network Security Group, and flips any expired rules from Allow to Deny.

By the end of this project, you'll have:

  • A working Azure Function that automatically detects and disables expired NSG security rules on a daily schedule.
  • A deployed Function App in Azure using a managed identity for secure, passwordless authentication.
  • Hands-on experience with the Python v2 programming model, timer triggers, and the Azure Network Management SDK.
  • A secret mission to build an HTTP endpoint that creates temporary firewall rules on demand.

Are there any prerequisites?

You'll need an Azure account with an active subscription. You should also have Azure CLI and VS Code installed on your machine.

Before We Start

Before We Start...

Set Up Your Development Environment

Before you can build an Azure Functions app that automatically disables expired Network Security Group rules, you need the right tools on your machine.

In this step, you'll install Python 3.11 and Azure Functions Core Tools. Then you'll scaffold a new project and install the Azure SDK packages your function needs.

In this step, get ready to:
  • Install Python 3.11 and Azure Functions Core Tools v4.
  • Create an Azure Functions project using the Python v2 programming model.
  • Install the Azure SDK packages for network management and authentication.
Install Python and Azure Functions Core Tools

Your function runs on Python 3.11, which is the runtime version Azure Functions supports.

Azure Functions Core Tools (the func command) lets you create, test, and deploy functions from your terminal.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check if Python 3.11 is already installed by running this command:
python3.11 --version

Pro tip: Windows users

On Windows, run py -3.11 --version instead. The py launcher is how Windows selects specific Python versions.

I see a version number

Python 3.11 is ready. Move on to checking Azure Functions Core Tools below.

Command not found

No problem. Install Python 3.11 for your operating system.

macOS

  • Install Python 3.11 with Homebrew by running this command:
brew install python@3.11
  • Verify the installation by running:
python3.11 --version

You should see a version number starting with 3.11.

Windows

  • Download Python 3.11 from the official Python downloads page.
  • Run the installer.
  • Check Add Python 3.11 to PATH before clicking Install Now.
  • Open a new terminal window and verify by running:
py -3.11 --version

You should see a version number starting with 3.11.

  • Check if Azure Functions Core Tools is installed by running this command:
func --version

I see a version number

Azure Functions Core Tools is ready. You should see a version starting with 4.

Command not found

Install Azure Functions Core Tools v4 for your operating system.

macOS

  • Install Azure Functions Core Tools v4 with Homebrew by running these commands:
brew tap azure/functions
brew install azure-functions-core-tools@4
  • Verify the installation by running:
func --version

You should see a version starting with 4.

Windows

  • Install Azure Functions Core Tools v4 with npm by running this command:
npm install -g azure-functions-core-tools@4 --unsafe-perm true
  • Verify the installation by running:
func --version

You should see a version starting with 4.

Create the project and virtual environment

A Python virtual environment keeps your project's packages isolated from the rest of your system. You'll create a project folder, set up a virtual environment inside it, and then scaffold an Azure Functions project.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir nsg-expiration-function
cd nsg-expiration-function
  • Create and activate a Python virtual environment by running these commands:

macOS

python3.11 -m venv .venv
source .venv/bin/activate

Windows

py -3.11 -m venv .venv
.venv\Scripts\activate

Your terminal prompt should now start with (.venv), confirming the virtual environment is active.

Don't see (.venv) in your prompt?

Make sure you ran the activation command for your operating system. On macOS, use source .venv/bin/activate. On Windows, use .venv\Scripts\activate.

Help me activate my Python virtual environment.

  • Initialize the Azure Functions project by running this command:
func init --worker-runtime python --model V2

What does this command do?

  • func init scaffolds a new Azure Functions project in your current folder.
  • --worker-runtime python sets up a Python project.
  • --model V2 uses the v2 programming model, which defines triggers with decorators directly in your code instead of separate config files.
  • Confirm the project files were created by running:
ls

You should see function_app.py, host.json, requirements.txt, and local.settings.json in the output.

Missing project files?

Make sure you ran func init from inside the nsg-expiration-function folder. If you ran it from the wrong directory, delete the generated files and try again from the correct folder.

Help me set up my Azure Functions project.

Install the Azure SDK packages

Your function needs two additional Azure SDK packages. One handles authentication with Azure, and the other provides the API for reading and updating NSG rules.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the nsg-expiration-function folder on your Desktop, then click Open.
  • In the Explorer sidebar on the left, click requirements.txt to open it.
  • Select all the existing content and replace it with the following:
azure-functions
azure-identity==1.25.3
azure-mgmt-network==30.2.0

What are these packages?

  • azure-functions provides the core Azure Functions framework, including trigger and binding definitions.
  • azure-identity handles authentication with Azure using DefaultAzureCredential. This works with your Azure CLI login locally and managed identity in production.
  • azure-mgmt-network is the Azure Network Management SDK. It lets you read, create, and update NSG rules from Python code.
  • Save requirements.txt.
  • Back in your terminal, install the packages by running:
pip install -r requirements.txt
  • You should see Successfully installed followed by the package names in your terminal output.

Seeing installation errors?

Make sure your virtual environment is active. You should see (.venv) at the start of your terminal prompt. If it's missing, re-run the activation command from the previous substep.

Also confirm you saved requirements.txt before running the install command.

Help me fix my pip install errors.

Awesome, I've got everything!

Make sure you've saved requirements.txt and that the install completed successfully.

I'd like to double check the full code

azure-functions
azure-identity==1.25.3
azure-mgmt-network==30.2.0

Your development environment is ready and your project is scaffolded. Next up, you'll create the Azure infrastructure your function targets: a resource group, a Network Security Group, and sample rules with built-in expiration dates.

Create the NSG with Sample Rules

Your development environment is ready with Python, Azure Functions Core Tools, and the Azure SDK installed. Now it's time to create the Azure resources your function will manage.

In this step, you'll create a Network Security Group with two sample firewall rules. Each rule stores an expiration date in its description field, which your function will later parse to decide which rules to disable.

In this step, get ready to:
  • Log in to Azure and create a resource group.
  • Create a Network Security Group with sample security rules.
  • Verify the rules appear in the Azure Portal.
Log in to Azure and create your resources

Before you can create any Azure resources, you need to authenticate with your Azure account. The Azure CLI handles this through a browser-based sign-in flow.

  • Log in to your Azure account by running this command:
az login

What happens when you run this?

Your default browser opens a Microsoft sign-in page. After you authenticate, the terminal displays a list of your Azure subscriptions as JSON output. If you have multiple subscriptions, note which one is marked as isDefault: true. That is the subscription your commands will target.

Browser not opening?

If no browser window appears, try running az login --use-device-code instead. This gives you a code to enter manually at https://microsoft.com/devicelogin.

If you see "No subscriptions found," make sure your Azure account has an active subscription.

Help me troubleshoot my Azure CLI login

Now you need two things: a resource group to hold your Azure resources, and a Network Security Group inside it. A resource group is a container that groups related resources so you can manage them together.

  • Create a resource group and a Network Security Group by running these commands:
az group create --name nsg-expiration-rg --location eastus
az network nsg create --resource-group nsg-expiration-rg --name demo-nsg

What do these commands do?

  • az group create creates a resource group called nsg-expiration-rg in the eastus region. All resources for this project go in this group.
  • az network nsg create creates a Network Security Group called demo-nsg. This is the virtual firewall your function will monitor for expired rules.
  • Check the terminal output after each command. You should see JSON output containing "provisioningState": "Succeeded" for both the resource group and the NSG.

Seeing AuthorizationFailed?

Your Azure account needs at least Contributor access on the subscription. Check with your administrator if you're using a work or school account.

Help me fix my Azure authorization error

Add sample rules with expiration dates

Your function needs a way to know when a rule expires. The approach here is simple: embed an expiration date directly in each rule's description field using the format expires:YYYY-MM-DD. Your function will later parse this date and compare it to today's date.

You'll create two rules: one with a past expiration date (already expired) and one with a future date (still valid). This gives you both scenarios to test against.

  • Create both sample security rules by running these commands:
az network nsg rule create \
  --resource-group nsg-expiration-rg \
  --nsg-name demo-nsg \
  --name AllowSSH-Contractor \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes "203.0.113.0/24" \
  --destination-port-ranges 22 \
  --description "Temp SSH access for contractor - expires:2026-04-01"

az network nsg rule create \
  --resource-group nsg-expiration-rg \
  --nsg-name demo-nsg \
  --name AllowHTTPS-Partner \
  --priority 200 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes "198.51.100.0/24" \
  --destination-port-ranges 443 \
  --description "Temp HTTPS access for partner - expires:2026-12-31"

What do these parameters mean?

  • --priority: lower numbers are evaluated first. 100 is checked before 200.
  • --source-address-prefixes: the IP range allowed by this rule. These are documentation-reserved IP ranges, not real addresses.
  • --destination-port-ranges: port 22 is SSH, port 443 is HTTPS.
  • --description: contains a human-readable note plus the expires:YYYY-MM-DD tag your function will parse. NSG descriptions are limited to 140 characters.
  • After both commands finish, confirm the rules were created by running:
az network nsg rule list --resource-group nsg-expiration-rg --nsg-name demo-nsg --output table

You should see both AllowSSH-Contractor and AllowHTTPS-Partner listed with access set to Allow.

Rules not showing up?

Double check that the --nsg-name is exactly demo-nsg and the --resource-group is nsg-expiration-rg. Names are case-sensitive.

If you see a "priority already in use" error, a rule with the same priority number already exists. Delete the conflicting rule with az network nsg rule delete and try again.

Help me troubleshoot my NSG rule creation

Verify the rules in the Azure Portal

The CLI confirmed your rules exist, but seeing them in the Azure Portal gives you a visual confirmation. This is also where you'd go to inspect rules in a real environment.

  • Open https://portal.azure.com in your browser.
  • Type demo-nsg in the search bar at the top and select the demo-nsg Network Security Group from the results.
  • Click Inbound security rules in the left sidebar under Settings.

You should see both AllowSSH-Contractor (priority 100) and AllowHTTPS-Partner (priority 200) listed with their descriptions. Click on either rule to see the full description with the expires: date.

Don't see your rules?

Make sure you're viewing Inbound security rules, not the default rules tab. Your custom rules appear separately from Azure's built-in default rules.

Also check that you're signed in to the correct Azure subscription. Your subscription name appears in the top-right corner of the portal.

Help me find my NSG rules in the Azure Portal

Your Network Security Group is ready with two sample rules. One is already expired and waiting to be caught. Next up, you'll write the Python function that scans these rules and automatically disables the expired ones.

Write the Expiration Checker Function

Your Network Security Group is loaded with two sample rules, each carrying an expiration date in its description. Now you need the Python function that reads those rules, checks the dates, and flips any expired ones from "Allow" to "Deny."

You'll use the Azure Functions Python v2 programming model, which lets you define triggers with decorators right in your code. Everything lives in a single file: function_app.py.

In this step, get ready to:
  • Write the timer-triggered function that scans and disables expired NSG rules.
  • Configure local.settings.json with your Azure subscription details and storage connection string.
  • Review the key patterns powering the expiration checker.
Write the function code

The function starts with imports, an app instance, and the setup logic. This includes the timer trigger decorator, environment variable reads, and authentication.

  • Open function_app.py in VS Code and replace its entire contents with the following code:
import datetime
import logging
import os
import re

import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.mgmt.network import NetworkManagementClient

# Create the Function App instance
app = func.FunctionApp()


# Timer trigger that reads its schedule from the TIMER_SCHEDULE app setting
@app.timer_trigger(
    schedule="%TIMER_SCHEDULE%",
    arg_name="mytimer",
    run_on_startup=True,
    use_monitor=False,
)
def expire_nsg_rules(mytimer: func.TimerRequest) -> None:
    # Log the start time in UTC
    utc_timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
    logging.info(f"NSG rule expiration check started at: {utc_timestamp}")

    # Check if the timer fired late (e.g. after a host restart)
    if mytimer.past_due:
        logging.warning("The timer is running late!")

    # Read target NSG details from environment variables
    subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"]
    resource_group = os.environ["RESOURCE_GROUP_NAME"]
    nsg_name = os.environ["NSG_NAME"]

    # Authenticate and create the network management client
    credential = DefaultAzureCredential()
    network_client = NetworkManagementClient(credential, subscription_id)

    # Fetch all security rules from the target NSG
    rules = network_client.security_rules.list(resource_group, nsg_name)
    today = datetime.date.today()

What does this code do?

  • The imports bring in Python's datetime, logging, os, and re modules alongside the Azure Functions SDK, identity library, and network management client.
  • app = func.FunctionApp() creates the function app instance that Azure uses to register your functions.
  • The @app.timer_trigger decorator tells Azure to run this function on a schedule. %TIMER_SCHEDULE% reads the schedule string from your app settings instead of hardcoding it.
  • run_on_startup=True makes the function fire immediately when the host starts. This is handy for testing.
  • DefaultAzureCredential() handles authentication. It picks up your Azure CLI session locally and managed identity when deployed to Azure.
  • security_rules.list() fetches every security rule from the target NSG so the function can iterate through them.

You won't see a change yet. The function still needs its core logic, which you'll add next.

  • Still in function_app.py, add the following code directly below the today = datetime.date.today() line. Make sure it stays inside the expire_nsg_rules function (indented with 4 spaces):
    for rule in rules:
        # Skip rules that are already set to Deny
        if rule.access != "Allow":
            logging.info(f"Skipping rule '{rule.name}' - already set to {rule.access}")
            continue

        # Skip rules with no description (no way to check expiration)
        if not rule.description:
            logging.info(f"Skipping rule '{rule.name}' - no description")
            continue

        # Look for the expires:YYYY-MM-DD pattern in the description
        match = re.search(r"expires:(\d{4}-\d{2}-\d{2})", rule.description)
        if not match:
            logging.info(f"Skipping rule '{rule.name}' - no expiration date found")
            continue

        # Parse the expiration date and compare to today
        expiration_date = datetime.date.fromisoformat(match.group(1))

        if expiration_date < today:
            logging.info(
                f"Rule '{rule.name}' expired on {expiration_date}. Disabling."
            )
            # Change the rule from Allow to Deny and push the update to Azure
            rule.access = "Deny"
            network_client.security_rules.begin_create_or_update(
                resource_group, nsg_name, rule.name, rule
            ).result()
            logging.info(f"Rule '{rule.name}' has been set to Deny.")
        else:
            logging.info(
                f"Rule '{rule.name}' is still valid until {expiration_date}."
            )

    logging.info("NSG rule expiration check completed.")

What does this code do?

  • The for loop iterates through every rule in the NSG.
  • Three continue guards skip rules that are already set to Deny, have no description, or don't contain the expires: pattern.
  • re.search() uses a regex to extract the date string after expires: from the rule description.
  • datetime.date.fromisoformat() converts the extracted date string into a Python date object so you can compare it to today's date.
  • If the expiration date is in the past, the function sets rule.access = "Deny" and pushes the update to Azure with begin_create_or_update(). The .result() call waits for the update to finish before moving on.
  • Save function_app.py.

You won't see any output yet. You'll test the function locally in the next step.

Seeing a syntax error in VS Code?

  • Double check that all the code inside the for loop is indented with 4 spaces (one level inside the function body). Python is sensitive to indentation.
  • Make sure the for rule in rules: line comes directly after today = datetime.date.today() with one blank line between them.
  • Check that you have the closing parenthesis on every logging.info() call. Multi-line log messages use opening and closing parentheses on separate lines.

Help me find the syntax error in my function_app.py.

Awesome, I've got everything!

Great. Double check you've saved function_app.py before moving on.

I'd like to double check the full code

import datetime
import logging
import os
import re

import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.mgmt.network import NetworkManagementClient

app = func.FunctionApp()


@app.timer_trigger(
    schedule="%TIMER_SCHEDULE%",
    arg_name="mytimer",
    run_on_startup=True,
    use_monitor=False,
)
def expire_nsg_rules(mytimer: func.TimerRequest) -> None:
    utc_timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
    logging.info(f"NSG rule expiration check started at: {utc_timestamp}")

    if mytimer.past_due:
        logging.warning("The timer is running late!")

    subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"]
    resource_group = os.environ["RESOURCE_GROUP_NAME"]
    nsg_name = os.environ["NSG_NAME"]

    credential = DefaultAzureCredential()
    network_client = NetworkManagementClient(credential, subscription_id)

    rules = network_client.security_rules.list(resource_group, nsg_name)
    today = datetime.date.today()

    for rule in rules:
        if rule.access != "Allow":
            logging.info(f"Skipping rule '{rule.name}' - already set to {rule.access}")
            continue

        if not rule.description:
            logging.info(f"Skipping rule '{rule.name}' - no description")
            continue

        match = re.search(r"expires:(\d{4}-\d{2}-\d{2})", rule.description)
        if not match:
            logging.info(f"Skipping rule '{rule.name}' - no expiration date found")
            continue

        expiration_date = datetime.date.fromisoformat(match.group(1))

        if expiration_date < today:
            logging.info(
                f"Rule '{rule.name}' expired on {expiration_date}. Disabling."
            )
            rule.access = "Deny"
            network_client.security_rules.begin_create_or_update(
                resource_group, nsg_name, rule.name, rule
            ).result()
            logging.info(f"Rule '{rule.name}' has been set to Deny.")
        else:
            logging.info(
                f"Rule '{rule.name}' is still valid until {expiration_date}."
            )

    logging.info("NSG rule expiration check completed.")
Configure local settings

Your function reads configuration values like the subscription ID and NSG name from environment variables. During local development, these values live in local.settings.json.

  • Retrieve your Azure subscription ID by running this command:
az account show --query id --output tsv
  • Copy the output. You'll paste it into local.settings.json shortly.

Timer triggers use a storage account to track schedule state and avoid duplicate executions. You need to create one and grab its connection string.

  • Create a storage account for the function by running this command:
az storage account create \
  --name nsgexpfuncstore \
  --resource-group nsg-expiration-rg \
  --location eastus \
  --sku Standard_LRS

Name already taken?

Storage account names must be globally unique across all of Azure. If nsgexpfuncstore is taken, try appending random characters (e.g., nsgexpfuncstore7x2). Use your chosen name in all subsequent commands that reference the storage account.

Help me troubleshoot my Azure storage account creation.

  • Retrieve the storage account's connection string by running:
az storage account show-connection-string \
  --name nsgexpfuncstore \
  --resource-group nsg-expiration-rg \
  --query connectionString \
  --output tsv
  • Copy the connection string output. You'll paste it into local.settings.json next.

Now you have both values you need. Time to configure the settings file.

  • Replace the contents of local.settings.json with the following. Swap your-subscription-id-here with the subscription ID you copied earlier, and your-storage-connection-string-here with the connection string you just retrieved:
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "your-storage-connection-string-here",
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "TIMER_SCHEDULE": "0 0 0 * * *",
    "AZURE_SUBSCRIPTION_ID": "your-subscription-id-here",
    "RESOURCE_GROUP_NAME": "nsg-expiration-rg",
    "NSG_NAME": "demo-nsg"
  }
}

What do these settings do?

  • AzureWebJobsStorage points to the storage account that the timer trigger uses to track schedule state.
  • TIMER_SCHEDULE holds the NCRONTAB expression 0 0 0 * * *, which means every day at midnight UTC.
  • AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP_NAME, and NSG_NAME tell the function which NSG to scan.
  • Save local.settings.json.

Pasted the wrong value?

If you need to retrieve your subscription ID again, run az account show --query id --output tsv. For the storage connection string, run the az storage account show-connection-string command from earlier.

Help me fix my local.settings.json configuration.

Review the code logic

Before you test the function, take a moment to understand the three key patterns that make it work.

What is a timer trigger?

A timer trigger fires your function on a schedule instead of waiting for an external event like an HTTP request. The schedule is defined using a 6-field NCRONTAB expression: {second} {minute} {hour} {day} {month} {day-of-week}.

Your schedule 0 0 0 * * * translates to "every day at midnight UTC." By wrapping it in %TIMER_SCHEDULE% in the decorator, you can change the schedule through app settings without redeploying code.

Why DefaultAzureCredential?

DefaultAzureCredential is a credential chain that tries multiple authentication methods in order. Locally, it picks up your Azure CLI session from az login. In production, it picks up the function app's managed identity.

This means you write one line of authentication code and it works in both environments. No API keys or connection strings for the Azure SDK needed.

How does the expires: pattern work?

NSG rule descriptions are free-text fields (up to 140 characters). By embedding expires:2026-04-01 at the end of a description, you give the function a machine-readable expiration date alongside a human-readable explanation.

The regex expires:(\d{4}-\d{2}-\d{2}) extracts the date portion. If the date is before today, the function changes the rule from "Allow" to "Deny" using the Azure Network Management SDK's begin_create_or_update() method.

Your function code is written and your local settings are configured. Next up, you'll run the function locally to see it detect and disable the expired rule in real time.

Test Your Function Locally

In the last step, you wrote the Python function that scans NSG rules and disables any that have passed their expiration date.

Now it's time to run the function on your machine and watch it work. You'll fire the function locally, see it detect the expired AllowSSH-Contractor rule, and confirm it was changed to "Deny" in Azure.

In this step, get ready to:
  • Authenticate your local environment with Azure CLI.
  • Run the function locally and watch it process NSG rules.
  • Verify the expired rule was changed to Deny in Azure.
Confirm your Azure CLI login

Your function uses DefaultAzureCredential to authenticate with Azure. During local development, this credential automatically picks up your active Azure CLI session.

  • Refresh your CLI session by running this command:
az login

A browser window opens for you to sign in with your Azure account. If you already logged in during Step 2, the command refreshes your session.

Browser not opening?

  • If the browser does not open automatically, copy the URL shown in the terminal and paste it into your browser manually.
  • If you see a subscription selection prompt, choose the subscription where you created your nsg-expiration-rg resource group.

Help me troubleshoot Azure CLI login issues

Run the function locally

The function's timer trigger is configured with run_on_startup=True. This means the function fires immediately when the host starts, instead of waiting for the next scheduled time.

  • Make sure your virtual environment is still active (you should see (.venv) in your terminal prompt).
  • Start the function host by running this command:
func start

The function host takes a few seconds to load, then the function fires automatically. Watch the terminal output for log lines showing the function's progress.

What should I see?

Your terminal should display log lines as the function processes each rule. Look for these key messages:

  • Rule 'AllowSSH-Contractor' expired on 2026-04-01. Disabling. confirms the function detected the expired rule.
  • Rule 'AllowSSH-Contractor' has been set to Deny. confirms the rule was updated in Azure.
  • Rule 'AllowHTTPS-Partner' is still valid until 2026-12-31. confirms the valid rule was left alone.

Seeing an error when the function runs?

  • If you see KeyError: 'AZURE_SUBSCRIPTION_ID', double check that your local.settings.json file has all the required environment variables from Step 3.
  • If you see an authentication error, make sure you ran az login successfully in the previous substep.
  • If you see No storage connection string found, verify that the AzureWebJobsStorage value in local.settings.json contains your full storage account connection string.
  • If the host starts but the function never fires, confirm that run_on_startup=True is set in the timer trigger decorator in function_app.py.

Help me debug my Azure Function that fails on startup

Verify the rule changed to Deny

The terminal logs show what the function did, but let's confirm the change by querying Azure directly.

  • Open a new terminal window or tab, keeping the function host running in the original one.
  • Check the access setting of the expired rule by running this command:
az network nsg rule show \
  --resource-group nsg-expiration-rg \
  --nsg-name demo-nsg \
  --name AllowSSH-Contractor \
  --query access \
  --output tsv

The output should be Deny.

What about the other rule?

The AllowHTTPS-Partner rule has an expiration date of 2026-12-31, which is still in the future. Your function correctly left it set to "Allow."

  • Switch back to the terminal running the function host and press Ctrl+C to stop it.

Still seeing Allow?

  • Make sure the expiration date in the rule's description is in the past. The AllowSSH-Contractor rule should have expires:2026-04-01 in its description.
  • Check that the RESOURCE_GROUP_NAME and NSG_NAME values in local.settings.json match the resources you created in Step 2.
  • Scroll up in the function host terminal to see if the function logged any errors during processing.

Help me figure out why my NSG rule was not changed to Deny

Your function works locally. Next up, you'll deploy it to Azure so it can run on its own daily schedule without your machine being on.

Deploy Your Function to Azure

Your function runs perfectly on your local machine. It found the expired rule and flipped it to Deny exactly as expected.

Now it's time to get this function running in the cloud, where it can enforce expirations on a daily schedule without your laptop involved. In this step, you'll create a Function App in Azure, deploy your code to it, and configure the environment variables it needs.

In this step, get ready to:
  • Create a Function App on the Consumption plan.
  • Deploy your function code to Azure.
  • Configure environment variables for the deployed function.
Create the Function App

A Function App is the Azure resource that hosts and runs your function code. The Consumption plan means Azure only allocates compute when your function actually runs.

Why the Consumption plan?

A daily timer trigger fires about 30 times per month. The Consumption plan's free tier covers 1 million executions per month and 400,000 GB-seconds of compute. Your function will cost $0.

Your Function App name must be globally unique across all of Azure because it becomes part of the app's URL. Pick a name like nsg-expiration-func-abc123, replacing the random characters with your own.

  • Create the Function App by running this command:
az functionapp create \
  --resource-group nsg-expiration-rg \
  --consumption-plan-location eastus \
  --runtime python \
  --runtime-version 3.11 \
  --functions-version 4 \
  --name [[FUNC_APP_NAME="nsg-expiration-func-abc123"]] \
  --storage-account [[STORAGE_ACCOUNT_NAME="nsgexpfuncstore"]] \
  --os-type Linux

What does this command do?

  • --consumption-plan-location eastus creates the app on the serverless Consumption plan in the East US region.
  • --runtime python --runtime-version 3.11 tells Azure to run your function with Python 3.11, matching your local environment.
  • --storage-account links the storage account you created earlier. Azure Functions uses this storage for internal state like timer trigger scheduling.
  • --os-type Linux runs the function on Linux, which is required for Python functions on the Consumption plan.

The command takes about a minute to complete. You should see a large JSON output containing your Function App's details, including its defaultHostName.

Seeing a name conflict error?

If Azure says the function app name is already taken, choose a different name. Try appending more random characters or your initials.

If you see an error about the storage account not being found, make sure the --storage-account value matches exactly what you named your storage account in the previous step.

Help me troubleshoot this az functionapp create error

Deploy the function code

The Function App is created, but it's empty. You need to push your local project code to it.

  • Publish your local project to the Function App by running:
func azure functionapp publish [[FUNC_APP_NAME="nsg-expiration-func-abc123"]]

This command packages your project, uploads it to Azure, and triggers a remote build. It takes a couple of minutes.

  • Watch the terminal output. You should see Remote build succeeded followed by a list of your deployed functions, including expire_nsg_rules.

Deployment failed?

Make sure your virtual environment is activated (source .venv/bin/activate) and that you're running the command from inside your nsg-expiration-function project folder.

If you see a permissions error, make sure you're logged in by running az login again.

Help me fix this Azure Functions deployment error

Configure app settings

Your local.settings.json file is for local development only. Azure does not deploy it to your Function App. You need to set the same environment variables as app settings directly on the deployed Function App.

Why isn't local.settings.json deployed?

The local.settings.json file often contains connection strings and secrets meant only for your machine. Azure excludes it from deployment by default to prevent sensitive values from being accidentally pushed to the cloud. App settings on the Function App serve the same purpose in production.

  • Add the required environment variables to your Function App by running this command. Replace your-subscription-id-here with the subscription ID you retrieved earlier:
az functionapp config appsettings set \
  --name [[FUNC_APP_NAME="nsg-expiration-func-abc123"]] \
  --resource-group nsg-expiration-rg \
  --settings \
    AZURE_SUBSCRIPTION_ID="your-subscription-id-here" \
    RESOURCE_GROUP_NAME="nsg-expiration-rg" \
    NSG_NAME="demo-nsg" \
    TIMER_SCHEDULE="0 0 0 * * *"

What do these settings do?

  • AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP_NAME, and NSG_NAME tell your function which NSG to scan. These are the same values from your local.settings.json.
  • TIMER_SCHEDULE sets the NCRONTAB expression. 0 0 0 * * * means "every day at midnight UTC."
  • The command outputs a JSON array of all app settings on the Function App. Confirm that AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP_NAME, NSG_NAME, and TIMER_SCHEDULE all appear in the output with the correct values.

Settings not showing up?

Double-check that the --name flag matches your Function App name exactly. A typo here creates settings on a nonexistent app and silently fails.

If you see a "ResourceNotFound" error, the Function App may not have finished creating. Wait a minute and try again.

Help me troubleshoot my Function App settings

Your function is live in Azure and configured to scan your NSG every day at midnight. There's one critical piece missing though. Right now, the Function App has no permissions to read or update NSG rules. In the next step, you'll grant it secure access using a managed identity.

Configure Managed Identity and Verify

Your function is deployed to Azure with the right environment variables. One critical piece is still missing.

The Function App has no permissions to read or update NSG rules. You will fix this by enabling a managed identity and assigning an RBAC role.

In this step, get ready to:
  • Enable a system-assigned managed identity for your Function App.
  • Assign the Network Contributor role to grant least-privilege access.
  • Trigger the deployed function and verify it disables the expired rule.
Enable system-assigned managed identity

A managed identity is an automatically managed service principal that Azure ties to your resource. When your function code calls DefaultAzureCredential() in production, it picks up this identity. No passwords or API keys required.

  • Enable the managed identity for your Function App by running this command:
az functionapp identity assign \
  --name nsg-expiration-func \
  --resource-group nsg-expiration-rg

What does this command do?

  • Azure registers a system-assigned identity for your Function App.
  • The identity's lifecycle is tied to the Function App. If you delete the app, the identity is automatically removed.
  • The JSON output includes a principalId field, which uniquely identifies this identity.
  • Look for the principalId field in the JSON output.
  • Copy this value. You will need it in the next substep to grant permissions.

Getting an authorization error?

  • Make sure you are logged in to Azure CLI. Run az login if your session expired.
  • Confirm the Function App name matches what you used in Step 5. If you chose a custom name, substitute it in the command.
  • Help me troubleshoot the az functionapp identity assign command.
Assign the Network Contributor role

An identity alone is not enough. Your function also needs explicit permission to interact with network resources. RBAC lets you assign a specific role at a specific scope, following the principle of least privilege.

  • Grant your Function App's managed identity the Network Contributor role by running this command:
az role assignment create \
  --assignee [[PRINCIPAL_ID="the principalId you copied"]] \
  --role "Network Contributor" \
  --scope /subscriptions/[[AZURE_SUBSCRIPTION_ID="your-subscription-id"]]/resourceGroups/nsg-expiration-rg

What does this command do?

  • The --assignee flag tells Azure which identity receives the role.
  • The Network Contributor role grants read and write access to network resources like NSG rules.
  • The --scope flag limits the role to your specific resource group, not the entire subscription.
  • You will not see a visible change yet. The next substep tests this permission by triggering the function.

Seeing a 'does not exist' error?

  • Double-check that the principalId value is correct. It should be a UUID from the previous command's output.
  • Make sure your subscription ID matches the one you retrieved earlier with az account show --query id --output tsv.
  • Help me fix the az role assignment create command.
Trigger the function and verify

Your function now has both an identity and permissions. Time to test the full deployed flow by resetting the expired rule and letting the function disable it again.

  • Reset the AllowSSH-Contractor rule back to Allow so you can test the deployed function. Run this command:
az network nsg rule update \
  --resource-group nsg-expiration-rg \
  --nsg-name demo-nsg \
  --name AllowSSH-Contractor \
  --access Allow

Why reset the rule?

Your local test in Step 4 already changed this rule to Deny. Resetting it to Allow lets you confirm the deployed function works the same way.

  • Open the Azure Portal.
  • In the top search bar, type nsg-expiration-func.
  • Select your Function App from the search results.
  • Click Functions in the left menu.
  • Click expire_nsg_rules.
  • Click Code + Test.
  • Click Run at the top of the page.

The function processes your NSG rules. This may take a few seconds.

  • Click Monitor in the left menu to view recent executions.
  • Click the most recent invocation to see its log output.

You should see log messages indicating the AllowSSH-Contractor rule expired and was set to Deny.

  • Back in your terminal, confirm the rule was updated by running this command:
az network nsg rule show \
  --resource-group nsg-expiration-rg \
  --nsg-name demo-nsg \
  --name AllowSSH-Contractor \
  --query access \
  --output tsv

The output should be Deny. Your deployed function successfully detected and disabled the expired rule.

Still seeing Allow?

  • Open Monitor in the Function App to check for error messages. Look for authentication or authorization failures.
  • Verify the role assignment is in place. Run az role assignment list --assignee YOUR_PRINCIPAL_ID --output table and confirm Network Contributor appears.
  • Make sure the app settings (AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP_NAME, NSG_NAME) are correct in your Function App configuration.
  • My deployed function is not changing the NSG rule to Deny. Help me debug.

Secret mission

Add an HTTP Trigger for On-Demand Rule Creation

Your timer-triggered function handles automatic cleanup, but what about creating new temporary rules on demand? In this secret mission, you'll add an HTTP endpoint that lets administrators grant temporary network access through a simple API call.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses the Azure Functions Consumption plan (free for up to 1M executions/month) and a storage account that costs pennies per month. The Network Security Group is free.

Cost warning

The storage account (nsgexpfuncstore) incurs a small ongoing charge even when the Function App is stopped. It costs only a few cents per month, but if you want to avoid any charges, delete the resource group entirely.

Resources you used:

  • Resource group: nsg-expiration-rg.
  • Function App: nsg-expiration-func (Consumption plan, free tier).
  • Storage Account: nsgexpfuncstore (minimal cost, pennies per month).
  • Network Security Group: demo-nsg (free, no cost).

Keep everything running

No action needed. Choose this if you are still actively building or want to keep the function running on its daily schedule.

  • Your function will continue running once per day at midnight UTC.
  • The Consumption plan costs $0 for this usage level (a daily timer trigger is roughly 30 executions/month, well within the 1M free executions).
  • The storage account will cost a few cents per month.

Pause - I'll come back to this later

Stop the Function App to prevent it from running on its schedule. Your code and configuration will be preserved.

  • In the Azure Portal, navigate to your Function App (nsg-expiration-func).
  • On the Overview page, click Stop.
  • The storage account will still incur minimal charges (a few cents per month).
  • To resume later, click Start on the same page.

Delete - I don't want to use this again

Remove all project resources by deleting the resource group. This deletes the Function App, storage account, NSG, and everything else inside the group.

  • Run this command to delete the entire resource group:
az group delete --name nsg-expiration-rg --yes --no-wait
  • The --no-wait flag lets the command return immediately while Azure deletes resources in the background.
  • Optionally, delete the local project folder (nsg-expiration-function) from your machine.

Mission accomplished

Nice Work

You just built a security automation tool that runs itself. Your Azure Function now enforces firewall rule expirations automatically, every single day, without anyone having to remember to revoke access.

Here's what you learned:

  • Built a timer-triggered Azure Function using the Python v2 programming model that scans NSG rules and disables expired ones on a daily NCRONTAB schedule.
  • Authenticated securely with DefaultAzureCredential for local development and managed identity in production, with least-privilege RBAC scoped to the resource group.
  • Deployed a serverless function to Azure on the free Consumption plan and used the Azure Network Management SDK to programmatically read and update NSG security rules.
  • Added an HTTP trigger endpoint for creating temporary NSG rules with custom expiration dates on demand.

Ready to quiz yourself?