Acceder a un App Service con Private Endpoint dentro de la misma Vnet

Estos días he estado jugando con diferentes escenarios a los que normalmente los clientes se enfrentan a nivel de comunicaciones. Este iba a ser un artículo súper largo que finalmente he decidido dividir en una serie de artículos por simplificar la lectura, y para que no mueras en el intento 🙂 Sin embargo, todos ellos terminan estando enlazados entre sí, para poco a poco ir viendo escenarios que muchas veces explicamos por separado, pero que luego terminamos encontrándonos combinaciones de ellos en el mundo real y nos complica la comprensión de lo que hay montado en muchos sitios. No pretendo contarte de qué va todo eso de las topologías de red en estrella o hub&spoke (al menos por ahora), sino más bien ir a un paso anterior para aquellos que nos cuesta más la parte de comunicaciones, porque no es nuestro día a día. Es por ello además que quiero agradecer de corazón a Kiko, que ha tenido que aguantarme durante dos días como una niña con el «¿y por qué? ¿y esto por qué?» 🙂
El escenario de hoy será el escenario 0, donde crearé el App Service con Private Endpoint e intentaré acceder a él desde dentro de la misma red.

App Service con Private Endpoint conectado a la red webapp-vnet

Creación de la web app y el private endpoint

Todo lo que voy a contar aquí puedes hacerlo a través del portal, pero para no perderte detalle, ya que el portal hace muchas cosas por ti, voy a compartir el paso a paso a través de Azure CLI. Lo primero que necesitamos es un sitio web al que acceder y para ello necesitamos crear: un grupo de recursos, un App Service Plan, la web app, una red virtual, con una subnet donde asociaremos nuestra web:

#Common
LOCATION="northeurope"

###### Scenario 0: Create a new web app with a private endpoint ######

# 1. Create the resource group for the web app
WEB_APP_RESOURCE_GROUP="WebApp-With-Private-Endpoint"
az group create -n $WEB_APP_RESOURCE_GROUP --location $LOCATION

# 2. Create App Service Plan
APP_SERVICE_PLAN="PremiumPlan"

az appservice plan create \
--name $APP_SERVICE_PLAN \
--resource-group $WEB_APP_RESOURCE_GROUP \
--location $LOCATION \
--sku P1V2

# 3. Create Web App
WEBAPP_NAME="internalweb"

az webapp create \
--name $WEBAPP_NAME \
--resource-group $WEB_APP_RESOURCE_GROUP \
--plan $APP_SERVICE_PLAN

# 4. Create a VNET

WEB_APP_VNET_NAME="webapp-vnet"
WEB_APP_VNET_CIDR=10.10.0.0/16
WEB_APP_SUBNET_NAME="webapps"
WEB_APP_SUBNET_CIDR=10.10.1.0/24

az network vnet create \
--name $WEB_APP_VNET_NAME \
--resource-group $WEB_APP_RESOURCE_GROUP \
--location $LOCATION \
--address-prefixes $WEB_APP_VNET_CIDR \
--subnet-name $WEB_APP_SUBNET_NAME \
--subnet-prefixes $WEB_APP_SUBNET_CIDR

# 5. You need to update the subnet to disable private endpoint network policies. 
az network vnet subnet update \
--name $WEB_APP_SUBNET_NAME \
--resource-group $WEB_APP_RESOURCE_GROUP \
--vnet-name $WEB_APP_VNET_NAME \
--disable-private-endpoint-network-policies true

Una vez que ya tienes todo esto, lo siguiente es crear el Private Endpoint:

# 6. Create a Private Endpoint for the Web App
# 6. 1 Get the web app ID
WEBAPP_ID=$(az webapp show --name $WEBAPP_NAME --resource-group $WEB_APP_RESOURCE_GROUP --query id --output tsv)

WEB_APP_PRIVATE_ENDPOINT="webapp-private-endpoint"

# 6. 2 Create a Private Endpoint
az network private-endpoint create \
--name $WEB_APP_PRIVATE_ENDPOINT \
--resource-group $WEB_APP_RESOURCE_GROUP \
--vnet-name $WEB_APP_VNET_NAME \
--subnet $WEB_APP_SUBNET_NAME \
--connection-name "webapp-connection" \
--private-connection-resource-id $WEBAPP_ID \
--group-id sites

¿pero qué es lo que hace esto? Cuando lo habilitas lo que hace es deshabilitar todo acceso público a la web y asigna una IP privada dentro de la red a la que has asociado el private endpoint. Es importante que sepas que para poder hacer uso de Private Endpoints con App Service el plan a utilizar debe ser Premium. El resultado de estos comandos es el siguiente:

Recursos creados a través de Azure CLI

El único recurso que puedes no saber de dónde proviene es el de tipo Network interface. Siempre que creas un private endpoint este por debajo crea una tarjeta de red para el recurso en cuestión, y tiene sentido ya que si no hay tarjeta de red no puedes estar en una red. Para comprobarlo, si ahora accedes al recurso webapp-vnet verás que tienes un dispositivo conectado, que es dicha NIC con la IP privada 10.10.1.4, dentro de la subnet que definiste:

red webapp-vnet con internalweb conectada a través de Private Endpoint

Private DNS Zone

Ahora viene un punto importante, ya que sin él todo lo anterior no funcionará. Lo primero que es importante que tengas en cuenta es que App Service sabe dónde enrutar las peticiones a través del hostname al que se hace la petición (ya que hay un proxy inverso que sabe enrutar por hostname), por lo que no es posible hacer la llamada directamente a través de la IP privada (devolvería un Invalid hostname). Es por ello que, para que el Private Endpoint funcione correctamente con App Service, estos deben ser resueltos a través de un servidor DNS. La forma más sencilla de tener uno es haciendo uso del recurso Private DNS Zone en este caso.

VNET con un Private DNS Zone asociado

Para crearlo puedes hacerlo de la siguiente forma:

# 7. Create Private DNS Zone
az network private-dns zone create \
--name privatelink.azurewebsites.net \
--resource-group $WEB_APP_RESOURCE_GROUP

# 7.1 Link between my VNET and the Private DNS Zone
az network private-dns link vnet create \
--name "${WEB_APP_VNET_NAME}-link" \
--resource-group $WEB_APP_RESOURCE_GROUP \
--registration-enabled false \
--virtual-network $WEB_APP_VNET_NAME \
--zone-name privatelink.azurewebsites.net

# 7.2 Create a DNS zone group
az network private-endpoint dns-zone-group create \
--name "webapp-group" \
--resource-group $WEB_APP_RESOURCE_GROUP \
--endpoint-name $WEB_APP_PRIVATE_ENDPOINT \
--private-dns-zone privatelink.azurewebsites.net \
--zone-name privatelink.azurewebsites.net

El primero de los comandos lo único que hace es crear el recurso de tipo Private DNS Zone. Los otros dos son muy importantes comprender bien qué es lo que hacen: El 7.1 va a crear una asociación entre este nuevo recurso y la red webapp-vnet, de tal forma que esta utilice este servidor DNS como resolvedor de nombres para los dispositivos que estén conectados a dicha red. Esta asociación puedes hacerla con varias VNETs, pero una VNET solo puede tener un Private DNS Zone asociado. En el portal de Azure puedes ver las asociaciones que tienes para un recurso de este tipo:

Redes asociadas al Private DNS Zone

El segundo punto, el 7.2, lo que hace es registrar en el servidor DNS los nombres asociados a nuestra web app, el de la web y la web de administración:

registros para el internalweb en el Private DNS zone

De hecho, si accedes al recurso Private Endpoint verás que tienes un apartado llamado DNS Configuration donde verás asociado el zone group que acabas de crear, con los nombres que resolverá para este recurso asociado a tu web app.

Ahora ya tienes todo lo que necesitas para que tu sitio pueda ser internamente accesible.

Acceso desde una máquina virtual en la misma VNET

Este sería el escenario más sencillo, donde una máquina dentro de la misma red quiere acceder a la web app con private endpoint.

Acceso a la Web App desde una VM en su misma red

Nota: si solamente fuera este el escenario final, tendría más sentido hacer uso de Service Endpoint en lugar de Private Endpoint, porque es más sencillo de implementar, pero como ya te he contado esta es solo una parte de la foto final.

Para conseguir lo que vemos en la imagen nos falta crear una nueva subnet, llamada vms, dentro de nuestra red webapp-vnet y la máquina virtual same-vnet-vm:

###### Scenario 1: Access the web app from the same VNET ######

VM_SUBNET_NAME="vms"
VM_SUBNET_CIDR=10.10.2.0/24
VM_NAME="same-vnet-vm"

# 8. Create a new subnet in the VNET
az network vnet subnet create \
--name $VM_SUBNET_NAME \
--resource-group $WEB_APP_RESOURCE_GROUP \
--vnet-name $WEB_APP_VNET_NAME \
--address-prefixes $VM_SUBNET_CIDR

# 9. Create a VM in the new subnet
az vm create \
--name $VM_NAME \
--resource-group $WEB_APP_RESOURCE_GROUP \
--vnet-name $WEB_APP_VNET_NAME \
--subnet $VM_SUBNET_NAME \
--image "Win2019Datacenter" \
--admin-username "azureuser" \
--admin-password "[email protected]" \
--nsg-rule NONE

Si te fijas en el comando de la creación de la VM, no he permitido el acceso por RDP (–nsg-rule a NONE). En este ejercicio lo he deshabilitado porque prefiero hacer uso de Azure Bastion, el cual me permite no tener que exponer los puertos de mis VMs y ni siquiera necesito un cliente RDP, ya que el acceso es a través del navegador.

Crear un host de Bastion

Para poder usar este servicio necesitas: una nueva subnet con un nombre especial, AzureBastionSubnet, una IP pública y el servicio en si:

# 10. Create a bastion host
BASTION_PUBLIC_IP_NAME="bastion-public-ip"
BASTION_HOST_NAME="bastion-host"
BASTION_SUBNET_CIDR=10.10.3.0/27

# 10.1 Create a subnet for the bastion host
az network vnet subnet create \
--name AzureBastionSubnet \
--resource-group $WEB_APP_RESOURCE_GROUP \
--vnet-name $WEB_APP_VNET_NAME \
--address-prefixes $BASTION_SUBNET_CIDR

# 10.2 Create a public IP
az network public-ip create \
--resource-group $WEB_APP_RESOURCE_GROUP \
--name $BASTION_PUBLIC_IP_NAME \
--sku Standard --location $LOCATION

# 10.3 Create a bastion host
az network bastion create --name $BASTION_HOST_NAME \
--resource-group $WEB_APP_RESOURCE_GROUP \
--location $LOCATION \
--vnet-name $WEB_APP_VNET_NAME \
--public-ip-address $BASTION_PUBLIC_IP_NAME

Una vez completes los pasos verás el nuevo servicio en el grupo de recursos:

Azure Bastion asociado a la VNET webapp-vnet

Para hacer la prueba de conectividad accede desde el portal a la máquina virtual, same-vnet-vm, y haz clic en el apartado Bastion. En él añade las credenciales de la máquina para conectarte.

Deshabilita en Server Manager > Local Server > IE Enhanced Security Configuration a Off para el administrador y abre un navegador dentro de la sesión y haz uso del FQDN de la web para comprobar que, en mi caso https://internalweb.azurewebsites.net, funciona correctamente:

Acceso a internalweb.azurewebsites.net

Sin embargo, ten en cuenta que el acceso por IP privada no es posible, como ya te conté antes. Si intentas acceder directamente a través de la IP privada el servicio te devolverá un 400 (Bad Request), y esto en ocasiones lleva a confusión en el caso de los App Services.

Intento de acceso a internalweb desde same-vnet-vm con Azure Bastion

Por otro lado, si intentas acceder desde otra máquina que no esté en la misma VNET el resultado será este:

Intento de acceso a internalweb.azurewebsites.net desde fuera de la VNET – Error 403

Los comandos los tienes en mi GitHub.

¡Saludos!