Publié le: 2013-08-20

Captive portal

Un portail captif est une page qui peut s’avérer essentielle dans certains cas, comme par exemple pour proposer une accès internet temporaire à vos visiteurs. Nous allons voir comment mettre en place un portail captif en utilisant Packet Filter, Apache et un client Radius.

En terme de législation nous n’autorisons les gens à utiliser le réseau que s’ils acceptent la charte informatique. Celle que vous trouverez ici est un modèle inspiré par l’Université Paris XI et repris par l’Institut Optique Graduate School.

Ils vous faudra au préalable un serveur DHCP et un DNS. Ce tutoriel sous entend que vous savez déjà ce qu’est Packet Filter et comment le configurer dans les grandes lignes. Si vous ne souhaitez pas rediriger le trafic HTTP vers squid, il suffit de nater le trafic, comme pour le HTTPS.

En ce qui concerne le partie web, nous utiliserons l’Apache fourni sur OpenBSD (Apache 1.4), en lui ajoutant le support de php5.3 et le client radtest pour notre authentification, contenu dans FreeRadius. Enfin nous aurons besoin de sudo pour utiliser pfctl

export PKG_PATH=http://ftp.fr.openbsd.org/pub/OpenBSD/5.2/packages/amd64/
pkg_add -i php
pkg_add -i freeradius
pkg_add -i sudo

Pour terminer sur la partie réseau, notre portail captif dispose de deux interfaces

  • em0: l’interface de sortie Internet
  • em1: l’interface des clients qui iront sur le portail captif

Configuration de Packet Filter

Dans un premier temps nous allons configurer notre firewall. Nous aurons besoin d’une table dynamique où stocker nos clients une fois qu’ils seront authentifiés. (rappel la configuration de pf est dans /etc/pf.conf).

La dynamique du portail captif marchera de la manière suivante:

  • Si le client n’est pas authentifié, on redirige le trafic HTTP/HTTPS vers la machine elle-même (ici 192.168.1.1 sera son IP, et le réseau à envoyer dans le portail 192.168.1.0/24)
  • Sinon on redirige le flux HTTP vers notre proxy et on nate le flux HTTPS

En résumé, et en plaçant les règles en respectant les bonnes pratiques de Packet Filter on aura l’ensemble de règles suivantes:

#
# macros & tables
#
localip = "192.168.1.1"
localnet = 192.168.1.0/24
outgoing_iface = em0

table <captiveportal_allow> persist

#
# nat & rdr
#
# For allowed clients (nat https and rdr http to squid)
pass out quick proto tcp from <captiveportal_allow> to any port 443 nat-to (em0)
pass in quick proto tcp from <captiveportal_allow> to any port 80 rdr-to $localip port 3128
# For other clients
pass in quick proto tcp from $localnet to any port 80 rdr-to $localip port 80
pass in quick proto tcp from $localnet to any port 443 rdr-to $localip port 443

#
# filtering
#
pass in quick proto tcp from $localnet to $localip port { 80 443 }

Configuration de sudo

Nous devons maintenant configurer sudo qui vous permettre d’utiliser la commande pfctl. Ouvrez le fichier /etc/sudoers et ajoutez y la ligne suivante

www     ALL=(ALL) NOPASSWD: /sbin/pfctl

Configuration d’Apache

La première chose à faire et des désactiver le chroot d’apache afin qu’il puisse accéder à la commande pfctl (en effet la jail est tellement bien faite que si on laisse le chroot il est impossible d’avoir accès au device /dev/pf). Ajoutez le flag -u à Apache. On va au passage ajouter le flag permettant d’activer SSL. Ajoutez le contenu suivant au fichier /etc/rc.conf.local (sous OpenBSD):

httpd_flags="-u -DSSL"

La seconde étape consiste à configurer Apache pour qu’il redirige les erreurs 403 et 404 vers une page de redirection (en l’occurence ici, ce sera login.php). Cette manipulation nous permettra d’intercepter les pages demandées par le client et qui arrivent sur le serveur web Ouvrez le fichier /var/www/conf/httpd.conf et à l’endroit mentionnant les ErrorDocument ajoutez les lignes suivantes:

ErrorDocument 403 /openbsdcaptiveportal.php
ErrorDocument 404 /openbsdcaptiveportal.php

On dimensionne ensuite le nombre de requêtes pouvant être traitées simultanément en allumant 20 processus Apache et en autorisant 1024 clients:

StartServers 20
MaxClients 1024

On va également n’autoriser que le réseau du portail captif à se connecter à celui-ci:

<Directory "/var/www/htdocs">
    Options -Indexes FollowSymLinks
    Order Deny,Allow
    Deny from all
    Allow from 192.168.1.0/24
</Directory>

Enfin on termine par le support SSL.

<VirtualHost _default_:443>
        SSLEngine On
        SSLCertificateFile /etc/ssl/server.crt
        SSLCertificateKeyFile /etc/ssl/private/server.key
</VirtualHost>

Il ne nous reste plus qu’à générer les certificats

 openssl req -x509 -nodes -days 3650 -newkey rsa:4096 -keyout /etc/ssl/private/server.key -out /etc/ssl/server.crt

Scripts d’authentification

La partie intéressante concerne l’authentification. Dans un premier temps, inscrivez la règle suivante dans /var/www/htdocs/index.html

<meta http-equiv="refresh" content="0;URL='openbsdcaptiveportal.php'">

Ce script va permettre qu’un utilisateur demandant la racine d’un site soit redirigé vers le portail captif. Cela permet notamment aux sondes Nagios de ne pas détecter une erreur 403 due à l’interdiction de lister les répertoires.

Voici maintenant le script d’authentification. Il sera explicité plus bas:

<?php
        function redirectIfNotOnPortal() {
                $url = (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"]=="on" ? "https://" : "http://").$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
                if($_SERVER["HTTP_HOST"] != "192.168.1.1") {
                        echo "<meta http-equiv="refresh" content="0;URL='http://192.168.1.1/login.php?url=".$url."'">";
                        exit(0);
                }
        }

        function showInfoChart() {
                $output = "<textarea disabled rows="10" cols="70">Charte d'utilisation

MyCompany

1\. Définition

L'acronyme MC définit MyCompany

2\. Champ d'application

La présente charte définit les règles d'utilisation des ressources informatiques de MC associée à ce service, en confirmité avec législation en vigueur. Elle décrit également les sanctions applicables en cas de non-respect de ces règles et rappelle les principaux textes de référence.

Elle s'applique à toute personne utilisant les ressources de MC Le non-respect de cette charte peut engager la responsabilité du signataire.

3\. Conditions d'accès au ressources informatiques

L'accès aux ressources informatiques de MC est soumis à autorisation et ne peut se faire que dans le cadre professionnel du signataire.

Le moyen d'accès aux ressources informatiques, de quelque nature qu'il soit (mot de passe, certificat, ) est strictement personnel et incessible. Il disparaît dès que son titulaire ne répond plus aux critères d'attribution tels que définis lors de l'attribution du-dit moyen. En cas de perte ou de vol, l'utilisateur doit contacter son correspondant informatique qui prendra les mesures jugées nécessaires.

Il est strictement interdit de mettre en place un équipement ou service informatique qui pourrait interférer dans le bon fonctionnement des ressources informatiques de MC.

4\. Respect de la déontologie informatique

Le signataire s'engage à ne pas effecter intentionnellement des opérations qui pourraient notamment, avoir pour conséquences:

- de dérober ou d'utiliser le moyen d'accès d'un autre utilisateur
- de masquer sa véritable identité ou d'usurper l'identité d'un tiers
- d'intercepter toute communication entre tiers
- d'altérer les données communicant entre tiers
- d'accéder aux données d'un tiers sans leur autorisation, de les supprimer ou de les modifier
- de porter atteinte à la vie privée d'un tiers
- d'interrompre ou d'altérer le fonctionnement normal du réseau ou d'un des systèmes connectés au réseau
- de contourner les contrôles d'accès et restrictions mis en place sur le réseau ou les systèmes connectés au réseau
- de reproduire, représenter, diffuser une oeuvre soumise aux droits d'auteurs

5\. Gestion des réseaux et systèmes informatiques

Le signataire est informé et accepte expressément que la Direction des Services Informatiques de MC procède à des contrôles de la bonne utilisation des ressources informatiques, pouvant avoir comme conséquence la connaissance de données à caractère privé ou confidentiel notamment les traces de connexion conservées pour une durée maximal d'un an.

Il accepte que la Direction des Services Informatiques prenne des mesures d'urgence, comme la limitation ou l'interruption temporaire du fonctionnement d'une partie ou de la totalité des réseaux et services de MC, afin de préserver la sécurité en cas d'incident ou de violation grave, telle que mentionnée dans le paragraphe précédent.

6\. Sanctions

En cas de manquement constaté aux règles énoncées dans la présente charte, le Service Informatique se réserve la possibilité de supprimer immédiatemment, pour une durée indéterminée, une partie ou la totalité des accès aux ressources informatique de MC. Après saisine des autorités compétences, le signataire pourra être poursuivi disciplinairement et/ou pénalement selon la nature du manquement.

7\. Cadre juridique

- loi 78-17 du 6 janvier 1978 relative à l'informatique, aux fichiers et aux libertés
- articles L335-2 et L335-3 du code de la propriété intellectuelle
- loi 2006-961 du 1er août 2006 relative aux droits d'auteurs et aux droits voisins dans la société de l'information
- loi 2004-575 du 21 juin 2004 pour la confiance dans l'économie numérique
- loi 2006-64 du 23 janvier 2006 relative à la lutte contre le terrorisme

8\. Modification de la charte

Le signataire est informé que cette charte peut être modifiée à tout moment.
</textarea>";
                return $output;
        }
        function showLoginContent($err="") {
                $url = (isset($_GET["url"]) && strlen($_GET["url"]) > 0 ? $_GET["url"] : "");
                $output = "<div id="logf"><img id="logi" width="400px" height="224px" src="logo_secu.jpg"/>
                        <div id="desc">Bienvenue sur le portail d'authentification de MyCompany.
                        Ce portail est strictement <b>réservé aux visiteurs</b>. Les <b>personnes enregistrxc3xa9es</b> sur notre annuaire doivent utiliser le rxc3xa9seau WiFi <b>MyCompany</b></div>
                        <form action="login.php?log=1".(strlen($url) > 0 ? "&url=".$url : "")."" method="POST">";
                $output .= "<h3>Charte d'utilisation</h3>".showInfoChart();
                $output .= "<h3>Authentification</h3>".$err."<center><table><tr><td style="text-align: right">Identifiant</td><td><input type="textbox" name="username"></td></tr>
                        <tr><td style="text-align: right;">Mot de passe</td><td><input type="password" name="password"></td></tr></table></center>
                        <input type="checkbox" name="charte" />J'accepte les conditions d'utilisation des ressources informatiques de MyCompany<br />
                        <input type="submit" id="send" value="Identification"></form></div>";
                return $output;
        }

        function showError($errid) {
                $output = "";
                switch($errid) {
                        case 1: $output .= "<div id="err">Certains champs n'ont pas été renseignés !</div>"; break;
                        case 2: $output .= "<div id="err">Identifiant ou mot de passe incorrect !</div>"; break;
                        case 3: $output .= "<div id="err">Impossible d'accéder au serveur RADIUS, veuillez contacter le support informatique !</div>"; break;
                        case 4: $output .= "<div id="err">Authentification correcte, néanmoins vous n'êtes pas autorisé à utiliser ce portail</div>"; break;
                        case 5: $output .= "<div id="err">Vous n'avez pas accepté la charte, accès refusé</div>";
                        case 6: $output .= "<div id="err">Erreur fatale au niveau du serveur. Service indisponible</div>";
                        default: break;
                }
                return $output;
        }

        function showStyle() {
                $output = "<style type="text/css">
                        body {
                                text-align: center;
                                font-size: 13px;
                                font-family: century gothic, verdana, sans-serif;
                                background-color: #FFF;
                        }
                        #err {
                                color: red;
                                width: 90%;
                                padding: 5px;
                                display: inline-block;
                                font-weight: bold;
                                margin: 15px;
                        }
                        #logf {
                                margin-top: 15px;
                                display: inline-block;
                                background-color: white;
                                border-radius: 5px;
                                padding: 0px 25px 25px 25px;
                                background-image: -moz-linear-gradient(center top , #FAFAFA 0px, #DCDCDC 100%);
                                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
                                width: 600px;
                        }
                        #logf input {
                                margin-bottom: 5px;
                        }
                        #logi {
                                margin-bottom: 15px;
                        }
                        #logf textarea, #desc {
                                text-align: left;
                        }
                        #logf #send {
                                margin-top: 15px;
                        }
                        #logok {
                                display: inline-block;
                                vertical-align: middle;
                                background-color: white;
                                border-radius: 5px;
                                padding: 25px;
                                background-image: -moz-linear-gradient(center top , #FAFAFA 0px, #DCDCDC 100%);
                                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
                                width: 600px;
                                top: 50%;
                                margin-left: -300px;
                                position: fixed;
                                margin-top: -100px;
                        }

                        </style>";
                return $output;
        }

        function tryAuthRadius($user,$pwd) {
                $out = "";
                exec("/usr/local/bin/radtest "".$user."" "".$pwd."" radiuserver.my.company:1812 0 radpwd",$out);
                for($i=0;$i<count($out);$i++) {
                        if(preg_match("#Access-Reject#",$out[$i]))
                                return 1;
                        else if(preg_match("#Access-Accept#",$out[$i]))
                                return 0;
                }
                if(!preg_match("#^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]).(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]).(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9]).(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$#",$_SERVER["REMOTE_ADDR"]))
                        return 4;
                return 2;
        }

        redirectIfNotOnPortal();

        // Header
        $output = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html><head>
                <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>MyCompany: Identification visiteur</title>';
        $output .= showStyle();

        $headeroutput = "";
        $url = "";

        if(isset($_GET["log"]) && $_GET["log"] == 1) {
                $username = (isset($_POST["username"]) && strlen($_POST["username"]) > 0 ? $_POST["username"] : "");
                $password = (isset($_POST["password"]) && strlen($_POST["password"]) > 0 ? $_POST["password"] : "");
                $charteaccept = (isset($_POST["charte"]) && strlen($_POST["charte"]) > 0 ? $_POST["charte"] : "");
                if(!$username || !$password) {
                        $tmpoutput .= showLoginContent(showError(1));
                }
                else if(!$charteaccept) {
                        $tmpoutput .= showLoginContent(showError(5));
                }
                else {
                        $errauth = tryAuthRadius($username,$password);
                        if($errauth != 0) {
                                switch($errauth) {
                                        case 1: $erroutput = showError(2); break;
                                        case 2: $erroutput = showError(3); break;
                                        case 3: $erroutput = showError(4); break;
                                        case 4: $erroutput = showError(6); break;
                                        default: $erroutput = ""; break;
                                }
                                $tmpoutput .= showLoginContent($erroutput);
                        }
                        else {
                                $tmpoutput .= "<div id="logok">Vous êtes maintenant identifié et pouvez accéder à Internet.<br /><br />";
                                if(isset($_GET["url"]) && strlen($_GET["url"]) > 0) {
                                        $url = $_GET["url"];
                                        $headeroutput = "<meta http-equiv="refresh" content="5;URL='".$url."'">";
                                        $tmpoutput .= "Vous serez redirigé dans 5 secondes vers l'adresse <a href="".$url."">".$url."</a></div>";
                                }
                                else {
                                        $tmpoutput .= "Vous pouvez maintenant quitter cette page et vous connecter aux sites web de votre choix";
                                }
                                $out="";
                                exec("/usr/bin/sudo /sbin/pfctl -t captiveportal_allow -T add ".$_SERVER["REMOTE_ADDR"]."",$out);
                        }
                }
        }
        else {
                $tmpoutput .= showLoginContent();
        }
        // End
        $output .= $headeroutput."</head><body>".$tmpoutput."</body></html>";
        echo $output;
?>

Ce script affiche une page web comportant 4 éléments fondamentaux:

  • Le logo et nom de l’entreprise
  • La charte d’utilisation
  • L’authentification par login/password
  • La signature de la charte

Au niveau de l’authentification 4 éléments sont testés:

  • L’existence de chacun des champs
  • La signature de la charte
  • L’authentification
  • L’adresse du client

Au niveau mécanisme de redirection, si l’utilisateur provient d’une URL, celle-ci est enregistrée afin de le rediriger sur la page demandée au bout de 5 secondes. En revanche s’il n’y en a pas il sera invité à quitter cette page.

Si l’authentification est réussie, l’utilisateur sera ajouté à la table captiveportal_allow.

Nettoyage des clients

La dernière étape consiste à nettoyer la table. Nous allons ajouter une règle au cron. Tapez crontab -e et ajoutez la ligne suivante:

# Captive portal
*     *     *     *     *     /sbin/pfctl -t captiveportal_allow -T expire 3600 > /dev/null