CransWiki:

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 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

Fonctions supplémentaires du parefeu de sortie (odlyd, sable et ipv6)

Filter

Nat

Mangle

Fonctions supplémentaires du parefeu adhérents

Filter

Legacy - pour les divers parefeu du crans


CransWiki: CransTechnique/Services/Re2oServices/re2o-firewall (dernière édition le 2018-11-18 02:14:37 par Chirac)