Le pare-feu
Le pare-feu (en anglais firewall) se place entre la machine qui a accès à Internet et Internet lui-même. Il protège l'une de l'autre et inversement en limitant les types de connexions qui ont le droit d'entrer et de sortir.
Sommaire
Il existe actuellement 2 pare-feus en production.
Le plus récent, /usr/scripts/gestion/gen_conf/firewall.py, remplace les anciens firewall v6 et v4 sur zamok, les radius et le routeur v6.
L'ancien parefeu v4, gen_conf/firewall4/firewall.py reste en production sur odlyd et sable, temporairement.
Dans les deux cas, les règles sont générées par un script, qui termine en remplaçant le set de règles courant par celui qu'il a généré. Cette opération est atomique (on utilise iptables-restore, donc il n'y a pas de phase pendant laquelle tout est ouvert (ou fermé) le temps que la régénération se termine.
Le nouveau parefeu
Utilisation
Il se lance avec /usr/scripts/gestion/gen_conf/firewall.py restart et se coupe avec l'instruction stop. Celle dernière injectera un set de règles vide, avec policy ACCEPT.
Principes
Architecture générale
Le script est une classe iptables qui a différentes sous-fonctions selon ce qu'on veut générer. Le début contient les fonctions de base, d'intialisation des chaînes iptables, etc.
def __init__(self): self.nat4 = "\n*nat" self.mangle4 = "\n*mangle" self.filter4 = "\n*filter" self.nat6 = "\n*nat" self.mangle6 = "\n*mangle" self.filter6 = "\n*filter" self.conn = shortcuts.lc_ldap_readonly() […] def commit(self, chain): self.add(chain, "COMMIT\n") def commit_filter(self): self.add("filter4", "COMMIT\n") self.add("filter6", "COMMIT\n") def commit_mangle(self): self.add("mangle4", "COMMIT\n") self.add("mangle6", "COMMIT\n") def commit_nat(self): self.add("nat4", "COMMIT\n") self.add("nat6", "COMMIT\n") def add(self, chain, value): setattr(self, chain, getattr(self, chain) + "\n" + value) def add_in_subtable(self, chain, subtable, value): if '4' in chain: self.add(chain, "-A " + subtable + " " + value) elif '6' in chain: self.add(chain, "-A " + subtable + " " + value) else: self.add(chain + '4', "-A " + subtable + " " + value) self.add(chain + '6', "-A " + subtable + " " + value)
On remarque donc déjà qu'il y a 6 attributs principaux à notre classe, filter4, filter6, nat4, nat6, mangle4 et mangle6.
Ainsi, au moment de l'injection des règles qui est faite au démarrage du parefeu, (iptables-restore) :
def restore_iptables(self, mode='4'): """Restoration de l'iptable générée""" if mode == '6': global_chain = self.nat6 + self.filter6 + self.mangle6 command_to_execute = ["sudo","-n","/sbin/ip6tables-restore"] else: global_chain = self.nat4 + self.filter4 + self.mangle4 command_to_execute = ["sudo","-n","/sbin/iptables-restore"] process = subprocess.Popen(command_to_execute, stdin=subprocess.PIPE, stdout=subprocess.PIPE) process.communicate(input=global_chain.encode('utf-8')) if self.export: print(global_chain)
Très simplement donc, on remet les règles ensembles pour le parefeu 6 d'un coté, le parefeu 4 de l'autre, et on restore. Savoir si on veut générer que le 4, le 6 ou les 2 est un argument de la fonction en question, on verra un peu plus tard comment c'est fait.
Ajout des chaines
L'idée a été de coder un parefeu unifié. Ainsi, on essaye de faire en sorte d'avoir le plus possible des tables compatibles avec iptables et ip6tables. Par exemple, l'acceptation des connexions déjà établies sera le même en v4 ou en v6
def accept_established(self, subtable='ESTABLISHED-CONN'): """Accepte les connexions déjà établies""" self.init_filter(subtable, decision="-") self.jump_all_trafic("filter", "FORWARD", subtable) self.jump_all_trafic("filter", "INPUT", subtable) self.add_in_subtable("filter", subtable, """-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Ainsi l'appel à init_filter(subtable) va initialiser la sous table ESTABLISHED-CONN dans la chaîne filter. Mais, étant donné que cette chaine n'existe pas (rappellez-vous, c'est filter4 ou filter6), il va l'ajouter dans les 2. Idem pour jump_all_trafic, qui va attraper tout le trafic sur toutes les interfaces, en v4 ou en v6, dans le forward ou dans l'input, et l'envoyer vers la sous-table ESTABLISHED-CONN. Enfin, la dernière règle peuple la sous-table avec une règle acceptant tout le trafic déjà établi.
Génération du firwall en v4 , v6 ou les 2 ?
Vous l'avez compris, on ne va pas choisir les mêmes chaines suivant si c'est un routeur v4, un routeur v6 ou encore zamok.
Regardons comment est appelé restore-iptables :
def reload(self): """Recharge le parefeu""" self.gen_mangle() self.gen_nat() self.gen_filter() if any('6' in role for role in self.role): self.restore_iptables(mode='6') return if any('4' in role for role in self.role): self.restore_iptables(mode='4') return self.restore_iptables(mode='6') self.restore_iptables(mode='4')
Ca parait clair, on regarde donc self.roles pour savoir si c'est un v4, un v6 ou les 2, et on restore. Mais comment faire la différence par exemple entre le parefeu v6 de zamok et le parefeu v6 de la VM ipv6 du routage ?
Sélection des rôles
Les rôles sont spécifiés dans firewall.py dans config.
Exemple :
firewall_config = { 'sable' : { 'role' : ['routeur4'], 'interfaces' : { 'sortie' : ['eth1.26', 'eth1'], 'non-routables' : ['eth0.2', 'eth0.3', 'eth0.4', 'eth0.7', 'eth0.9'], 'routables' : ['eth0.1', 'eth0.21', 'eth0.22', 'eth1.23', 'eth1.24'], }, 'dev' : { 'out' : 'eth1', 'zayo' : 'eth1.26', 'fil' : 'eth0.1', }, }, 'ipv6-zayo' : { 'role' : ['routeur6'], 'interfaces' : { 'sortie' : ['ens22'], 'non-routables' : ['ens19', 'ens23'], 'routables' : ['ens2', 'ens1', 'ens22', 'ens21', 'ens20', 'ens18'], }, }, 'zamok' : { 'role' : ['users'], 'interfaces' : { 'non-routables' : ['eth0.2'], 'routables' : ['eth0.1'], }, },
On spécifie donc ici les roles, ne pas mettre 4 ou 6 générera un parefeu v4 et v6. On spécifie également les interfaces non routables, routables, et si il y a lieu l'interface de sortie vers le monde extérieur.
Ne pas mettre de roles générera un parefeu minimal, cf plus bas.
Application des rôles
Regardons donc gen_filter :
def gen_filter(self, empty=False): self.init_filter("INPUT") self.init_filter("FORWARD") self.init_filter("OUTPUT") if not empty: if self.verbose: print("Filter : icmp") self.filter_icmp() if self.verbose: print("Filter : icmpv6") self.filter_icmpv6() if self.verbose: print("Filter : accept established") self.accept_established() for role in self.role: if hasattr(self, role): getattr(self, role)('filter') self.commit_filter()
On initialise donc ici les tables FILTER, FORWARD et OUTPUT de filter, en v4 et en v6. On ajoute des règles générales (accept established, filter_icmp) communes à tous les parefeu. Enfin, on appelle la fonction self."role" avec l'option filter, pour l'ensemble des roles définis pour self.
Sélections des chaînes à ajouter
Exemple pour le rôle routeur6 (role donc du routeur ipv6), on appelle base_filter (une factorisation de plusieurs règles, vous le trouverez dessous) et différentes fonctions necessaires, tel que le filtrage ports en v6, la limitation des connexions ssh, etc.
def routeur6(self, table): """Methode appelée spécifiquement pour le parefeu v6""" if table == "filter": self.base_filter() if self.verbose: print("Filter : interdit les machines blacklistées en forward") self.blacklist_hard_forward() if self.verbose: print("Filter : filtage ports v6") self.filtrage_ports(ip_type='6') if self.verbose: print("Filter : limit connexions forward") self.limit_ssh_connexion_forward() if self.verbose: print("Filter : Limit connexion src") self.limit_connexion_srcip() elif table == "mangle": self.log() else: pass
Base filter est appelée par pas mal de roles, (zamok, routeur etc) c'était donc plus joli de tout factoriser.
def base_filter(self): if self.verbose: print("Filter : reseaux non routables") self.reseaux_non_routables() if self.verbose: print("Filter : bl hard") self.blacklist_hard() if self.verbose: print("Filter : connexion input") if self.verbose: print("Limitation des connexions") self.limit_ssh_connexion_input() self.limit_connexion_dstip()
Voilà,a priori, là on a tout pour générer notre fichier iptables. Evidemment, se référer aux docstrings pour voir ce que chaque fonction fait.
Cahier des charges
Fonctions de base commmunes
Fonctions du parefeu v4 et v6 sur les routeurs de sortie
Filter
- Acceptation de toute connexion déjà établie
- Acceptation du ping v4 et v6 en input et forward
ping ⊊ ICMP. Réfléchir à si on veut tout l'ICMP (ne pas oublier que les "ça a pas marché", c'est aussi de l'ICMP, mais pas du ping). -- Wiki20-100 2018-05-17 13:46:48
- Blocage des connexions entrantes vers les réseaux admin (adm, admbornes etc) sur l'interface
Oui. Mais je pense qu'il manque la fin de la phrase. -- Wiki20-100 2018-05-17 13:46:48
- DROP du traffic illégitime venant de l'extérieur (broadcast et multicast qu'on n'est pas censés recevoir, IP non routables, etc.)
- Blacklist hard des connexions entrantes et sortantes des utilisateurs blacklistés
- Filtrage des ports en v4 et v6 sur les interfaces d'entrée de la zone crans
- Limitation des connexions ssh entrantes pour éviter les attaques par bruteforce
- Limitation des connexions tcp et udp en input (limit connexions dstip) afin d'éviter le flood de connexions (cf attaque sur le wiki par un adhérent)
Fonctions supplémentaires du parefeu de sortie (odlyd, sable et ipv6)
Filter
- Blacklist hard des utilisateurs blacklistés en forward
- Filtrage des ports en v4 et v6 sur les interfaces de sortie de la zone crans
- Limitation des connexions ssh en forward pour éviter les attaques bruteforce
Pas sûr que ce soit vraiment notre soucis -- Wiki20-100 2018-05-17 13:46:48
- Limitation des connexions sortantes tcp/udp pour éviter le flood d'une machine vérolée vers l'éxtérieur (limit connexions srcip)
Pareil -- Wiki20-100 2018-05-17 13:46:48
Nat
- NAT des ip privées filaires et wifi en v4
Mangle
- Log de toutes les nouvelles connexions sur les interfaces de sortie et routables
Fonctions supplémentaires du parefeu adhérents
Filter
- Interdiction de l'output vers adm pour non nounous/apprentis
- Interdiction de l'output vers odlyd pour les non à jour de cotiz (todo)
Legacy - pour les divers parefeu du crans