Secure Azure Storage with a Private Endpoint
Lock down an Azure Storage Account so it's only reachable inside a VNet.
Introduction
30 Second Summary
When you store sensitive data in the cloud, you probably assume it is locked away safely. In reality, most cloud storage services are reachable from the public internet by default.
In this project, you will create a Private Endpoint that locks down an Azure Storage Account so it can only be accessed from within a Virtual Network. Traffic between your resources and storage never touches the public internet.
What You'll Build
A secure networking architecture that connects an Azure Storage Account to a Virtual Network through a Private Endpoint, eliminating public internet exposure entirely.
By the end of this project, you'll have:
- A Virtual Network with a dedicated subnet hosting a private endpoint that assigns a private IP to your storage account.
- An Azure Storage Account with public access fully disabled and a Private DNS Zone that resolves the storage address to the private IP automatically.
- A test Virtual Machine that proves connectivity works exclusively over the private path and that public access is blocked.
- Secret Mission: Add Network Security Group rules for fine-grained access control within the subnet.
Are there any prerequisites?
You need an Azure account to complete this project. The Azure free trial provides $200 in credit for 30 days, which is more than enough to cover everything here.
Before We Start
Every Azure resource in this project is created through terminal commands. The tool that translates those commands into Azure API calls is the Azure CLI.
In this step, you'll install the CLI (or use a browser-based version), sign in to your Azure account, and confirm you're targeting the correct subscription.
In this step, get ready to:
- Install or verify the Azure CLI.
- Sign in to your Azure account.
- Set your default subscription.
Install or verify Azure CLI
You have two options for running Azure CLI commands throughout this project.
- Local terminal: Install the CLI on your machine. Best if you want to keep the tools for future projects.
- Azure Cloud Shell: A browser-based terminal at https://shell.azure.com that comes with the CLI pre-installed. No setup needed.
What is Azure Cloud Shell?
Azure Cloud Shell is a free, browser-based terminal hosted by Microsoft. It already has the Azure CLI installed and keeps you authenticated automatically.
If you choose Cloud Shell, open https://shell.azure.com in your browser and select Bash when prompted. Then skip ahead to the sign-in substep.
If you're using a local terminal, check whether the Azure CLI is already installed.
- Check your Azure CLI version by running this command:
az version
✔️ I see version 2.86.0 or higher
Your Azure CLI is up to date. Move on to the next substep.
ⓧ I see an older version
You have the CLI installed but need a newer version. The current release is 2.86.0.
- Upgrade the Azure CLI on macOS by running this command:
brew upgrade azure-cli
- After the upgrade finishes, verify your version again:
az version
You should now see 2.86.0 or higher in the output.
Upgrade not working?
If Homebrew itself is outdated, run brew update first, then retry the upgrade command.
Alternatively, use Azure Cloud Shell at https://shell.azure.com which always has the latest version.
Help me upgrade Azure CLI on my machine
ⓧ Command not found
The Azure CLI is not installed on your machine yet.
- Install the Azure CLI on macOS by running this command:
brew install azure-cli
- After installation completes, verify it worked:
az version
You should see a JSON object with "azure-cli": "2.86.0" in the output.
Not on macOS?
Use Azure Cloud Shell at https://shell.azure.com instead. It works in any browser on any operating system and already has the CLI installed.
🙋♀️ Brew command not found?
Homebrew needs to be installed first. Visit https://brew.sh and follow the one-line install instruction, then retry the command above.
Help me install Azure CLI on my machine
Sign in to Azure
The CLI needs to know who you are before it can create resources in your Azure account.
- Authenticate with Azure by running this command:
az login
What happens when you run this?
Your default web browser opens to a Microsoft sign-in page. Sign in with the same account you used to create your Azure subscription.
After successful authentication, the browser shows a confirmation message and your terminal displays a JSON list of your subscriptions.
- Confirm your subscription is active by running:
az account show
You should see a JSON object containing your subscription name, subscription ID, and a state field showing Enabled.
Browser didn't open?
If your browser doesn't open automatically, copy the URL shown in the terminal and paste it into your browser manually.
If you're in a headless environment (like a remote server), use the device code flow instead:
az login --use-device-code
Help me sign in to Azure CLI
Set your default subscription
If your Azure account has multiple subscriptions, you need to tell the CLI which one to use. This ensures all commands target the correct subscription.
- Find your subscription ID in the output from az account show. Look for the id field.
- Set your default subscription by running this command:
az account set --subscription [[SUBSCRIPTION_ID="your-subscription-id"]]
What does this command do?
This tells the Azure CLI to route all future commands to your chosen subscription. If you only have one subscription (common with free trials), this confirms it as the default.
- Verify the correct subscription is set by running:
az account show --query "{name:name, id:id, state:state}" --output table
You should see a table with your subscription name, its ID, and the state Enabled.
Seeing a different subscription?
Double-check that you copied the full subscription ID from the az account show output. It should be a long string of letters, numbers, and dashes.
If you have multiple subscriptions and aren't sure which to use, look for the one with isDefault: true or with state: Enabled in the az login output.
Help me find and set my Azure subscription
Your Azure CLI is authenticated and ready. Next up, you'll create the network foundation: a Virtual Network and subnet that will host your private endpoint.
Set Up Azure CLI
Every Azure resource you create in this project will be provisioned through the Azure CLI. This is a command-line tool that lets you manage Azure resources directly from your terminal.
In this step, you will get a working terminal environment with the Azure CLI, sign in to your Azure account, and make sure all commands target the correct subscription.
In this step, get ready to:
- Install or verify Azure CLI.
- Sign in to Azure.
- Set your default subscription.
Install or verify Azure CLI
You have two options for running Azure CLI commands. You can install the CLI on your local machine, or you can use Azure Cloud Shell in your browser with no install required.
I'll install locally (macOS)
- Open your terminal.
- Install Azure CLI using Homebrew by running this command:
brew install azure-cli
What does this command do?
Homebrew is a package manager for macOS. This command downloads and installs the Azure CLI along with all its dependencies.
- Verify the installation by running:
az version
You should see version information in the output. The current version is 2.86.0.
✔️ I see a version number
Your Azure CLI is installed and ready. Continue to the next substep.
ⓧ I see an older version
- Upgrade to the latest version by running:
brew upgrade azure-cli
- Run az version again to confirm the update.
ⓧ Command not found
Homebrew may not be installed on your machine. Follow the instructions at https://brew.sh to install Homebrew first, then re-run the install command above.
I'll use Azure Cloud Shell
Azure Cloud Shell is a browser-based terminal that comes with the Azure CLI pre-installed. No local setup needed.
- Navigate to https://shell.azure.com in your browser.
- Sign in with your Azure account credentials.
- When prompted, select **Bash** as your shell environment.
First-time Cloud Shell setup
The first time you open Cloud Shell, you will be prompted to choose a storage option. Select **No storage account required** for an ephemeral session, or **Mount storage account** if you want files to persist between sessions.
- Verify the CLI is available by running:
az version
You should see version 2.86.0 or later in the output.
Sign in to Azure
The Azure CLI needs to authenticate with your Azure account before it can create or manage resources.
- Sign in to Azure by running this command:
az login
What happens when you run this?
On macOS and Linux, az login opens your default web browser to an Azure sign-in page. Sign in with your Azure credentials there, then return to your terminal.
If a browser cannot open, the CLI falls back to a device code flow. It will display a URL and a code. Open that URL on any device and enter the code to authenticate.
- After signing in, confirm your subscription is active by running:
az account show
You should see JSON output showing your subscription name, ID, and state as Enabled.
Login not working?
If the browser does not open automatically, try running az login --use-device-code instead. This gives you a code to enter manually at a URL.
If you are using the Azure free trial, make sure you completed the sign-up process and your subscription shows as Enabled in the output above.
Help me troubleshoot my Azure CLI login.
Set your default subscription
If your Azure account has multiple subscriptions, you need to tell the CLI which one to target. All resources you create will be billed to this subscription.
- Copy the subscription ID from the az account show output above.
- Set it as your default by running:
az account set --subscription [[SUBSCRIPTION_ID="your-subscription-id"]]
What does this command do?
This ensures every az command you run targets the correct subscription. If you only have one subscription, this step is still good practice to confirm it explicitly.
- Verify the subscription is set correctly by running:
az account show --query "{name:name, id:id, state:state}" --output table
You should see a table with your subscription name, ID, and state showing Enabled.
Wrong subscription showing?
Double-check that you copied the full subscription ID (a long string of letters, numbers, and hyphens). Run az account list --output table to see all available subscriptions and pick the correct one.
Help me find and set the right Azure subscription.
Your terminal is now authenticated and pointing at the right subscription. Next up, you will create the resource group and virtual network that will host your private endpoint.
Create the Resource Group and Virtual Network
Before you can create a private endpoint, you need a network for it to live in. Azure resources are organized into resource groups, and private endpoints need a Virtual Network with at least one subnet.
In this step, you will create a resource group to hold all your project resources, then build a Virtual Network with a dedicated subnet where the private endpoint will eventually be placed.
In this step, get ready to:
- Create a resource group in East US 2.
- Create a virtual network with a subnet.
- Verify the network configuration.
Create the resource group
A resource group is a container that holds all the Azure resources for a project. Putting everything in one group makes it easy to manage and delete later.
- Create a resource group named test-rg in the East US 2 region by running this command:
az group create --name test-rg --location eastus2
What does this command do?
- az group create tells Azure to provision a new resource group.
- --name test-rg gives the group a name you will reference in every later command.
- --location eastus2 sets the Azure region where the group's metadata is stored.
- You should see JSON output with "provisioningState": "Succeeded" confirming the resource group was created.
Seeing an authorization or subscription error?
- Make sure you set your subscription in the previous step with az account set --subscription. If the CLI targets the wrong subscription, the resource group creation fails.
- Confirm your account has Contributor or Owner permissions on the subscription. If you are on a free trial, permissions are granted automatically.
- help me fix an authorization error when running az group create
Create a VNet with a subnet
A Virtual Network (VNet) is an isolated network in Azure where your resources communicate privately. Inside the VNet, subnets divide the address space into smaller segments.
- Create a virtual network named vnet-1 with a subnet named subnet-1 by running this command:
az network vnet create --resource-group test-rg --location eastus2 --name vnet-1 --address-prefixes 10.0.0.0/16 --subnet-name subnet-1 --subnet-prefixes 10.0.0.0/24
What does this command do?
- --address-prefixes 10.0.0.0/16 defines the overall address space of the VNet. This gives you 65,536 possible IP addresses.
- --subnet-name subnet-1 creates a subnet inside the VNet where the private endpoint will live.
- --subnet-prefixes 10.0.0.0/24 allocates 256 addresses within that subnet. The private endpoint will receive one of these addresses.
VNets themselves are free of charge in Azure. You only pay for resources deployed inside them.
- You should see JSON output with the VNet details and "provisioningState": "Succeeded" confirming both the network and subnet were created.
Command failed or resource group not found?
- Double-check that you typed --resource-group test-rg exactly as you named it in the previous command. Resource group names are case-sensitive.
- If you see a resource group not found error, confirm you are in the correct subscription by running az account show.
- help me troubleshoot why my az network vnet create command is failing
Verify the network
Before moving on, confirm that the VNet and subnet were created with the correct address ranges.
- Verify your network configuration by running this command:
az network vnet show --resource-group test-rg --name vnet-1 --query "{name:name, addressSpace:addressSpace.addressPrefixes, subnets:subnets[].name}" --output table
What does this command do?
- az network vnet show retrieves the details of your virtual network.
- --query filters the output to show only the VNet name, address space, and subnet names. This avoids a wall of JSON.
- --output table formats the result as a readable table instead of raw JSON.
- You should see a table showing vnet-1 with address space 10.0.0.0/16 and subnet subnet-1.
Output looks empty or missing the subnet?
- Make sure you copied the command exactly. The --query flag uses JMESPath syntax and is sensitive to spacing and braces.
- If you see an error about the VNet not being found, re-run az network vnet create from the previous substep.
- help me figure out why my az network vnet show command is not returning the expected output
Your network foundation is in place. Next up, you will create the storage account and lock it down so only private traffic can reach it.
Create and Lock Down the Storage Account
Your network is ready. Now you need something to protect inside it.
In this step, you will create an Azure Storage Account, add a blob container with test data, and then disable all public access. Once locked down, the only way to reach this storage account will be through the private endpoint you create in the next step.
In this step, get ready to:
- Create a storage account in your resource group.
- Create a blob container for test data.
- Disable public network access on the storage account.
Create the storage account
A storage account provides a unique namespace for your Azure Blob Storage data. The name you choose must be globally unique across all of Azure.
- Choose a unique name for your storage account. It must be 3-24 characters, lowercase letters and numbers only. A good pattern is your initials plus a word, like jdprivatestor01.
- Create the storage account by running this command (replace the name with your own):
az storage account create --name [[STORAGE_NAME="your-unique-storage-name"]] --resource-group test-rg --location eastus2 --sku Standard_LRS --kind StorageV2
What does this command do?
- --sku Standard_LRS sets Locally Redundant Storage, keeping three copies of your data in one datacenter. This is the cheapest option and fine for testing.
- --kind StorageV2 creates a general-purpose v2 account that supports blobs, files, queues, and tables.
- Wait for the command to complete. You should see a JSON output with "provisioningState": "Succeeded" confirming the account was created.
Seeing a naming error?
Storage account names must be globally unique. If you get a conflict error, try a different name with more random characters at the end.
Also confirm your name is 3-24 characters, all lowercase, and uses only letters and numbers.
Help me fix my storage account name error.
Create a blob container
Before you lock down the storage account, you need to create a container inside it to hold test data. You will do this now while public access is still enabled.
- Retrieve the storage account key by running this command (replace the account name with your own):
az storage account keys list --resource-group test-rg --account-name [[STORAGE_NAME="your-unique-storage-name"]] --query "[0].value" --output tsv
What does this command do?
This retrieves the first access key for your storage account. The --query "[0].value" filter extracts just the key string, and --output tsv outputs it as plain text you can copy.
- Copy the key value from the output. You will use it in the next command.
- Create the blob container by running this command (replace the account name and paste your key):
az storage container create --name testcontainer --account-name [[STORAGE_NAME="your-unique-storage-name"]] --account-key your-account-key-here
What does this command do?
This creates a container named testcontainer inside your storage account. Containers are like folders that hold blobs (files). You are authenticating with the account key because the default auth mode requires additional role assignments.
- You should see "created": true in the JSON output confirming the container exists.
Container creation failed?
Make sure you pasted the full account key with no extra spaces or line breaks.
Double-check that your storage account name matches exactly what you used in the create command.
Help me debug my container creation error.
Disable public network access
Now that your container is in place, you can lock the door. Disabling public network access means no one outside your Virtual Network can reach this storage account.
- Disable public access by running this command:
az storage account update --name [[STORAGE_NAME="your-unique-storage-name"]] --resource-group test-rg --public-network-access Disabled
What does this command do?
The --public-network-access Disabled flag blocks ALL traffic from the public internet to this storage account. After this, even requests with a valid account key will be rejected unless they come from an authorized private endpoint inside your VNet.
- You should see a large JSON output. Look for "publicNetworkAccess": "Disabled" in the response to confirm the change took effect.
Don't see publicNetworkAccess in the output?
Run the following command to verify the current state:
az storage account show --name [[STORAGE_NAME="your-unique-storage-name"]] --resource-group test-rg --query publicNetworkAccess --output tsv
It should return Disabled. If it still says Enabled, make sure you saved the correct storage account name in your update command.
Help me verify my storage account's public access setting.
Your storage account is now locked down. The only problem? Nothing can reach it yet. In the next step, you will create a private endpoint that gives resources inside your VNet a private path to this storage account.
Create the Private Endpoint
Your storage account is locked down. Public access is disabled, which means nobody on the internet can reach it.
Now you need a way for resources inside your Virtual Network to access the storage account privately. A Private Endpoint solves this by creating a network interface in your subnet with a private IP address that maps directly to the storage account's blob service.
In this step, get ready to:
- Retrieve the storage account's resource ID.
- Create a private endpoint in your subnet targeting the blob service.
- Verify the private endpoint has a private IP assigned.
Get the storage account resource ID
The private endpoint needs to know the full Azure Resource Manager ID of the resource it connects to. This ID is a long path that uniquely identifies your storage account within your Azure subscription.
- Retrieve your storage account's resource ID by running this command:
az storage account show --name [[STORAGE_NAME="your storage account name"]] --resource-group test-rg --query id --output tsv
What does this command do?
The --query id flag uses a JMESPath query to extract only the id property from the full JSON response.
The --output tsv flag returns the value as plain text without quotes, making it easy to copy and paste into the next command.
- Copy the entire output string. It looks something like /subscriptions/.../resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/yourstorageaccount.
- Save this value somewhere handy. You will paste it into the next command.
Getting an empty result?
Make sure the --name value matches the exact storage account name you created in the previous step. Storage account names are case-sensitive and must be lowercase alphanumeric only.
help me troubleshoot why az storage account show is returning empty
Create the private endpoint
This is the core command. It creates a network interface inside subnet-1 and connects it to your storage account's blob service through Azure Private Link. Traffic between the endpoint and the storage account stays on the Microsoft backbone network.
- Create the private endpoint by running this command:
az network private-endpoint create --name pe-storage --resource-group test-rg --vnet-name vnet-1 --subnet subnet-1 --private-connection-resource-id [[RESOURCE_ID="your storage account resource ID from the previous command"]] --group-id blob --connection-name pe-storage-connection
What does this command do?
- --name pe-storage names the private endpoint resource.
- --vnet-name vnet-1 --subnet subnet-1 tells Azure which subnet to place the endpoint's network interface in.
- --private-connection-resource-id points to the storage account you want to connect to privately.
- --group-id blob specifies the sub-resource type. For blob storage, the group ID is always blob.
- --connection-name pe-storage-connection gives a friendly name to the private link connection between the endpoint and the storage account.
💡 What is a Private Endpoint?
A private endpoint is a special network interface that gets assigned a private IP address from your subnet's address range (10.0.0.0/24 in this case). It uses Azure Private Link to create a secure connection to the target service.
The same connection strings and FQDNs that applications already use continue to work. The only difference is that DNS resolution now returns a private IP instead of a public one.
- Wait 1-2 minutes for the command to complete. You will see a large JSON output when it finishes.
Seeing a permissions or authorization error?
Confirm you are signed into the correct subscription with az account show. The resource ID must belong to a storage account in the same subscription.
Also check that you pasted the full resource ID from the previous command. It should start with /subscriptions/ and end with your storage account name.
help me fix a permissions error when creating a private endpoint
Verify the private endpoint
You need to confirm the endpoint was created successfully and has a private IP assigned from your subnet's range.
- Check the private endpoint's status and IP address by running:
az network private-endpoint show --name pe-storage --resource-group test-rg --query "{name:name, provisioningState:provisioningState, privateIP:customDnsConfigs[0].ipAddresses[0]}"
What should I see?
The output should show three values:
- name shows pe-storage.
- provisioningState shows Succeeded.
- privateIP shows an IP address in the 10.0.0.x range (for example, 10.0.0.4 or 10.0.0.5).
The private IP confirms that Azure placed a network interface in your subnet and assigned it an address from the 10.0.0.0/24 range. This IP is what clients inside the VNet will use to reach the storage account's blob service.
🙋♀️ Seeing null for privateIP?
The customDnsConfigs field may take a moment to populate after provisioning completes. Wait 30 seconds and re-run the show command.
If provisioningState shows Failed, delete the endpoint with az network private-endpoint delete --name pe-storage --resource-group test-rg and re-create it after checking that the resource ID is correct.
help me troubleshoot my private endpoint showing null for the private IP
Your private endpoint is live. It has a private IP inside your subnet that maps to the storage account's blob service. Next up, you will configure DNS so that the storage account's domain name resolves to this private IP automatically.
Configure Private DNS
Your private endpoint has a private IP, but there is a gap. If a VM in the VNet tries to reach <storage-name>.blob.core.windows.net, DNS still resolves to the public IP.
Without the right DNS configuration, the connection fails because public access is disabled. In this step, you will create a Private DNS Zone that overrides the public resolution and points the storage account's FQDN to the private IP instead.
In this step, get ready to:
- Create a Private DNS Zone for blob storage.
- Link the DNS zone to your VNet so VMs can resolve it.
- Associate the DNS zone with the private endpoint for automatic A-record management.
Create the Private DNS Zone
Azure requires a specific DNS zone name for each service that supports private endpoints. For blob storage, the zone must be named privatelink.blob.core.windows.net.
- Create the Private DNS Zone by running this command:
az network private-dns zone create --resource-group test-rg --name "privatelink.blob.core.windows.net"
What does this command do?
This creates a Private DNS Zone named privatelink.blob.core.windows.net in your resource group. The zone will hold DNS records that map storage account FQDNs to private IPs.
The zone name is mandatory per Microsoft's documentation. Each Azure service has a specific privatelink zone name that Azure recognizes for private endpoint DNS resolution.
- You should see a JSON response with the zone name and a provisioningState of Succeeded.
Zone creation failed?
Make sure the zone name is exactly privatelink.blob.core.windows.net with no typos. A single wrong character means the DNS resolution will not work.
If you see a permissions error, confirm you are signed in to the correct subscription with az account show.
help me troubleshoot my Private DNS Zone creation error
Link the DNS zone to the VNet
The DNS zone exists, but VMs inside your VNet cannot resolve it yet. You need to create a virtual network link that tells the VNet to check this zone during DNS lookups.
- Create a VNet link for the DNS zone by running this command:
az network private-dns link vnet create --resource-group test-rg --zone-name "privatelink.blob.core.windows.net" --name dns-link --virtual-network vnet-1 --registration-enabled false
What does this command do?
- --zone-name specifies which Private DNS Zone to link.
- --name dns-link gives the link a name you can reference later.
- --virtual-network vnet-1 tells Azure which VNet should use this zone for DNS resolution.
- --registration-enabled false disables automatic VM hostname registration. You only need name resolution here, not auto-registration.
- You should see JSON output showing the link with a provisioningState of Succeeded.
VNet link creation failed?
Double-check that the virtual network name is vnet-1 and the zone name matches exactly. The link connects two resources, so both must be spelled correctly.
help me troubleshoot my VNet DNS link error
Create a DNS zone group on the private endpoint
The final piece is connecting the DNS zone to the private endpoint itself. A DNS zone group tells Azure to automatically create and maintain A-records in the zone whenever the private endpoint's IP changes.
- Associate the DNS zone with the private endpoint by running this command:
az network private-endpoint dns-zone-group create --resource-group test-rg --endpoint-name pe-storage --name zone-group --private-dns-zone "privatelink.blob.core.windows.net" --zone-name blob
What does this command do?
- --endpoint-name pe-storage targets the private endpoint you created earlier.
- --name zone-group names the DNS zone group resource on the endpoint.
- --private-dns-zone points to the zone that should hold the A-records.
- --zone-name blob identifies which sub-resource's DNS config this is (the blob service).
Once this runs, Azure automatically creates an A-record in the Private DNS Zone mapping your storage account's FQDN to the private endpoint's IP (10.0.0.x).
- You should see JSON output with provisioningState showing Succeeded and a privateDnsZoneConfigs array containing your zone.
DNS zone group creation failed?
Ensure the endpoint name is pe-storage and the DNS zone privatelink.blob.core.windows.net was created successfully in the previous substep.
If you see a conflict error, the zone group may already exist. Run az network private-endpoint dns-zone-group show --resource-group test-rg --endpoint-name pe-storage --name zone-group to check.
help me troubleshoot my DNS zone group creation error
💡 Why is DNS so critical for private endpoints?
Without this DNS setup, any client trying to reach your storage account resolves the FQDN to a public IP address. Since you disabled public access, the connection fails with no helpful error message.
The Private DNS Zone intercepts the lookup inside the VNet and returns the private IP instead. Same connection string, completely different network path.
DNS is now fully configured. Any VM in your VNet that queries <storage-name>.blob.core.windows.net will get the private IP from the DNS zone. Next up, you will deploy a test VM and prove this works end-to-end.
Test Private Connectivity
You have built the entire private networking stack. A Private Endpoint assigns a private IP to your storage account, and a Private DNS Zone ensures that DNS queries resolve to that IP instead of a public address.
Now you need to prove it actually works. You will deploy a test VM inside the VNet, run a DNS lookup from it, and confirm the storage account resolves to a private IP. Then you will run the same lookup from your local machine to confirm that public access is blocked.
In this step, get ready to:
- Deploy a test VM inside the VNet with no public IP.
- Verify DNS resolution returns the private IP from inside the VNet.
- Confirm public access is blocked from outside the VNet.
Deploy a test VM in the VNet
To test DNS resolution from inside the VNet, you need a compute resource sitting in the same subnet as the private endpoint. A small Windows VM works perfectly because nslookup is built in.
- Create a test VM in subnet-1 with no public IP by running this command:
az vm create --resource-group test-rg --name vm-test --image Win2022Datacenter --vnet-name vnet-1 --subnet subnet-1 --public-ip-address "" --admin-username azureuser --size Standard_B1s
What does this command do?
- --image Win2022Datacenter deploys a Windows Server 2022 VM, which has nslookup available out of the box.
- --public-ip-address "" ensures the VM has no public IP. It can only communicate within the VNet.
- --size Standard_B1s is the smallest burstable VM size. It costs roughly $0.012/hour.
- The VM is placed in vnet-1/subnet-1, the same subnet as your private endpoint.
- The CLI will prompt you for a password. Enter a password that meets Azure complexity requirements: 12+ characters, with uppercase, lowercase, a number, and a special character.
- Wait for the command to complete. VM provisioning takes 1-2 minutes.
Why no public IP?
The whole point of this project is to prove that the storage account is only reachable through the private path. If the VM had a public IP and outbound internet access, you could not be sure traffic was flowing through the private endpoint. By removing the public IP, the VM communicates exclusively over the VNet.
🙋♀️ VM creation failed?
- If you see an error about password complexity, make sure your password is at least 12 characters with a mix of uppercase, lowercase, numbers, and special characters.
- If you see a quota error, your subscription may not have enough vCPU quota in East US 2. Try a different region or request a quota increase in the Azure portal.
Still stuck? Help me troubleshoot my VM creation error.
Test DNS resolution from inside the VNet
With the VM running inside the VNet, you can execute a DNS lookup remotely using Azure's Run Command feature. This lets you run a script on the VM without needing RDP or a public IP.
- Run nslookup on the VM by executing this command (replace your storage account name with your actual storage account name):
az vm run-command invoke --resource-group test-rg --name vm-test --command-id RunPowerShellScript --scripts "nslookup [[STORAGE_NAME=\"your storage account name\"]].blob.core.windows.net"
What does this command do?
- az vm run-command invoke executes a script on the VM remotely through the VM agent. No SSH or RDP connection is needed.
- --command-id RunPowerShellScript tells Azure to run the script as PowerShell on the Windows VM.
- --scripts contains the inline script to execute. In this case, it runs nslookup against your storage account's blob endpoint.
- Wait for the command to return. It may take 30-60 seconds.
- In the output, look for the resolved IP address. You should see your storage account name resolving to a private IP in the 10.0.0.x range (for example, 10.0.0.10).
- The Name field should show <your-account>.privatelink.blob.core.windows.net. This confirms DNS is routing through the Private DNS Zone.
What should I see?
The output includes a message field with the nslookup results. A successful result shows the Name as <account>.privatelink.blob.core.windows.net and the Address as a private IP like 10.0.0.10. If you see a public IP instead, your DNS zone is not linked correctly.
🙋♀️ Seeing a public IP instead of 10.0.0.x?
- Confirm the DNS zone group was created on the private endpoint. Run az network private-endpoint dns-zone-group list --resource-group test-rg --endpoint-name pe-storage and check that it shows your zone.
- Confirm the VNet link exists. Run az network private-dns link vnet list --resource-group test-rg --zone-name "privatelink.blob.core.windows.net" and verify vnet-1 is listed.
Need more help? Help me debug why my nslookup returns a public IP instead of a private IP.
Test DNS resolution from outside the VNet
The final proof is showing that the same storage account is unreachable from outside the VNet. Running nslookup from your local machine demonstrates that public access is truly blocked.
- On your local terminal (not the Azure VM), run this command:
nslookup [[STORAGE_NAME="your storage account name"]].blob.core.windows.net
What should I see?
From outside the VNet, nslookup shows that the storage account's FQDN resolves to a privatelink CNAME. However, because public network access is disabled on the storage account, any attempt to connect to it from outside will fail.
This proves the architecture is working. Inside the VNet, DNS resolves to the private IP and connections succeed. Outside the VNet, the endpoint is unreachable.
- Compare the two results. The inside-VNet lookup returned a private IP (10.0.0.x). The outside lookup shows the privatelink CNAME but cannot establish a connection to the storage account.
Local nslookup not showing the privatelink CNAME?
- Some local DNS resolvers cache older records. Try flushing your DNS cache first, then re-run the lookup.
- On macOS, flush with sudo dscacheutil -flushcache. On Windows, use ipconfig /flushdns.
Still seeing unexpected results? Help me understand my local nslookup output for my private endpoint storage account.
You have proven that your private endpoint architecture works end to end. DNS resolves correctly inside the VNet, and the storage account is completely unreachable from the public internet. Next up, take on the Secret Mission and add Network Security Group rules for even finer-grained access control.
Secret mission
Add NSG Rules to Control Private Endpoint Access
Your private endpoint is only reachable from inside the VNet. But what about controlling which resources within the VNet can talk to it? Add a Network Security Group with granular inbound rules that restrict access to a specific IP range on port 443.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project provisions cloud resources that incur hourly charges while they exist.
Cost warning
Your Private Endpoint charges $0.01/hour (~$7.30/month) and the Virtual Machine charges ~$0.012/hour. Left running, the full set of resources costs approximately $15-20/month.
If you are on the Azure free trial, these charges come out of your $200 credit. Delete everything when you are done to preserve your remaining balance.
Resources you used:
- Resource group test-rg (contains all other resources below).
- Virtual Network vnet-1 with subnet subnet-1.
- Storage Account with public access disabled and blob container testcontainer.
- Private Endpoint pe-storage with private IP in subnet-1.
- Private DNS Zone privatelink.blob.core.windows.net with VNet link and zone group.
- Virtual Machine vm-test (Standard_B1s, no public IP).
- Network Security Group nsg-pe associated with subnet-1.
Keep everything running
No action needed. Choose this if you are still actively experimenting with private endpoints or plan to extend the architecture.
- All resources remain active in test-rg.
- Expect approximately $15-20/month in charges against your subscription or free trial credit.
- Azure CLI remains installed on your local machine (no ongoing cost).
Pause - I'll come back to this later
Shut down the VM to stop compute charges while keeping your networking configuration intact.
- Deallocate the VM to stop compute billing by running this command:
az vm deallocate --resource-group test-rg --name vm-test
- The Private Endpoint ($0.01/hour) and Storage Account (pennies/month) continue to incur charges.
- When you return, start the VM again with az vm start --resource-group test-rg --name vm-test.
Delete - I don't want to use this again
Remove all project resources and stop all charges immediately. Deleting the resource group removes everything inside it in one command.
- Delete the entire resource group and all its contents by running:
az group delete --name test-rg --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 Azure deletes the resources in the background.
This single command deletes the VNet, Storage Account, Private Endpoint, Private DNS Zone, VM, NSG, and all associated sub-resources.
- Confirm deletion is in progress by running:
az group list --query "[?name=='test-rg']" --output table
- If the group still appears, its provisioningState shows Deleting. Once fully deleted, it no longer appears in the output.
- To uninstall Azure CLI from macOS (if you installed via Homebrew), run:
brew uninstall azure-cli
- If you used Azure Cloud Shell instead, no local uninstall is needed.
Nice Work!
Nice Work!
You just locked down an Azure Storage Account so it is completely unreachable from the public internet. Only resources inside your Virtual Network can access it through the Private Endpoint.
You've learned how to:
- Built a Virtual Network with a dedicated subnet and deployed a Private Endpoint that assigns a private IP to an Azure Storage Account's blob service.
- Configured a Private DNS Zone so the storage account's FQDN automatically resolves to the private IP within the VNet.
- Proved end-to-end private connectivity by testing DNS resolution from inside the VNet and confirming public access is blocked from outside.
- Added Network Security Group rules for fine-grained access control within the subnet.
Ready to quiz yourself?