## page was renamed from CransTechnique/Services/Re2oServices/FireWall ## page was renamed from CransTechnique/Services/FireWall ## page was renamed from CransTechnique/FireWall = 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. <> :s 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. La table est généré à partir des infos de re2o, cf [[CransTechnique/Services/Re2oServices|re2o-services]] == Le nouveau parefeu == === Utilisation === Il se lance avec {{{/var/local/re2o-services/firewall/main.py restart}}} (potentiellement avec l'option --force) 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.subnet_ports = api_client.list("firewall/subnet-ports/") self.interface_ports = api_client.list("firewall/interface-ports/") self.normal_users = api_client.list("users/normaluser/") self.verbose = False self.action = None self.export = False self.role = getattr(firewall_config, 'role', None) self.interfaces_settings = getattr(firewall_config, 'interfaces_type', None) self.nat_settings = getattr(firewall_config, 'nat', None) self.portail_settings = getattr(firewall_config, 'portail', None) 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 firewall 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_config.py}}} installé par Bcfg2 ({{{/var/local/bcfg2/Python/var/local/re2o-services/firewall/firewall_config.py}}} Exemple : {{{ ─( 15:34:36 )─< /var/local/re2o-services/firewall >───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────(git)─[firewall/crans]─[ 0 ]─ jacomme@gulp $ cat firewall_config.py #! role = ['routeur4'] ### Specify each interface role interfaces_type = { 'routable' : ['eno1.1', 'ens1f0.21', 'ens1f0.22', 'ens1f0.23', 'ens1f0.24'], 'sortie' : ['ens1f0.26', 'ens1f0.1132'], 'admin' : ['eno1.2', 'eno1.3'] } ### Specify nat settings: name, interfaces with range, and global range for nat ### WARNING : "interface_ip_to_nat' MUST contain /24 ranges, and ip_sources MUST ### contain /16 range nat = [ { 'name' : 'Wifi', 'interfaces_ip_to_nat' : { 'ens1f0.26' : '185.230.76.0/24', 'eno1.1' : '138.231.144.0/24', 'ens1f0.1132' : '138.231.144.0/24', }, 'ip_sources' : '10.53.0.0/16' }, { 'name' : 'Filaire', 'interfaces_ip_to_nat' : { 'ens1f0.26' : '185.230.77.0/24', 'eno1.1' : '138.231.145.0/24', 'ens1f0.1132' : '138.231.145.0/24', }, 'ip_sources' : '10.54.0.0/16' } ] }}} 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. === Fonction des différents serveurs === ==== Synchronisation en re2o-services ==== Certaines instances du parefeu (odlyd, gulp, ipv6 et zamok) ont besoin d'être regen, par ex à l'ajout d'un adhérent, ou d'ouvertures d'un port, en conséquence ils sont gérés dans la page machines/services; et regen par le cron re2o-services quand il est nécessaires. Les autres instances (radius, routeur) sont purement statiques; et seulement générées au boot (donc cron @reboot --force dans leur etc/cron.d) ==== Routage principal (Odlyd, gulp et ipv6-zayo) ==== Gulp et odlyd ont la même instance de parefeu. Leur rôle est routeur4, ce qui signifie que seule la partie v4 donc iptables est générée et appliquée. En plus des fonctions de base (filtrage icmp/ssh/etc); ce parefeu contient en gros la protection en forward de toute la zone crans. Il s'appuie uniquement sur les interfaces pour être souple. Il possède des chaines filter, nat et mangle. {{{ def routeur4(self, table): """Methode appellée spécifiquement pour le parefeu v4""" if table == "filter": self.base_filter() if self.verbose: print("Filter : filtrage ports 4") self.filtrage_ports(ip_type='4') if self.verbose: print("Filter : limit ssh connexion forward") self.limit_ssh_connexion_forward() if self.verbose: print("Filter : limit connexion src ip") self.limit_connexion_srcip() elif table == "mangle": if self.verbose: print("Mangle : Mise en place des logs") self.log() if self.verbose: print("Mangle : Réglage correct du MSS") self.mss() elif table == "nat": for nat_to_do in self.nat_settings: if self.verbose: print("Nat : priv" + nat_to_do['name']) self.nat_prive_ip(nat_to_do) }}} ===== Filter ===== Enfin il contient la fonction filtrage port, qui récupère auprès de re2o-server, les politiques par default de chaque range et les politiques particulières. Ce serait fastidieux de print tout le code ici... disons que la chaine filtrage-port est automatiquement appellée pour tout paquet entrant et sortant de la zone crans (FORWARD). D'abord est appliquée la règle par default, puis les règles particulières, et si rien ne match, le paquet est rejeté. ===== Nat ===== Ce qui est remarquable ici est la chaine de nat, comme son nom l'indique qui va nater de manière statique et par plage de ports (25 ip privées sur 2000 ports chacun d'une ip publique). On a donc 25*255 ip privées potentiellement natées. {{{ nat_prive_ip_plage = nat_type['ip_sources'] for nat_ip_range in range(1, 26): range_name = 'nat' + nat_prive_ip_plage.split('.')[1] + '_' + str("%02d" % nat_ip_range ) self.init_nat(range_name, decision="-") self.add_in_subtable("nat", subtable, '-s ' + '.'.join(nat_prive_ip_plage.split('.')[:2]) + '.' + str(nat_ip_range) + '.0/24 -j ' + range_name) for nat_ip_range in range(1, 26): range_name = 'nat' + nat_prive_ip_plage.split('.')[1] + '_' + str("%02d" % nat_ip_range) for nat_ip_subrange in range(16): subrange_name = range_name + '_' + str(hex(nat_ip_subrange)[2:]) self.init_nat(subrange_name, decision="-") }}} etc ===== Mangle ===== Dans cette chaine, on log tout ce qui rentre et sort de la zone + mise en place du mss pour éviter les prb de drop dû au MTU. ==== Serveur des adhérents (zamok) ==== La particularité principale de ce serveur (tout le reste c'est base_filter), est la chaine forbid_adm qui empèche les utilisateurs présents sur le serveur autre que des personnes dotées d'un droit critique (donc ayant un droit dont critical=True), de sortir sur adm. Très simplement : {{{ def forbid_adm(self, subtable='ADMIN-VLAN'): """Interdit aux users non admin de parler sur les vlans admin""" self.init_filter(subtable, decision="-") for interface in self.interfaces_settings['admin']: self.jump_traficto("filter", interface, "OUTPUT", subtable) for user in self.normal_users: self.add_in_subtable("filter", subtable, """-m owner --uid-owner %s -j REJECT""" % user['uid']) }}} ==== Serveur radius ==== Le but est de protéger les radius sur leur interface extérieure, et de n'accepter en input que ce qui vient des serveurs proxy radius federez. On bloque tout le reste. (on accepte que udp 1812-1814) {{{ def accept_freerad_from_server(self, subtable='RADIUS-SERVER'): """Accepte uniquement le trafique venant des serveurs radius federez""" self.init_filter(subtable, decision="-") for interface in self.interfaces_settings['sortie']: self.jump_traficfrom("filter", interface, "INPUT", subtable) for server in self.config_firewall.radius_server: self.add_in_subtable("filter4", subtable, """-s %s -p %s -m multiport --dports %s -j ACCEPT""" % (server['ipaddr'], server['protocol'], ','.join(server['port']))) self.add_in_subtable("filter6", subtable, """-s %s -p %s -m multiport --dports %s -j ACCEPT""" % (server['ip6addr'], server['protocol'], ','.join(server['port']))) self.add_in_subtable("filter", subtable, """-j REJECT""") }}} ==== Serveur routeur ==== IL s'agit du serveur du portail captif sur accueil; le but est d'autoriser à ne parler qu'à un certain nombre de sites (dont intranet, wiki) et nater le cas échéant pour donner accès par ex au site de paiement de comnpay sur internet. {{{ def portail(self, table): if table == "filter": self.base_filter() if self.verbose: print("Filter : autorisation des ip en sortie") self.captif_autorized_ip() if table == "nat": if self.verbose: print("Nat : nat et captures les connexions du portail masquerade") self.nat_connexion_portail() self.capture_connexion_portail() }}} On a base filter évidemment, on autorise en filter à ne parler qu'à un set défini d'ip. Enfin on nat toutes les connexions sortantes vers le portail captif (site web du crans). Les réglages sont dans self.portail_settings (self.portail_settings = getattr(firewall_config, 'portail', None)) Ex : {{{ portail = { 'autorized_hosts' : { 'tcp' : { '138.231.136.12' : ['22'], '138.231.136.98' : ['20', '21', '80', '111', '1024:65535'], '138.231.136.145' : ['80', '443'], etc }}} On autorise que le set d'ip choisi : {{{ def captif_autorized_ip(self, subtable='FILTRE-IP-PORTAIL'): ... for protocol in self.portail_settings['autorized_hosts']: for ip, ports in self.portail_settings['autorized_hosts'][protocol].items(): self.add_in_subtable("filter4", subtable, """-p %s -d %s -m multiport --dports %s -j ACCEPT""" % (protocol, ip, ','.join(ports))) self.add_in_subtable("filter4", subtable, """-j REJECT""") }}} Enfin on capture le reste pour l'envoyer vers le portail : {{{ def capture_connexion_portail(self, subtable="PORTAIL-CAPTIF-REDIRECT"): ... for protocol in self.portail_settings['autorized_hosts']: for ip, ports in self.portail_settings['autorized_hosts'][protocol].items(): self.add_in_subtable("nat4", subtable, """-p %s -d %s -m multiport --dports %s -j RETURN""" % (protocol, ip, ','.join(ports))) for ip_range, destination in self.portail_settings['ip_redirect'].items(): for protocol, ip in destination.items(): for ip_dest, ports in ip.items(): self.add_in_subtable("nat4", subtable, """-p %s -s %s -m multiport --dports %s -j DNAT --to %s""" % (protocol, ip_range, ','.join(ports), ip_dest)) }}} == Cahier des charges == === Fonctions de base commmunes === ==== 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]] <> * Blocage des connexions entrantes vers les réseaux admin (adm, admbornes etc) sur l'interface correspondante * Oui. Mais je pense qu'il manque la fin de la phrase. -- [[Wiki20-100]] <> * 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]] <> * 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]] <> ==== 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 == <> ---- * CatégorieCrans * CatégoriePagePublique