Introduction
Il y a une approche que je trouve particulièrement pertinente à mettre en place sur les services mutualisés (e.g. interconnexion on premise, exposition internet entrante/sortante,…): n’autoriser que les types de ressources strictement nécessaires à la fourniture du service et bloquer toutes les autres. On réduit ainsi considérablement la surface d’attaque (et la capacité d’usage de services pour se latéraliser en cas de compromission de la souscription) . On empêche également le déploiement de nouveaux types de ressources sans que celui-ci est fait l’objet d’une analyse sécurité.
Cette approche est plus facilement réalisable sur les services mutualisés car la liste de services nécessaires est généralement connue, très restreinte, et sans évolutions fréquentes.
Sur AWS, j’utilise des Service control Policy (SCP) que j’applique sur le(s) compte(s) de production du service mutualisé. Dans le cloud de Microsoft, cela passe par les azures policies. Azure fournit une policy permettant de lister les types de ressources à autoriser: Allowed resource types. Cependant, celle-ci implique de lister un à un chaque type de ressources ce qui peut vite devenir fastidieux. Si je souhaite autoriser toutes les ressources réseaux , je souhaite pouvoir spécifier Microsoft.Network/* et non chaque ressource réseau possible.
Dans cet article, je vous propose de voir comment construire une azure policy custom qui permet d’autoriser des ensembles de services avec des wildard.
Analyse de la builtin policy
voici un extrait de la définition de la builtin policy Allowed resource types dans sa version 1.1.0:
...
"parameters": {
"listOfResourceTypesAllowed": {
"type": "Array",
"metadata": {
"description": "The list of resource types that can be deployed.",
"displayName": "Allowed resource types",
"strongType": "resourceTypes"
}
},
...
"policyRule": {
"if": {
"not": {
"field": "type",
"in": "[parameters('listOfResourceTypesAllowed')]"
}
},
"then": {
"effect": "[parameters('effect')]"
...
La première chose à remarquer est l’usage de strongType dans le paramètre qui permet de passer un tableau de ressources à autoriser: listOfResourceTypesAllowed. Celui-ci impose de fournir des types de ressources qui existent. Ceux avec des wildcard comme Microsoft.Network/* ne seront pas reconnus comme un type de ressource valide.
On pourrait donc penser qu’il suffit de dupliquer cette policy en retirant le strongType. C’est là qu’arrive le second élément: la policy rule utilise la condition in qui recherche une correspondance exacte dans un array de chaine de caractères (i.e de type string):
"in": ["stringValue1","stringValue2"]
Ainsi, il faudrait trouver une condition qui autorise l’usage de wildcard. Or, la seule disponible est like/notLike qui n’est applicable que sur des chaines de caractères:
"like": "stringValue"
Cherchons une méthode qui permettrait d’itérer sur chaque élément de l’array tout en opérant une comparaison de type like qui permettrait l’usage de wildcard.
Première approche: à éviter
Fort des constats sur la builtin policy décrits à la section précédente, une première approche simpliste serait de créer une azure policy qui comparerait le type de la ressource à une chaine de caractères. En reprenant l’extrait précédent, cela donnerait:
...
"parameters": {
"resourceTypeToAuthorize": {
"type": "String",
"metadata": {
"description": "resource type a autoriser",
"displayName": "resource type to authorize",
}
},
"policyRule": {
"if": {
"not": {
"field": "type",
"notLike": "[parameters('resourceTypeToAuthorize')]"
}
},
"then": {
"effect": "[parameters('effect')]"
}
...
Ensuite on créerait une initative Azure qui appellerait n fois l’azure policy ,une pour chaque type (ex: Microsoft.Network/publicIPAddresses) ou ensemble de ressources (e.g. Microsoft.Network/publicIPAddresses) que l’on passerait dans le paramètre resourceTypeToAuthorize.
Cela peut fonctionner, de la même manière que l’on pourrait réussir à vider une piscine avec une cuillère à café. Cependant, c’est s’infliger bien du mal. En effet, bien que l’on peut aller jusqu’à 1000 policies par initiative d’après les quota azure, cela deviendra inmaintenable bien avant. Quant aux débugs, je vous laisse imaginer…
Non clairement il faut trouver mieux.
L’opérateur count à la rescousse
Le langage de policy azure dispose d’un opérateur count utilisable dans les policyRule. Il permet de compter le nombre d’élément d’un array qui satisfait une ou plusieurs conditions. On peut ensuite appliquer une condition sur le résultat obtenu, comme par exemple tester si le nombre de match est supérieur à un certain seuil.
C’est exactement ce qu’il nous faut!
On pourrait compter le nombre d’éléments dans notre tableau de type de ressources autorisées qui match le type de ressource de la requête:
- l’utilisation de la condition
likedans le compte permet d’utiliser des wildcard dans les éléments de notre array; - si le nombre de match est supérieur à 1, cela signifie que la ressource de la requête fait partie du tableau;
- si le nombre de match est à 0 cela signifie que le type de ressource de la requête n’est pas dans le tableau (et n’est donc pas autorisé)
La structure de count se présenterait ainsi :
"count": {
"value": "[[parameters('listOfResourceTypesAllowed')]",
"name": "res",
"where": {
"field": "type",
"like": "[[current('res')]"
}
}
Ici, on récupère listOfResourceTypesAllowed des paramètres de la policy. le nom res sera utilisé pour le référencer dans les conditions du bloc count (i.e. dans le champs where). Dans la condition, on utilise l’opérateur like ainsi fonction current() qui retourne l’élément du tableau en cours d’évaluation à chaque itération.
La PolicyRule devient:
"policyRule": {
"if": {
"not": {
"count": {
"value": "[[parameters('resourceTypesAllowed')]",
"name": "res",
"where": {
"field": "type",
"like": "[[current('res')]"
}
},
"greater": 0
}
},
"then": {
"effect": "[[parameters('effect')]"
}
}
Si la valeur renvoyée par count n’est pas plus grande que 0 alors l’effet lui aussi passé en paramètre sera appliqué. les effets possibles seront:
- Deny;
- Audit;
- Disabled
J’aurais pu également utiliser la condition lower au lieu de greateret du bloc Not.
Ainsi, on obtient l’azure policy suivante:
{
"properties": {
"displayName": "Allowed resource types with wildcard",
"description": "This policy allows to define which resource types or set of resources that are allowed and deny all the others",
"policyType": "Custom",
"mode": "All",
"metadata": {
"version": "1.0.0"
},
"parameters": {
"resourceTypesAllowed": {
"type": "array",
"metadata": {
"displayName": "Allowed resource types",
"description": "list of allowed resource types or set of resource type with wildcard"
}
},
"effect": {
"type": "String",
"allowedValues": [
"Audit",
"Deny",
"Disabled"
],
"defaultValue": "Deny"
}
},
"policyRule": {
"if": {
"not": {
"count": {
"value": "[[parameters('ressourceTypesAllowed')]",
"name": "res",
"where": {
"field": "type",
"like": "[[current('res')]"
}
},
"greater": 0
}
},
"then": {
"effect": "[[parameters('effect')]"
}
}
}
Quelques recommandations complémentaires
Pour finir, il faut veiller à respecter ces quelques recommandations avant de vous lancer (vos collègues vous remercieront):
- à chaque modification de policy, tester avec l’effet cible (ici
Deny) en l’appliquant sur un ressource group ou une souscription de test; - si un environnement de pré-production reproduisant à l’identique la production est disponible , appliquer la policy en
denyet demander aux équipes métiers de tester afin de s’assurer qu’aucun type de ressource ne manque; - Si aucun environnement de pré-production est disponible, commencer par appliquer la policy en audit pour s’assurer qu’aucun type de ressource ne manque
Il faut également veiller à ne pas bloquer les évolutions de services. Aussi, j’ai tendance à recommander d’appliquer la policy en mode deny sur les souscriptions de productions. Sur les souscriptions de hors productions, et à condition qu’elles ne disposent d’aucun lien avec les souscriptions de productions, je recommande de la positionner en mode audit: cela permet de laisser de la liberté aux équipes dans le développement de nouvelles fonctionnalités, tout en ayant la capacité d’identifier de nouveaux services nécessaires à faire autoriser avant mise en production (processus à inclure avant mep). Il s’agit toujours de trouver un équilibre entre l’innovation et les risques sécurité.
Enfin, si cette approche peut parfaitement convenir sur des services mutualisés dont l’impact en cas de compromission peut rapidement être catastrophique, elle peut ne pas être adaptée à des comptes applicatifs. Cela dépends de nombreux facteurs dont:
- le modèle opérationnel et sécuritaire de l’entreprise;
- le niveau de maturité des équipes en terme de sécurité;
- les risques identifiés;
- …
Aussi, pas de généralisation sans étude.
C’est tout pour moi. A vous les studios!
Références
Count operator - Microsoft learn
Author policies for array properties on Azure resources - Microsoft learn
*Image d’illustration du post générée via Firefly
