Private Endpoint
Configure private ingress for ports exposed by Sandboxes in a Sandbox Group.
Why use a Private Endpoint
Use a Private Endpoint when you must reach Sandbox workloads from a VNet, peered network, VPN, or ExpressRoute connection without exposing Sandbox ingress to the public internet. The Private Endpoint assigns a private IP from your VNet and carries traffic to a Container Apps Environment configured with mode=Express over Azure Private Link.
One Private Endpoint can provide this private ingress path for every Sandbox Group linked to the Container Apps Environment. You keep using the Sandbox port FQDN returned by the service; Private DNS resolves that FQDN to the Private Endpoint IP from connected networks.
Domain names used in this guide: *.{region}.adcproxy.io is the default Sandbox ingress domain. After you link a Sandbox Group, its port FQDNs use *.{region}.azurecontainerapps.io, the Container Apps Environment ingress domain.
Sandbox ingress uses the same Private Endpoint and Private DNS model as an Azure Container Apps Environment. See the Container Apps Private Endpoint concept and Private Endpoint tutorial.
Understand the ingress flow
A Private Endpoint for Sandbox ingress targets a Container Apps Environment configured with mode=Express. It doesn't target an individual Sandbox Group or Sandbox. One Private Endpoint on the Container Apps Environment provides private ingress for ports exposed by Sandboxes in every linked Sandbox Group.
*.{region}.adcproxy.io*.{region}.azurecontainerapps.ioYour VNet
Sandbox private ingress
Linking a Sandbox Group to the Container Apps Environment changes ingress only. The Sandbox Group's vnetConnections child resource and each Sandbox's CustomerVnetConnectionName continue to control egress without changes. The Container Apps Environment's VNet and subnet don't affect the Sandbox Group or its Sandboxes. To give a Sandbox outbound access to private resources in your VNet, use a VNet connection.
The recommended order is:
- Create the Container Apps Environment with
mode=Express. - Create and approve the Private Endpoint.
- Disable public network access on the Container Apps Environment.
- Link the Sandbox Group after the private ingress path is ready.
This order prevents an ingress interruption for an existing Sandbox Group before it is linked.
Prerequisites
- An Azure account with an active subscription.
- The latest Azure CLI and Container Apps extension with preview features enabled:
az extension add --name containerapp --upgrade --allow-preview true. - An existing Sandbox Group with a Sandbox that exposes an HTTP port.
- Contributor access to the resource group that contains the Sandbox Group and the resource group where you create the Container Apps Environment and Private Endpoint.
- A VNet and subnet for the Private Endpoint.
- Network Contributor access to the VNet and subnet used by the Private Endpoint.
- Permission to create or link the Azure Private DNS Zone.
- The
Microsoft.App/managedEnvironments/join/actionpermission on the Container Apps Environment for the identity that links the Sandbox Group. - A VM or other test client that can resolve DNS and send HTTPS requests from the VNet.
The Sandbox Group's environmentId is set once. After you link the Sandbox Group, you can't unlink it or move it to another Container Apps Environment. To use another Container Apps Environment, create a new Sandbox Group.
Private Endpoints incur Azure Private Link and Container Apps Private Endpoint infrastructure charges. See Private Endpoint billing.
If your network filters DNS names, permit the Container Apps Environment ingress domain, *.{region}.azurecontainerapps.io, before you link the Sandbox Group. Existing Sandbox port FQDNs migrate from the default Sandbox ingress domain, *.{region}.adcproxy.io.
Create a Container Apps Environment with mode=Express
Create the Container Apps Environment with mode=Express and public network access enabled. The Sandbox Group isn't linked yet, so its existing ingress URLs are unchanged.
- Bash
- PowerShell
RESOURCE_GROUP="${ACA_EXAMPLES_RG}"
LOCATION="${ACA_EXAMPLES_REGION}"
ENVIRONMENT_NAME="${ENVIRONMENT_NAME:-sandbox-private-env}"
az containerapp env create \
--name "${ENVIRONMENT_NAME}" \
--resource-group "${RESOURCE_GROUP}" \
--location "${LOCATION}" \
--environment-mode Express \
--public-network-access Enabled \
--output none
az containerapp env show \
--name "${ENVIRONMENT_NAME}" \
--resource-group "${RESOURCE_GROUP}" \
--query "{mode:properties.environmentMode,state:properties.provisioningState,domain:properties.defaultDomain,publicAccess:properties.publicNetworkAccess}"
$ResourceGroup = $env:ACA_EXAMPLES_RG
$Location = $env:ACA_EXAMPLES_REGION
$EnvironmentName = if ($env:ENVIRONMENT_NAME) { $env:ENVIRONMENT_NAME } else { 'sandbox-private-env' }
az containerapp env create `
--name $EnvironmentName `
--resource-group $ResourceGroup `
--location $Location `
--environment-mode Express `
--public-network-access Enabled `
--output none
az containerapp env show `
--name $EnvironmentName `
--resource-group $ResourceGroup `
--query "{mode:properties.environmentMode,state:properties.provisioningState,domain:properties.defaultDomain,publicAccess:properties.publicNetworkAccess}"
Continue when mode is Express, state is Succeeded, and domain contains the Container Apps Environment ingress domain.
Create the Private Endpoint
Create the Private Endpoint before you link the Sandbox Group.
- Open the Container Apps Environment in the Azure portal.
- Select Networking > Private endpoints.
- Select Create.
- On Basics, choose the subscription, resource group, region, and Private Endpoint name.
- On Resource, confirm the Container Apps Environment and select managedEnvironments for the target subresource.
- On Virtual network, choose the VNet and subnet used by your private clients.
- On DNS, select Integrate with private DNS zone. Use
privatelink.<region>.azurecontainerapps.io. - Select Review + create, then Create.
- Wait for the Private Endpoint connection state to become Approved.
Confirm that the Private DNS Zone is linked to the client VNet before you continue.
For Azure CLI steps to create the Private Endpoint, see Use a Private Endpoint with a Container Apps Environment.
Disable public network access on the Container Apps Environment
Run this step only when the Private Endpoint connection is Approved and the Private DNS Zone is linked to the client VNet. Disabling public network access on the Container Apps Environment blocks public ingress and leaves the Private Endpoint as the allowed ingress path.
- Bash
- PowerShell
RESOURCE_GROUP="${ACA_EXAMPLES_RG}"
ENVIRONMENT_NAME="${ENVIRONMENT_NAME:-sandbox-private-env}"
az containerapp env update \
--name "${ENVIRONMENT_NAME}" \
--resource-group "${RESOURCE_GROUP}" \
--public-network-access Disabled \
--output none
az containerapp env show \
--name "${ENVIRONMENT_NAME}" \
--resource-group "${RESOURCE_GROUP}" \
--query "{state:properties.provisioningState,publicAccess:properties.publicNetworkAccess}"
$ResourceGroup = $env:ACA_EXAMPLES_RG
$EnvironmentName = if ($env:ENVIRONMENT_NAME) { $env:ENVIRONMENT_NAME } else { 'sandbox-private-env' }
az containerapp env update `
--name $EnvironmentName `
--resource-group $ResourceGroup `
--public-network-access Disabled `
--output none
az containerapp env show `
--name $EnvironmentName `
--resource-group $ResourceGroup `
--query "{state:properties.provisioningState,publicAccess:properties.publicNetworkAccess}"
Continue when state is Succeeded and publicAccess is Disabled.
Link the Sandbox Group
Link the Sandbox Group after the Container Apps Environment's private ingress path is ready. This asynchronous operation sets properties.environmentId, properties.defaultDomain, and properties.publicNetworkAccess, and migrates existing Sandbox port FQDNs from the default Sandbox ingress domain to the Container Apps Environment ingress domain.
The poller follows the Azure-AsyncOperation response header every 10 seconds and stops after 15 minutes.
- Bash
- PowerShell
SUBSCRIPTION_ID="$(az account show --query id -o tsv)"
RESOURCE_GROUP="${ACA_EXAMPLES_RG}"
SANDBOX_GROUP="${ACA_EXAMPLES_GROUP}"
ENVIRONMENT_NAME="${ENVIRONMENT_NAME:-sandbox-private-env}"
SBG_API_VERSION="2026-02-01-preview"
ARM_RESOURCE="https://management.azure.com/"
ENVIRONMENT_ID="/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.App/managedEnvironments/${ENVIRONMENT_NAME}"
SANDBOX_GROUP_ID="/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.App/sandboxGroups/${SANDBOX_GROUP}"
ARM_TOKEN="$(az account get-access-token --resource "${ARM_RESOURCE}" --query accessToken -o tsv)"
HEADERS_FILE="$(mktemp)"
RESPONSE_FILE="$(mktemp)"
trap 'rm -f "${HEADERS_FILE}" "${RESPONSE_FILE}"' EXIT
HTTP_STATUS="$(curl --silent --show-error \
--request PATCH \
--url "https://management.azure.com${SANDBOX_GROUP_ID}?api-version=${SBG_API_VERSION}" \
--header "Authorization: Bearer ${ARM_TOKEN}" \
--header "Content-Type: application/merge-patch+json" \
--data "{\"properties\":{\"environmentId\":\"${ENVIRONMENT_ID}\"}}" \
--dump-header "${HEADERS_FILE}" \
--output "${RESPONSE_FILE}" \
--write-out "%{http_code}")"
if (( HTTP_STATUS < 200 || HTTP_STATUS >= 300 )); then
cat "${RESPONSE_FILE}" >&2
exit 1
fi
ASYNC_URL="$(awk 'tolower($1) == "azure-asyncoperation:" { gsub(/\r/, "", $2); print $2 }' "${HEADERS_FILE}")"
if [[ -z "${ASYNC_URL}" ]]; then
echo "Sandbox Group link completed synchronously."
else
DEADLINE=$((SECONDS + 900))
LINK_COMPLETED=false
while (( SECONDS < DEADLINE )); do
OPERATION_STATUS="$(az rest \
--method get \
--url "${ASYNC_URL}" \
--resource "${ARM_RESOURCE}" \
--query status \
--output tsv)"
case "${OPERATION_STATUS}" in
Succeeded)
echo "Sandbox Group link completed."
LINK_COMPLETED=true
break
;;
Failed|Canceled)
echo "Sandbox Group link ${OPERATION_STATUS}." >&2
exit 1
;;
*)
echo "Sandbox Group link status: ${OPERATION_STATUS:-InProgress}"
sleep 10
;;
esac
done
if [[ "${LINK_COMPLETED}" != true ]]; then
echo "Timed out after 15 minutes waiting for the Sandbox Group link." >&2
exit 1
fi
fi
$SubscriptionId = az account show --query id --output tsv
$ResourceGroup = $env:ACA_EXAMPLES_RG
$SandboxGroup = $env:ACA_EXAMPLES_GROUP
$EnvironmentName = if ($env:ENVIRONMENT_NAME) { $env:ENVIRONMENT_NAME } else { 'sandbox-private-env' }
$SbgApiVersion = '2026-02-01-preview'
$ArmResource = 'https://management.azure.com/'
$EnvironmentId = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.App/managedEnvironments/$EnvironmentName"
$SandboxGroupId = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.App/sandboxGroups/$SandboxGroup"
$ArmToken = az account get-access-token --resource $ArmResource --query accessToken --output tsv
$LinkBody = @{
properties = @{
environmentId = $EnvironmentId
}
} | ConvertTo-Json -Depth 3 -Compress
$Response = Invoke-WebRequest `
-Method Patch `
-Uri "https://management.azure.com$SandboxGroupId`?api-version=$SbgApiVersion" `
-Headers @{ Authorization = "Bearer $ArmToken" } `
-ContentType 'application/merge-patch+json' `
-Body $LinkBody `
-SkipHttpErrorCheck
if ($Response.StatusCode -lt 200 -or $Response.StatusCode -ge 300) {
throw "Sandbox Group link failed with HTTP $($Response.StatusCode): $($Response.Content)"
}
$AsyncUrl = [string]$Response.Headers['Azure-AsyncOperation']
if (-not $AsyncUrl) {
Write-Host 'Sandbox Group link completed synchronously.'
} else {
$Deadline = (Get-Date).AddMinutes(15)
do {
Start-Sleep -Seconds 10
$OperationStatus = az rest `
--method get `
--url $AsyncUrl `
--resource $ArmResource `
--query status `
--output tsv
Write-Host "Sandbox Group link status: $OperationStatus"
if ($OperationStatus -eq 'Succeeded') {
break
}
if ($OperationStatus -in 'Failed', 'Canceled') {
throw "Sandbox Group link $OperationStatus."
}
} while ((Get-Date) -lt $Deadline)
if ($OperationStatus -ne 'Succeeded') {
throw 'Timed out after 15 minutes waiting for the Sandbox Group link.'
}
}
Confirm the migrated Sandbox port FQDN
Use the ACA CLI to get the current port FQDN from the Sandbox. The migrated FQDN uses the Container Apps Environment ingress domain, *.{region}.azurecontainerapps.io.
- Bash
- PowerShell
SANDBOX_GROUP="${ACA_EXAMPLES_GROUP}"
SANDBOX_NAME="${SANDBOX_NAME:-demo-port}"
SANDBOX_PORT_FQDN="$(aca sandbox get \
--group "${SANDBOX_GROUP}" \
-l "name=${SANDBOX_NAME}" \
--query portUrl.fqdn \
--output tsv)"
echo "Migrated port FQDN: ${SANDBOX_PORT_FQDN}"
export SANDBOX_PORT_FQDN
$SandboxGroup = $env:ACA_EXAMPLES_GROUP
$SandboxName = if ($env:SANDBOX_NAME) { $env:SANDBOX_NAME } else { 'demo-port' }
$env:SANDBOX_PORT_FQDN = aca sandbox get `
--group $SandboxGroup `
-l "name=$SandboxName" `
--query portUrl.fqdn `
--output tsv
Write-Host "Migrated port FQDN: $($env:SANDBOX_PORT_FQDN)"
The migration is transparent to clients that use the current port URL returned by the service. Update DNS filters and domain allowlists that only permit the default Sandbox ingress domain, *.{region}.adcproxy.io.
Verify Private Endpoint ingress
The Private DNS Zone contains an A record for the Container Apps Environment ingress domain label that points to the Private Endpoint IP address.
Run the test from a VM or client in the VNet that contains the Private Endpoint, or from a connected network that uses the same Private DNS path.
- Bash
- PowerShell
: "${SANDBOX_PORT_FQDN:?Run the migrated FQDN step first}"
nslookup "${SANDBOX_PORT_FQDN}"
curl -i --max-time 40 "https://${SANDBOX_PORT_FQDN}" | sed -n '1,20p'
if (-not $env:SANDBOX_PORT_FQDN) {
throw 'Run the migrated FQDN step first.'
}
Resolve-DnsName $env:SANDBOX_PORT_FQDN
Invoke-WebRequest -Uri "https://$($env:SANDBOX_PORT_FQDN)" -TimeoutSec 40
DNS should return the Private Endpoint IP, and the HTTPS request should succeed. The same FQDN remains blocked from the public internet while public network access is disabled on the Container Apps Environment.
If the VNet uses a custom DNS server, conditionally forward privatelink.<region>.azurecontainerapps.io to Azure DNS at 168.63.129.16. For on-premises clients, resolve the zone through Azure DNS Private Resolver or another DNS server that can query the linked Private DNS Zone.
Remove Private Endpoint ingress
To restore public ingress:
- Set public network access on the Container Apps Environment to Enabled.
- Confirm the existing Sandbox port FQDN works from the public internet.
- Delete the Private Endpoint and its DNS Zone Group if they are no longer needed.
Re-enabling public network access doesn't move port FQDNs back to the default Sandbox ingress domain. Because the Sandbox Group remains linked to the Container Apps Environment, its existing port FQDNs continue to use the Container Apps Environment ingress domain and work through public ingress.
You can't unlink the Sandbox Group from the Container Apps Environment. Before you delete the Container Apps Environment, delete its Private Endpoints and every linked Sandbox Group.
Troubleshooting
| Symptom | Cause and action |
|---|---|
| The Sandbox Group link returns a set-once error | The Sandbox Group is already linked. Create a new Sandbox Group if you need another Container Apps Environment. |
| The link poller times out | Get the operation URL from the Azure-AsyncOperation response header and check its status. Don't retry the link with another Container Apps Environment. |
The port FQDN still uses the default Sandbox ingress domain, *.{region}.adcproxy.io | The migration operation isn't complete. Check the async operation, then run aca sandbox get again. |
| DNS returns a public IP | Confirm the Private DNS Zone name, VNet link, A record, and DNS Zone Group. |
| DNS fails after the FQDN migration | Permit the Container Apps Environment ingress domain, *.{region}.azurecontainerapps.io, in DNS filters and domain allowlists. |
| The Private Endpoint connection is pending | Approve the connection from the Container Apps Environment's Private Endpoint connections page. |
| The private request returns 403 or times out | Confirm the client is in or connected to the VNet, resolves the FQDN to the private IP, and can reach the Private Endpoint subnet on HTTPS. |
For general endpoint diagnostics, see Private Endpoint troubleshooting.