Automate Azure Firewall Rules with Logic Apps
Build a private security pipeline that auto-updates Azure Firewall rules via Logic Apps.
Introduction
30 Second Summary
Imagine your building's security team could instantly update access rules the moment a threat is detected. No phone calls, no manual gate adjustments. The system spots the issue and locks things down automatically.
In this project, you will build a security automation pipeline where a web app sends a request to a private Logic App that programmatically updates Azure Firewall rules. The entire chain runs inside a virtual network, invisible to the public internet.
What You'll Build
You'll trigger a web app from your browser and watch a new firewall rule appear in the Azure Portal seconds later, created automatically through a private channel that rejects any public internet traffic.
By the end of this project, you'll have:
- A Logic App Standard workflow that receives HTTP requests and updates Azure Firewall Policy rules using a managed identity.
- A private endpoint that blocks all public access to the Logic App. Public callers get a 403 Forbidden response, proving the lockdown is real.
- A VNet-integrated App Service that calls the Logic App over the private network to prove the end-to-end automation works.
- Secret Mission: Add input validation and duplicate-rule detection to make the workflow production-ready.
Are there any prerequisites?
You'll need an active Azure subscription and the Azure CLI installed on your machine. This project assumes familiarity with VNets, subnets, and private endpoints.
Before We Start
Before diving into any infrastructure, let's lock in what you're about to build and why it matters.
Deploy the Network Foundation and Azure Firewall
Every component in your security automation pipeline needs its own isolated network space. The VNet you create here is the backbone that connects the Azure Firewall, the Logic App, and the App Service while keeping each one in a dedicated subnet.
In this step you will build the full network foundation: a VNet with four purpose-built subnets, a Firewall Policy that your Logic App will programmatically manage, and the Logic App Standard instance that will eventually call the Firewall REST API.
In this step, get ready to:
- Create a resource group and VNet with four subnets.
- Deploy an Azure Firewall with a Firewall Policy.
- Deploy a Logic App Standard instance on a dedicated plan.
Create the resource group and VNet with subnets
The VNet segments your infrastructure so the firewall, Logic App private endpoint, Logic App outbound traffic, and App Service each occupy a dedicated subnet with its own security boundary.
- Create the resource group and VNet (with the first subnet) by running these commands:
az group create --name rg-firewall-logic --location centralus
az network vnet create \
--resource-group rg-firewall-logic \
--name vnet-firewall-logic \
--address-prefix 10.0.0.0/16 \
--subnet-name AzureFirewallSubnet \
--subnet-prefix 10.0.1.0/26
What do these commands do?
The first command creates a resource group named rg-firewall-logic in the centralus region to hold all your project resources.
The second command creates a VNet with a 10.0.0.0/16 address space and its first subnet. Azure Firewall requires a subnet named exactly AzureFirewallSubnet. Any other name causes the firewall deployment to fail.
- You should see JSON output for both commands confirming the resource group and VNet were created with provisioningState of Succeeded.
- Add the three remaining subnets by running these commands:
az network vnet subnet create \
--resource-group rg-firewall-logic \
--vnet-name vnet-firewall-logic \
--name logic-app-pe-subnet \
--address-prefix 10.0.2.0/28
az network vnet subnet create \
--resource-group rg-firewall-logic \
--vnet-name vnet-firewall-logic \
--name logic-app-vnet-subnet \
--address-prefix 10.0.3.0/28 \
--delegations Microsoft.Web/serverFarms
az network vnet subnet create \
--resource-group rg-firewall-logic \
--vnet-name vnet-firewall-logic \
--name app-service-subnet \
--address-prefix 10.0.4.0/28 \
--delegations Microsoft.Web/serverFarms
Why four separate subnets?
Each component gets isolation. logic-app-pe-subnet hosts the private endpoint NIC (no delegation needed). logic-app-vnet-subnet and app-service-subnet are delegated to Microsoft.Web/serverFarms because VNet integration requires a subnet exclusively owned by App Service.
- Verify all four subnets exist by running:
az network vnet subnet list \
--resource-group rg-firewall-logic \
--vnet-name vnet-firewall-logic \
--query "[].name" -o tsv
- You should see four subnet names printed: AzureFirewallSubnet, logic-app-pe-subnet, logic-app-vnet-subnet, and app-service-subnet.
Seeing an error about address space overlap?
Each subnet prefix must fit within the VNet's 10.0.0.0/16 range and must not overlap with another subnet. Double-check that you used the exact prefixes above.
If you see a delegation error, confirm you typed Microsoft.Web/serverFarms with the exact capitalisation shown. help me debug my subnet creation error
Deploy the Azure Firewall with a Firewall Policy
The Firewall Policy is the management layer your Logic App will target via the REST API. The firewall itself enforces the rules the policy defines.
- Create the Firewall Policy and a static public IP by running:
az network firewall policy create \
--resource-group rg-firewall-logic \
--name fw-policy-logic \
--sku Standard
az network public-ip create \
--resource-group rg-firewall-logic \
--name pip-firewall \
--sku Standard \
--allocation-method Static
What do these commands do?
The Firewall Policy (fw-policy-logic) stores rule collection groups that your Logic App will create programmatically. The public IP (pip-firewall) gives the firewall an internet-facing address for outbound and inbound traffic inspection.
- You should see JSON output for both confirming provisioningState is Succeeded.
- Deploy the Azure Firewall by running this command:
az network firewall create \
--resource-group rg-firewall-logic \
--name fw-logic \
--sku AZFW_VNet \
--tier Standard \
--vnet-name vnet-firewall-logic \
--firewall-policy fw-policy-logic \
--public-ip pip-firewall
How long does this take?
Azure Firewall deployment takes 5-10 minutes. The CLI will block until it completes. Leave the terminal running and do not interrupt it.
- Once the command returns, you should see a large JSON object. Look for "provisioningState": "Succeeded" in the output to confirm the firewall is ready.
Firewall deployment failed?
The most common cause is a quota limit on public IPs or firewall resources in the eastus region. Check your subscription's resource provider quotas in the Azure Portal under your subscription's Usage + quotas blade.
Also confirm the subnet AzureFirewallSubnet uses a /26 prefix. Azure Firewall requires at least a /26 subnet. help me fix my firewall deployment error
Deploy the Logic App Standard
Logic Apps Standard runs on a dedicated App Service plan (unlike the Consumption tier). It supports private endpoints and VNet integration, which is why you need it for this project.
- Create a storage account (required by Logic Apps Standard for workflow state) by running:
STORAGE_NAME=stlogicfwrule$RANDOM
az storage account create \
--resource-group rg-firewall-logic \
--name $STORAGE_NAME \
--sku Standard_LRS
echo "Storage account name: $STORAGE_NAME"
Why the random suffix?
Azure storage account names must be globally unique. The $RANDOM bash variable appends a random number to avoid naming conflicts. The echo at the end prints the generated name so you can reference it if needed.
- You should see JSON output confirming the storage account was created, followed by the printed name.
- Create the App Service plan and Logic App by running these commands (stay in the same terminal session so $STORAGE_NAME remains set):
az appservice plan create \
--resource-group rg-firewall-logic \
--name plan-logic-fw \
--sku WS1
az logicapp create \
--resource-group rg-firewall-logic \
--name logic-fw-updater \
--storage-account $STORAGE_NAME \
--plan plan-logic-fw
What does WS1 mean?
WS1 is the Workflow Standard tier 1 SKU for Logic Apps Standard. It gives you a dedicated compute instance that supports private endpoints and VNet integration. The Consumption tier cannot do either.
- Confirm the Logic App was created by running:
az logicapp show \
--resource-group rg-firewall-logic \
--name logic-fw-updater \
--query "{name:name, state:state}" -o table
- You should see a table with logic-fw-updater and a state of Running.
Logic App creation failed?
If you see a storage account error, confirm you are in the same terminal session where you ran the previous command. Run echo $STORAGE_NAME to verify the variable is still set.
If the name logic-fw-updater is taken (it must be globally unique), append a short random suffix to make it unique, and use that name for the rest of the project. help me fix my Logic App creation error
Before you move on, do you think the firewall's provisioning state will still show Succeeded if you query it again now?
- Run the final verification to confirm the firewall is healthy:
az network firewall show \
--resource-group rg-firewall-logic \
--name fw-logic \
--query provisioningState
- You should see "Succeeded" printed to the terminal. This confirms your firewall is fully deployed and attached to fw-policy-logic.
Your network foundation is in place. Next up, you will build a workflow inside the Logic App and test it over its public endpoint before locking it down.
Build and Test the Workflow (Public)
Your network foundation is in place with the firewall running and ready to accept rules. Now you need a working workflow that can receive HTTP requests and respond to them correctly.
Before locking anything down with private endpoints, you need to prove the trigger and response logic are correct. Testing over the public endpoint first gives you a baseline to compare against once the private endpoint is in place.
In this step, get ready to:
- Create a stateful workflow with an HTTP request trigger.
- Add a Response action that returns a confirmation message.
- Test the workflow from your terminal with a curl POST request.
Create the Request-triggered workflow in the Azure Portal
The Logic App needs a workflow before it can do anything. You will create a stateful workflow with an HTTP trigger that defines the shape of incoming firewall rule requests.
- In the Azure Portal, navigate to your Logic App logic-fw-updater.
- In the left sidebar, under Workflows, select Workflows.
- On the Workflows page toolbar, select + Add.
- In the New workflow pane, enter update-firewall-rule as the workflow name.
- Select Stateful as the State type, then click Create.
The workflow appears in the list. Now open its designer to add a trigger.
- Select the update-firewall-rule workflow from the list.
- In the workflow sidebar, under Tools, select Designer.
- On the designer surface, select Add a trigger.
- In the search box, type Request and select the built-in trigger named When a HTTP request is received.
What is this trigger?
The Request trigger creates an HTTP endpoint on your workflow. Any service that sends a POST to this endpoint will start the workflow.
The JSON schema you define next tells the workflow what fields to expect in the request body. The designer generates tokens from the schema so you can reference those fields in later actions.
Now define the schema so the trigger knows what data to expect.
- In the trigger's information pane, find the Request Body JSON Schema box.
- Paste the following JSON schema into that box:
{
"type": "object",
"properties": {
"ruleName": { "type": "string" },
"sourceAddress": { "type": "string" },
"destinationAddress": { "type": "string" },
"destinationPort": { "type": "string" },
"protocol": { "type": "string" },
"action": { "type": "string" }
},
"required": ["ruleName", "sourceAddress", "destinationAddress", "destinationPort", "protocol", "action"]
}
What does this schema define?
Each field represents part of a firewall rule request:
- ruleName identifies the rule.
- sourceAddress and destinationAddress define the IP addresses or ranges.
- destinationPort specifies which port to allow or deny.
- protocol is the network protocol (TCP, UDP, ICMP, or Any).
- action is either Allow or Deny.
- On the designer toolbar, click Save.
After saving, the HTTP URL box in the trigger's Parameters tab now shows a generated URL. This is the endpoint you will use to trigger the workflow.
Don't see the HTTP URL?
The URL is generated only after the first save of a workflow with a Request trigger. Make sure you clicked Save on the designer toolbar.
If the URL still shows as blank, try a different browser or check the workflow's Overview page where the Workflow URL is also displayed.
help me find the workflow HTTP URL
Add a Response action and test the workflow
The trigger can receive requests, but it needs an action that sends something back. Adding a Response action completes the request-response loop so you can verify the workflow processes data correctly.
- On the designer surface, select the plus sign (+) below the trigger, then select Add an action.
- In the search box, type Response and select the built-in Response action.
- Set Status Code to 200.
- In the Body field, enter:
{"status": "received", "ruleName": "@{triggerBody()?['ruleName']}"}
What does this Response body do?
The expression @{triggerBody()?['ruleName']} dynamically inserts the ruleName value from the incoming request. This proves the workflow correctly parsed the JSON body you sent.
- Click Save on the designer toolbar.
- Select the When a HTTP request is received trigger on the designer canvas.
- In the trigger's Parameters tab, find the HTTP URL property.
- Click the copy icon next to the URL to copy it. Save this URL somewhere. You will need it shortly.
Before you run the test command, what do you expect to get back from the workflow?
- Return to your terminal and test the workflow by running this command (replace the URL with the one you just copied):
curl -X POST "your url here" \
-H "Content-Type: application/json" \
-d '{"ruleName":"allow-web","sourceAddress":"10.0.0.0/24","destinationAddress":"8.8.8.8","destinationPort":"443","protocol":"TCP","action":"Allow"}'
You should receive a 200 response with this body:
{"status": "received", "ruleName": "allow-web"}
The workflow correctly received your POST, parsed the JSON body, and echoed back the rule name. This confirms the trigger and response logic work over the public endpoint.
Getting a 404 or no response?
Make sure you replaced the placeholder URL with the full URL you copied from the trigger's HTTP URL field. The URL includes a SAS signature that authenticates the request.
If you get a 404 Not Found, double check that you saved the workflow after adding both the trigger and the Response action.
help me debug my Logic App workflow not responding to curl
Your workflow handles requests and responds correctly over the public internet. Next, you will lock it down with a private endpoint so only resources inside your VNet can reach it.
Lock Down the Logic App with a Private Endpoint
Your workflow is live and responding to requests. That is the good news. The bad news is that anyone on the internet can call it right now.
A private endpoint brings the Logic App into your VNet via a private IP address. Once you disable public access, the same curl command that returned 200 moments ago will return 403 Forbidden. That proves the lockdown is real.
In this step, get ready to:
- Create a private endpoint and private DNS zone for the Logic App.
- Disable public network access and verify the lockdown with curl.
Create the private endpoint
A private endpoint places a network interface card (NIC) inside your VNet subnet. Traffic to the Logic App resolves to that private IP instead of a public one.
- Return to your terminal and create the private endpoint by running these commands:
# Store the Logic App resource ID
LOGIC_APP_ID=$(az logicapp show \
--resource-group rg-firewall-logic \
--name logic-fw-updater \
--query id -o tsv)
# Create the private endpoint in the dedicated subnet
az network private-endpoint create \
--resource-group rg-firewall-logic \
--name pe-logic-fw \
--vnet-name vnet-firewall-logic \
--subnet logic-app-pe-subnet \
--private-connection-resource-id $LOGIC_APP_ID \
--group-id sites \
--connection-name logic-fw-connection
What do these commands do?
- The first command fetches the full resource ID for logic-fw-updater and stores it in a variable.
- The second command creates a private endpoint named pe-logic-fw in the logic-app-pe-subnet subnet. The --group-id sites flag tells Azure this endpoint targets an App Service-style resource (which Logic Apps Standard is).
- Wait for the command to complete. You should see JSON output with "provisioningState": "Succeeded" confirming the endpoint exists.
Seeing a permission or subnet error?
- Make sure the logic-app-pe-subnet subnet is NOT delegated to any service. Private endpoint subnets must be empty of delegations.
- Confirm you are logged in with a subscription owner or contributor role by running az account show.
Why is my private endpoint failing to create?
Configure the private DNS zone
A private endpoint alone is not enough. Clients inside the VNet still need to resolve the Logic App's hostname to the private IP. A private DNS zone handles that name resolution automatically.
- Create the DNS zone, link it to your VNet, and attach it to the private endpoint by running these commands:
# Create the private DNS zone for App Service endpoints
az network private-dns zone create \
--resource-group rg-firewall-logic \
--name privatelink.azurewebsites.net
# Link the DNS zone to your VNet so resources can resolve it
az network private-dns link vnet create \
--resource-group rg-firewall-logic \
--zone-name privatelink.azurewebsites.net \
--name logic-dns-link \
--virtual-network vnet-firewall-logic \
--registration-enabled false
# Attach the DNS zone group to the private endpoint
az network private-endpoint dns-zone-group create \
--resource-group rg-firewall-logic \
--endpoint-name pe-logic-fw \
--name default \
--private-dns-zone privatelink.azurewebsites.net \
--zone-name privatelink.azurewebsites.net
What do these commands do?
- The DNS zone privatelink.azurewebsites.net is the standard zone Azure uses for App Service private endpoints.
- Linking the zone to vnet-firewall-logic means any resource in that VNet can resolve the Logic App hostname to its private IP.
- The zone group ties the private endpoint's IP address to a DNS A record inside the zone automatically.
- Wait for all three commands to complete. The final command's JSON output confirms the DNS zone group is attached.
DNS zone already exists?
If you see an error that privatelink.azurewebsites.net already exists in the resource group, skip the zone create command and proceed with the link and zone group commands.
Why is my private DNS zone configuration failing?
Disable public access and verify the lockdown
The private endpoint is ready, but the Logic App still accepts public traffic. You need to explicitly disable public network access so the only path in is through the VNet.
- Switch to the Azure Portal in your browser.
- Navigate to your Logic App logic-fw-updater.
- In the left menu under Settings, select Networking.
- In the Inbound traffic configuration section, set Public network access to Disabled.
- Click Save.
Why disable public access separately?
Creating a private endpoint does not automatically block public access. Azure gives you both paths until you explicitly turn off the public one. This two-step approach lets you verify private connectivity before cutting off the old path.
Now for the moment of truth. Before you run the next command, think about what you expect to happen. The same curl that returned 200 OK minutes ago is about to hit the Logic App again. What do you think the response will be now that public access is disabled?
- Return to your terminal and test the lockdown by running this command:
curl -X POST "<workflow-url>" \
-H "Content-Type: application/json" \
-d '{"ruleName":"test","sourceAddress":"10.0.0.0/24","destinationAddress":"8.8.8.8","destinationPort":"443","protocol":"TCP","action":"Allow"}'
You receive a 403 Forbidden response. The Logic App is now only reachable from within the VNet. The same request that succeeded earlier is now rejected because your local machine is outside the private network.
Still getting 200 OK?
- Confirm you clicked Save after changing the public access setting in the Portal.
- Wait 30-60 seconds for the change to propagate, then try the curl command again.
- Make sure you set Public network access to Disabled, not to Enabled from select virtual networks and IP addresses.
Why am I still getting a 200 response after disabling public access?
Your Logic App is locked down. Public callers get rejected at the door. Next up, you will give the Logic App the identity and permissions it needs to actually update firewall rules on your behalf.
Wire Up the Firewall Rule Update
Your Logic App is locked down behind a private endpoint, but right now it just echoes back a confirmation message. It does not actually update any firewall rules.
In this step, you give the Logic App a managed identity with permission to modify the Firewall Policy. Then you replace the placeholder Response action with an HTTP action that calls the Azure Firewall Policy REST API. This is where the automation becomes real.
In this step, get ready to:
- Enable a system-assigned managed identity on the Logic App and assign RBAC permissions.
- Add VNet integration for outbound traffic and configure the HTTP action in the workflow.
- Test the workflow end-to-end and confirm the new rule appears in the Firewall Policy.
Enable the system-assigned managed identity
For the Logic App to call the Azure Firewall Policy REST API on your behalf, it needs its own identity. A system-assigned managed identity is tied to the Logic App's lifecycle and gives Azure a way to authenticate the app without storing credentials.
- Enable the system-assigned managed identity on your Logic App by running this command:
az logicapp identity assign \
--resource-group rg-firewall-logic \
--name logic-fw-updater
What does this command do?
This enables a system-assigned managed identity on the Logic App. Azure creates a service principal in your tenant that the Logic App can use to authenticate against other Azure services without any stored secrets.
- Confirm the identity is enabled by running:
az logicapp identity show \
--resource-group rg-firewall-logic \
--name logic-fw-updater \
--query principalId -o tsv
You should see a GUID printed to the terminal. This is the principal ID of your Logic App's managed identity.
Not seeing a principal ID?
If the output is empty or you get an error, the identity assignment may not have propagated yet. Wait 30 seconds and re-run the az logicapp identity show command.
help me troubleshoot why my Logic App managed identity isn't showing a principal ID
Assign the Network Contributor role to the managed identity
The managed identity exists, but it has zero permissions. You need to grant it the Network Contributor role scoped to the Firewall Policy so it can create and update rule collection groups.
- Store the principal ID and Firewall Policy resource ID in variables, then create the role assignment by running these commands:
IDENTITY_PRINCIPAL_ID=$(az logicapp identity show \
--resource-group rg-firewall-logic \
--name logic-fw-updater \
--query principalId -o tsv)
FW_POLICY_ID=$(az network firewall policy show \
--resource-group rg-firewall-logic \
--name fw-policy-logic \
--query id -o tsv)
az role assignment create \
--assignee $IDENTITY_PRINCIPAL_ID \
--role "Network Contributor" \
--scope $FW_POLICY_ID
What does this do?
The first two commands capture the managed identity's principal ID and the Firewall Policy's full resource ID into shell variables.
The third command creates an RBAC role assignment that grants Network Contributor to the Logic App's identity, but only on the Firewall Policy resource. This follows least-privilege: the identity can modify firewall rules without broader subscription access.
- Verify the role assignment was created by running:
az role assignment list \
--assignee $IDENTITY_PRINCIPAL_ID \
--scope $FW_POLICY_ID \
--query "[].roleDefinitionName" -o tsv
You should see Network Contributor in the output.
Seeing an empty result or authorization error?
If the output is blank, the role assignment may take up to 60 seconds to propagate. Wait and re-run the command.
If you see an authorization error, confirm your own account has the Owner or User Access Administrator role on the Firewall Policy resource or the resource group.
help me fix an RBAC role assignment error for my Logic App managed identity
Add VNet integration and update the workflow with the HTTP action
The Logic App needs outbound VNet integration so it can reach Azure management endpoints (management.azure.com) through the VNet. Without this, the HTTP action inside the workflow cannot call the Firewall Policy REST API.
- Add VNet integration to the Logic App by running this command:
az logicapp vnet-integration add \
--resource-group rg-firewall-logic \
--name logic-fw-updater \
--vnet vnet-firewall-logic \
--subnet logic-app-vnet-subnet
What does this command do?
This connects the Logic App's outbound traffic to the logic-app-vnet-subnet subnet. All outbound requests from the Logic App (including the HTTP action calling the REST API) now travel through your VNet rather than over the public internet.
- Confirm VNet integration is active by running:
az logicapp vnet-integration list \
--resource-group rg-firewall-logic \
--name logic-fw-updater \
--query "[].vnetResourceId" -o tsv
You should see the resource ID of vnet-firewall-logic in the output.
VNet integration command failed?
The most common cause is that the subnet is not delegated to Microsoft.Web/serverFarms. If you created the subnets in Step 1, this delegation is already in place. If you see an error about delegation, re-run the subnet creation command from Step 1 with the --delegations Microsoft.Web/serverFarms flag.
help me fix VNet integration for my Logic App Standard
Now you update the workflow in the Azure Portal to add the HTTP action that calls the Firewall Policy REST API.
- In the Azure Portal, navigate to your Logic App logic-fw-updater.
- Select **Workflows** in the left menu, then click on update-firewall-rule.
- Click **Designer** to open the workflow designer.
- Click the + between the Request trigger and the Response action, then select **Add an action**.
- Search for HTTP and select the **HTTP** built-in action.
Why the built-in HTTP action?
Built-in connectors run in the same cluster as the Logic Apps runtime and can use VNet integration directly. Shared (managed) connectors run in Microsoft's multi-tenant infrastructure and cannot leverage VNet integration. The built-in HTTP action is required for this private network scenario.
- Set the **Method** field to PUT.
- In the **URI** field, paste the following value (replace the subscription ID with your own):
https://management.azure.com/subscriptions/[[SUBSCRIPTION_ID="your Azure subscription ID"]]/resourceGroups/rg-firewall-logic/providers/Microsoft.Network/firewallPolicies/fw-policy-logic/ruleCollectionGroups/rcg-logic-managed?api-version=2025-05-01
Find your subscription ID
Run az account show --query id -o tsv in your terminal to get your subscription ID quickly.
- Under **Headers**, add a header with key Content-Type and value application/json.
- In the **Body** field, paste the following JSON:
{
"properties": {
"priority": 200,
"ruleCollections": [
{
"name": "logic-app-rules",
"ruleCollectionType": "FirewallPolicyFilterRuleCollection",
"priority": 100,
"action": {
"type": "@{triggerBody()?['action']}"
},
"rules": [
{
"name": "@{triggerBody()?['ruleName']}",
"ruleType": "NetworkRule",
"sourceAddresses": ["@{triggerBody()?['sourceAddress']}"],
"destinationAddresses": ["@{triggerBody()?['destinationAddress']}"],
"destinationPorts": ["@{triggerBody()?['destinationPort']}"],
"ipProtocols": ["@{triggerBody()?['protocol']}"]
}
]
}
]
}
}
What does this JSON body do?
This is the request body for the Azure Firewall Policy Rule Collection Groups REST API. It creates (or replaces) a rule collection group named rcg-logic-managed with priority 200.
Inside, it defines a single rule collection called logic-app-rules that contains one network rule. The @{triggerBody()?['...']} expressions dynamically pull values from the incoming HTTP request, so each trigger call can specify a different rule name, IP range, port, protocol, and action.
- Scroll down to the **Authentication** section of the HTTP action.
- Set **Authentication type** to **Managed identity**.
- Set **Managed identity** to **System-assigned managed identity**.
- Set **Audience** to https://management.azure.com.
Why set the audience?
The audience tells Azure which resource the token is intended for. Setting it to https://management.azure.com means the managed identity requests a token scoped to the Azure Resource Manager API. This is required for any REST call to management.azure.com endpoints.
- Click on the **Response** action to expand it.
- Update the **Body** field to the following:
{"status": "rule-updated", "ruleName": "@{triggerBody()?['ruleName']}", "firewallResponse": "@{body('HTTP')}"}
What changed in the response?
The response now includes the Firewall Policy API's response in the firewallResponse field. This gives the caller visibility into whether the rule was actually created or if an error occurred at the API level.
- Click **Save** in the top toolbar to save the workflow.
Your workflow now has three parts: a Request trigger that receives the input, an HTTP action that calls the Firewall Policy REST API using managed identity authentication, and a Response action that returns the result.
Test the workflow using Run Trigger
Since public access is disabled, you cannot test from your local machine with curl. The Azure Portal's built-in Run Trigger feature executes the workflow from within Azure's infrastructure, bypassing the public access restriction.
- In the Azure Portal, navigate to the update-firewall-rule workflow's **Overview** page.
- Click **Run Trigger**, then select **Run with payload**.
- Paste the following test payload:
{
"ruleName": "allow-dns",
"sourceAddress": "10.0.0.0/24",
"destinationAddress": "8.8.8.8",
"destinationPort": "53",
"protocol": "UDP",
"action": "Allow"
}
What does this test payload do?
This creates a network rule called allow-dns that allows UDP traffic on port 53 (DNS) from the 10.0.0.0/24 subnet to Google's public DNS server at 8.8.8.8. This is a safe, verifiable rule for testing.
- Click **Run** to execute the workflow.
Before you check the result, what do you expect to see? The workflow should succeed and the Firewall Policy should now have a new rule collection group.
- Wait for the run to complete (check the **Run History** tab for a green checkmark).
- Navigate to **Firewall Policies** in the Azure Portal.
- Click on fw-policy-logic.
- Select **Rule Collection Groups** in the left menu.
You should see rcg-logic-managed listed with priority 200. Click into it and confirm the allow-dns rule is present inside the logic-app-rules collection.
Workflow run failed or rule not appearing?
Open the failed run in **Run History** and expand the HTTP action to see the error details.
- If you see a 403 Forbidden from the REST API, the role assignment has not propagated yet. Wait 2 minutes and run the trigger again.
- If you see a 400 Bad Request, double-check the JSON body in the HTTP action for typos or missing dynamic expressions.
- If the run succeeded but the rule is missing, confirm you are checking the correct Firewall Policy (fw-policy-logic) in the correct resource group (rg-firewall-logic).
help me debug why my Logic App workflow failed to update the Azure Firewall Policy
Your Logic App now programmatically manages firewall rules. It authenticates with its own managed identity, calls the REST API through the VNet, and creates real network rules. Next up, you will deploy an App Service inside the VNet that triggers this workflow over the private endpoint, proving the full end-to-end automation chain works.
Deploy the App Service and Trigger End-to-End
In the previous step, you gave the Logic App a managed identity and wired the HTTP action to update the Firewall Policy. The workflow is fully functional and tested via the Portal.
Now you need a caller that lives inside the VNet. A web app integrated into the same network can reach the Logic App's private endpoint, simulating the real-world pattern: your application detects a threat and autonomously updates firewall rules through a private, secure channel.
In this step, get ready to:
- Create a VNet-integrated App Service that can reach the private Logic App.
- Deploy a Node.js trigger app that sends firewall rule requests to the Logic App.
- Prove end-to-end automation by triggering a rule update from the App Service.
Create the App Service plan and web app
The App Service needs its own plan and must be configured with the Node.js runtime to run the trigger application.
- Create the App Service plan and web app by running these commands:
az appservice plan create \
--resource-group rg-firewall-logic \
--name plan-trigger-app \
--sku B1 \
--is-linux
az webapp create \
--resource-group rg-firewall-logic \
--plan plan-trigger-app \
--name trigger-fw-app \
--runtime "NODE:20-lts"
What do these commands do?
The first command creates a Linux App Service plan on the B1 (Basic) tier. The second creates a web app named trigger-fw-app running Node.js 20 LTS on that plan.
- Confirm the web app was created by running:
az webapp show \
--resource-group rg-firewall-logic \
--name trigger-fw-app \
--query "{name:name, state:state, defaultHostName:defaultHostName}" \
-o table
You should see the app name, a state of Running, and the default hostname trigger-fw-app.azurewebsites.net.
Add VNet integration and configure the Logic App URL
VNet integration routes the App Service's outbound traffic through the VNet. This is what allows the web app to reach the Logic App's private endpoint.
- Add VNet integration to the web app by running:
az webapp vnet-integration add \
--resource-group rg-firewall-logic \
--name trigger-fw-app \
--vnet vnet-firewall-logic \
--subnet app-service-subnet
Why VNet integration?
Without VNet integration, the App Service would try to reach the Logic App over the public internet and receive a 403 Forbidden. VNet integration routes the request through app-service-subnet, which has access to the private DNS zone resolving logic-fw-updater to its private IP.
Next, the App Service needs to know the Logic App workflow URL so it can call it at runtime.
- In the Azure Portal, navigate to the Logic App logic-fw-updater.
- Open Workflows, then click update-firewall-rule.
- Go to Overview and copy the Workflow URL (the HTTP POST URL from the trigger).
- Set this URL as an app setting by running:
az webapp config appsettings set \
--resource-group rg-firewall-logic \
--name trigger-fw-app \
--settings LOGIC_APP_URL="[[LOGIC_APP_WORKFLOW_URL="your workflow URL from the trigger"]]"
- Verify the setting was stored by running:
az webapp config appsettings list \
--resource-group rg-firewall-logic \
--name trigger-fw-app \
--query "[?name=='LOGIC_APP_URL'].value" -o tsv
You should see the workflow URL printed back to the terminal.
URL looks empty or incorrect?
Make sure you copied the full URL including the sig query parameter. The URL contains a SAS signature that authenticates callers. Without it, the Logic App rejects requests even from within the VNet.
help me troubleshoot my Logic App workflow URL configuration
Deploy the trigger application
The trigger app is a lightweight Express server that exposes a /trigger endpoint. When called, it sends a POST request to the Logic App with a firewall rule payload.
- Move to your Desktop and create the project folder by running:
cd ~/Desktop
mkdir trigger-app && cd trigger-app
- Create package.json with the following content:
{
"name": "trigger-fw-app",
"version": "1.0.0",
"description": "App Service that triggers the private Logic App to update firewall rules",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "4.21.2"
}
}
What does this file do?
This defines the Node.js project with Express 4.21.2 as its only dependency. The start script tells Azure how to launch the app.
- Install the dependency by running:
npm install
You should see a node_modules folder appear and a success message in the terminal.
- Create index.js and paste the setup and root route:
const express = require("express");
const app = express();
// Use the port Azure assigns, or 8080 locally
const port = process.env.PORT || 8080;
// Read the Logic App workflow URL from environment
const LOGIC_APP_URL = process.env.LOGIC_APP_URL;
// Root route shows usage info
app.get("/", (req, res) => {
res.json({
message: "Firewall Rule Trigger App",
usage: "POST /trigger with JSON body to update firewall rules"
});
});
app.use(express.json());
- Below that code, add the POST /trigger handler that accepts a custom payload or uses defaults:
app.post("/trigger", async (req, res) => {
const payload = req.body.ruleName ? req.body : {
ruleName: "app-triggered-rule",
sourceAddress: "10.0.0.0/24",
destinationAddress: "8.8.4.4",
destinationPort: "443",
protocol: "TCP",
action: "Allow"
};
try {
const response = await fetch(LOGIC_APP_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
- Still inside the POST handler's try block, add the response parsing and error handling to complete the route:
const data = await response.text();
let parsed;
try {
parsed = JSON.parse(data);
} catch {
parsed = { raw: data };
}
res.json({
status: response.status,
logicAppResponse: parsed,
ruleRequested: payload
});
} catch (error) {
res.status(500).json({
error: "Failed to call Logic App",
details: error.message
});
}
});
What does the POST handler do?
If the incoming request body contains a ruleName, it forwards that body to the Logic App. Otherwise it sends a default rule allowing TCP 443 from 10.0.0.0/24 to 8.8.4.4. The response from the Logic App is parsed and returned to the caller.
- Below the POST handler, add the GET /trigger handler for quick browser testing:
app.get("/trigger", (req, res) => {
const payload = {
ruleName: "get-triggered-rule",
sourceAddress: "10.0.0.0/24",
destinationAddress: "8.8.4.4",
destinationPort: "443",
protocol: "TCP",
action: "Allow"
};
fetch(LOGIC_APP_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
- Finally, add the .then/.catch chain and the server start to close out the file:
.then(async (response) => {
const data = await response.text();
let parsed;
try {
parsed = JSON.parse(data);
} catch {
parsed = { raw: data };
}
res.json({
status: response.status,
logicAppResponse: parsed,
ruleRequested: payload
});
})
.catch((error) => {
res.status(500).json({
error: "Failed to call Logic App",
details: error.message
});
});
});
app.listen(port, () => {
console.log(`Trigger app listening on port ${port}`);
});
What does the GET handler do?
The GET /trigger route lets you test from a browser without needing to craft a POST body. It sends a hardcoded default rule to the Logic App and returns the response.
- Save index.js.
✔️ Awesome, I've got everything!
Great. Double check you saved both package.json and index.js in the trigger-app folder.
ⓧ I'd like to double check the full code
Scroll up and confirm these code blocks are present in your index.js, in order:
- Setup block: require("express"), port, LOGIC_APP_URL, root GET route, and express.json() middleware.
- POST /trigger handler with payload, fetch call, response parsing, and error handling.
- GET /trigger handler with payload, fetch call, .then/.catch chain.
- Server start: app.listen(port, ...).
Now zip the project and deploy it to the App Service.
- Create the zip archive and deploy by running:
zip -r app.zip .
az webapp deploy \
--resource-group rg-firewall-logic \
--name trigger-fw-app \
--src-path app.zip
What does this command do?
The zip command packages your project files (including node_modules) into a zip archive. The az webapp deploy command pushes the zip to the App Service and restarts the app so it picks up your code.
Wait for the deployment to complete. You should see a success message in the terminal.
Deployment failed or timed out?
Make sure you ran npm install before zipping. The zip must include node_modules since the Linux App Service does not run npm install during zip deploy by default. Also verify the zip was created from inside the trigger-app folder so the files sit at the root of the archive, not inside a subfolder.
help me debug my App Service zip deployment
Test the end-to-end flow
This is the moment of truth. A request to the App Service will travel through the VNet to the private Logic App, which will call the Firewall Policy REST API, and a new rule will appear in the Portal.
Before you run this, what do you think will happen? Will the App Service be able to reach the Logic App even though public access is disabled?
- Trigger the end-to-end flow by running:
curl https://trigger-fw-app.azurewebsites.net/trigger
You should receive a JSON response with a status of 200 and a logicAppResponse containing "status": "rule-updated". The App Service successfully reached the private Logic App through the VNet.
- In the Azure Portal, navigate to fw-policy-logic.
- Click Rule Collection Groups in the left menu.
- Click rcg-logic-managed to open it.
- Confirm you see the new rule get-triggered-rule in the logic-app-rules collection.
The request traveled from your terminal to the App Service, through the VNet's private endpoint to the Logic App, from the Logic App to the Azure Firewall Policy REST API, and the rule now exists in your firewall.
Getting a 500 error or empty response?
Check that the LOGIC_APP_URL app setting contains the full workflow URL including the sig parameter. You can verify it with az webapp config appsettings list.
If the Logic App returns a 403, confirm VNet integration is active on trigger-fw-app by checking Networking in the Portal. The Outbound traffic section should show app-service-subnet as the integrated subnet.
help me troubleshoot my end-to-end Logic App trigger flow
Secret mission
Add Input Validation and Duplicate Detection
Your workflow accepts any input and blindly writes it to the Firewall Policy. In production, a malformed IP, an invalid protocol, or a duplicate rule name would break your firewall or create silent conflicts. Make the workflow production-ready by rejecting bad requests before they reach the REST API.
Clean Up Your Resources
Clean Up Your Resources
This project uses Azure resources that cost approximately $1.50-1.75 per hour while running. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Cost warning
Azure Firewall alone costs $1.25 per hour. Combined with the Logic App Standard (WS1) and App Service (B1), leaving everything running totals approximately $36 per day.
Delete the resource group now unless you plan to extend this project immediately.
Resources you used:
- Azure Firewall (fw-logic) in resource group rg-firewall-logic.
- Firewall Policy (fw-policy-logic) with rule collection group rcg-logic-managed.
- Logic App Standard (logic-fw-updater) on a WS1 plan with private endpoint and VNet integration.
- App Service (trigger-fw-app) on a B1 Linux plan with VNet integration.
- VNet (vnet-firewall-logic) with four subnets, a private endpoint, and a private DNS zone.
- Storage account, public IP (pip-firewall), and App Service plan (plan-trigger-app).
- Local project folder trigger-app on your Desktop.
Keep everything running
No action needed. Choose this if you plan to continue extending this project right now.
- All resources remain active and accessible.
- Your Azure Firewall continues to charge $1.25 per hour.
- The Logic App (WS1) and App Service (B1) continue to charge at their respective rates.
Pause - I'll come back to this later
Azure Firewall cannot be paused or deallocated. The most effective way to reduce costs is to delete the firewall while keeping your other resources intact.
- Delete the Azure Firewall (saves $1.25/hr) by running:
- Stop the App Service and Logic App to eliminate their compute charges.
az network firewall delete --resource-group rg-firewall-logic --name fw-logic
- Stop the App Service by running:
az webapp stop --name trigger-fw-app --resource-group rg-firewall-logic
- Stop the Logic App by running:
az logicapp stop --name logic-fw-updater --resource-group rg-firewall-logic
What stays active after pausing?
The VNet, private endpoint, private DNS zone, storage account, and public IP remain. These incur minimal or no ongoing charges. When you return, you will need to redeploy the Azure Firewall and restart the App Service and Logic App.
Delete - I don't want to use this again
Remove all project resources and start fresh. Deleting the resource group removes every Azure resource you created in this project in one command.
- Delete the entire resource group by running:
az group delete --name rg-firewall-logic --yes --no-wait
What does this command do?
The --yes flag skips the confirmation prompt. The --no-wait flag returns control to your terminal immediately while deletion continues in the background.
This removes the Azure Firewall, Firewall Policy, Logic App, App Service, VNet, subnets, private endpoint, DNS zone, storage account, public IP, and App Service plan.
- Delete the local project folder. On macOS or Linux, run:
rm -rf ~/Desktop/trigger-app
- On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\trigger-app
- Confirm the resource group is gone by running:
az group list --query "[?name=='rg-firewall-logic']" -o table
If deletion is still in progress, you will see the group with a state of Deleting. Once complete, no results are returned.
Nice Work!
Nice Work!
You just built a private security automation pipeline on Azure. A web app inside your VNet can now programmatically update firewall rules through a Logic App that the public internet cannot reach.
Here's what you accomplished:
- Built a Logic App Standard workflow that programmatically updates Azure Firewall Policy rules via the REST API, authenticated with a managed identity.
- Locked the Logic App behind a private endpoint, proving it returns 403 Forbidden to public callers.
- Deployed a VNet-integrated App Service that triggers the Logic App over the private network, proving the full end-to-end flow from web app to firewall rule creation.
- Secret Mission: Added input validation and duplicate-rule detection to make the workflow production-ready.
Ready to quiz yourself?