Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
95bb7f9
feat(operate): App Service SKU selection, custom domains, networking …
paulyuk Apr 1, 2026
cd8f38b
Update plugin/skills/azure-prepare/references/services/app-service/ne…
apwestgarth Apr 21, 2026
5a6920a
Update plugin/skills/azure-prepare/references/services/app-service/ne…
apwestgarth Apr 22, 2026
5222654
docs(azure-prepare): align App Service VNet routing property usage
Copilot Apr 22, 2026
a63a0ff
docs(azure-prepare): clarify pricing basis in app service sku guide
Copilot Apr 23, 2026
dcbcc0a
Apply suggestions from code review
apwestgarth Apr 27, 2026
0dc1aed
docs(azure-prepare): capture SSL cert thumbprint in CLI flow
Copilot Apr 27, 2026
6f19797
docs(azure-prepare): add Bicep TLS binding follow-up step
Copilot Apr 27, 2026
67551e6
Update networking.md
apwestgarth Apr 28, 2026
0ddea3a
Apply suggestions from code review
apwestgarth Apr 28, 2026
f837c78
Update plugin/skills/azure-prepare/references/services/app-service/ne…
apwestgarth Apr 28, 2026
92bf0a2
Apply suggestions from code review
apwestgarth Apr 29, 2026
ae870a4
Updated decision tree
apwestgarth Apr 29, 2026
eff24b6
Update skill references for App Service
apwestgarth Apr 29, 2026
1c007db
Added mention of Hybrid Connections support at Basic tier level
apwestgarth Apr 29, 2026
14b7464
Fixed duplication of title in Decision tree
apwestgarth Apr 29, 2026
47eaed1
Fixed duplication of title in Decision tree
apwestgarth Apr 29, 2026
9999dda
Update plugin/skills/azure-prepare/references/services/app-service/ne…
apwestgarth Apr 29, 2026
c1d32e5
docs(azure-prepare): align app service decision guidance with SKU matrix
Copilot Apr 29, 2026
b0dced3
Apply suggestion from @apwestgarth
apwestgarth Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ Endpoint should return 200 OK when healthy.
- [Bicep Patterns](bicep.md)
- [Deployment Slots](deployment-slots.md)
- [Auto-Scaling](scaling.md)
- [Networking](networking.md)
- [SKU Selection](sku-selection.md)
- [Custom Domains](custom-domains.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# App Service Custom Domains and Managed TLS

## Prerequisites
Comment thread
apwestgarth marked this conversation as resolved.
Comment thread
apwestgarth marked this conversation as resolved.

| Requirement | Details |
|------------|---------|
| SKU tier | Basic (B1) or higher |
| DNS access | Ability to create CNAME, A, and TXT records |
| Domain ownership | Verified via TXT record |

Comment thread
apwestgarth marked this conversation as resolved.
## DNS Configuration

### Subdomain (CNAME)

| Record Type | Name | Value |
|------------|------|-------|
| CNAME | `www` | `<app-name>.azurewebsites.net` |
| TXT | `asuid.www` | `<verification-id>` |

### Apex / Root Domain (A Record)

| Record Type | Name | Value |
|------------|------|-------|
| A | `@` | `<app-ip-address>` |
| TXT | `asuid` | `<verification-id>` |

Get the verification ID and IP address:

```bash
# Get verification ID
az webapp show -n $APP -g $RG --query "customDomainVerificationId" -o tsv

# Get IP address (for A records)
az webapp show -n $APP -g $RG --query "inboundIpAddress" -o tsv
```

> 💡 **Tip:** Prefer CNAME records for subdomains. For apex domains, consider using an Azure DNS alias record to avoid hardcoding IP addresses that may change.

## Bind Custom Domain via CLI

```bash
# Add custom domain
az webapp config hostname add -n $APP -g $RG --hostname www.contoso.com

# Create managed certificate (free)
az webapp config ssl create -n $APP -g $RG --hostname www.contoso.com

# Capture certificate thumbprint
THUMBPRINT=$(az webapp config ssl list -n $APP -g $RG \
--query "[?contains(hostNames, 'www.contoso.com')].thumbprint | [0]" -o tsv)

# Bind the certificate
az webapp config ssl bind -n $APP -g $RG \
--certificate-thumbprint $THUMBPRINT --ssl-type SNI
Comment thread
apwestgarth marked this conversation as resolved.
```

## Bicep — Custom Domain with Managed Certificate

```bicep
resource customDomain 'Microsoft.Web/sites/hostNameBindings@2022-09-01' = {
parent: webApp
name: 'www.contoso.com'
properties: {
siteName: webApp.name
hostNameType: 'Verified'
sslState: 'Disabled' // enable after cert is created
}
Comment thread
apwestgarth marked this conversation as resolved.
}

resource managedCert 'Microsoft.Web/certificates@2022-09-01' = {
name: 'www.contoso.com'
location: location
properties: {
serverFarmId: appServicePlan.id
canonicalName: 'www.contoso.com'
}
dependsOn: [customDomain]
Comment thread
apwestgarth marked this conversation as resolved.
}
Comment thread
apwestgarth marked this conversation as resolved.
```

Then run a follow-up Bicep deployment to enable SNI and bind the managed certificate to the hostname:

```bicep
resource managedCert 'Microsoft.Web/certificates@2022-09-01' existing = {
name: 'www.contoso.com'
}

resource customDomainTlsBinding 'Microsoft.Web/sites/hostNameBindings@2022-09-01' = {
parent: webApp
name: 'www.contoso.com'
properties: {
siteName: webApp.name
hostNameType: 'Verified'
sslState: 'SniEnabled'
thumbprint: managedCert.properties.thumbprint
}
}
```

> ⚠️ **Warning:** Managed certificate creation requires the DNS records to be in place first. The hostname binding must exist before requesting the certificate.

## Terraform — Custom Domain with Managed Certificate

```hcl
resource "azurerm_app_service_custom_hostname_binding" "domain" {
hostname = "www.contoso.com"
app_service_name = azurerm_linux_web_app.app.name
resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_app_service_managed_certificate" "cert" {
custom_hostname_binding_id = azurerm_app_service_custom_hostname_binding.domain.id
}

resource "azurerm_app_service_certificate_binding" "binding" {
hostname_binding_id = azurerm_app_service_custom_hostname_binding.domain.id
certificate_id = azurerm_app_service_managed_certificate.cert.id
ssl_state = "SniEnabled"
}
```

## TLS Options

| Option | Cost | Renewal | Use Case |
|--------|------|---------|----------|
| App Service Managed Certificate | Free | Auto-renewed | Standard custom domains |
| App Service Certificate (purchased) | ~$70/yr | Auto-renewed | Extended validation, wildcard |
| Bring your own certificate | Varies | Manual | Enterprise PKI, specific CA |

### Enforce HTTPS Only

```bicep
resource webApp 'Microsoft.Web/sites@2022-09-01' = {
name: appName
location: location
properties: {
httpsOnly: true
// ...
}
}
```

```hcl
resource "azurerm_linux_web_app" "app" {
name = var.app_name
# ...
https_only = true
}
```

## Minimum TLS Version

```bash
# Set minimum TLS version to 1.2
az webapp config set -n $APP -g $RG --min-tls-version 1.2
```

```bicep
siteConfig: {
minTlsVersion: '1.2'
}
```

> ⚠️ **Warning:** TLS 1.0 and 1.1 are deprecated. Always set minimum TLS version to 1.2 for production workloads.

## Troubleshooting

| Issue | Cause | Fix |
|-------|-------|-----|
| Domain verification fails | Missing TXT record | Add `asuid` TXT record and wait for DNS propagation |
| Certificate creation fails | DNS not yet propagated | Wait 5-15 min for propagation; verify with `nslookup` |
| SSL binding error | SKU too low | Upgrade to Basic (B1) or higher |
| Managed cert not renewing | DNS record changed | Verify CNAME/A record still points to the app |
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# App Service Networking

VNet integration, Private Endpoints, Access Restrictions, and Hybrid Connections.
Comment thread
apwestgarth marked this conversation as resolved.

## Feature Availability by SKU

| Feature | Free | Basic | Standard | Premium | Isolated |
|---------|:-:|:-:|:-:|:-:|:-:|
| VNet integration (outbound) | ❌ | ✅ | ✅ | ✅ | ✅ (native) |
| Private Endpoints (inbound) | ❌ | ✅ | ✅ | ✅ | ✅ |
| Access Restrictions | ✅ | ✅ | ✅ | ✅ | ✅ |
| Hybrid Connections | ❌ | 5 | 25 | 200 | 200 |
Comment thread
apwestgarth marked this conversation as resolved.
| Access to service-endpoint-protected resources | ❌ | ✅ | ✅ | ✅ | ✅ |
> Note: Service endpoints are configured on VNets/subnets and downstream services (e.g., Storage, SQL). App Service accesses them via VNet integration rather than enabling service endpoints directly on the app.

Comment thread
apwestgarth marked this conversation as resolved.
## VNet Integration (Outbound)

Routes outbound traffic from the app through a VNet subnet, enabling access to private resources (databases, storage, VMs).

### Subnet Requirements

| Requirement | Value |
|------------|-------|
| Minimum subnet size | `/26` (64 addresses) recommended |
| Delegation | `Microsoft.Web/serverFarms` |
| Dedicated | One subnet per App Service plan |

### Bicep — VNet Integration

```bicep
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-11-01' = {
parent: vnet
name: 'app-service-subnet'
properties: {
addressPrefix: '10.0.1.0/26'
delegations: [
{
name: 'Microsoft.Web.serverFarms'
properties: { serviceName: 'Microsoft.Web/serverFarms' }
}
]
}
}

resource webApp 'Microsoft.Web/sites@2024-11-01' = {
name: appName
location: location
properties: {
serverFarmId: appServicePlan.id
virtualNetworkSubnetId: subnet.id
outboundVnetRouting: {
allTraffic: true // route all outbound through VNet
}
}
Comment thread
apwestgarth marked this conversation as resolved.
}
```

### CLI - VNet Integration

```bash
# Configure virtual network integration
az webapp vnet-integration add --resource-group RG --name APP --vnet VNET --subnet SUBNET

# Update app configuration to route all outbound traffic through the virtual network integration
az resource update --resource-group RG --name APP --resource-type "Microsoft.Web/sites" --set properties.outboundVnetRouting.allTraffic=true
```


> 💡 **Tip:** Set `outboundVnetRouting.allTraffic: true` to route ALL outbound traffic through the VNet. Without this, only RFC1918 traffic is routed through the VNet.

## Private Endpoints (Inbound)

Expose the app on a private IP address within your VNet. Public access can be disabled entirely.

### Bicep — Private Endpoint

```bicep
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = {
name: '${appName}-pe'
location: location
properties: {
subnet: { id: privateEndpointSubnet.id }
privateLinkServiceConnections: [
{
name: '${appName}-connection'
properties: {
privateLinkServiceId: webApp.id
groupIds: ['sites']
Comment thread
apwestgarth marked this conversation as resolved.
}
}
]
}
}
Comment thread
apwestgarth marked this conversation as resolved.

resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
name: 'privatelink.azurewebsites.net'
location: 'global'
}

resource dnsLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
parent: privateDnsZone
name: '${vnet.name}-link'
location: 'global'
properties: {
virtualNetwork: { id: vnet.id }
registrationEnabled: false
}
}
Comment thread
apwestgarth marked this conversation as resolved.
Comment thread
apwestgarth marked this conversation as resolved.

resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = {
parent: privateEndpoint
name: 'default'
properties: {
privateDnsZoneConfigs: [
{
name: 'webapp-dns-zone'
properties: {
privateDnsZoneId: privateDnsZone.id
}
}
]
}
}
```

Comment thread
jongio marked this conversation as resolved.
### CLI - Private Endpoint

```bash
# Retrieve web app resource id
id=$(az webapp show --name APP --resource-group RG --query id --output tsv)

# Create Private Endpoint
az network private-endpoint create --connection-name CONNECTIONNAME --name private-endpoint --private-connection-resource-id $id --resource-group RG --subnet SUBNET --group-id sites --vnet-name VNET

# Create Private DNS Zone
az network private-dns zone create --resource-group RG --name "privatelink.azurewebsites.net"

# Link the DNS Zone to virtual network
az network private-dns link vnet create --resource-group RG --zone-name "privatelink.azurewebsites.net" --name dns-link --virtual-network VNET --registration-enabled false

```

> ⚠️ **Warning:** Private Endpoints require Basic (B1+) or higher tier. The private DNS zone `privatelink.azurewebsites.net` must be linked to the VNet for name resolution.

## Access Restrictions

Control inbound access with IP-based or service-tag rules. Available on all SKUs.

### Bicep — Access Restrictions

```bicep
siteConfig: {
ipSecurityRestrictions: [
{
name: 'allow-office'
priority: 100
action: 'Allow'
ipAddress: '203.0.113.0/24'
}
{
name: 'deny-all'
priority: 2147483647
action: 'Deny'
ipAddress: 'Any'
Comment thread
apwestgarth marked this conversation as resolved.
}
]
scmIpSecurityRestrictionsUseMain: true
}
```

### CLI - Access Restrictions

```bash
# Add restriction to allow traffic from set range used by the office
az webapp config access-restriction add --resource-group RG --name APP --rule-name 'allow-office' --action Allow --ip-address 203.0.113.0/24 --priority 100

# Add restriction to deny access from any other address range
az webapp config access-restriction add --resource-group RG --name APP --rule-name 'deny-all' --action Deny --ip-address Any --priority 2147483647

# Set SCM Site (Kudu) to use same access restrictions as main site
az webapp config access-restriction set -g RG -n APP --use-same-restrictions-for-scm-site true
```

> 💡 **Tip:** Always restrict the SCM/Kudu site too. Use `scmIpSecurityRestrictionsUseMain: true` to inherit main site rules, or define separate SCM rules.

## Hybrid Connections

Connect to on-premises resources without VPN. Requires Basic tier or higher. Uses Hybrid Connection Manager (HCM) agent on-premises relaying through Azure Relay.

> ⚠️ **Warning:** Each Hybrid Connection maps to a single host:port endpoint. Basic tier supports 5; Standard tier supports 25; Premium/Isolated support 200.

## Troubleshooting

| Issue | Cause | Fix |
|-------|-------|-----|
| Cannot reach private DB | VNet integration not enabled | Enable VNet integration; check `outboundVnetRouting.allTraffic` |
| DNS resolution fails | Private DNS zone not linked | Link `privatelink.*` DNS zone to VNet |
| Access restriction not working | Priority ordering wrong | Lower numbers = higher priority; check rule order |
| Hybrid Connection timeout | HCM not running | Verify HCM service status on-premises |
| Outbound traffic blocked | NSG rules on subnet | Allow outbound to required services in NSG |
Comment thread
jongio marked this conversation as resolved.
Loading
Loading