Cómo automatizar el refresco de las IPs públicas de Microsoft Azure en un firewall

En varias ocasiones es posible que sea necesario mantener el listado de IPs públicas que Microsoft Azure utiliza para sus servicios. Normalmente suele ser porque necesitas restringir el acceso a ciertos recursos, tanto en la nube como on-prem, y quieres añadir en tu firewall solo aquellas IPs estrictamente necesarias para el acceso. Para ello me he creado un script, que he contenerizado, el cual recupera el archivo de los rangos de IP públicos que utiliza Microsoft Azure, el cual se actualiza semanalmente, y actualiza las reglas de un firewall, en este caso de un Azure Data Lake Gen 1, pero podría ser para cualquier otro firewall o necesidad.

El script

El encargado de hacer todas estas tareas es el siguiente script:

#!/bin/bash

MICROSOFT_IP_RANGES_URL="https://www.microsoft.com/en-us/download/confirmation.aspx?id=56519"
JSON_FILE_NAME="azure-public-ips.json"

#login first
az login --service-principal -u $SPUSERNAME -p $SPPASSWORD --tenant $TENANT

#Get the last version of the Public Azure IP ranges
#https://stackoverflow.com/questions/28798014/is-there-a-way-to-automatically-and-programmatically-download-the-latest-ip-rang
UPDATED_FILE=$(curl -Lfs ${MICROSOFT_IP_RANGES_URL} | grep -Eoi '<a [^>]+>' | grep -Eo 'href="[^\"]+"' | grep "download.microsoft.com/download/" | grep -m 1 -Eo '(http|https)://[^"]+')
echo last version of the Azure Public IP Ranges ${UPDATED_FILE}
curl $UPDATED_FILE -o $JSON_FILE_NAME

echo "Added ${SERVICE_NAME} to the firewall"

#Delete all firewall rules
SERVICE_NAME_RULE_PREFIX="${SERVICE_NAME}_Rule_"
echo "First, we delete all firewall rules with prefix $SERVICE_NAME_RULE_PREFIX for $DLS_ACCOUNT_NAME in $RESOURCE_GROUP"

for ruleName in $(az dls account firewall list --account $DLS_ACCOUNT_NAME -g $RESOURCE_GROUP | jq '.[] | select(.name | startswith('\"${SERVICE_NAME_RULE_PREFIX}\"'))' | jq -r '.name'); do    
    echo "Deleting rule $ruleName"    
    az dls account firewall delete --account $DLS_ACCOUNT_NAME -g $RESOURCE_GROUP --firewall-rule-name $ruleName
done

echo "Now, we added the new rules from $JSON_FILE_NAME"

COUNTER=0

for ipRange in $(cat ${JSON_FILE_NAME} | jq --arg s "${SERVICE_NAME}" '.values[] | select(.name==$s) | .properties.addressPrefixes' | jq -r ".[]"); do
    COUNTER=$(($COUNTER + 1))
    echo Checking if $ipRange is IPv4
    FIRST_IP=$(prips $ipRange | head -n 1)

    if [[ $FIRST_IP =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
        echo "success! Try to add it to the firewall"
        #https://docs.microsoft.com/en-us/cli/azure/dls/account/firewall?view=azure-cli-latest
        az dls account firewall create --account $DLS_ACCOUNT_NAME -g $RESOURCE_GROUP --firewall-rule-name "${SERVICE_NAME}_Rule_${COUNTER}" --start-ip-address $FIRST_IP --end-ip-address $(prips $ipRange | tail -n 1) 
    else
        echo "fail"
    fi

done

echo "Done!"

Para ello he necesitado tres componentes principales:

  • prips: se trata de una herramienta que me devuelve todas las IPs que están contenidas en un rango CIDR. Lo necesito porque en el JSON que me descargo no vienen todas las IPs sino los rangos, pero el firewall necesita la IP de inicio y la IP de fin. Esta herramienta viene como parte de la distribución Ubuntu, así que ha sido esta la que he utilizado para mi contenedor, por simplicidad.
  • Azure CLI: como en este ejemplo voy a actualizar el firewall de un recurso de Microsoft Azure necesito tenerlo instalado para poder hacer esta operación. Si quisiera actualizar otro firewall de otro servicio necesitaría modificar esta parte con las acciones necesarias.
  • jq: para recuperar y manipular los JSONs que recibo en los diferentes pasos.

Por todo ello, el Dockerfile que voy a utilizar para dockerizar mi script es el siguiente:

FROM ubuntu

#Service principal
ENV SPUSERNAME=""
ENV SPPASSWORD=""
ENV TENANT=""

#Data Lake store
ENV DLS_ACCOUNT_NAME=""
ENV RESOURCE_GROUP=""

#The service you want to allow
ENV SERVICE_NAME="PowerBI"

WORKDIR /app

#Add the script
COPY script.sh .

#We need prips, jq and azure cli
RUN apt-get update \
    && apt-get -y install ca-certificates curl apt-transport-https lsb-release gnupg prips jq \
    && curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null \
    && AZ_REPO=$(lsb_release -cs) \
    && echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | tee /etc/apt/sources.list.d/azure-cli.list \
    && apt-get update \
    && apt-get install azure-cli


ENTRYPOINT [ "/bin/bash","script.sh" ]

A partir de aquí, puedes tener varias formas de crear y ejecutar este contenedor.

Ejecutar el contenedor en local

Si quieres probar este contenedor en local puedes hacerlo con los siguientes comandos:

#0. Variables
SERVICE_PRINCIPAL_NAME="play-with-public-azure-ip-addresses"
SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID"
RESOURCE_GROUP_NAME="DATALAKE_RESOURCE_GROUP"
DATALAKE_STORE_NAME="DATALAKE_NAME"

# 1. Create the service principal for the resource group where the Data Lake Store Gen 1 is
az ad sp create-for-rbac --name $SERVICE_PRINCIPAL_NAME --role "Contributor" --scopes /subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}
#Output example:
# {
#   "appId": "xxxxx-xxxx-xxx-xxx-xxxxxx",
#   "displayName": "play-with-public-azure-ip-addresses",
#   "name": "http://play-with-public-azure-ip-addresses",
#   "password": "RANDOM_PASSWORD",
#   "tenant": "YOUR_TENANT_ID"
# }

#Copy the output here
SP_APP_ID=""
SP_NAME=""
SP_PASSWORD=""
SP_TENANT_ID=""

# 2. Build the Docker image (locally)
docker build -t play-with-azure-public-ips .

# 3. Run the container (locally)
docker run -e SPUSERNAME=$SP_NAME \
    -e SPPASSWORD=$SP_PASSWORD \
    -e TENANT=$SP_TENANT_ID \
    -e RESOURCE_GROUP=$RESOURCE_GROUP_NAME \
    -e DLS_ACCOUNT_NAME=$DATALAKE_STORE_NAME \
    play-with-azure-public-ips

Lo primero que debes hacer es configurar las variables, con el id de la suscripción donde está el recurso del cual vamos a modificar el firewall, el grupo de recursos y el nombre del Data Lake Store en este ejemplo.
Para que el contenedor pueda ejecutar la eliminación e insercción de las reglas en el firewall necesitamos crear también un service principal con el que lanzar el comando az login. Una vez que tenemos todo ello podemos generar la imagen en local, a través de docker build, y por último, con docker run, podemos ejecutar el contenedor en local pasándole como variables de entorno los parámetros necesarios para tu entorno.

Ejecutar en Azure Container Instances

Si no tienes Docker instalado en tu local y aún así quieres probar este escenario puedes delegar las tareas en Azure Container Registry y Azure Container Instances.

# 4. Create an Azure Container Registry
ACR_RG="Play-With-Public-Azure-IP-Addresses"
LOCATION="northeurope"
ACR_NAME="azuremantainance"
IMAGE_NAME="play-with-azure-public-ips"

az group create --name $ACR_RG --location $LOCATION
az acr create --resource-group $ACR_RG --name $ACR_NAME --sku Basic --admin-enabled true

# 5. Build the Docker image (Azure Container Registry)
az acr build --registry $ACR_NAME -g $ACR_RG --image $IMAGE_NAME .

REGISTRY_PASSWORD=$(az acr credential show -n $ACR_NAME -g $ACR_RG | jq -r '.passwords[] | select(.name=="password") | .value')

# 6. Run the container (Azure Container Registry)
az container create -g $ACR_RG \
    --name updateips \
    --image $ACR_NAME.azurecr.io/$IMAGE_NAME \
    --registry-login-server $ACR_NAME.azurecr.io \
    --registry-username $ACR_NAME \
    --registry-password $REGISTRY_PASSWORD \
    --environment-variables SPUSERNAME=$SP_NAME SPPASSWORD=$SP_PASSWORD TENANT=$SP_TENANT_ID RESOURCE_GROUP=$RESOURCE_GROUP_NAME DLS_ACCOUNT_NAME=$DATALAKE_STORE_NAME \
    --cpu 2 --memory 4 \
    --restart-policy Never

Como ves, en este caso me creo un grupo de recursos donde genero un Azure Container Registry y lanzo una tarea de build con los elementos que tengo en local. Esto generará la misma imagen que creamos en local pero directamente en este ACR. Una vez que tenemos la misma ya podemos crear un contenedor en Azure Container Instances con el acceso al ACR que acabas de crear y las mismas variables que cuando lo lanzaste en local.

El resultado

Si todo ha ido bien, deberías de tener dadas de alta todas las reglas relativas a las IPs del servicio de Microsoft Azure que necesitas dar acceso en el firewall del servicio:

En este ejemplo solamente estoy añadiendo las IPs para Power BI, pero podrías modificarlo para recuperar del servicio que te interese en tu caso. Por último, todo esto podrías integrarlo como parte de una Logic App que tenga programado que semanalmente haga este proceso.

El código de ejemplo lo tienes en mi GitHub.

¡Saludos!