Después de varios días generando un script extra largo, he estado jugando con una prueba de concepto que te permite utilizar Azure Front Door, y su WAF, como punto de entrada a tu clúster de AKS que, a su vez, hace uso de Traefik como Ingress Controller, e incluso lo he integrado con la API de GoDaddy para configurar mis dominios 🤓. Todo ello haciendo uso de un Private Link que le permite a Azure Front Door comunicarse con el balanceador interno de traefik de manera privada. En este artículo quiero compartirte todos los pasos.
El script
Como no quería asumir que tienes que saber Terraform he generado todo el entorno haciendo uso de Azure CLI. Esto me llevó a tener hasta 1.000 lineas de código, por lo que lo he dividido en capítulos que hasta que me ha permitido reutilizar algunas partes de forma sencilla 🤓
Las variables
En el primero fichero que debes fijarte es en llamado 00-variables.sh. En él justamente hago eso, configurar las variables necesarias para el resto de scripts:
# Variables
RESOURCE_GROUP="afd-and-aks-with-traefik-demo"
LOCATION="westeurope"
AKS_CLUSTER_NAME="aks-cluster"
AKS_VNET="aks-vnet"
AKS_SUBNET="aks-subnet"
PLS_SUBNET="pls-subnet"
# Azure Front Door variables
AZURE_FRONT_DOOR_PROFILE_NAME="${RESOURCE_GROUP}"
ENDPOINT_NAME="traefik-endpoint"
AFD_ORIGIN_GROUP_NAME="aks-origin-group"
AFD_ROUTE_NAME="traefik-route"
STORAGE_ACCOUNT_NAME="herostore${RANDOM}"
# Colors for the shell
HIGHLIGHT='\033[1;34m'
VERBOSE_COLOR='\033[0;32m'
# GoDaddy variables
GODADDY_KEY="<YOUR_PRODUCTION_GODADDY_KEY>"
GODADDY_SECRET="<YOUR_PRODUCTION_GODADDY_SECRET>"
GODADDY_ENDPOINT="https://api.godaddy.com"
echo -e "${HIGHLIGHT}Variables set! 🫡"
Ten en cuenta que en mi ejemplo estoy utilizando GoDaddy como proveedor de mis dominios, por lo que si utilizas otro diferente deberías de modificar tanto las credenciales como el script relacionado con la actualización de los registros en el DNS.
Crear el cluster de Kubernetes
Para poder probar este entorno lo primero que necesitas crear es un clúster en AKS. En el script 01-create-aks-cluster.sh tienes la creación tanto del grupo de recursos, la red y el cluster:
# Create a resource group
echo -e "${HIGHLIGHT}Creating the resource group '$RESOURCE_GROUP' 📦 ...${VERBOSE_COLOR}"
az group create --name $RESOURCE_GROUP --location $LOCATION
# Create a vnet for the cluster
echo -e "${HIGHLIGHT}Creating a vnet '$AKS_VNET' 🕸️ ...${VERBOSE_COLOR}"
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $AKS_VNET \
--address-prefixes 10.0.0.0/8 \
--subnet-name $AKS_SUBNET \
--subnet-prefixes 10.10.0.0/16
# Create a subnet for the Private Links
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name $AKS_VNET \
--name $PLS_SUBNET \
--address-prefixes 10.20.0.0/24
# Create user identity for the AKS cluster
echo -e "${HIGHLIGHT}Create an identity for the cluster 🧑 ...${VERBOSE_COLOR}"
az identity create --name $AKS_CLUSTER_NAME-identity --resource-group $RESOURCE_GROUP
echo -e "${HIGHLIGHT}Waiting 60 seconds for the identity..."
sleep 60
IDENTITY_ID=$(az identity show --name $AKS_CLUSTER_NAME-identity --resource-group $RESOURCE_GROUP --query id -o tsv)
IDENTITY_CLIENT_ID=$(az identity show --name $AKS_CLUSTER_NAME-identity --resource-group $RESOURCE_GROUP --query clientId -o tsv)
# Get VNET id
VNET_ID=$(az network vnet show --resource-group $RESOURCE_GROUP --name $AKS_VNET --query id -o tsv)
# Assign Network Contributor role to the user identity
echo -e "${HIGHLIGHT}Assign roles to the identity 🎟️ ...${VERBOSE_COLOR}"
az role assignment create --assignee $IDENTITY_CLIENT_ID --scope $VNET_ID --role "Network Contributor"
# Permission granted to your cluster's managed identity used by Azure may take up 60 minutes to populate.
# Get roles assigned to the user identity
az role assignment list --assignee $IDENTITY_CLIENT_ID --all -o table
AKS_SUBNET_ID=$(az network vnet subnet show --resource-group $RESOURCE_GROUP --vnet-name $AKS_VNET --name $AKS_SUBNET --query id -o tsv)
# Create an AKS cluster
echo -e "${HIGHLIGHT}Creating the cluster $AKS_CLUSTER_NAME ☸️...${VERBOSE_COLOR}"
time az aks create \
--resource-group $RESOURCE_GROUP \
--node-vm-size Standard_B4ms \
--name $AKS_CLUSTER_NAME \
--enable-managed-identity \
--vnet-subnet-id $AKS_SUBNET_ID \
--assign-identity $IDENTITY_ID \
--generate-ssh-keys
# Get AKS credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_CLUSTER_NAME --overwrite-existing
echo -e "${HIGHLIGHT}The cluster is ready 🥳${VERBOSE_COLOR}"
Hasta aquí todo bastante estándar 😃.
Desplegar Traefik en el nuevo clúster
En este ejemplo he utilizado Traefik como Ingress Controller pero podrías reemplazarlo por el que más te guste. Para desplegarlo he seguido la documentación oficial con algunos matices en el script 02-install-traefik.sh:
# Create a cluster role
kubectl apply -f - <<EOF
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: traefik-role
rules:
- apiGroups:
- ""
resources:
- services
- endpoints
- secrets
verbs:
- get
- list
- watch
- apiGroups:
- extensions
- networking.k8s.io
resources:
- ingresses
- ingressclasses
verbs:
- get
- list
- watch
- apiGroups:
- extensions
- networking.k8s.io
resources:
- ingresses/status
verbs:
- update
EOF
# Crete a service account
kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-account
EOF
# Create a cluster role binding
kubectl apply -f - <<EOF
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: traefik-role-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: traefik-role
subjects:
- kind: ServiceAccount
name: traefik-account
namespace: default # Using "default" because we did not specify a namespace when creating the ClusterAccount.
EOF
# Create a deployment with traefik
kubectl apply -f - <<EOF
kind: Deployment
apiVersion: apps/v1
metadata:
name: traefik-deployment
labels:
app: traefik
spec:
replicas: 1
selector:
matchLabels:
app: traefik
template:
metadata:
labels:
app: traefik
spec:
serviceAccountName: traefik-account
containers:
- name: traefik
image: traefik:v2.9
args:
- --api.insecure
- --providers.kubernetesingress
ports:
- name: web
containerPort: 80
- name: dashboard
containerPort: 8080
EOF
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
# Create services
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: traefik-dashboard-service
spec:
type: LoadBalancer
ports:
- port: 8080
targetPort: dashboard
selector:
app: traefik
---
apiVersion: v1
kind: Service
metadata:
name: traefik-web-service
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true"
service.beta.kubernetes.io/azure-pls-create: "true"
service.beta.kubernetes.io/azure-pls-name: traefik-lb-private-link
service.beta.kubernetes.io/azure-pls-ip-configuration-subnet: "$PLS_SUBNET" # Private Link subnet name
service.beta.kubernetes.io/azure-pls-ip-configuration-ip-address-count: "1"
service.beta.kubernetes.io/azure-pls-ip-configuration-ip-address: 10.20.0.10
service.beta.kubernetes.io/azure-pls-visibility: "*"
spec:
type: LoadBalancer
ports:
- targetPort: web
port: 80
selector:
app: traefik
EOF
ip=""
while [ -z $ip ]; do
echo -e "${HIGHLIGHT}Waiting for external IP"
ip=$(kubectl get svc traefik-web-service --template="{{range .status.loadBalancer.ingress}}{{.ip}}{{end}}")
[ -z "$ip" ] && sleep 10
done
echo -e "${HIGHLIGHT}Found external IP: ${ip}"
# Deploy whoami
kubectl apply -f - <<EOF
kind: Deployment
apiVersion: apps/v1
metadata:
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami
ports:
- name: web
containerPort: 80
EOF
# And it's service
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: whoami
spec:
ports:
- name: web
port: 80
targetPort: web
selector:
app: whoami
EOF
# Create traefik ingress
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami-ingress
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
name: web
EOF
# Check traefik logs
kubectl logs $(kubectl get pods -l app=traefik -o jsonpath='{.items[0].metadata.name}')
# # Test access whoami internally
# kubectl run -it --rm aks-ingress-test --image=mcr.microsoft.com/aks/fundamental/base-ubuntu:v0.0.11
# apt-get update && apt-get install -y curl
# # Directly to the service
# curl http://whoami
# # Through the ingress
# curl http:/traefik-web-service/
# exit
echo -e "${HIGHLIGHT}Traefik installed 🚦👍🏻!"
Para este entorno, he especificado que el Servicio de traefik-web-service, de tipo interno, va a generar un private link al que otros recursos podrán solicitar generar un private endpoint para poder comunicarse con él. En este punto, hasta que el servicio asociado a traefik-web-service no esté listo el script permanecerá a la espera.
Crear Azure Front Door asociado al private link de Traefik
Ya estamos llegando a la parte interesante. El punto de entrada queremos que sea Azure Front Door, en este caso porque los límites de dominios que podemos asociar en el modo Premium, donde también tenemos la opción de WAF, es de 500, que era el motivo principal de esta prueba de concepto. Para crear este tipo de recurso lo que hacemos en primer lugar es crear un perfil de Azure Front Door, el cual a su vez tiene como origen el private link que hemos generado para el balanceador interno de Traefik. En el script 04-create-azure-front-door-profile.sh tienes el paso a paso de todo lo que necesitas:
# Deploy Azure Front Door
echo -e "${HIGHLIGHT}Deploy Azure Front Door 🚪 ...${VERBOSE_COLOR}"
az afd profile create \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--resource-group $RESOURCE_GROUP \
--sku Premium_AzureFrontDoor
# Add an endpoint
echo "${HIGHLIGHT}Add an endpoint for the AKS ...${VERBOSE_COLOR}"
az afd endpoint create \
--resource-group $RESOURCE_GROUP \
--endpoint-name $ENDPOINT_NAME \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--enabled-state Enabled
# Create an origin group
# origin group that will define the traffic and expected responses for your app instances.
# Origin groups also define how origins should be evaluated by health probes, which you'll also define in this step.
echo "Create an origin group...${VERBOSE_COLOR}"
az afd origin-group create \
--resource-group $RESOURCE_GROUP \
--origin-group-name $AFD_ORIGIN_GROUP_NAME \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--probe-request-type GET \
--probe-protocol Http \
--probe-interval-in-seconds 60 \
--probe-path / \
--sample-size 4 \
--successful-samples-required 3 \
--additional-latency-in-milliseconds 50
# Get the alias for the private link service
AKS_RESOURCE_GROUP=$(az aks show -g $RESOURCE_GROUP -n $AKS_CLUSTER_NAME --query nodeResourceGroup -o tsv)
PRIVATE_LINK_ALIAS=$(az network private-link-service show -g $AKS_RESOURCE_GROUP -n traefik-lb-private-link --query "alias" -o tsv)
PLS_RESOURCE_ID=$(az network private-link-service show -g $AKS_RESOURCE_GROUP -n traefik-lb-private-link --query "id" -o tsv)
echo -e "${HIGHLIGHT}Create an origin for that origin group...${VERBOSE_COLOR}"
az afd origin create \
--resource-group $RESOURCE_GROUP \
--origin-group-name $AFD_ORIGIN_GROUP_NAME \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--origin-name aks-traefik-lb-endpoint \
--host-name $PRIVATE_LINK_ALIAS \
--http-port 80 \
--https-port 443 \
--enable-private-link true \
--private-link-location $LOCATION \
--private-link-resource $PLS_RESOURCE_ID \
--private-link-request-message 'Please approve this request' \
--enabled-state Enabled
# Approve the private link request
echo -e "${HIGHLIGHT}Approve the private link request from the origin${VERBOSE_COLOR}"
CONNECTION_RESOURCE_ID=$(az network private-endpoint-connection list --name traefik-lb-private-link --resource-group $AKS_RESOURCE_GROUP --type Microsoft.Network/privateLinkServices --query "[0].id" -o tsv)
az network private-endpoint-connection approve --id $CONNECTION_RESOURCE_ID
# Add a route
echo -e "${HIGHLIGHT}Add a route 🚏...${VERBOSE_COLOR}"
az afd route create \
--resource-group $RESOURCE_GROUP \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--endpoint-name $ENDPOINT_NAME \
--forwarding-protocol MatchRequest \
--route-name $AFD_ROUTE_NAME \
--https-redirect Disabled \
--origin-group $AFD_ORIGIN_GROUP_NAME \
--supported-protocols Http \
--link-to-default-domain Enabled
AFD_HOST_NAME=$(az afd endpoint show --resource-group $RESOURCE_GROUP --profile-name $AZURE_FRONT_DOOR_PROFILE_NAME --endpoint-name $ENDPOINT_NAME --query "hostName" -o tsv)
echo -e "${HIGHLIGHT}Now you have an Azure Front Door with an endpoint set${VERBOSE_COLOR}"
echo http://$AFD_HOST_NAME/
echo -e "${HIGHLIGHT}Waiting for the endpoint to be ready${VERBOSE_COLOR}"
# Loop until 200
status_code=-1
while [ $status_code != 200 ]; do
echo -e "${HIGHLIGHT} Wait for it..."
status_code=$(curl -s -o /dev/null -w "%{http_code}" http://$AFD_HOST_NAME/)
if [[ $status_code != 200 ]]; then sleep 10; fi
done
curl http://$AFD_HOST_NAME/
Si todo ha ido bien ya deberías de ver el resultado de whoami.
Llegados a este punto podemos confirmar que la configuración hecha hasta ahora funciona correctamente.
Añadir dominios a Azure Front Door
Hasta ahora, lo que podemos asegurar es que Azure Front Door llega a nuestro traefik a través del private endpoint asociado al private link generado por el servicio. Sin embargo, falta por comprobar que si damos de alta diferentes dominios asociados a este servicio, va a ser capaz de hacerle llegar el hostname con el que hiciste la petición para que Traefik te enrute al servicio que toca. Para ello puedes lanzar el script 05-add-custom-domains.sh para dar de alta tus dominios:
echo -e "${HIGHLIGHT}Domain: $1 Subdomain: $2${VERBOSE_COLOR}"
SUBDOMAIN=$2
CUSTOM_DOMAIN_NAME=$1
CUSTOM_DOMAIN_WITH_DASHES=$(echo "$SUBDOMAIN.$CUSTOM_DOMAIN_NAME" | sed 's/\./-/g')
# Create a custom domain
echo -e "${HIGHLIGHT} Creating custom domain $CUSTOM_DOMAIN_NAME...${VERBOSE_COLOR}"
az afd custom-domain create \
--resource-group $RESOURCE_GROUP \
--custom-domain-name $CUSTOM_DOMAIN_WITH_DASHES \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--host-name "$SUBDOMAIN.$CUSTOM_DOMAIN_NAME" \
--minimum-tls-version TLS12 \
--certificate-type ManagedCertificate
# Get the TXT value to add to the DNS record
TXT_VALIDATION_TOKEN=$(az afd custom-domain show --resource-group $RESOURCE_GROUP --profile-name $AZURE_FRONT_DOOR_PROFILE_NAME --custom-domain-name $CUSTOM_DOMAIN_WITH_DASHES --query "validationProperties.validationToken" -o tsv)
# You should add a TXT record to the DNS zone of the custom domain
echo "Record type: TXT"
echo "Record name: _dnsauth.$SUBDOMAIN"
echo "Record value: $TXT_VALIDATION_TOKEN"
echo -e "${HIGHLIGHT}Call GoDaddy to register TXT record${VERBOSE_COLOR}"
source 5.1-configure-goddady.sh $CUSTOM_DOMAIN_NAME TXT _dnsauth.$SUBDOMAIN $TXT_VALIDATION_TOKEN
# Verify the custom domain
az afd custom-domain wait \
--resource-group $RESOURCE_GROUP \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--custom-domain-name $CUSTOM_DOMAIN_WITH_DASHES \
--custom "domainValidationState!='Pending'" \
--interval 30 --debug \
--timeout 60
# Get TXT record from a domain
dig TXT _dnsauth.$SUBDOMAIN.$CUSTOM_DOMAIN_NAME
nslookup -type=TXT _dnsauth.$SUBDOMAIN.$CUSTOM_DOMAIN_NAME
# You should add a CNAME record to the DNS zone of the custom domain
echo "Record type: CNAME"
echo "Record name: $SUBDOMAIN"
echo "Record value: $AFD_HOST_NAME."
echo -e "${HIGHLIGHT}Call GoDaddy to register the CNAME record${VERBOSE_COLOR}"
source 5.1-configure-goddady.sh $DOMAIN_NAME CNAME $SUBDOMAIN $AFD_HOST_NAME.
echo -e "${HIGHLIGHT}Get all custom domains associated to the endpoint${VERBOSE_COLOR}"
CUSTOM_DOMAINS_ID=$(az afd route show \
--resource-group $RESOURCE_GROUP \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--endpoint-name $ENDPOINT_NAME \
--route-name $AFD_ROUTE_NAME \
--query "customDomains[].id" -o tsv)
CUSTOM_DOMAINS_NAMES=$(az afd custom-domain show \
--ids $CUSTOM_DOMAINS_ID \
--query "name" -o tsv | tr '\n' ' ')
if [[ $CUSTOM_DOMAINS_NAMES == '' ]]
then
CUSTOM_DOMAINS_NAMES=$(az afd custom-domain show \
--ids $CUSTOM_DOMAINS_ID \
--query "[].name" -o tsv | tr '\n' ' ')
fi
echo -e "${HIGHLIGHT}Add the custom domain ${CUSTOM_DOMAIN_NAME} to the route${VERBOSE_COLOR}"
az afd route update \
--resource-group $RESOURCE_GROUP \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--endpoint-name $ENDPOINT_NAME \
--route-name $AFD_ROUTE_NAME \
--custom-domains $(echo $CUSTOM_DOMAINS_NAMES) $CUSTOM_DOMAIN_WITH_DASHES
Este a su vez llama a otro script llamado 5.1-configure-goddady.sh que, en mi caso particular, actualiza a través de la API de GoDaddy los dominios que estoy utilizando para este entorno:
echo "Domain name: $1 Record: $2 Record name: $3 Value: $4"
DOMAIN_NAME=$1
RECORD_TYPE=$2
RECORD_NAME=$3
RECORD_VALUE=$4
echo -e "${HIGHLIGHT} Calling GoDaddy to add $RECORD_NAME with value $RECORD_VALUE for $DOMAIN_NAME${VERBOSE_COLOR}"
# Set TXT record
curl -X PUT -H "Authorization: sso-key $GODADDY_KEY:$GODADDY_SECRET" \
"$GODADDY_ENDPOINT/v1/domains/$DOMAIN_NAME/records/$RECORD_TYPE/$RECORD_NAME" \
--data "[{\"data\": \"$RECORD_VALUE\",\"ttl\": 600}]" -H "Content-Type: application/json"
echo -e "${HIGHLIGHT} Done 👍"
Si estás utilizando otro proveedor deberías de modificar este archivo, y las variables con las credenciales, para adecuarlo a tu proveedor en cuestión. La otra opción sería traerte la configuración a Azure DNS zone y gestionar los registros desde Azure.
En mi ejemplo, para dar de alta los diferentes subdominios de mis dominios los llamo de la siguiente forma:
## 05 - Add custom domains to the endpoint
# Parameters: domain subdomain
source 05-add-custom-domains.sh azuredemo.es www
source 05-add-custom-domains.sh matrixapp.es www
source 05-add-custom-domains.sh domaingis.com www
source 05-add-custom-domains.sh azuredemo.es api
source 05-add-custom-domains.sh matrixapp.es api
source 05-add-custom-domains.sh domaingis.com api
source 05-add-custom-domains.sh azuredemo.es dev
source 05-add-custom-domains.sh matrixapp.es dev
source 05-add-custom-domains.sh domaingis.com dev
Configurar politicas de WAF
La siguiente configuración trata de añadir políticas de WAF en función del subdominio. Por ejemplo, me interesa que los subdominios de tipo www tengan unas políticas diferentes a las de los subdominios dev o api. Para ello, este fragmento de script, 06-add-waf-policies.sh, nos permite dar de alta diferentes políticas en base justamente al subdominio pasado como parámetro.
# A security policy includes a web application firewall (WAF) policy and one or more domains to provide centralized protection for your web applications.
# https://docs.microsoft.com/en-us/azure/frontdoor/front-door-security-policies
SUBDOMAIN=$1
WAF_POLICY_NAME=$1
MODE=$2
ACTION=Block
if [[ $MODE == 'Detection' ]]; then ACTION=Log; fi
# Create a Azure Front Door policy
echo -e "${HIGHLIGHT}Creating a WAF policy 🧱 for ${SUBDOMAIN} subdomains${VERBOSE_COLOR}"
az network front-door waf-policy create \
--resource-group $RESOURCE_GROUP \
--name $WAF_POLICY_NAME \
--mode $MODE \
--sku Premium_AzureFrontDoor
# Add a managed rule to the WAF policy
echo -e "${HIGHLIGHT}Add managed rules 📐${VERBOSE_COLOR}"
az network front-door waf-policy managed-rules add \
--resource-group $RESOURCE_GROUP \
--policy-name $WAF_POLICY_NAME \
--type Microsoft_DefaultRuleSet \
--version 2.1 \
--action $ACTION
# Add bot rules
echo -e "${HIGHLIGHT}Add bot rules 🤖${VERBOSE_COLOR}"
az network front-door waf-policy managed-rules add \
--resource-group $RESOURCE_GROUP \
--policy-name $DEV_WAF_POLICY_NAME \
--type Microsoft_BotManagerRuleSet \
--version 1.0 \
--action $ACTION
# Create custom rule
echo -e "${HIGHLIGHT}Add a custom rule 📏${VERBOSE_COLOR}"
az network front-door waf-policy rule create \
--resource-group $RESOURCE_GROUP \
--policy-name $WAF_POLICY_NAME \
--name "${SUBDOMAIN}customrule" \
--priority 1 \
--rule-type MatchRule \
--action $ACTION \
--defer
# Add a condition to the custom rule
az network front-door waf-policy rule match-condition add \
--match-variable QueryString \
--operator Contains \
--values "blockme" \
--name "${SUBDOMAIN}customrule" \
--resource-group $RESOURCE_GROUP \
--policy-name $WAF_POLICY_NAME
# Check custom rules
az network front-door waf-policy rule list \
--resource-group $RESOURCE_GROUP \
--policy-name $WAF_POLICY_NAME
# Custom error page
echo -e "${HIGHLIGHT} You can also use a custom page for 403 errors 💀${VERBOSE_COLOR}"
az network front-door waf-policy update \
--resource-group $RESOURCE_GROUP \
--name $WAF_POLICY_NAME \
--custom-block-response-body $(cat custom-error/403.html | base64)
# Get the WAF policy ID
WAF_POLICY_ID=$(az network front-door waf-policy show \
--resource-group $RESOURCE_GROUP \
--name $WAF_POLICY_NAME --query "id" -o tsv)
# Get custom domains
echo -e "${HIGHLIGHT} Associate custom domains that contains ${SUBDOMAIN} in their name to the security group${VERBOSE_COLOR}"
CUSTOM_DOMAINS_ID=$(az afd custom-domain list \
--resource-group $RESOURCE_GROUP \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--query "[?contains(hostName,'${SUBDOMAIN}')].id" -o tsv)
# Glue the WAF policy to the security policy
az afd security-policy create \
--security-policy-name $SUBDOMAIN \
--resource-group $RESOURCE_GROUP \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--waf-policy $WAF_POLICY_ID \
--domains $(echo $CUSTOM_DOMAINS_ID | tr '\n' ' ')
echo -e "${HIGHLIGHT} Security policy '${SUBDOMAIN}' created 🤓"
Para invocar a este script puedes hacerlo de la siguiente forma, donde especificas el subdominio y si quieres que la política que se genera esté en modo detección o prevención:
## 06 - Assign WAF Policies to the subdomains
# Parameters: subdomain waf_mode
source 06-add-waf-policies.sh www Prevention
source 06-add-waf-policies.sh api Prevention
source 06-add-waf-policies.sh dev Detection
Configurar Diagnostic Settings para Azure Front Door
Siempre es recomendable configurar el apartado Diagnostic settings para poder recuperar los logs y métricas que generan los servicios de Azure. En este caso, es además importante para poder visualizar aquellos eventos que ha generado el WAF en el modo detección, como es el caso de los subdominios dev.* configurados en mi entorno. Para configurarlo puedes valerte del script 07-add-diagnostics.sh:
echo -e "${HIGHLIGHT} First, let's create a workspace for the logs 💙...${VERBOSE_COLOR}"
AZURE_FRONT_DOOR_PROFILE_ID=$(az afd profile show \
--resource-group $RESOURCE_GROUP \
--profile-name $AZURE_FRONT_DOOR_PROFILE_NAME \
--query "id" -o tsv)
# Create workspace for diagnostics
WORKSPACE_NAME="FrontDoorWorkspace"
WORKSPACE_ID=$(az monitor log-analytics workspace create \
--resource-group $RESOURCE_GROUP \
--workspace-name $WORKSPACE_NAME \
--query "id" -o tsv)
# Configure Diagnostic Settings for Azure Front Door
az monitor diagnostic-settings create \
--resource $AZURE_FRONT_DOOR_PROFILE_ID \
--name FrontDoorDiagnostics \
--workspace $WORKSPACE_ID \
--logs "[{category:FrontdoorAccessLog,enabled:true},{category:FrontdoorWebApplicationFirewallLog,enabled:true},{category:FrontDoorHealthProbeLog,enabled:true}]"
# Check diagnostic settings configuration
az monitor diagnostic-settings show \
--resource $AZURE_FRONT_DOOR_PROFILE_ID \
--name FrontDoorDiagnostics
echo -e "${HIGHLIGHT} Done 💙"
Configurar ejemplos para el Ingress Controller
Ahora que ya tienes toda la infraestructura configurada lo último que te queda es probarla 🤘. Para ello tienes otro script, 08-test-ingress-controller.sh, que te permite generar diferentes despliegues de podInfo con distintas configuraciones en base al dominio al que atienda, para comprobar que responden unos u otros en base de la petición:
echo -e "${HIGHLIGHT} Deploy and configure demos for $1 with subdomains $2 $3 $4"
# Create Azure Storage Account
STORAGE_AVAILABLE=$(az storage account check-name --name $STORAGE_ACCOUNT_NAME --query "reason")
if [[ ! ($STORAGE_AVAILABLE -eq "AlreadyExists") ]]
then
echo -e "${HIGHLIGHT} Creating storage account ${STORAGE_ACCOUNT_NAME}...${VERBOSE_COLOR}"
az storage account create \
--name $STORAGE_ACCOUNT_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION
# Enable static website
az storage blob service-properties update \
--account-name $STORAGE_ACCOUNT_NAME \
--static-website
# Get static website url for heroes images
STATIC_WEB_SITE_URL=$(az storage account show \
--name $STORAGE_ACCOUNT_NAME \
--resource-group $RESOURCE_GROUP \
--query primaryEndpoints.web \
--output tsv)
echo -e "${HIGHLIGHT}Upload images ${VERBOSE_COLOR}"
az storage blob upload-batch \
--account-name $STORAGE_ACCOUNT_NAME \
--destination \$web \
--source images
else
echo -e "${HIGHLIGHT} Deploy demos...${VERBOSE_COLOR}"
NAMESPACE_NAME=$(echo $1 | sed 's/\./-/g')
for i in "${@:2}"
do
# Get random file in images
IMAGE_NAME=$(ls images | sort -R | awk 'NR==2')
# brew install gettext
# brew link --force gettext
NAMESPACE_NAME=$NAMESPACE_NAME DOMAIN=$1 SUBDOMAIN=$i STATIC_WEB_SITE_URL=$STATIC_WEB_SITE_URL IMAGE_NAME=$IMAGE_NAME \
envsubst < demos/ingress-demo.yaml | kubectl apply -f -
done
fi
kubectl get all -n $NAMESPACE_NAME
kubectl get ingress -n $NAMESPACE_NAME
Para invocar a este script puedes hacerlo de la siguiente manera:
## 08 - Test Ingress Controller
source 08-test-ingress-controller.sh azuredemo.es www api dev
source 08-test-ingress-controller.sh matrixapp.es www api dev
source 08-test-ingress-controller.sh domaingis.com www api dev
El manifiesto que utilizo para cada uno de los ejemplos es este:
apiVersion: v1
kind: Namespace
metadata:
name: ${NAMESPACE_NAME}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: "${SUBDOMAIN}-${NAMESPACE_NAME}"
namespace: ${NAMESPACE_NAME}
spec:
minReadySeconds: 3
revisionHistoryLimit: 5
progressDeadlineSeconds: 60
strategy:
rollingUpdate:
maxUnavailable: 0
type: RollingUpdate
selector:
matchLabels:
app: "${SUBDOMAIN}-${NAMESPACE_NAME}"
template:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9797"
labels:
app: "${SUBDOMAIN}-${NAMESPACE_NAME}"
spec:
containers:
- name: podinfod
image: stefanprodan/podinfo:6.3.6
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 9898
protocol: TCP
- name: http-metrics
containerPort: 9797
protocol: TCP
- name: grpc
containerPort: 9999
protocol: TCP
command:
- ./podinfo
- --port=9898
- --port-metrics=9797
- --grpc-port=9999
- --grpc-service-name=podinfo
- --level=info
- --random-delay=false
- --random-error=false
env:
- name: PODINFO_UI_COLOR
value: "#34577c"
- name: PODINFO_UI_MESSAGE
value: "${SUBDOMAIN}.${DOMAIN}"
- name: PODINFO_UI_LOGO
value: "$STATIC_WEB_SITE_URL/$IMAGE_NAME"
livenessProbe:
exec:
command:
- podcli
- check
- http
- localhost:9898/healthz
initialDelaySeconds: 5
timeoutSeconds: 5
readinessProbe:
exec:
command:
- podcli
- check
- http
- localhost:9898/readyz
initialDelaySeconds: 5
timeoutSeconds: 5
resources:
limits:
cpu: 2000m
memory: 512Mi
requests:
cpu: 100m
memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
name: "${SUBDOMAIN}-${NAMESPACE_NAME}"
namespace: ${NAMESPACE_NAME}
spec:
selector:
app: "${SUBDOMAIN}-${NAMESPACE_NAME}"
ports:
- name: http
port: 9898
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: "${SUBDOMAIN}-${NAMESPACE_NAME}"
namespace: ${NAMESPACE_NAME}
spec:
rules:
- host: ${SUBDOMAIN}.${DOMAIN}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: "${SUBDOMAIN}-${NAMESPACE_NAME}"
port:
name: http
Este lo voy modificando con envsubst en base al dominio y subdominio que le toque. Obviamente esta configuración está ligada a los dominios que hayas dado de alta. A partir de este momento puedes probar los diferentes subdominios configurados y comprobar que puedes ver el podInfo asociado.
Probar las políticas de WAF
Por último, para probar las políticas de WAF puedes usar este último script, 06-add-waf-policies.sh:
CUSTOM_DOMAIN=$1
echo "${HIGHLIGHT} Let's test ${CUSTOM_DOMAIN} 🧪! ${VERBOSE_COLOR}"
# SQL Injection
echo "SQL Injection: http://${CUSTOM_DOMAIN}?id=1%20or%201=1"
curl -I "http://${CUSTOM_DOMAIN}?id=1%20or%201=1"
# Test custom rule
echo "${VERBOSE_COLOR}Custom rule: http://${CUSTOM_DOMAIN}?id=blockme"
curl -I "http://${CUSTOM_DOMAIN}?id=blockme"
# Check custom page
echo "http://${CUSTOM_DOMAIN}?id=1%20or%201=1"
echo "${HIGHLIGHT}Check Diagnostics Logs"
# Get workspace GUID
WORKSPACE_GUID=$(az monitor log-analytics workspace show \
--workspace-name $WORKSPACE_NAME \
--resource-group $RESOURCE_GROUP \
--query "customerId" -o tsv)
# Get logs
az monitor log-analytics query -w $WORKSPACE_GUID --analytics-query "AzureDiagnostics | where Category == 'FrontDoorWebApplicationFirewallLog' | project requestUri_s, ruleName_s, action_s" -o table
Al que le puedes ir pasando los diferentes dominios que has dado de alta:
## 09 - Test WAF
source 09-test-waf.sh www.azuredemo.es
source 09-test-waf.sh api.matrixapp.es
source 09-test-waf.sh dev.domaingis.com
En el archivo steps.sh tienes las llamadas a cada uno de los scripts de forma ordenada para que puedas ir evolucionando el entorno paso a paso.
Tienes el ejemplo completo en mi GitHub.
¡Saludos!