Amon est un pare-feu hautement configurable développé par et pour l'Éducation nationale et utilisé ailleurs (ministères, collectivités territoriales, entreprises). Il est basé sur la distribution GNU Linux Eole, qui propose un ensemble de solutions intégrées intranet-internet, (du serveur de fichiers dans les établissements scolaires jusqu'au concentrateur VPN inter-académique, en passant par la gestion de parc et la configuration automatisée de serveurs). Au sein de l'Éducation nationale, les questions de sécurité des réseaux se posent de manière cruciale. L'outil Era, « Éditeur de Règles pour Amon », conçu à l'origine comme un éditeur de règles de pare-feu, est devenu un framework de compilation et d'interprétation de directives de sécurité. Il a permis de mettre en place dans les établissements scolaires une politique globale de sécurité à l'échelle nationale.
1. Eole et Amon
1.1 Historique
Le pare-feu Amon est né au centre informatique du rectorat de l'académie de Dijon en 2000, sous l'égide de Luc Bourdot, actuellement ingénieur de recherche et chef de projet Eole. Au départ, il s'agissait d'installer des pare-feu dans quelques établissements scolaires de l'académie avec des outils libres. Amon était alors basé sur ipchains. Le passage à iptables est venu assez rapidement avec le noyau 2.4 un an plus tard.
Luc ne se doutait certainement pas à l'époque de l'ampleur qu'allait prendre ses travaux. Au début, la génération des règles iptables était un ensemble de scripts Bash. Il est devenu, ensuite, nécessaire de mieux gérer la pile d'instructions iptables. De plus, il était nécessaire de gérer les données de configuration des serveurs (notamment pour pouvoir les centraliser). Il a été décidé de tout enregistrer dans des fichiers XML.
Aujourd'hui, les différents fichiers de configuration, scripts et autres sont générés à partir d'interfaces graphiques codées en python-GTK (gen_config). Il est possible de l'exécuter sur le serveur au prompt, depuis des applications Web ou GTK décentralisées.
En complément, Bruno Boiget, ingénieur d'études au pôle développement travaille sur un système de Single Sign On (SSO) pour toutes les applications Web Eole. Les bibliothèques de programmation réseau de bas niveau utilisent python-twisted (le framework de programmation réseau Twisted Matrix).
Et comme tous les produits, Eole, c'est du libre, diffusé sous licence CECILL. Des paquets Ubuntu sont mis à disposition sur le dépôt Eole, et pendant que j'y suis, pour compiler des paquets, eole-epack est un outil graphique de compilation à partir d'un dépôt Subversion (merci à Jérôme et Joël). C'est assez pratique. Cet outil sera même bientôt utilisable depuis l'extérieur. Dans la ferme de compilation Eole, on génère une distrib entière un peu comme on génère une page Web, c'est-à-dire sans plus y penser (mais si, mais si ;)
Une grande partie des logiciels Eole sont des logiciels de configuration. À l'installation, ce qui prend le plus de temps, c'est la copie du CD sur le disque dur. Pour la configuration globale, la technologie Créole (pour « Création Eole ») est utilisée. Elle est rapide et permet l'industrialisation de la mise en place des serveurs.
1.2 Amon et l'outil Era
Revenons à Era. C'est un logiciel en deux parties installé par défaut sur les pare-feu Amon. La première partie du logiciel est graphique (GTK). La deuxième partie est un outil en ligne de commande, le compilateur-générateur de règles iptables, de QOS (qualité de service) et de règles authentifiantes (grâce à l'intégration de NuFW dans Amon). Era peut être lancé directement sur un Amon ou bien depuis n'importe quel poste de travail ou même... installé sur un poste Windows (...il n'y a pas de honte. Au labo, il y a des Vmware avec des Windows dedans rien que pour voir si ça s'installe bien ;).
Fig. 1 : L'interface d'Era au lancement, la fenêtre principale représente le tableau des flux
À première vue, l'interface d'édition est déjà très différente de celle d'un éditeur de règles classique à la Firewall Builder. La configuration de pare-feu est orientée flux de directives. Nous y reviendrons.
Sur un Amon, après avoir édité et enregistré le XML dans l'interface Era, il faut lancer la ligne de commande en root pour appliquer les règles de pare-feu :
$ service bastion restart
Si vous êtes à distance, vous pouvez stocker la configuration XML sur un serveur de centralisation des configurations (le serveur Zephir). Ce serveur est utilisé dans le cadre de la gestion de parc pour déployer les modèles. Il est ainsi possible de reconfigurer plusieurs centaines de pare-feu en quelques minutes. Pratique en cas d'alerte de sécurité.
Sur un autre type de pare-feu qu'Amon, vous ne pouvez pas bénéficier de cette possibilité d'envoi de la configuration, mais vous pouvez simplement générer un script de règles iptables à lancer manuellement. Si vous êtes directement sur un Amon, vous pouvez en profiter pour réinstaller la configuration complète du système en tapant en root :
$ reconfigure
Voilà, quand même, c'est pas trop compliqué. Si vous voulez adapter le pare-feu à vos différents besoins, il va vous falloir comprendre un peu le fonctionnement de l'interface graphique.
2. La représentation du réseau avec Era
2.1 Les niveaux de sécurité
Voici l'idée principale : la problématique d'intégrité et de cohérence doit être assurée au moment de la configuration du pare-feu. Et pour raisonner sur un réseau, il est nécessaire de simplifier l'information, de sorte que l'observation du système fournisse une vue d'ensemble.
Le modèle est en fait une gestion par flux : je donne plus de droits à ce à quoi je fais le plus confiance (j'autorise du plus sécurisé vers le moins sécurisé) et je donne moins de droits à ce à quoi je fais le moins confiance. C'est le modèle de l'écoulement fluide : ça passe, sauf directive contraire (directive de type barrage), de ce qui est le plus sécurisé vers le moins sécurisé (flux descendant), et ça ne passe pas, sauf directive contraire (directive de type pont ou aqueduc), de ce qui est le moins sécurisé vers le plus sécurisé (flux montant).
Centrons-nous sur le pare-feu (c'est la zone la plus sécurisée). Autour, il y a internet (la zone extérieure, c'est la zone la moins sécurisée). Puis, d'autres cartes réseau ou tunnels (dans un établissement scolaire, il y a typiquement deux autres interfaces réseau : la zone pédagogique et la zone administrative et au moins un tunnel vers l'académie).
Affectons ensuite des niveaux de sécurité à chacune de ces zones.
Fig. 2 : Les niveaux de sécurité
Ce choix détermine une vue par flux entre chaque zone. Par zone, entendons une carte réseau. À l'intérieur, vont être définies des directives de sécurité. Et, enfin, une politique par défaut (par exemple, sauf directive contraire, ce qui vient de l'extérieur est peu sécurisé, donc doit être bloqué).
2.2 Le modèle devient exécutable
Au niveau du code, le modèle en mémoire après chargement du XML ressemble à ce qu'on peut reproduire ici au prompt Python après avoir importé les objets de la bibliothèque :
>>> from era import fwobjects
>>> z1 = Zone('z1', 30)
>>> z2 = Zone('z2', 50)
>>> assert z1 < z2
True
>>>
Pour créer une zone, il faut un nom et un niveau de sécurité (en plus de l'interface réseau). Les zones sont alors immédiatement ordonnées les unes par rapport aux autres, ce qui donne le tableau des flux montants et descendants une fois inséré dans l'objet matrice de flux :
>>> matrix = MatrixModel()
>>> matrix.add_zone(z1)
>>> matrix.add_zone(z2)
>>> # deux flux sont créés :
[Flux : [z1 <===> z2], Flux : [z2 <===> z1]]
>>>
Le modèle exécutable apparaît comme bien plus pratique dans le cadre des méthodes agiles (en principe, au labo, on programme toujours en binôme, et les développements sont dirigés par les tests). Il dérive directement de la représentation que l'on se fait du réseau, la logique métier, sans avoir à être décrit dans un ou plusieurs langages de haut niveau ou un langage de configuration.
Bien sûr, ces créations d'objets sont transparentes. Elles se font naturellement depuis l'interface graphique (par exemple, la fenêtre d'édition des zones). Ce qu'il est important de constater, c'est que, derrière l'interface graphique, il y a un modèle objet qui évolue au fur et à mesure de la conception du pare-feu.
2.3 Les directives de sécurité
Au plan des décisions à prendre, il existe des niveaux d'analyse qu'il faut privilégier. La notion de directive représente ce niveau d'abstraction. Qu'est-ce qu'une directive ? C'est l'abstraction d'une action comme « j'autorise les profs à allez surfer sur internet » ou « j'interdis aux élèves l'accès à la zone administrative ».
« j'autorise, j'interdis, je redirige vers... », des actions simples qui vont produire des quantités de lignes d'instruction réseau, ou de QOS, ou de règles authentifiantes dont l'imbrication est complexe, mais dont nous sommes sûrs qu'elle reste cohérente. De plus, la configuration de pare-feu devient possible pour quelqu'un qui ne maîtrise pas tous ces langages de bas niveau (iptables, trafic control...).
Un mécanisme de directives optionnelles permet d'activer ou de désactiver des directives depuis diverses sources, notamment depuis des interfaces Web. Cela permet de définir des consignes qui ne sont réellement appliquées que sur demande. Par exemple, il est possible de décider s'il faut couper momentanément l'accès au Web ou au chat pour une salle informatique pendant un cours.
Les directives dépendent de l'échelle. À une grande échelle, (c'est-à-dire une petite surface, celle d'une salle info par exemple), il faut pouvoir rendre la configuration adaptable. À une plus petite échelle (une plus grande surface, l'échelle d'un établissement ou d'une académie), on peut choisir de rediriger systématiquement les élèves vers le proxy établissement.
En plus, un mécanisme de directives dites « cachées » est prévu : ces directives ne s'activent que si nécessaire. Par exemple, Amon peut détecter s'il dispose d'un proxy installé sur le serveur et ainsi l'activer en conséquence.
Enfin, à une échelle encore plus petite, l'échelle nationale, Eole fournit les moyens techniques pour appliquer de grandes orientations et préconisations de sécurité en proposant des modèles. Ce sont des fichiers XML, qu'on appelle « fichiers de modèles de pare-feu prédéfinis » (modèle 3 zones, 4 zones, 5 zones).
Ces modèles XML peuvent être « templatisés », c'est-à-dire que les valeurs des IP et des réseaux ne sont pas renseignées directement dans le fichier de manière à assurer la généricité. Il est aussi possible d'imbriquer hiérarchiquement les modèles pour en faciliter la maintenance (un modèle 4 zones hérite d'un modèle 3 zones, si on modifie le 3 zones les autres modèles répercutent la modification).
Bien souvent, dans les académies, il suffit de modifier un seul fichier XML pour mettre à jour en quelques minutes plusieurs centaines de pare-feu Amon. Allez donc tenter de faire la même chose avec un simple éditeur de règles !
2.4 Les directives dans le modèle
Pour poursuivre sur les objets du modèle, il se passe quelque chose comme ceci dans la matrice au moment de l'ajout d'une directive :
Soit à partir du XML :
>>> dir_node = '''<directive id='1' service='serv1' priority='1' action='1' libelle='directive'>
<source name='extr1' /><source name='extr2' /><destination name='extr3' />
</directive>'''
>>> xmldoc = parseString(dir_node)
>>> directive = domparsers.instantiate_directive_from_dom(xmldoc)
Fig. 3 : L'éditeur de directives. Ici pour affecter une authentification à la directive, il suffit de glisser-déposer le groupe d'utilisateurs choisi.
Soit depuis l'interface d'édition des directives, il se passe alors cela :
>>>directive = Directive([extremite1], [extremite2]], service1, ACTION_DENY, 1)
Puis, dans tous les cas, la directive est insérée dans la matrice au bon endroit dans les flux :
>>>matrix.add_directive(directive)
2.5 Utilisation de l'interface graphique
Voilà, maintenant que vous en savez plus sur le modèle, la prise en main de l'interface graphique sera plus évidente. Le but du jeu est de créer des directives. Au lancement, vous avez le tableau des flux. Vous pouvez alors ajouter ou supprimer des zones. Ce tableau a autant de lignes et de colonnes que de zones. En cliquant droit sur une zone, vous pouvez accéder à sa configuration et y créer des extrémités (ajout de machines ou de sous-réseaux).
Dans le tableau, il y a autant de cases que de flux. Cliquez sur une case pour ajouter une directive, vous obtiendrez la liste des directives existantes. Lors de l'ajout, vous accéderez à l'éditeur de directives fonctionnant par glisser-déposer d'extrémités, de services et/ou d'utilisateur.
Pour entrer un peu dans les détails, une extrémité est une IP, une plage d'IP ou un sous-réseau. Un service est un ensemble protocole, port ou plage de ports. L'action peut-être une autorisation, une interdiction, une redirection ou du DNAT ou SNAT. La matrice comptabilise les directives et leur priorité, et les ajoute aux bons emplacements dans les flux.
Pour plus de détails, je vous renvoie au manuel d'utilisation.
3. Le compilateur
3.1 Fonctionnement du compilateur
Du point de vue de la sécurité, il n'y a pas de niveau d'analyse privilégié : au final tout est ramené à une pile d'instructions réseau. Récapitulons : ces échelles sont toutes très différentes, c'est le modèle et la définition des directives qui permet de les unifier. Mais, comment garantir l'intégrité et la cohérence d'une pile entière d'instruction iptables ? Et entre les différents réseaux ? Et par rapport aux différentes technologies (ipsec, QOS...) ?
Le compilateur de règles récupère les directives, les classes par types (directives standards en ACCEPT ou en DROP, de redirection, de DNAT ou de SNAT) et génère les règles iptables, les règles de QOS et les règles authentifiantes correspondantes.
Regardons comment ça se passe au niveau du code et décomposons les différentes étapes :
Récupérons la matrice :
>>> from era.initialize import initialize_app
>>> matrix_model = initialize_app('3zones.xml')
Initialisons le compilateur iptables :
>>> from era.compiler import Compiler
>>> from era.iptwriter import IPTWriter
>>> compiler = Compiler(IPTWriter, sys.stdout)
Passons la matrice en paramètre au compilateur :
>>> compiler.compile(matrix_model)
Les règles iptables sont alors générées (dans ce cas, sur la sortie standard).
Il y a des processeurs de directives. Ce qui revient, lorsqu'on dispose d'une directive, à faire ceci :
>>> proc = era.processors.get_processor(directive)
>>> rules = proc.process()
>>> for rule in rules:
>>> writer.append_rule(rule)
À un processeur, correspond un type de directive (DNAT, SNAT,...).
Observez la dernière ligne ci-dessus (l'itération sur la liste). Ce sont des objets modélisant une règle iptables.
3.2 La génération des règles iptables
Maintenant, nous arrivons au niveau des règles iptables. Au passage, le compilateur intègre une modélisation objet de bas niveau des règles iptables.
Là aussi, plutôt que de créer une syntaxe de haut niveau qui pourra ensuite être compilée en langage de plus bas niveau, avec Era, il y a un modèle exécutable, mais sans syntaxe (sans langage associé). Chacun sait que la définition d'un langage et de son arbre de syntaxe est lourde en temps et en moyens. Les projets comme HLFL (high level firewalling language) ou WallFire cherchent depuis des années à définir des syntaxes possibles.
Au niveau du compilateur, une directive n'est pas nécessairement associée à une unique règle iptables. Suivant le type de directive, le comportement du compilateur est différent. En effet, il se peut que le compilateur rajoute des règles implicitement, par exemple, dans le cas d'une redirection, une règle iptables de FORWARD doit être accompagnée d'une règle INPUT (si je redirige les élèves depuis internet sur le port 3128 d'un proxy de la dmz, je dois bien ouvrir ce port sur le bastion d'abord).
Une seule directive, si elle est composées d'un groupe de services, d'une liste d'extrémités ou autre, peut générer une grande quantité de lignes iptables. Une directive va générer autant de règles qu'elle compte d'extrémités, de services et des règles implicites.
Toutes ces règles sont indispensables pour que ça marche. Une seule action de haut niveau doit se traduire en plusieurs règles iptables.
3.3 Le filtrage authentifié et la qualité de service
Les règles authentifiantes sont gérées par NuFW.
Era récupère les directives authentifiantes au niveau du compilateur iptables et rajoute l'instruction -J NFQUEUE. De plus, pour les redirections, un marquage est affecté. Cette marque est spécifique à chaque groupe d'utilisateur. Un fichier d'ACL est généré sous cette forme :
[directive_description]
proto = 6
srcport = 1024-65535
outdev = eth0
indev = eth2
gid = 10001
...
Le GID permet de déterminer à qui appartient cette ACL.
Quant à la QOS, elle est limitée à la patte externe. Un script est généré. Il représente une sous-partie du modèle global de la matrice de flux.
C'est réglable depuis l'interface graphique. Il s'agit de diverses poignées de déplacement qui permettent de répartir la bande passante vers les différentes zones. La taille allouée est proportionnelle et la vue correspond simplement à des pourcentages de la bande passante totale (que l'on définit).
3.4 La simulation et les tests
La sémantique d'Era (c'est-à-dire ce que fait effectivement le programme) est conforme à sa spécification (c'est-à-dire ce que l'on voulait que le programme fasse). Les techniques de tests de génération des règles sur des jeux de réseau ou de simulation sur un réseau physique passent difficilement à l'échelle : en bref, il est impossible de traiter tous les cas. Notre manière de vérifier a été de faire des tests, de constater que le programme correspond bien dans un grand nombre de cas donnés.
Il y a eu pas mal de tests et on aurait pu encore pousser ça avec des simulateurs comme nf-sim, un simulateur Netfilter dans l'espace utilisateur. En exécutant ou en simulant le programme dans un grand nombre d'environnements suffisamment représentatifs des exécutions possibles, il devient clair qu'on peut dormir sur ses deux oreilles.
3.5 La compilation dynamique
Récapitulons : une règle iptables générée aura beau être syntaxiquement correcte et bien positionnée, si elle ne s'inscrit pas dans un contexte elle pourra être sémantiquement fausse (la sémantique dans le sens où ce que fait effectivement la pile d'instructions réseau).
Le choix qui a été fait est une modélisation qui prend de la distance par rapport à une syntaxe. C'est l'interface graphique, donc le modèle, qui permet de fédérer les décisions, ainsi que le compilateur et son comportement de bas niveau.
Y a-t-il un moyen de faire du « reverse », de décompiler ? De passer d'un résultat de iptables -L ou iptables-save à un modèle objet ? C'est possible en l'état actuel du compilateur. C'est possible parce que le compilateur est statique, c'est-à-dire non contextuel.
Mais, si l'on veut dépasser les limitations liées au cumul des caractéristiques d'une directive (l'authentification, la qualité de service, le marquage, la journalisation...), le modèle lui-même doit devenir dynamique.
La modélisation par flux et les espaces d'objets qui constituent le modèle doivent devenir dynamiquement adaptables, contextuels. Un peu comme une syntaxe dont la grammaire serait contextuelle. Cela devient indispensable notamment si l'on veut approfondir les possibilités dans Era liées au filtrage authentifié.
Conclusion
Dans l'avenir, le compilateur devra modifier son comportement en fonction de la pile des instructions réseau qu'il rencontre. Si la compilation et les espaces d'objets dynamique vous intéressent, je vous suggère pour approfondir cette question d'aller faire un tour du côté de PyPy, le compilateur dynamique du langage dynamiquement typé Python, codé en Python lui-même, et de revenir lire le code d'Era dans... pas pour tout de suite en tout cas...
Que Patrick McHardy change nos habitudes en nous proposant nftables, peu importe. C'est très certainement mieux. Les règles en seront plus condensées, mais les problématiques d'accumulation resteront : il s'agit toujours d'une pile d'instructions réseau. Que les règles soient générées ou non, si on n'y prend pas garde, elles finissent par tourner au plat de spaghettis, se confondre, puis, au final, se contredire les unes les autres, d'où l'importance de disposer d'une vue d'ensemble.
Jusqu'à présent, les outils Eole se sont adaptés à une échelle de travail nationale sur des milliers de serveurs et avec des centaines de milliers d'utilisateurs (les élèves, les profs et les administratifs). Nous allons continuer nos efforts.
Remerciements
Merci à toute l'équipe Eole (eole@ac-dijon.fr), à son chef de projet ainsi qu'aux différents acteurs et contributeurs Era, tout particulièrement Samuel Morin (samuel.morin@ac-dijon.fr), Klaas Tjebbes (klaas.tjebbes@ac-dijon.fr), Emmanuel Garette (egarette@inl.fr), Jérome Soyer (jsoyer@inl.fr), Joël Cuissinat (joel.cuissinat@ac-dijon.fr), ainsi qu'à Bruno, Gaston, Laurent, David, Adrien...
Références
- Le projet Eole :
http://eole.orion.education.fr
- La page d'accueil Era : http://eole.orion.education.fr/diff/article.php3?id_article=29
- Le wiki sur Era : http://eole.orion.education.fr/wiki/index.php/Era et la documentation sur la matrice de flux : http://eole.orion.education.fr/wiki/index.php/EraPresentation
- Une introduction à l'interprétation abstraite : http://www.di.ens.fr/~cousot/AI/IntroAbsInt.html
- L'interprétation abstraite dans PyPy et les espaces d'objets : http://codespeak.net/pypy/dist/pypy/doc/theory.html