Introduction

App Service et Azure SQL sont deux composants PaaS publiques. Cela veut dire qu’ils disposent d’adresses IP publiques.

Azure SQL ou Azure Database pour MySQL ou Azure Database pour PostgreSQL dispose d’un mécanisme de firewall qui permet de filtrer les connexions entrantes.

Ce firewall dispose d’une case à cocher permettant d’autoriser ou non le trafic Azure. Malheureusement, ce trafic Azure ne se limite pas au trafic de sa souscription. Ainsi n’importe quelle VM ou App service sera autorisée.

Pour autant, App Service dispose d’une liste d’IP de sortie que l’on pourra utiliser pour restreindre l’accès à la base sans aller vers de l’App Service Environment.

Autant il est possible de manuellement récupérer cette liste d’IP dans le portail Azure et ajouter ces IP au firewall de la base, autant on va essayer d’automatiser au mieux la solution par l’utilisation d’ARM template.

Il ne s’agit pas ici de se focaliser uniquement sur le firewall comme moyen unique de protection. Mais autant le configurer correctement pour agir comme première barrière.

Organisation de l’ARM template

Nous allons ici utiliser le mécanisme de modèle imbriqué ou nested templates. Ainsi, la création et la configuration des ressources vont se passer en 3 étapes comme montré sur la figure ci-dessous 

Organisation de l’ARM template

Les adresses IP d’un App Service

Il est possible de récupérer les adresses IP d’un App Service et de les rendre disponible dans l'`output` de l'ARM Template comme suit :

"outputs": {
    "possibleOutboundIps": {
        "type": "array",
        "value": "[split(reference(parameters('siteName'), variables('apiVersion')).possibleOutboundIpAddresses, ',')]"
    }
}

Grâce à la fonction split, on récupère directement un tableau des adresses IP.

On peut aussi noter l’utilisation possibleOutboundIpAddresses, ce qui donne une liste assez large mais qui va empêcher des arrêts de service si l’App Service doit changer d’infrastructure pour une raison ou pour une autre.

Formatage des règles

La liste des IP peut être récupérer à partir du template parent grâce à la syntaxe suivante, avec appserviceNestedDeployment le nom du déploiement du nested template pour l’App Service :

[reference('appserviceNestedDeployment').outputs.possibleOutboundIps.value]

L’objectif de l’étape de formatage va être créer un nouveau tableau d’objet prêt à l’emploi pour la dernière étape. On va donc parcourir le tableau d’IP grâce à une boucle et créer un tableau d’objet avec le nom de la règle (sur la base d’un incrément) et une des IP (startIpAddress et endIpAddress sont égaux).

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "inputArray": {
            "type": "array",
            "metadata": {
                "description": "string array of ip addresses"
            }
        },
        "ruleNamePrefix": {
            "type": "string",
            "maxLength": 26,
            "metadata": {
                "description": "prefix to use in the name of the rule"
            }
        }
    },
    "variables": {
        "outputArray": {
            "copy": [
                {
                    "name": "items",
                    "count": "[length(parameters('inputArray'))]",
                    "input": {
                        "startIpAddress": "[parameters('inputArray')[copyIndex('items')]]",
                        "endIpAddress": "[parameters('inputArray')[copyIndex('items')]]",
                        "name": "[concat(parameters('ruleNamePrefix'), copyIndex('items'))]"
                    }
                }
            ]
        }
    },
    "resources": [],
    "outputs": {
        "firewallRules": {
            "value": "[variables('outputArray').items]",
            "type": "array"
        }
    }
}

Le tableau ainsi généré est placé dans l'output pour être récupérer dans l’étape finale.

Configuration du firewall

Dans le cas de MySQL (le cas est similaire pour Azure SQL), on va donc parcourir le tableau d’objet précédemment créé pour créer la règle de firewall.

{
    "name": "[concat(parameters('mysqlServerName'),'/',parameters('firewallrules')[copyIndex()].name)]",
    "type": "Microsoft.DBforMySQL/servers/firewallRules",
    "apiVersion": "2017-12-01",
    "location": "[parameters('location')]",
    "dependsOn": [
        "[concat('Microsoft.DBforMySQL/servers/', parameters('mysqlServerName'))]"
    ],
    "copy": {
        "name": "firewallRulesCopy",
        "count": "[length(parameters('firewallrules'))]"
    },
    "properties": {
        "StartIpAddress": "[parameters('firewallrules')[copyIndex()].startIpAddress]",
        "EndIpAddress": "[parameters('firewallrules')[copyIndex()].endIpAddress]"
    }
}

ARM templates

Cette mécanique a été utilisée pour les templates de phpOIDC. On peut donc retrouver un exemple complet sur GitHub.

Il existe certainement plein de façons d’arriver au même résultat. L’utilisation des nested templates permet d’assurer une certaine flexibilité. Ainsi, la liste des IP aurait pu être concaténée avec une autre liste, grâce à la fonction union. Également, cette méthode peut être conservée pour Azure SQL ou Azure Database pour MySQL ou Azure Database pour PostgreSQL.

Je recommande l’utilisation du script deploy.ps1 qui facilite grandement l’utilisation des modèles imbriqués. Il permet de :

  1. Créer un compte de stockage s’il n’existe pas

  2. Copier les ARM templates.

  3. Générer un token SAS

  4. Déployer l’ARM