App Service con Private Endpoint y dominio personalizado

La semana pasada tuve que preparar para un cliente lo que podría ser una continuación de lo que ya te expliqué de App Services con Private Endpoints, para que un sitio fuera privado, pero, en este caso, con un dominio personalizado que sepamos resolver desde la red interna. Gracias a mi compañero David Sancho, cuando al principio era un cacao interesante, hoy te puedo contar cómo lo resolví.

App Service con Private endpoint y custom domain

Para esta prueba de concepto, utilicé GoDaddy que es donde tengo mis dominios de pruebas, pero puedes usar cualquier otro. ¡Empecemos!

Crear un App Service

Lo primero que necesitas es crear el App Service, que acto seguido haremos privado al habilitar el Private Endpoint. Los pasos son los mismos que ya te mostré en este artículo:

# Variables
RESOURCE_GROUP="Private-AppService-With-Custom-Domain"
LOCATION="northeurope"
APP_SERVICE_PLAN="AppService-Demo-Plan"
WEBAPP_NAME="myinternal-webapp"
# Create a resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
# Create an App Service plan (It has to be premium to use the private endpoint)
az appservice plan create \
--name $APP_SERVICE_PLAN \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku P1V2
# Create a web app
az webapp create \
--name $WEBAPP_NAME \
--resource-group $RESOURCE_GROUP \
--plan $APP_SERVICE_PLAN

Es importante que tengas en cuenta que para poder usar Private Endpoints es necesario que el App Service Plan sea de tipo Premium.

Crear una red virtual

El siguiente paso es crear una red virtual donde añadir tu App Service. Durante la creación ya voy a generar una subnet, llamada webapps, donde voy a alojar mi web:

# Create a virtual network and subnet
VNET_NAME="internal-vnet"
VNET_CIDR=10.10.0.0/16
SUBNET_NAME="webapps"
SUBNET_CIDR=10.10.1.0/24
az network vnet create \
--name $VNET_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--address-prefixes $VNET_CIDR \
--subnet-name $SUBNET_NAME \
--subnet-prefixes $SUBNET_CIDR

Crear un private endpoint para el App Service

Ahora ya puedes conectar los dos puntos anteriores, la web y la red. Para ello, sigue los siguientes pasos:

# You need to update the subnet to disable private endpoint network policies. 
az network vnet subnet update \
--name $SUBNET_NAME \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--disable-private-endpoint-network-policies true
# Create a private endpoint for the web app
WEBAPP_ID=$(az webapp show --name $WEBAPP_NAME --resource-group $RESOURCE_GROUP --query id --output tsv)
WEB_APP_PRIVATE_ENDPOINT="${WEBAPP_NAME}-private-endpoint"
az network private-endpoint create \
--name $WEB_APP_PRIVATE_ENDPOINT \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--subnet $SUBNET_NAME \
--connection-name "webapp-connection" \
--private-connection-resource-id $WEBAPP_ID \
--group-id sites

Con ellos se generará una tarjeta de red asociada al App Service y a la red creada anteriormente, lo que nos dará una IP privada para nuestra web. Al llevar a cabo esta operación, automáticamente nuestro sitio dejará de ser público.

Crear un DNS privado

Para completar la configuración, como ya comenté en el primer artículo que compartí sobre App Service y Private Endpoints, es la creación de un DNS privado que sepa resolver dentro de nuestra red el endpoint privado:

# Create a private DNS zone
az network private-dns zone create \
--name  privatelink.azurewebsites.net \
--resource-group $RESOURCE_GROUP
# Link between my VNET and the Private DNS Zone
az network private-dns link vnet create \
--name "${VNET_NAME}-link" \
--resource-group $RESOURCE_GROUP \
--registration-enabled false \
--virtual-network $VNET_NAME \
--zone-name privatelink.azurewebsites.net
#Create a DNS group
az network private-endpoint dns-zone-group create \
--name "webapp-group" \
--resource-group $RESOURCE_GROUP \
--endpoint-name $WEB_APP_PRIVATE_ENDPOINT \
--private-dns-zone privatelink.azurewebsites.net \
--zone-name privatelink.azurewebsites.net

Ahora ya tienes tu web perfectamente configurada, sin acceso desde Internet.

Crear una máquina virtual de salto

Para poder comprobar que nuestra prueba funciona correctamente, voy a crear una máquina de salto dentro de la misma red que está el App Service, ya que ya no puedo acceder desde fuera.

# Create a VM in the VNET
VM_SUBNET_NAME="vms"
VM_SUBNET_CIDR=10.10.2.0/24
VM_NAME="jumpbox"
# Create a new subnet in the VNET
az network vnet subnet create \
--name $VM_SUBNET_NAME \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--address-prefixes $VM_SUBNET_CIDR
# Create a VM in the new subnet
az vm create \
--name $VM_NAME \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--subnet $VM_SUBNET_NAME \
--image "Win2019Datacenter" \
--admin-username "azureuser" \
--admin-password "[email protected]" \
--public-ip-address "" \
--nsg-rule NONE

He procurado que la misma no tenga habilitado el puerto RDP para forzarme a usar Azure Bastion, ya que es más seguro, porque evita justamente tener este y SSH expuestos (además de que el acceso es vía web y puedo acceder con cualquier dispositivo 😊).

Crear Azure Bastion para acceder a la máquina virtual

Antes de poder acceder a la máquina, necesitamos crear este servicio, asociado a la red:

# Create a Bastion Host
BASTION_PUBLIC_IP_NAME="bastion-public-ip"
BASTION_HOST_NAME="bastion-host"
BASTION_SUBNET_CIDR=10.10.3.0/27
# Create a subnet for the bastion host
az network vnet subnet create \
--name AzureBastionSubnet \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--address-prefixes $BASTION_SUBNET_CIDR
# Create a public IP for the bastion host
az network public-ip create \
--resource-group $RESOURCE_GROUP \
--name $BASTION_PUBLIC_IP_NAME \
--sku Standard --location $LOCATION
# Create a bastion host
az network bastion create --name $BASTION_HOST_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--vnet-name $VNET_NAME \
--public-ip-address $BASTION_PUBLIC_IP_NAME \
--sku Basic

Una vez que el mismo esté listo, bastará con acceder al recurso de la máquina virtual y hacer clic en la opción Connect > Bastion e introducir el usuario y contraseña para la misma. Una vez hecho esto podrás comprobar, en mi caso, que la dirección https://myinternal-webapp.azurewebsites.net no es accesible desde Internet, pero si desde la máquina de salto:

Acceso al App Service desde la máquina de salto, usando la dirección de Azure

Ahora que todo funciona hasta aquí, el siguiente paso es añadir un dominio personalizado al sitio para poder acceder de la misma forma.

Configurar dominio para validar la propiedad

Siempre que quieres añadir un dominio personalizado en App Service es necesario que verifiques que el mismo es tuyo, como es lógico. Es por ello que el servicio te pide que añadas un registro TXT en el mismo con el ID de verificación que genera el servicio, el cual puede recuperar de esta forma:

# Get the domain verification ID
CUSTOM_DOMAIN_VERIFICATION_ID=$(az webapp show --name $WEBAPP_NAME --resource-group $RESOURCE_GROUP --query "customDomainVerificationId" --output tsv)
# Create a TXT record in GoDaddy asuid.internal > CUSTOM_DOMAIN_VERIFICATION_ID

En el caso de GoDaddy, para configurarlo, he accedido al apartado Administrar DNS de mi dominio azuredemo.es y he añadido un registro del tipo TXT con el nombre asuid.internal, que es el subdominio que he elegido para esta prueba, y el valor recuperado del comando anterior.

Registro TXT con el ID de verificación del App Service

Configurar App Service con el dominio personalizado

Con el registro TXT en su sitio ya podemos añadir el dominio personalizado con el siguiente comando:

# The domain name
DOMAIN_NAME="azuredemo.es"
# Add custom domain to the web app
az webapp config hostname add \
--resource-group $RESOURCE_GROUP \
--webapp-name $WEBAPP_NAME \
--hostname "internal.$DOMAIN_NAME"

Si el TXT no está configurado correctamente este dará un error (Bad request).

Crear un certificado auto firmado

Para este ejemplo he creado un certificado auto-firmado con OpenSSL con los siguientes comandos:

WEBAPP_NAME="myinternal-webapp"
BASE_PATH="/home/gis/projects/private-appservice/certs"
CN="internal.azuredemo.es"
ORG=Demo
mkdir -p $BASE_PATH/$CN
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -out $BASE_PATH/$CN/$WEBAPP_NAME.crt \
-keyout $BASE_PATH/$CN/$WEBAPP_NAME.key -subj "/CN=$CN/O=$ORG" -addext "extendedKeyUsage = 1.3.6.1.5.5.7.3.1"
openssl pkcs12 -export -out $BASE_PATH/$CN/$WEBAPP_NAME.pfx -in $BASE_PATH/$CN/$WEBAPP_NAME.crt -inkey $BASE_PATH/$CN/$WEBAPP_NAME.key -passout pass:Passw0rd!

Estos comandos generarán un directorio, en mi caso llamado internal.azuredemo.es, donde tendrás el pfx que necesitas subir a tu App Service en el apartado TLS/SSL settings y dentro de este hacer clic en el botón Private Key Certificates (.pfx). Al subirlo te pedirá la contraseña utilizada al generar este archivo (en mi ejemplo Passw0rd!) y ya quedará listo para ser asociado al dominio personalizado.

Certificado subido, listo para asociarlo a un dominio personalizado

Para enlazarlo con el dominio, accede ahora al apartado Custom Domains y haz clic en el enlace Add binding y asócialo al certificado que acabas de subir.

Enlazar el certificado con el dominio personalizado

Si ahora intentaras acceder a tu sitio haciendo uso de este dominio personalizado, utilizando tu máquina de salto, comprobarás que el navegador intentará resolverlo a través de Internet y no será capaz de redirigirte a tu web privada. Es por ello que nos falta el paso final, que es un servidor DNS privado que nos resuelva este último punto.

Crear un DNS privado para resolver internal.azuredemo.es

Como comentaba, hasta este punto, todavía no eres capaz de saber que internal.azuredemo.es debe resolver myinternal-webapp.azurewebsites.net. Para ello necesitamos un servidor DNS que pueda redirigir este primero a este último, pero que además al intentar acceder a este segundo resuelva con la IP privada del private endpoint generado para el App Service. Para ello, puedes crear un private DNS en Azure:

#Create Azure Private DNS Zone
az network private-dns zone create \
--name  $DOMAIN_NAME \
--resource-group $RESOURCE_GROUP
#Add CNAME record that points to the web app
az network private-dns record-set cname set-record \
--record-set-name internal \
--cname "$WEBAPP_NAME.azurewebsites.net" \
--resource-group $RESOURCE_GROUP \
--zone-name $DOMAIN_NAME

Y que este tenga un CNAME que apunte nuestro dominio personalizado al que nos provee Azure:

CNAME que apunta internal.azuredemo.es al dominio de Azure para ese App Service

Pero eso no es todo, ya que este debe estar además asociado a nuestra red virtual para que pueda usarlo como resolvedor de internal.azuredemo.es.

#Link the Private DNS Zone to the virtual network
az network private-dns link vnet create \
--name "${VNET_NAME}-link" \
--resource-group $RESOURCE_GROUP \
--registration-enabled false \
--virtual-network $VNET_NAME \
--zone-name $DOMAIN_NAME

De esta forma, si intentas acceder de nuevo a tu sitio, con tu domino, verás que ya todo funciona correctamente.

Resultado

¡Saludos!

logo lemoncode

 

 

Bootcamp Backend

Si tienes ganas de ponerte al día con tecnologías de backend, formo parte del
equipo de docentes del Bootcamp Backend de Lemoncode, ¿Te animas a
aprender con nosotros?

Más Info