The previous lesson ended with an uncomfortable observation: everything Contoso Airlines has deployed so far — the legacy engine's VMs, the API scale set, the App Service applications and the boarding pass storage account — communicates over the internet. It works, but no serious architecture stays like that. And fixing it afterwards is far more expensive than doing it right from the start, because a virtual network's address space conditions everything that connects to it for years.
That is why every serious architecture starts with the network. Changing a VM's size takes two minutes; changing the address range of a virtual network with a hundred resources inside it and a VPN to two offices is a project with a service outage attached.
In this lesson you will design and deploy Contoso's complete network: the address space of vnet-contoso-pro with CIDR arithmetic explained from scratch, its four subnets, the network security groups that open only what is strictly necessary, private DNS resolution, peering between networks with the hub-and-spoke pattern, the decisive difference between service endpoints and Azure Private Link, how App Service connects to the network, and why nobody should expose SSH to the internet when Azure Bastion exists.
Cost warning: virtual networks, subnets and NSGs cost nothing. What does cost money are private endpoints (per hour and per data processed), static public IPs and, above all, Azure Bastion, which is billed by the hour from the moment it exists. The cleanup is at the end.
Contents
- Why the network comes first
- Virtual networks and address space
- CIDR arithmetic without the pain
- Subnet design for
vnet-contoso-pro - Addresses reserved by Azure in every subnet
- Network security groups (NSGs)
- Service tags and application security groups
- Public and private IPs, static and dynamic
- DNS in Azure and private DNS zones
- Virtual network peering and hub-and-spoke
- Service endpoints versus Azure Private Link
- App Service integration with the virtual network
- Azure Bastion versus exposing SSH and RDP
- Verification with Network Watcher
- The complete topology and cleanup
- Common Mistakes and Tips
- Exercises
- Conclusion
- Why the network comes first
A virtual network (VNet) is your private network inside Azure: an isolated address space that you control, in which you place resources that can see each other and through which you decide what comes in and what goes out.
What the network determines, and why it comes first:
| Network decision | What it conditions |
|---|---|
| Address space | Which on-premises networks you will be able to connect to without overlaps, forever |
| Subnet sizes | How many resources fit; growing a subnet that already has resources is problematic |
| Segmentation | What can talk to what when somebody gets in where they should not |
| Private connectivity | Whether your data travels over the internet or never leaves Microsoft's network |
| Region | A VNet lives in one region; it connects to others through peering |
And a warning about what you cannot do: two networks with overlapping address spaces cannot be peered or joined by VPN. If you pick 10.0.0.0/16 because it is the example in every tutorial, and your Barcelona office uses 10.0.0.0/16, you have created a problem that can only be fixed by renumbering one of the two.
- Virtual networks and address space
A VNet has one or more address spaces in CIDR notation, taken from the private ranges of RFC 1918:
| Private range | Addresses | Common use |
|---|---|---|
10.0.0.0/8 |
16.7 million | Large corporate networks; the most used in Azure |
172.16.0.0/12 |
1 million | Medium-sized networks |
192.168.0.0/16 |
65,536 | Home networks and small offices |
Contoso Airlines' addressing plan, decided by Marta Ríos with the office VPN in mind (lesson 02-06):
| Network | Space | Use |
|---|---|---|
vnet-contoso-hub-pro |
10.10.0.0/16 |
Hub: VPN gateway, Bastion, shared services |
vnet-contoso-pro |
10.20.0.0/16 |
Production spoke: web, application, data, management |
vnet-contoso-dev |
10.30.0.0/16 |
Development spoke |
| Barcelona office | 10.100.0.0/16 |
Existing on-premises network |
| Palma office | 10.101.0.0/16 |
Existing on-premises network |
Notice the discipline: separate /16 blocks with nothing overlapping, and gaps reserved between them for growth. This costs nothing now and avoids a painful migration three years from now.
NET_GROUP="rg-contoso-red-pro" # the long-lived networking group from module 1
REGION="westeurope"
TAGS=(entorno=produccion proyecto=contoso-reservas centro-coste=CC-1042
[email protected] criticidad=alta)
az network vnet create \
--resource-group "${NET_GROUP}" \
--name vnet-contoso-pro \
--location "${REGION}" \
--address-prefixes 10.20.0.0/16 \
--tags "${TAGS[@]}" \
--output tableRemember why the network lives in rg-contoso-red-pro and not alongside the applications: the grouping criterion from lesson 01-05 was lifecycle + environment, and the network outlives many generations of applications.
- CIDR arithmetic without the pain
If CIDR already feels natural to you, skip to the next section. If not, this is the bare minimum, and three ideas are enough.
Idea 1: the number after the slash is the count of fixed bits. An IPv4 address has 32 bits. In 10.20.1.0/24, the first 24 bits are the network (fixed) and the remaining 8 identify the host (variable).
Idea 2: the more fixed bits, the smaller the network. A /24 is smaller than a /16. The arithmetic is direct: 2^(32 - prefix) addresses.
| Prefix | Total addresses | Usable in Azure (−5) | Equivalent to |
|---|---|---|---|
/16 |
65,536 | 65,531 | A complete virtual network |
/20 |
4,096 | 4,091 | A large block of subnets |
/24 |
256 | 251 | A typical subnet |
/26 |
64 | 59 | A small subnet (the minimum for Bastion) |
/27 |
32 | 27 | The minimum for GatewaySubnet |
/29 |
8 | 3 | The smallest subnet allowed in Azure |
Idea 3: to carve things up, you move block by block. A /24 covers 256 addresses, so inside 10.20.0.0/16 the /24 subnets step one at a time through the third octet:
10.20.0.0/16 → from 10.20.0.0 to 10.20.255.255 (65,536 addresses) ├── 10.20.1.0/24 → 10.20.1.0 to 10.20.1.255 ├── 10.20.2.0/24 → 10.20.2.0 to 10.20.2.255 ├── 10.20.3.0/24 → 10.20.3.0 to 10.20.3.255 └── 10.20.4.0/24 → 10.20.4.0 to 10.20.4.255
And if a subnet needs less, it is carved up with longer prefixes. Inside 10.20.250.0/24:
10.20.250.0/26 → 10.20.250.0 to 10.20.250.63 (64 addresses) 10.20.250.64/26 → 10.20.250.64 to 10.20.250.127 10.20.250.128/26 → 10.20.250.128 to 10.20.250.191 10.20.250.192/26 → 10.20.250.192 to 10.20.250.255
A mental rule that solves 90% of day-to-day cases: a /24 is "a block of 256 with the same third octet". Plan with /24 unless you know you need something else, and leave gaps between subnets for growth.
- Subnet design for
vnet-contoso-pro
vnet-contoso-proA subnet is a portion of the virtual network's address space where the resources are actually placed. You segment by function, because segmentation is what lets you apply different rules to each layer.
| Subnet | Range | What it contains | Who talks to it |
|---|---|---|---|
snet-web |
10.20.1.0/24 |
Public front end, Application Gateway | The internet (443 only) |
snet-app |
10.20.2.0/24 |
Availability API, App Service integration | snet-web only |
snet-datos |
10.20.3.0/24 |
Private endpoints for SQL and Storage | snet-app only |
snet-gestion |
10.20.4.0/24 |
Legacy engine VM, administration servers | snet-gestion and Bastion only |
AzureBastionSubnet |
10.20.250.0/26 |
Azure Bastion (mandatory name) | A managed service |
GatewaySubnet |
10.20.255.0/27 |
VPN gateway (mandatory name, lesson 02-06) | A managed service |
NET_GROUP="rg-contoso-red-pro"
VNET="vnet-contoso-pro"
# Functional subnets.
for PAIR in "snet-web:10.20.1.0/24" "snet-app:10.20.2.0/24" \
"snet-datos:10.20.3.0/24" "snet-gestion:10.20.4.0/24"; do
NAME="${PAIR%%:*}"
RANGE="${PAIR##*:}"
az network vnet subnet create \
--resource-group "${NET_GROUP}" \
--vnet-name "${VNET}" \
--name "${NAME}" \
--address-prefixes "${RANGE}" \
--output none
echo "Subnet ${NAME} created with ${RANGE}"
done
# Managed service subnets: the name is mandatory and literal.
az network vnet subnet create -g "${NET_GROUP}" --vnet-name "${VNET}" \
--name AzureBastionSubnet --address-prefixes 10.20.250.0/26 --output none
az network vnet subnet create -g "${NET_GROUP}" --vnet-name "${VNET}" \
--name GatewaySubnet --address-prefixes 10.20.255.0/27 --output none
# Check.
az network vnet subnet list -g "${NET_GROUP}" --vnet-name "${VNET}" \
--query "[].{Subnet:name, Range:addressPrefix}" --output tableReserved names that have to be respected to the letter, because Azure looks for them literally:
| Mandatory name | Service | Recommended minimum size |
|---|---|---|
GatewaySubnet |
VPN or ExpressRoute gateway | /27 (better /26) |
AzureBastionSubnet |
Azure Bastion | /26 |
AzureFirewallSubnet |
Azure Firewall | /26 |
And a restriction with consequences: a subnet can be grown, but not if resources get in the way, and it cannot be shrunk with resources inside. Size with headroom.
- Addresses reserved by Azure in every subnet
In a /24 subnet you do not get 256 addresses, nor 254, but 251. Azure reserves five in every subnet:
Address (in 10.20.1.0/24) |
Reserved for |
|---|---|
10.20.1.0 |
Network identifier (standard) |
10.20.1.1 |
Azure's default gateway |
10.20.1.2 |
Assigned to Azure DNS (virtual server mapping) |
10.20.1.3 |
Reserved for future Azure use |
10.20.1.255 |
Broadcast (standard) |
So the first assignable address in snet-web is 10.20.1.4. This matters when you size right to the limit: a /29 subnet has 8 addresses and only 3 usable. If you planned "eight servers in a /29", they do not fit.
- Network security groups (NSGs)
A network security group is a list of stateful filtering rules applied to a subnet or to a NIC. Stateful means that if you allow an inbound connection, the outbound response is allowed automatically; there is no need to write the reverse rule.
Each rule has:
| Field | What it is |
|---|---|
| Priority | From 100 to 4096. Evaluated from lowest to highest, and the first match wins |
| Source / Destination | An IP, a CIDR, a service tag or an application security group |
| Ports | Source (almost always *) and destination |
| Protocol | Tcp, Udp, Icmp or * |
| Direction | Inbound or Outbound |
| Action | Allow or Deny |
Default rules
Every NSG comes with invisible rules that cannot be deleted, only overridden with lower priorities:
| Priority | Name | Effect |
|---|---|---|
| 65000 | AllowVnetInBound |
Allows all traffic within the virtual network |
| 65001 | AllowAzureLoadBalancerInBound |
Allows the load balancer's probes |
| 65500 | DenyAllInBound |
Denies everything else coming in |
| 65000 | AllowVnetOutBound |
Allows outbound traffic within the virtual network |
| 65001 | AllowInternetOutBound |
Allows outbound traffic to the internet |
| 65500 | DenyAllOutBound |
Denies the rest of the outbound traffic |
Two consequences to internalize:
- By default, all traffic within the VNet is allowed, even between different subnets. Segmentation is not automatic: you have to write it.
- By default, everything can go out to the internet. If you want to prevent that, you have to deny it explicitly.
Segmenting Contoso's layers
NET_GROUP="rg-contoso-red-pro"
REGION="westeurope"
# --- Web layer NSG ---
az network nsg create -g "${NET_GROUP}" -n nsg-snet-web -l "${REGION}" --output none
# Allow HTTPS from the internet.
az network nsg rule create -g "${NET_GROUP}" --nsg-name nsg-snet-web \
--name permitir-https-internet --priority 100 \
--direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes Internet --source-port-ranges '*' \
--destination-address-prefixes '*' --destination-port-ranges 443 \
--description "Public traffic for Contoso Bookings" --output none
# Deny plain HTTP: the application redirects, but the network does not accept it.
az network nsg rule create -g "${NET_GROUP}" --nsg-name nsg-snet-web \
--name denegar-http-plano --priority 110 \
--direction Inbound --access Deny --protocol Tcp \
--source-address-prefixes Internet --destination-port-ranges 80 \
--output none
az network vnet subnet update -g "${NET_GROUP}" --vnet-name vnet-contoso-pro \
--name snet-web --network-security-group nsg-snet-web --output none
# --- Application layer NSG: it only accepts traffic from the web layer ---
az network nsg create -g "${NET_GROUP}" -n nsg-snet-app -l "${REGION}" --output none
az network nsg rule create -g "${NET_GROUP}" --nsg-name nsg-snet-app \
--name permitir-api-desde-web --priority 100 \
--direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes 10.20.1.0/24 --destination-port-ranges 8080 \
--description "Availability API from snet-web only" --output none
# Explicitly deny the rest of the internal traffic (it overrides AllowVnetInBound).
az network nsg rule create -g "${NET_GROUP}" --nsg-name nsg-snet-app \
--name denegar-resto-vnet --priority 4000 \
--direction Inbound --access Deny --protocol '*' \
--source-address-prefixes VirtualNetwork --destination-port-ranges '*' \
--output none
az network vnet subnet update -g "${NET_GROUP}" --vnet-name vnet-contoso-pro \
--name snet-app --network-security-group nsg-snet-app --output noneThe denegar-resto-vnet rule with priority 4000 is the key piece of the segmentation: it is evaluated before the default 65000 rule that allowed all internal traffic, but after the priority 100 rule that authorizes the web layer. The result is exactly what we were after: snet-app only listens to snet-web.
Checking the effective rules, including the invisible ones:
az network nsg rule list -g "${NET_GROUP}" --nsg-name nsg-snet-app \
--include-default \
--query "sort_by([].{Priority:priority, Name:name, Dir:direction, Action:access, Source:sourceAddressPrefix, Port:destinationPortRange}, &Priority)" \
--output table
- Service tags and application security groups
Writing IP ranges by hand ages badly: the IPs of Azure services change, and so do those of your machines. Azure offers two abstractions so that you do not depend on them.
Service tags
A service tag represents a set of IP prefixes that Microsoft keeps up to date for you.
| Tag | What it represents |
|---|---|
Internet |
Everything outside your network |
VirtualNetwork |
Your virtual network, peered networks and connected on-premises networks |
AzureLoadBalancer |
Azure's load balancer (needed for the health probes) |
Storage / Storage.WestEurope |
Azure Storage, globally or in one region |
Sql / Sql.WestEurope |
Azure SQL Database |
AzureCloud |
All the public Azure services |
AzureMonitor |
The monitoring endpoints (module 7) |
# Allow outbound traffic ONLY to Azure SQL in West Europe, without knowing a single IP.
az network nsg rule create -g "${NET_GROUP}" --nsg-name nsg-snet-app \
--name permitir-salida-sql --priority 200 \
--direction Outbound --access Allow --protocol Tcp \
--source-address-prefixes VirtualNetwork \
--destination-address-prefixes Sql.WestEurope \
--destination-port-ranges 1433 --output noneApplication security groups (ASGs)
An ASG is a logical label that groups NICs. Instead of writing rules per IP range, you write rules between groups: "whatever is in asg-web can talk to whatever is in asg-api". When you add a machine to the group, it inherits the rules without touching the NSG.
# 1. Create the groups.
az network asg create -g "${NET_GROUP}" -n asg-web -l "${REGION}" --output none
az network asg create -g "${NET_GROUP}" -n asg-api -l "${REGION}" --output none
# 2. A rule between groups, without a single IP written down.
az network nsg rule create -g "${NET_GROUP}" --nsg-name nsg-snet-app \
--name permitir-web-a-api --priority 90 \
--direction Inbound --access Allow --protocol Tcp \
--source-asgs asg-web --destination-asgs asg-api \
--destination-port-ranges 8080 --output none
# 3. Associate a NIC with its group.
az network nic ip-config update \
-g rg-contoso-reservas-pro --nic-name nic-api-01 --name ipconfig1 \
--application-security-groups asg-api --output noneThis is how you keep rules readable as the network grows: they read like business sentences, not like lists of octets.
- Public and private IPs, static and dynamic
| Type | Assignment | Behavior | When to use it |
|---|---|---|---|
| Private dynamic | Azure picks one from the subnet | Kept as long as the VM exists; it can change on deallocate and reallocate | The default for normal VMs |
| Private static | You choose it | Always fixed | Domain controllers, DNS servers, appliances |
| Public dynamic | Azure assigns it at start-up | It changes when the VM is deallocated | Testing only |
| Public static | Reserved for you | Fixed; billed even while the VM is powered off | Load balancers, gateways, anything in a DNS record |
# A static private IP for the legacy engine's VM.
az network nic ip-config update \
--resource-group rg-contoso-reservas-pro \
--nic-name nic-motor-disponibilidad-01 \
--name ipconfig1 \
--private-ip-address 10.20.4.10 \
--output noneAbout public IP SKUs: Standard is the current one (always static, closed by default — it needs an NSG that explicitly allows the traffic — and zone-compatible). The Basic SKU has been retired. Always use Standard.
One outbound nuance that confuses a lot of people: a VM without a public IP can still reach the internet through Azure's default outbound access, using an IP you do not control and which Microsoft is retiring for new networks. For predictable, auditable outbound traffic you use a NAT Gateway, which gives the whole subnet a fixed outbound IP and prevents SNAT port exhaustion.
- DNS in Azure and private DNS zones
Inside a VNet, Azure provides automatic DNS resolution: machines on the same network resolve each other by host name without you configuring anything, through the virtual server at 168.63.129.16 (an address that shows up in every Azure diagnostic and is worth recognizing).
That automatic resolution has limits: it does not work between peered networks and it does not resolve private service names. That is what private DNS zones are for.
# 1. Contoso's own private zone.
az network private-dns zone create \
--resource-group rg-contoso-red-pro \
--name interno.contosoairlines.example \
--output none
# 2. Link the zone to the virtual network, with automatic VM registration.
az network private-dns link vnet create \
--resource-group rg-contoso-red-pro \
--zone-name interno.contosoairlines.example \
--name enlace-vnet-contoso-pro \
--virtual-network vnet-contoso-pro \
--registration-enabled true \
--output none
# 3. A manual record for the legacy engine.
az network private-dns record-set a add-record \
--resource-group rg-contoso-red-pro \
--zone-name interno.contosoairlines.example \
--record-set-name motor \
--ipv4-address 10.20.4.10 \
--output noneNow any resource on the network resolves motor.interno.contosoairlines.example to 10.20.4.10. With --registration-enabled true, VMs also register themselves as they are created.
Private zones are essential for Private Link: they are the mechanism by which sql-contoso-reservas-pro.database.windows.net stops resolving to a public IP and starts resolving to an IP in your subnet. We look at that now.
Public DNS (registering contosoairlines.example and publishing its records to the world) is Azure DNS, and it is covered in lesson 02-06.
- Virtual network peering and hub-and-spoke
Peering connects two virtual networks so that they see each other as if they were one: private traffic over Microsoft's backbone, low latency, with no gateways and no internet in between.
Characteristics you need to know:
- It works within a region and between regions (global peering).
- It is not transitive: if A peers with B and B with C, A does not see C. This is the point that surprises people the most and the one that shapes the hub-and-spoke topology.
- The address spaces cannot overlap.
- It is billed by data transferred in both directions.
- It has to be created in both directions: two commands, one per network.
NET_GROUP="rg-contoso-red-pro"
# From the hub to the production spoke.
az network vnet peering create \
--resource-group "${NET_GROUP}" \
--name peer-hub-a-pro \
--vnet-name vnet-contoso-hub-pro \
--remote-vnet vnet-contoso-pro \
--allow-vnet-access \
--allow-gateway-transit \
--output none
# From the spoke to the hub (mandatory: peering is bidirectional by definition).
az network vnet peering create \
--resource-group "${NET_GROUP}" \
--name peer-pro-a-hub \
--vnet-name vnet-contoso-pro \
--remote-vnet vnet-contoso-hub-pro \
--allow-vnet-access \
--use-remote-gateways \
--output noneThe two important options: --allow-gateway-transit on the hub means "you may use my VPN gateway", and --use-remote-gateways on the spoke means "I will use it". Thanks to that pair, a single VPN gateway in the hub provides connectivity to every spoke, instead of paying for one per network. That is the pattern's main economic argument.
The hub-and-spoke pattern
graph TD
OFI["Barcelona and Palma offices<br/>10.100.0.0/16 and 10.101.0.0/16"] -->|site-to-site VPN| GW["VPN gateway<br/>(lesson 02-06)"]
GW --> HUB["vnet-contoso-hub-pro<br/>10.10.0.0/16<br/>Bastion, shared services"]
HUB -->|peering| PRO["vnet-contoso-pro<br/>10.20.0.0/16<br/>production"]
HUB -->|peering| DEV["vnet-contoso-dev<br/>10.30.0.0/16<br/>development"]
PRO -.->|"no direct peering:<br/>production and development<br/>do NOT see each other"| DEV
Because peering is not transitive, production and development do not see each other even though both see the hub. That is not a limitation: it is exactly the isolation property we want, and it comes for free.
- Service endpoints versus Azure Private Link
Here is the most important decision of the lesson, and the one that solves the problem we opened with: Contoso's database and storage account are reachable from the internet.
| Aspect | Service endpoint | Private endpoint (Private Link) |
|---|---|---|
| What it does | Extends your subnet's identity towards the service, which keeps its public IP | Creates a NIC with a private IP from your subnet for that service |
| Destination address | The service's public IP (even though the traffic does not go out to the internet) | A private IP from snet-datos |
| Is the service still exposed to the internet? | Yes, only restricted by the service's firewall | No, you can close public access completely |
| Access from the on-premises network over VPN | Does not work | Yes |
| Scope | The whole subnet towards the whole service | One specific resource (this account, this database) |
| DNS | Unchanged | Requires a private DNS zone to resolve to the private address |
| Cost | Free | Per hour and per data processed |
The practical difference that settles it: with a service endpoint, somebody holding the storage account key can still get in from anywhere the firewall authorizes; and the offices connected by VPN cannot use it. With a private endpoint, the resource has an IP inside your network, it can be closed off from the world entirely and the offices reach it over the VPN.
Contoso chooses private endpoints for the sql-contoso-reservas-pro database and for the sttarjetascontosopro account. The added cost is small compared with exposing passengers' data.
NET_GROUP="rg-contoso-red-pro"
VNET="vnet-contoso-pro"
# 1. Turn off network policies on the data subnet (a Private Link requirement).
az network vnet subnet update -g "${NET_GROUP}" --vnet-name "${VNET}" \
--name snet-datos --disable-private-endpoint-network-policies true --output none
# 2. Private endpoint towards the boarding pass storage account.
ACCOUNT_ID=$(az storage account show -g rg-contoso-reservas-pro \
-n sttarjetascontosopro --query id -o tsv)
az network private-endpoint create \
--resource-group "${NET_GROUP}" \
--name pe-storage-tarjetas \
--vnet-name "${VNET}" --subnet snet-datos \
--private-connection-resource-id "${ACCOUNT_ID}" \
--group-id blob \
--connection-name conexion-blob-tarjetas \
--output none
# 3. The service's private DNS zone: without this, the name still resolves to the public IP.
az network private-dns zone create -g "${NET_GROUP}" \
--name "privatelink.blob.core.windows.net" --output none
az network private-dns link vnet create -g "${NET_GROUP}" \
--zone-name "privatelink.blob.core.windows.net" \
--name enlace-blob-vnet-pro --virtual-network "${VNET}" \
--registration-enabled false --output none
# 4. Register the endpoint in the zone automatically.
az network private-endpoint dns-zone-group create \
--resource-group "${NET_GROUP}" \
--endpoint-name pe-storage-tarjetas \
--name grupo-zonas-blob \
--private-dns-zone "privatelink.blob.core.windows.net" \
--zone-name blob --output none
# 5. Close the account's public access: now it is only reachable from the network.
az storage account update -g rg-contoso-reservas-pro -n sttarjetascontosopro \
--public-network-access Disabled --output noneStep 3 is the one most people forget and the one that causes the classic failure: you create the private endpoint, close public access and the application stops working, because it still resolves the name to the public IP that is now closed. Without the linked private DNS zone, Private Link is worth nothing.
Verification from a VM on the network:
nslookup sttarjetascontosopro.blob.core.windows.net
# Expected: a CNAME to sttarjetascontosopro.privatelink.blob.core.windows.net
# and a 10.20.3.x (private) address, not a public IP.The private zones for the services Contoso will use: privatelink.blob.core.windows.net (Storage), privatelink.database.windows.net (SQL, module 3), privatelink.vaultcore.azure.net (Key Vault, 04-03) and privatelink.azurewebsites.net (App Service).
- App Service integration with the virtual network
With App Service you have to distinguish two directions, and confusing them is a source of lost hours.
| Need | Mechanism | What it does |
|---|---|---|
| Outbound: the application reaching private resources | Virtual network integration | Routes outbound traffic through a delegated subnet |
| Inbound: the application only being reachable from the network | Private endpoint or access restrictions | Takes the application off the internet |
# 1. A dedicated subnet delegated to App Service (the delegation is mandatory
# and the subnet cannot be shared with other resources).
az network vnet subnet create \
-g rg-contoso-red-pro --vnet-name vnet-contoso-pro \
--name snet-integracion-app --address-prefixes 10.20.5.0/24 \
--delegations Microsoft.Web/serverFarms --output none
# 2. Outbound integration for the bookings website.
az webapp vnet-integration add \
-g rg-contoso-reservas-pro -n app-contoso-reservas-pro \
--vnet vnet-contoso-pro --subnet snet-integracion-app --output none
# 3. Restricted inbound access for the internal API: from snet-web only.
az webapp config access-restriction add \
-g rg-contoso-reservas-pro -n app-contoso-api-disponibilidad-pro \
--rule-name permitir-solo-web --priority 100 \
--action Allow --vnet-name vnet-contoso-pro --subnet snet-web --output nonePractical details: outbound integration requires the Basic tier or above, the delegated subnet is for the exclusive use of that plan, and by default only traffic to private addresses is routed (to force all outbound traffic through the network you set WEBSITE_VNET_ROUTE_ALL=1, which is what Contoso does in production so that outbound traffic is auditable).
- Azure Bastion versus exposing SSH and RDP
In lesson 02-01 we opened port 22 to the internet with --nsg-rule SSH and warned that it was temporary. The time has come to do it properly.
| Aspect | SSH/RDP with a public IP | Azure Bastion |
|---|---|---|
| Ports exposed to the internet | 22 or 3389, attacked within minutes | None |
| A public IP on each VM | Required (and billed) | None needed at all |
| Access | An SSH or RDP client | A browser (HTTPS) or a native client over a tunnel |
| Auditing | The operating system's | Sessions logged at the platform level |
| Cost | Low | Per hour, from the moment it exists |
# Bastion needs AzureBastionSubnet (/26) and a Standard public IP.
az network public-ip create -g rg-contoso-red-pro -n ip-bastion-contoso-pro \
--sku Standard --allocation-method Static --output none
az network bastion create \
--resource-group rg-contoso-red-pro \
--name bastion-contoso-pro \
--vnet-name vnet-contoso-pro \
--public-ip-address ip-bastion-contoso-pro \
--location westeurope \
--sku Standard \
--output none
# SSH connection with no public IP on the VM and no port 22 open.
az network bastion ssh \
--name bastion-contoso-pro \
--resource-group rg-contoso-red-pro \
--target-resource-id "$(az vm show -g rg-contoso-reservas-pro -n vm-motor-disponibilidad-01 --query id -o tsv)" \
--auth-type ssh-key --username azureuser --ssh-key ~/.ssh/contoso_motorImportant cost warning: Bastion is billed by the hour as long as it exists, plus outbound traffic. In a lab, create it, use it and delete it the same day. In production, its cost compares favorably with managing public IPs and surviving an incident caused by exposed SSH.
A cheaper alternative for occasional administration: az ssh vm with Microsoft Entra ID and the corresponding extension, or a simple tunnel over a point-to-site VPN, which is exactly what Marta Ríos will use from home (lesson 02-06).
- Verification with Network Watcher
When something does not connect, guessing is expensive. Network Watcher tells you exactly which rule is blocking what.
# 1. IP flow verify: is this specific connection allowed or denied?
az network watcher test-ip-flow \
--resource-group rg-contoso-reservas-pro \
--vm vm-motor-disponibilidad-01 \
--direction Inbound --protocol TCP \
--local 10.20.4.10:22 --remote 10.20.1.5:60000 \
--output json
# The response includes "access": "Allow"/"Deny" and the exact rule responsible.
# 2. Effective security rules on a NIC (the sum of the subnet's and the NIC's NSGs).
az network nic list-effective-nsg \
--resource-group rg-contoso-reservas-pro \
--name nic-motor-disponibilidad-01 \
--output json
# 3. End-to-end connection troubleshooting.
az network watcher test-connectivity \
--resource-group rg-contoso-reservas-pro \
--source-resource vm-motor-disponibilidad-01 \
--dest-address sttarjetascontosopro.privatelink.blob.core.windows.net \
--dest-port 443 \
--output tabletest-ip-flow is the tool to use before touching any rule: it gives you the name of the rule that decides, so you stop changing things at random. And NSG flow logs, which send everything allowed and denied to Log Analytics, are the basis of the analysis you will see in lesson 07-02.
- The complete topology and cleanup
graph TB
NET["Internet"] -->|443| WEB
subgraph VNET["vnet-contoso-pro · 10.20.0.0/16"]
WEB["snet-web · 10.20.1.0/24<br/>nsg-snet-web: inbound 443 only"]
APP["snet-app · 10.20.2.0/24<br/>nsg-snet-app: from snet-web only"]
DAT["snet-datos · 10.20.3.0/24<br/>private endpoints"]
GES["snet-gestion · 10.20.4.0/24<br/>legacy engine, no public IP"]
INT["snet-integracion-app · 10.20.5.0/24<br/>delegated to App Service"]
BAS["AzureBastionSubnet · 10.20.250.0/26"]
GWS["GatewaySubnet · 10.20.255.0/27<br/>(lesson 02-06)"]
end
WEB --> APP
APP --> DAT
INT --> DAT
DAT -.->|Private Link| SQL["sql-contoso-reservas-pro"]
DAT -.->|Private Link| ST["sttarjetascontosopro"]
BAS --> GES
HUB["vnet-contoso-hub-pro · 10.10.0.0/16"] <-->|peering| VNET
Cleaning up what bills:
# 1. Bastion: the most expensive thing in this lesson. Delete it as soon as you finish.
az network bastion delete -g rg-contoso-red-pro -n bastion-contoso-pro
az network public-ip delete -g rg-contoso-red-pro -n ip-bastion-contoso-pro
# 2. Private endpoints (billed by the hour).
az network private-endpoint delete -g rg-contoso-red-pro -n pe-storage-tarjetas
# 3. VNets, subnets and NSGs cost nothing: they can stay.
# If you still want to delete everything in a lab:
# az group delete --name rg-contoso-red-pro --yes --no-waitA residual-spend check: az network public-ip list --query "[?ipConfiguration==null]" -o table and az network private-endpoint list -o table.
Common Mistakes and Tips
- Using
10.0.0.0/16because it is the tutorial's example. It is the range half of corporate internet uses; the day you build the office VPN, it will overlap. Plan the addressing before creating anything. - Sizing subnets right to the limit. Azure reserves 5 addresses per subnet and growing one with resources inside is problematic. Leave headroom.
- Forgetting the mandatory names.
GatewaySubnetandAzureBastionSubnetare written exactly like that; with any other name, the service does not deploy. - Assuming that subnets are isolated by default. The
AllowVnetInBoundrule allows all internal traffic. Segmentation has to be written. - Getting the priority order wrong. Evaluation goes from lowest to highest and the first match wins. A
Denyrule with priority 100 overrides anAllowwith priority 200. - Writing IP ranges where a service tag would do. The IPs of Azure services change; the tags update themselves.
- Expecting peering to be transitive. It is not. In hub-and-spoke, the spokes do not see each other (and normally that is what you want).
- Creating the peering in one direction only. It stays in the Initiated state and does not work until the reverse link exists.
- Creating a private endpoint without its private DNS zone. The name still resolves to the public IP and, once you close public access, the application stops working. It is Private Link's number one failure.
- Leaving Azure Bastion running in a lab. It is billed by the hour even if you do not use it.
- Debugging connectivity by trying rules out.
az network watcher test-ip-flowgives you the guilty rule in one command. - Tip: document the addressing plan in a file in the repository, alongside the scripts. The network is the part of the architecture the most people need to consult and the fewest people remember.
- Tip: apply NSGs to subnets, not to individual NICs, except in justified cases. Per-NIC rules get forgotten and create invisible holes.
Exercises
Exercise 1: planning the addressing
Contoso is opening a third office in Seville and also wants a virtual network for load testing, isolated from production.
- Assign address spaces to the Seville office and to the new
vnet-contoso-pruebasnetwork, consistent with the existing plan and with no overlaps. - Divide
vnet-contoso-pruebasinto three/24subnets with names consistent with the course's naming convention. - How many usable addresses does each
/24subnet have? And a/27? - Explain why you cannot use
10.20.0.0/16for the test network.
Exercise 2: segmenting with NSGs
Write the NSG rules (with priorities) that implement exactly this policy for vnet-contoso-pro:
snet-webaccepts 443 from the internet and nothing else.snet-appaccepts 8080 fromsnet-webonly; no other internal traffic.snet-datosaccepts 1433 fromsnet-apponly.snet-gestionaccepts nothing from the internet; administration from Bastion only.snet-appcan go out to Azure SQL in West Europe, but not to the rest of the internet.
Exercise 3: making the database private
Describe, with the corresponding commands, the steps to make sql-contoso-reservas-pro unreachable from the internet and reachable only from snet-app:
- Prepare the subnet.
- Create the private endpoint.
- Configure DNS resolution.
- Close public access.
- Verify that resolution returns a private IP.
Explain as well what would happen if you skipped step 3.
Solutions
Solution 1:
- Addressing consistent with the existing plan:
| Network | Space | Reason |
|---|---|---|
| Seville office | 10.102.0.0/16 |
It follows the office series (Barcelona 10.100, Palma 10.101) |
vnet-contoso-pruebas |
10.40.0.0/16 |
It follows the Azure network series (hub 10.10, pro 10.20, dev 10.30) |
- Subnets:
az network vnet create -g rg-contoso-red-pro -n vnet-contoso-pruebas \
--address-prefixes 10.40.0.0/16 --location westeurope --output none
az network vnet subnet create -g rg-contoso-red-pro --vnet-name vnet-contoso-pruebas \
-n snet-web --address-prefixes 10.40.1.0/24 --output none
az network vnet subnet create -g rg-contoso-red-pro --vnet-name vnet-contoso-pruebas \
-n snet-app --address-prefixes 10.40.2.0/24 --output none
az network vnet subnet create -g rg-contoso-red-pro --vnet-name vnet-contoso-pruebas \
-n snet-datos --address-prefixes 10.40.3.0/24 --output none-
A
/24has 256 addresses, minus the 5 Azure reserves: 251 usable. A/27has 32, minus 5: 27 usable. -
Because
10.20.0.0/16is already the space ofvnet-contoso-pro. Two networks with overlapping ranges cannot be peered or connected by VPN, and the routing would be ambiguous. Even if you were not going to peer them today, reusing the range closes that door forever.
Solution 2:
G="rg-contoso-red-pro"
# 1. snet-web: 443 from the internet, nothing else (the denial comes from the default 65500 rule).
az network nsg rule create -g $G --nsg-name nsg-snet-web -n permitir-https \
--priority 100 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes Internet --destination-port-ranges 443 --output none
# 2. snet-app: 8080 from snet-web only; the rest of the internal traffic, denied.
az network nsg rule create -g $G --nsg-name nsg-snet-app -n permitir-web \
--priority 100 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes 10.20.1.0/24 --destination-port-ranges 8080 --output none
az network nsg rule create -g $G --nsg-name nsg-snet-app -n denegar-resto-vnet \
--priority 4000 --direction Inbound --access Deny --protocol '*' \
--source-address-prefixes VirtualNetwork --destination-port-ranges '*' --output none
# 3. snet-datos: 1433 from snet-app only.
az network nsg rule create -g $G --nsg-name nsg-snet-datos -n permitir-app-sql \
--priority 100 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes 10.20.2.0/24 --destination-port-ranges 1433 --output none
az network nsg rule create -g $G --nsg-name nsg-snet-datos -n denegar-resto-vnet \
--priority 4000 --direction Inbound --access Deny --protocol '*' \
--source-address-prefixes VirtualNetwork --destination-port-ranges '*' --output none
# 4. snet-gestion: nothing from the internet; SSH and RDP from the Bastion subnet only.
az network nsg rule create -g $G --nsg-name nsg-snet-gestion -n permitir-bastion \
--priority 100 --direction Inbound --access Allow --protocol Tcp \
--source-address-prefixes 10.20.250.0/26 --destination-port-ranges 22 3389 --output none
az network nsg rule create -g $G --nsg-name nsg-snet-gestion -n denegar-internet \
--priority 200 --direction Inbound --access Deny --protocol '*' \
--source-address-prefixes Internet --destination-port-ranges '*' --output none
# 5. snet-app outbound: only Azure SQL in the region; the rest of the internet, denied.
az network nsg rule create -g $G --nsg-name nsg-snet-app -n permitir-salida-sql \
--priority 200 --direction Outbound --access Allow --protocol Tcp \
--destination-address-prefixes Sql.WestEurope --destination-port-ranges 1433 --output none
az network nsg rule create -g $G --nsg-name nsg-snet-app -n denegar-salida-internet \
--priority 4000 --direction Outbound --access Deny --protocol '*' \
--destination-address-prefixes Internet --destination-port-ranges '*' --output noneA detail on point 5: the outbound rule to SQL must have a lower priority number (200) than the general denial (4000), or the legitimate traffic would be blocked.
Solution 3:
G="rg-contoso-red-pro"; V="vnet-contoso-pro"
# 1. Prepare the subnet that will host the endpoint.
az network vnet subnet update -g $G --vnet-name $V -n snet-datos \
--disable-private-endpoint-network-policies true --output none
# 2. Private endpoint towards the SQL server (group-id sqlServer).
SQL_ID=$(az sql server show -g rg-contoso-reservas-pro -n sql-contoso-reservas-pro --query id -o tsv)
az network private-endpoint create -g $G -n pe-sql-reservas \
--vnet-name $V --subnet snet-datos \
--private-connection-resource-id "${SQL_ID}" --group-id sqlServer \
--connection-name conexion-sql-reservas --output none
# 3. The service's private DNS zone, linked to the network and tied to the endpoint.
az network private-dns zone create -g $G -n "privatelink.database.windows.net" --output none
az network private-dns link vnet create -g $G \
--zone-name "privatelink.database.windows.net" -n enlace-sql-vnet-pro \
--virtual-network $V --registration-enabled false --output none
az network private-endpoint dns-zone-group create -g $G \
--endpoint-name pe-sql-reservas -n grupo-zonas-sql \
--private-dns-zone "privatelink.database.windows.net" --zone-name sql --output none
# 4. Close the server's public access.
az sql server update -g rg-contoso-reservas-pro -n sql-contoso-reservas-pro \
--enable-public-network false --output none
# 5. Verify from a VM on the network.
nslookup sql-contoso-reservas-pro.database.windows.net
# Expected: a CNAME to ...privatelink.database.windows.net and a 10.20.3.x IPIf you skip step 3, the private endpoint exists and has its IP, but the name sql-contoso-reservas-pro.database.windows.net still resolves to the public IP. As soon as you run step 4, the application will try to connect to that now-closed public IP and will fail with a connection timeout, with no clue at all pointing at DNS. It is Private Link's most frequent and most baffling failure.
Conclusion
You now have the platform's third leg. You know why the network comes first and which decisions get frozen for years. You can handle private address spaces, enough CIDR arithmetic to carve a /16 into subnets with no overlaps, and you know the five addresses Azure reserves in every subnet. You have designed and deployed vnet-contoso-pro with snet-web, snet-app, snet-datos, snet-gestion and the mandatorily named AzureBastionSubnet and GatewaySubnet subnets. You write NSG rules understanding priorities and the default rules — including the most dangerous one, AllowVnetInBound, which means segmentation is not automatic — and you use service tags and application security groups so that rules do not depend on IP lists. You can tell public from private IPs and static from dynamic, with their effect on the bill. You know how Azure's DNS resolution works and what private DNS zones are for. You understand peering and its lack of transitivity, which is exactly what shapes the hub-and-spoke pattern and lets you pay for a single VPN gateway. And you have made the decision that most protects passengers' data: private endpoints with their DNS zone for the database and the storage account, rather than service endpoints. On top of that, you have connected App Service to the network, you have replaced exposed SSH with Azure Bastion and you know how to diagnose with Network Watcher instead of guessing.
One piece is left to close the module, and it is the one that connects Azure with Contoso Airlines' real world. The Barcelona and Palma offices are still outside this network: their staff cannot reach the private resources you have just locked down, the legacy check-in system still lives on premises, and Marta Ríos cannot administer anything from home without exposing something. At the same time, a customer buying a ticket from South America suffers the latency of crossing the Atlantic on every request to West Europe.
In the module's last lesson, Hybrid Connectivity and Global Delivery, we solve both ends: a point-to-site VPN for Marta and a site-to-site one for the offices, with their gateways and their CLI deployment; ExpressRoute and when it justifies its price; Azure Virtual WAN as the evolution of hybrid hub-and-spoke; and global delivery of the site with Azure Front Door, Azure CDN and Traffic Manager, including the data transfer out charges that surprise everyone the first time they read a bill.
Azure Course
Module 1: Introduction to Azure
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
