Skip to content

Project 7: Private Endpoint Implementation ​

Overview ​

Secure Azure PaaS services using Private Endpoints and Private Link. This project covers connecting to Storage, SQL, and Key Vault privately, eliminating public internet exposure.

Difficulty: Intermediate
Duration: 3-4 hours
Cost: ~$30-50/month (Private endpoints, DNS zones)

Architecture Diagram ​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              INTERNET                                            β”‚
β”‚                                  ❌                                              β”‚
β”‚                           (Public Access Blocked)                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         VNet: vnet-privatelink (10.0.0.0/16)                     β”‚
β”‚                                                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                 Subnet: snet-workloads (10.0.1.0/24)                       β”‚  β”‚
β”‚  β”‚                                                                            β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚  β”‚
β”‚  β”‚  β”‚  vm-client-01    β”‚  β”‚  vm-client-02    β”‚  β”‚  vm-client-03            β”‚ β”‚  β”‚
β”‚  β”‚  β”‚  Windows Server  β”‚  β”‚  Ubuntu Linux    β”‚  β”‚  App Server              β”‚ β”‚  β”‚
β”‚  β”‚  β”‚                  β”‚  β”‚                  β”‚  β”‚                          β”‚ β”‚  β”‚
β”‚  β”‚  β”‚  Access PaaS     β”‚  β”‚  Access PaaS     β”‚  β”‚  Access PaaS             β”‚ β”‚  β”‚
β”‚  β”‚  β”‚  via Private IPs β”‚  β”‚  via Private IPs β”‚  β”‚  via Private IPs         β”‚ β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚  β”‚
β”‚  β”‚           β”‚                     β”‚                          β”‚              β”‚  β”‚
β”‚  β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚  β”‚
β”‚  β”‚                                 β”‚                                          β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                    β”‚                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚             Subnet: snet-privateendpoints (10.0.2.0/24)                    β”‚  β”‚
β”‚  β”‚                                 β”‚                                          β”‚  β”‚
β”‚  β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚  β”‚
β”‚  β”‚     β”‚                           β”‚                               β”‚          β”‚  β”‚
β”‚  β”‚     β–Ό                           β–Ό                               β–Ό          β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚  β”‚
β”‚  β”‚  β”‚  pe-storage      β”‚  β”‚  pe-sql          β”‚  β”‚  pe-keyvault             β”‚ β”‚  β”‚
β”‚  β”‚  β”‚  Private         β”‚  β”‚  Private         β”‚  β”‚  Private                 β”‚ β”‚  β”‚
β”‚  β”‚  β”‚  Endpoint        β”‚  β”‚  Endpoint        β”‚  β”‚  Endpoint                β”‚ β”‚  β”‚
β”‚  β”‚  β”‚                  β”‚  β”‚                  β”‚  β”‚                          β”‚ β”‚  β”‚
β”‚  β”‚  β”‚  IP: 10.0.2.4    β”‚  β”‚  IP: 10.0.2.5    β”‚  β”‚  IP: 10.0.2.6            β”‚ β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚  β”‚
β”‚  β”‚           β”‚                     β”‚                          β”‚              β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚              β”‚                     β”‚                          β”‚                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚           β”‚    Private DNS Zonesβ”‚                          β”‚              β”‚  β”‚
β”‚  β”‚           β”‚                     β”‚                          β”‚              β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚  β”‚
β”‚  β”‚  β”‚privatelink.      β”‚  β”‚privatelink.      β”‚  β”‚privatelink.             β”‚ β”‚  β”‚
β”‚  β”‚  β”‚blob.core.        β”‚  β”‚database.windows  β”‚  β”‚vaultcore.azure.net      β”‚ β”‚  β”‚
β”‚  β”‚  β”‚windows.net       β”‚  β”‚.net              β”‚  β”‚                         β”‚ β”‚  β”‚
β”‚  β”‚  β”‚                  β”‚  β”‚                  β”‚  β”‚                         β”‚ β”‚  β”‚
β”‚  β”‚  β”‚A: st*.blob...    β”‚  β”‚A: sql-*.database β”‚  β”‚A: kv-*.vault...         β”‚ β”‚  β”‚
β”‚  β”‚  β”‚   β†’ 10.0.2.4     β”‚  β”‚   β†’ 10.0.2.5     β”‚  β”‚   β†’ 10.0.2.6            β”‚ β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
                                    β”‚ Private Link Connection
                                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                            AZURE PAAS SERVICES                                   β”‚
β”‚                         (Public Endpoints Disabled)                              β”‚
β”‚                                                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  Azure Storage   β”‚  β”‚  Azure SQL       β”‚  β”‚  Azure Key Vault             β”‚   β”‚
β”‚  β”‚  Account         β”‚  β”‚  Database        β”‚  β”‚                              β”‚   β”‚
β”‚  β”‚                  β”‚  β”‚                  β”‚  β”‚                              β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚   β”‚
β”‚  β”‚  β”‚ Blob       β”‚  β”‚  β”‚  β”‚ SQL Server β”‚  β”‚  β”‚  β”‚ Secrets               β”‚  β”‚   β”‚
β”‚  β”‚  β”‚ Container  β”‚  β”‚  β”‚  β”‚ Databases  β”‚  β”‚  β”‚  β”‚ Keys                  β”‚  β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚  β”‚ Certificates          β”‚  β”‚   β”‚
β”‚  β”‚                  β”‚  β”‚                  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚   β”‚
β”‚  β”‚  Public: ❌      β”‚  β”‚  Public: ❌      β”‚  β”‚  Public: ❌                  β”‚   β”‚
β”‚  β”‚  Private: βœ…     β”‚  β”‚  Private: βœ…     β”‚  β”‚  Private: βœ…                 β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

DNS Resolution Flow:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Client VM   │───▢│  Azure DNS       │───▢│  Private DNS     β”‚
β”‚  Requests    β”‚    β”‚  (168.63.129.16) β”‚    β”‚  Zone Lookup     β”‚
β”‚  blob.core.. β”‚    β”‚                  β”‚    β”‚  Returns 10.0.2.4β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What You'll Learn ​

  • Create Private Endpoints for PaaS services
  • Configure Private DNS Zones
  • Link Private DNS to VNet
  • Disable public access on PaaS services
  • Test private connectivity
  • Implement service endpoints vs private endpoints

Prerequisites ​

  • Azure subscription
  • Azure CLI installed
  • Basic networking knowledge

Phase 1: Create Network Infrastructure ​

Step 1.1: Create Resource Group and VNet ​

bash
# Set variables
LOCATION="eastus"
RG_NAME="rg-privatelink-lab-eastus"

# Create resource group
az group create \
  --name $RG_NAME \
  --location $LOCATION \
  --tags Project=PrivateEndpoint Environment=Lab

# Create VNet
az network vnet create \
  --resource-group $RG_NAME \
  --name vnet-privatelink \
  --address-prefix 10.0.0.0/16 \
  --location $LOCATION

echo "VNet created"

Step 1.2: Create Subnets ​

bash
# Create workload subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-privatelink \
  --name snet-workloads \
  --address-prefix 10.0.1.0/24

# Create private endpoint subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-privatelink \
  --name snet-privateendpoints \
  --address-prefix 10.0.2.0/24

# Create Azure Bastion subnet
az network vnet subnet create \
  --resource-group $RG_NAME \
  --vnet-name vnet-privatelink \
  --name AzureBastionSubnet \
  --address-prefix 10.0.3.0/27

# Disable private endpoint network policies on PE subnet
az network vnet subnet update \
  --resource-group $RG_NAME \
  --vnet-name vnet-privatelink \
  --name snet-privateendpoints \
  --disable-private-endpoint-network-policies true

echo "Subnets created"

Phase 2: Create Azure PaaS Services ​

Step 2.1: Create Storage Account ​

bash
# Generate unique name
STORAGE_NAME="stprivate$(date +%s | tail -c 8)"

# Create storage account
az storage account create \
  --resource-group $RG_NAME \
  --name $STORAGE_NAME \
  --location $LOCATION \
  --sku Standard_LRS \
  --kind StorageV2 \
  --min-tls-version TLS1_2

# Create a blob container
az storage container create \
  --name "documents" \
  --account-name $STORAGE_NAME

# Upload test file
echo "Hello from private endpoint!" > test.txt
az storage blob upload \
  --account-name $STORAGE_NAME \
  --container-name "documents" \
  --name "test.txt" \
  --file "test.txt"

echo "Storage account created: $STORAGE_NAME"

Step 2.2: Create Azure SQL Database ​

bash
# Generate unique name
SQL_SERVER_NAME="sql-private-$(date +%s | tail -c 8)"
SQL_ADMIN="sqladmin"
SQL_PASSWORD="P@ssw0rd123!Complex"

# Create SQL Server
az sql server create \
  --resource-group $RG_NAME \
  --name $SQL_SERVER_NAME \
  --location $LOCATION \
  --admin-user $SQL_ADMIN \
  --admin-password $SQL_PASSWORD

# Create SQL Database
az sql db create \
  --resource-group $RG_NAME \
  --server $SQL_SERVER_NAME \
  --name "testdb" \
  --edition Basic \
  --capacity 5

echo "SQL Server created: $SQL_SERVER_NAME"

Step 2.3: Create Key Vault ​

bash
# Generate unique name
KV_NAME="kv-private-$(date +%s | tail -c 8)"

# Create Key Vault
az keyvault create \
  --resource-group $RG_NAME \
  --name $KV_NAME \
  --location $LOCATION \
  --sku standard \
  --enable-rbac-authorization true

# Add a secret
az keyvault secret set \
  --vault-name $KV_NAME \
  --name "DatabasePassword" \
  --value "$SQL_PASSWORD"

echo "Key Vault created: $KV_NAME"

Phase 3: Create Private Endpoints ​

Step 3.1: Get Resource IDs ​

bash
# Get Storage Account ID
STORAGE_ID=$(az storage account show \
  --resource-group $RG_NAME \
  --name $STORAGE_NAME \
  --query id -o tsv)

# Get SQL Server ID
SQL_ID=$(az sql server show \
  --resource-group $RG_NAME \
  --name $SQL_SERVER_NAME \
  --query id -o tsv)

# Get Key Vault ID
KV_ID=$(az keyvault show \
  --resource-group $RG_NAME \
  --name $KV_NAME \
  --query id -o tsv)

echo "Resource IDs retrieved"

Step 3.2: Create Private Endpoint for Storage ​

bash
# Create private endpoint for blob storage
az network private-endpoint create \
  --resource-group $RG_NAME \
  --name pe-storage-blob \
  --vnet-name vnet-privatelink \
  --subnet snet-privateendpoints \
  --private-connection-resource-id $STORAGE_ID \
  --group-id blob \
  --connection-name "StorageBlobConnection" \
  --location $LOCATION

# Get private endpoint IP
STORAGE_PE_IP=$(az network private-endpoint show \
  --resource-group $RG_NAME \
  --name pe-storage-blob \
  --query "customDnsConfigs[0].ipAddresses[0]" -o tsv)

echo "Storage Private Endpoint created with IP: $STORAGE_PE_IP"

Step 3.3: Create Private Endpoint for SQL ​

bash
# Create private endpoint for SQL
az network private-endpoint create \
  --resource-group $RG_NAME \
  --name pe-sql \
  --vnet-name vnet-privatelink \
  --subnet snet-privateendpoints \
  --private-connection-resource-id $SQL_ID \
  --group-id sqlServer \
  --connection-name "SQLServerConnection" \
  --location $LOCATION

# Get private endpoint IP
SQL_PE_IP=$(az network private-endpoint show \
  --resource-group $RG_NAME \
  --name pe-sql \
  --query "customDnsConfigs[0].ipAddresses[0]" -o tsv)

echo "SQL Private Endpoint created with IP: $SQL_PE_IP"

Step 3.4: Create Private Endpoint for Key Vault ​

bash
# Create private endpoint for Key Vault
az network private-endpoint create \
  --resource-group $RG_NAME \
  --name pe-keyvault \
  --vnet-name vnet-privatelink \
  --subnet snet-privateendpoints \
  --private-connection-resource-id $KV_ID \
  --group-id vault \
  --connection-name "KeyVaultConnection" \
  --location $LOCATION

# Get private endpoint IP
KV_PE_IP=$(az network private-endpoint show \
  --resource-group $RG_NAME \
  --name pe-keyvault \
  --query "customDnsConfigs[0].ipAddresses[0]" -o tsv)

echo "Key Vault Private Endpoint created with IP: $KV_PE_IP"

Phase 4: Create Private DNS Zones ​

Step 4.1: Create DNS Zones ​

bash
# Create Private DNS Zone for Blob Storage
az network private-dns zone create \
  --resource-group $RG_NAME \
  --name "privatelink.blob.core.windows.net"

# Create Private DNS Zone for SQL
az network private-dns zone create \
  --resource-group $RG_NAME \
  --name "privatelink.database.windows.net"

# Create Private DNS Zone for Key Vault
az network private-dns zone create \
  --resource-group $RG_NAME \
  --name "privatelink.vaultcore.azure.net"

echo "Private DNS zones created"
bash
# Link Blob DNS Zone to VNet
az network private-dns link vnet create \
  --resource-group $RG_NAME \
  --zone-name "privatelink.blob.core.windows.net" \
  --name "blob-vnet-link" \
  --virtual-network vnet-privatelink \
  --registration-enabled false

# Link SQL DNS Zone to VNet
az network private-dns link vnet create \
  --resource-group $RG_NAME \
  --zone-name "privatelink.database.windows.net" \
  --name "sql-vnet-link" \
  --virtual-network vnet-privatelink \
  --registration-enabled false

# Link Key Vault DNS Zone to VNet
az network private-dns link vnet create \
  --resource-group $RG_NAME \
  --zone-name "privatelink.vaultcore.azure.net" \
  --name "kv-vnet-link" \
  --virtual-network vnet-privatelink \
  --registration-enabled false

echo "DNS zones linked to VNet"

Step 4.3: Create DNS Records ​

bash
# Create A record for Storage
az network private-dns record-set a create \
  --resource-group $RG_NAME \
  --zone-name "privatelink.blob.core.windows.net" \
  --name $STORAGE_NAME

az network private-dns record-set a add-record \
  --resource-group $RG_NAME \
  --zone-name "privatelink.blob.core.windows.net" \
  --record-set-name $STORAGE_NAME \
  --ipv4-address $STORAGE_PE_IP

# Create A record for SQL
az network private-dns record-set a create \
  --resource-group $RG_NAME \
  --zone-name "privatelink.database.windows.net" \
  --name $SQL_SERVER_NAME

az network private-dns record-set a add-record \
  --resource-group $RG_NAME \
  --zone-name "privatelink.database.windows.net" \
  --record-set-name $SQL_SERVER_NAME \
  --ipv4-address $SQL_PE_IP

# Create A record for Key Vault
az network private-dns record-set a create \
  --resource-group $RG_NAME \
  --zone-name "privatelink.vaultcore.azure.net" \
  --name $KV_NAME

az network private-dns record-set a add-record \
  --resource-group $RG_NAME \
  --zone-name "privatelink.vaultcore.azure.net" \
  --record-set-name $KV_NAME \
  --ipv4-address $KV_PE_IP

echo "DNS records created"

Phase 5: Disable Public Access ​

Step 5.1: Disable Public Access on Storage ​

bash
# Disable public blob access
az storage account update \
  --resource-group $RG_NAME \
  --name $STORAGE_NAME \
  --allow-blob-public-access false

# Configure network rules - deny all public access
az storage account update \
  --resource-group $RG_NAME \
  --name $STORAGE_NAME \
  --default-action Deny

# Allow Azure services (for management)
az storage account update \
  --resource-group $RG_NAME \
  --name $STORAGE_NAME \
  --bypass AzureServices

echo "Storage public access disabled"

Step 5.2: Disable Public Access on SQL ​

bash
# Deny public network access
az sql server update \
  --resource-group $RG_NAME \
  --name $SQL_SERVER_NAME \
  --enable-public-network false

echo "SQL public access disabled"

Step 5.3: Disable Public Access on Key Vault ​

bash
# Disable public network access
az keyvault update \
  --resource-group $RG_NAME \
  --name $KV_NAME \
  --public-network-access Disabled

echo "Key Vault public access disabled"

Phase 6: Deploy Test VM and Azure Bastion ​

Step 6.1: Create Azure Bastion ​

bash
# Create public IP for Bastion
az network public-ip create \
  --resource-group $RG_NAME \
  --name pip-bastion \
  --sku Standard \
  --allocation-method Static

# Create Azure Bastion
az network bastion create \
  --resource-group $RG_NAME \
  --name bastion-privatelink \
  --public-ip-address pip-bastion \
  --vnet-name vnet-privatelink \
  --sku Basic \
  --location $LOCATION

echo "Azure Bastion deployed"

Step 6.2: Create Test VM ​

bash
ADMIN_USER="azureadmin"
ADMIN_PASSWORD="P@ssw0rd123!Complex"

# Create Windows VM for testing
az vm create \
  --resource-group $RG_NAME \
  --name vm-client-01 \
  --vnet-name vnet-privatelink \
  --subnet snet-workloads \
  --image Win2022Datacenter \
  --size Standard_D2s_v3 \
  --admin-username $ADMIN_USER \
  --admin-password $ADMIN_PASSWORD \
  --public-ip-address "" \
  --nsg ""

echo "Test VM created"

Phase 7: Testing Private Connectivity ​

Step 7.1: Connect via Bastion ​

  1. Navigate to Azure Portal β†’ vm-client-01
  2. Click "Connect" β†’ "Bastion"
  3. Enter credentials and connect

Step 7.2: Test DNS Resolution ​

From the VM, open PowerShell:

powershell
# Test DNS resolution for Storage
nslookup $env:STORAGE_NAME.blob.core.windows.net
# Should return private IP (10.0.2.x)

# Test DNS resolution for SQL
nslookup $env:SQL_SERVER_NAME.database.windows.net
# Should return private IP (10.0.2.x)

# Test DNS resolution for Key Vault
nslookup $env:KV_NAME.vault.azure.net
# Should return private IP (10.0.2.x)

Step 7.3: Test Storage Access ​

powershell
# Install Azure PowerShell module if needed
Install-Module -Name Az -Force -AllowClobber

# Connect to Azure
Connect-AzAccount

# Test storage access
$ctx = New-AzStorageContext -StorageAccountName "$env:STORAGE_NAME" -UseConnectedAccount
Get-AzStorageBlob -Container "documents" -Context $ctx

# Download test file
Get-AzStorageBlobContent -Container "documents" -Blob "test.txt" -Destination "C:\test.txt" -Context $ctx
Get-Content "C:\test.txt"

Step 7.4: Test SQL Access ​

powershell
# Install SQL Server module
Install-Module -Name SqlServer -Force

# Test SQL connection
$connString = "Server=$env:SQL_SERVER_NAME.database.windows.net;Database=testdb;User Id=sqladmin;Password=P@ssw0rd123!Complex;"
$conn = New-Object System.Data.SqlClient.SqlConnection($connString)

try {
    $conn.Open()
    Write-Host "SQL connection successful via private endpoint!" -ForegroundColor Green
    
    # Run a test query
    $cmd = $conn.CreateCommand()
    $cmd.CommandText = "SELECT @@VERSION"
    $result = $cmd.ExecuteScalar()
    Write-Host $result
}
catch {
    Write-Host "Connection failed: $_" -ForegroundColor Red
}
finally {
    $conn.Close()
}

Step 7.5: Test Key Vault Access ​

powershell
# Test Key Vault access
$secret = Get-AzKeyVaultSecret -VaultName "$env:KV_NAME" -Name "DatabasePassword" -AsPlainText
Write-Host "Secret retrieved: $secret"

Step 7.6: Verify Public Access is Blocked ​

From your local machine (outside Azure VNet):

bash
# Try to access storage (should fail)
curl https://$STORAGE_NAME.blob.core.windows.net/documents/test.txt
# Error: Public access is not permitted

# Try to access SQL (should timeout/fail)
# Connection attempt will fail

# Try to access Key Vault (should fail)
az keyvault secret show --vault-name $KV_NAME --name "DatabasePassword"
# Error: Public network access is disabled

Phase 8: Monitor Private Endpoints ​

Step 8.1: View Private Endpoint Connections ​

bash
# List all private endpoints
az network private-endpoint list \
  --resource-group $RG_NAME \
  --output table

# Show connection status
az network private-endpoint show \
  --resource-group $RG_NAME \
  --name pe-storage-blob \
  --query "privateLinkServiceConnections[0].privateLinkServiceConnectionState.status" -o tsv

Step 8.2: View DNS Records ​

bash
# List DNS records in zone
az network private-dns record-set a list \
  --resource-group $RG_NAME \
  --zone-name "privatelink.blob.core.windows.net" \
  --output table

Summary ​

ServicePrivate EndpointDNS ZonePublic Access
Storagepe-storage-blobprivatelink.blob.core.windows.netDisabled
SQLpe-sqlprivatelink.database.windows.netDisabled
Key Vaultpe-keyvaultprivatelink.vaultcore.azure.netDisabled

Cleanup ​

bash
# Delete resource group (includes all resources)
az group delete --name $RG_NAME --yes --no-wait

echo "Cleanup initiated"

Key Takeaways ​

  1. Private Endpoints: Bring PaaS services into your VNet
  2. Private DNS Zones: Enable name resolution to private IPs
  3. Disable Public Access: Block internet access after PE is configured
  4. DNS Integration: Link private DNS zones to VNet for automatic resolution
  5. Security: No data traverses the public internet

Service Endpoints vs Private Endpoints ​

FeatureService EndpointsPrivate Endpoints
IP AddressUses public IPUses private IP
DNSNo change neededRequires private DNS
CostFree~$7.50/endpoint/month
Use CaseBasic network isolationFull private connectivity
Cross-regionSame region onlyCross-region supported
On-premisesNot accessibleAccessible via VPN/ER

Next Steps ​

  • Implement Private Endpoints for more services (Cosmos DB, Event Hub)
  • Configure Private Link Service for custom services
  • Set up Azure Private Resolver for hybrid DNS
  • Implement Azure Firewall with Private Endpoints

Released under the MIT License.