[couverture de Linux Magazine 88]

Article publié dans Linux Magazine 88, novembre 2006.

Copyright © 2005 - Laurent Gautrot

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

Pourquoi ?

Une plate-forme d'échange non sécurisée est utilisable sur un réseau de confiance, entre des machines de confiance. Sur un réseau intranet, dans une certaine mesure, on peut imaginer que ces critères sont globalement réunis.

Cependant, dès que les données échangées contiennent des informations nominatives ou des informations dont l'on doit garantir la confidentialité, le protocole FTP n'est pas la solution à privilégier, à plus forte raison sur un réseau public comme Internet.

Il faut donc échanger des fichiers contenant des informations à ne pas diffuser entre deux machines. La mise en place d'un réseau privé virtuel basé sur IPSec est complexe et lourde. D'autre part, cette technologie est plutôt utilisée pour l'interconnexion de réseaux, mais pas pour la connexion de deux machines.

L'utilisation d'un tunnel SSH peut nous aider grandement. SSH est un protocole ouvert qui permet d'échanger des données de manière sécurisée entre deux machines sur un réseau non fiable. Avec quelques précautions, il est possible de construire une machine bastion assez robuste pour remplir cette tâche.

Cette machine permettra des connexions SSH, pour obtenir un shell et pouvoir effectuer des opérations de maintenance administratives, ou seulement d'utiliser scp.

Serveur bastion et ses deux types de clients
Serveur bastion et ses deux types de clients

scp regroupe un certain nombre de commandes relativement « inoffensives » comme ls, mv, cp ou scp exécutables par un shell obtenu à travers une connexion SSH ou localement (mais c'est tout de même beaucoup moins intéressant).

En supposant que l'on ne dispose que du shell scponly, il est possible avec un client SSH d'exécuter une commande comme :

   ssh mon_serveur /bin/ls /mon/repertoire

Outils pour la mise en œuvre

Nous aurons besoin d'une distribution Linux minimaliste. Une Debian avec le strict nécessaire sera largement suffisante. deborphan et dpkg avec son option -l pourront être utiles pour déterminer les paquets inutiles installés ou les paquets orphelins.

Il faudra tout de même quelques paquets supplémentaires :

Il faudra configurer un démon SSH (OpenSSH est un excellent choix) de manière à n'autoriser que des utilisateurs connus mais en ne leur laissant pas le loisir de manipuler leurs propres informations d'authentification. Le fichier de configuration /etc/ssh/sshd_config pourrait ressembler à ceci :

   Port 22
   Protocol 2
   HostKey /etc/ssh/ssh_host_rsa_key
   HostKey /etc/ssh/ssh_host_dsa_key
   UsePrivilegeSeparation yes
   KeyRegenerationInterval 3600
   ServerKeyBits 768
   SyslogFacility AUTH
   LogLevel INFO
   LoginGraceTime 600
   PermitRootLogin no
   StrictModes yes
   AllowUsers mon-compte-scponly
   RSAAuthentication yes
   PubkeyAuthentication yes
   AuthorizedKeysFile /etc/ssh/UserKeys/authorized_keys.%u
   IgnoreRhosts yes
   RhostsRSAAuthentication no
   HostbasedAuthentication no
   PermitEmptyPasswords no
   PasswordAuthentication no
   X11Forwarding no
   PrintMotd no
   PrintLastLog no
   KeepAlive yes
   UsePAM yes

On n'y autorise que certains utilisateurs (AllowUsers + PermitRootLogin), les mots de passe et les méthodes d'authentification faibles sont interdites (RSAAuthentication, PubkeyAuthentication, PermitEmptyPasswords, PasswordAuthentication, IgnoreRhosts, RhostsRSAAuthentication).

Les clefs publiques pour la version 2 du protocole (Protocol) sont installées dans un emplacement spécifique hors des répertoires des utilisateurs (AuthorizedKeysFile).

Pour une utilisation avec scponly de base, il suffit de créer le compte et de lui accorder comme shell /usr/bin/scponly. Ce shell doit apparaître dans /etc/shells.

   useradd -s /usr/bin/scponly mon-compte-scponly

Pour une utilisation avec scponlyc (scponly chrooté), le compte doit être ajouté par le script setup_chroot.sh inclus dans la distribution de scponly. Il faut juste le décompresser, puis l'exécuter et répondre aux questions.

   cd /usr/share/doc/scponly/setup_chroot
   sudo gunzip setup_chroot.sh.gz
   sudo ./setup_chroot.sh

Vous pouvez objecter, à juste titre, que modifier les fichiers de /usr/share n'est pas très propre. Il est préférable de décompresser ce fichier à un emplacement que vous maîtrisez, ou même de l'exécuter après décompression grâce à gzip -cd ou son alias zcat.

   zcat /usr/share/doc/scponly/setup_chroot/setup_chroot.sh.gz | sudo sh

Si vraiment la sécurité vous obnubile, gardez présent à l'esprit que scponlyc est setuid root ... Déduisez-en les conséquences en cas de faille de sécurité !

Dans la même optique, la préparation de la cage copie un certain nombre de fichiers, dont en particulier libc6.so. Si une faille de sécurité est découverte dans la bibliothèque C de GNU, vous aurez tout intérêt à préparer une nouvelle cage.

Le pare-feu est configuré de manière assez stricte pour n'autoriser que les connexions en SSH depuis certaines adresses IP. Tout le reste du trafic est ignoré (DROP) et les règles par défaut sont d'ignorer (default policy = DROP).

S'il n'y a pas de serveur DNS accessible, il n'y a pas de résolution de noms. Si c'était le cas, il faudrait ajouter des règles supplémentaires comme :

   ${IPTABLES} -A OUTPUT -p tcp --dport 53 --m state --state NEW -j ACCEPT
   ${IPTABLES} -A OUTPUT -p udp --dport 53 --m state --state NEW -j ACCEPT

Ou de manière plus précise en spécifiant en plus les adresses des serveurs DNS.

Il faut penser à ajouter l'IP d'une machine de rebond si nécessaire dans les règles du pare-feu.

Dans tous les cas aucun trafic n'est autorisé en sortie à l'exception du trafic relatif à des connexions déjà établies.

Un script complet d'initialisation du pare-feu pourrait être le suivant. Il comprend les paramètres start, stop, passoire et status mais n'est vraiment pas destiné à remplacer votre script init (System V). En fait, il sera certainement beaucoup plus intéressant d'utiliser les commandes iptables-save(8) et iptables-restore(8)

   #!/bin/sh
   # Script d'initialisation d'un pare-feu local pour une machine d'échange en DMZ
   # Ce script est fourni sans aucune garantie
   # Quelques remarques :
   # En fonction du trafic entrant, il peut être souhaitable de ne pas enregistrer
   # les tentatives de connexions et donc, de commenter les lignes contenant un
   # saut à la chaîne LOG (« -j LOG »)
   
   IP_LOCAL=W.X.Y.Z
   LISTE_IP_OK=ip.ok
   IPTABLES=/sbin/iptables
   MODPROBE=/sbin/modprobe
   DEFAULT_POLICY=DROP
   
   [ -x ${IPTABLES} ] || exit 2
   [ -x ${MODPROBE} ] || exit 3
   
   # Affichage des règles en cours
   function status() {
    ${IPTABLES} --line-numbers --list -v
   }
   
   # Définit une règle par défaut
   function default_policy() {
    case $1 in
     ACCEPT)
      POLICY=ACCEPT
     ;;
     DENY)
      POLICY=DENY
     ;;
     DROP)
      POLICY=DROP
     ;;
     *)
      POLICY=${DEFAULT_POLICY}
     ;;
    esac
   
    for CHAIN in {IN,OUT}PUT FORWARD
    do
     ${IPTABLES} -P ${CHAIN} ${POLICY}
    done
   }
   
   # Purge des règles.
   # En fonction des règles par défaut (DEFAULT_POLICY), on peut scier la branche
   # sur laquelle on est assis ... :(
   function flush() {
    for ACTION in flush delete-chain
    do
     ${IPTABLES} --${ACTION}
    done
   }
   
   # Validation systématique du dialogue sur la boucle locale
   # À vérifier son utilité ou sa pertinence dans chaque contexte
   function local_ok() {
    ${IPTABLES} -A INPUT -i lo -j ACCEPT
    ${IPTABLES} -A OUTPUT -o lo -j ACCEPT
   }
   
   # Autorisation des connexions entrantes en SSH pour une liste d'adresses IP
   # Cette fonction s'appuie sur le contenu d'un fichier qui contient une liste
   # d'adresses IP (séparées par des espaces ou des sauts à la ligne)
   function user_ok() {
    for SOURCE in `cat ${LISTE_IP_OK}`
    do
     ${IPTABLES} -A INPUT -s ${SOURCE} -p tcp -m state --state NEW --dport 22 -j ACCEPT
     ${IPTABLES} -A INPUT -s ${SOURCE} -p tcp -m state --state RELATED,ESTABLISHED -j ACCEPT
    done
   }
   
   # On jette tout le trafic martien, comme des datagrammes dont l'adresse
   # de provenance est improbable ou impossible.
   # À corriger si vous avez justement des adresses ou des plages d'adresses
   # valides dans ce qui suit.
   function anti_spoof() {
    for SOURCE in \
     255.0.0.0/8      0.0.0.0/8       127.0.0.0/8 \
     192.168.0.0/16   172.16.0.0/12   10.0.0.0/8  \
     ${IP_LOCAL}
    do
     ${IPTABLES} -A INPUT -s ${SOURCE} -j LOG --log-prefix "Spoof IP "
     ${IPTABLES} -A INPUT -s ${SOURCE} -j DROP
    done
   }
   
   # Filtrage et journalisation du trafic entrant
   # À rapprocher de la première remarque concernant la journalisation
   function inbound_filter() {
    ${IPTABLES} -A INPUT -p tcp ! --syn -m state --state NEW -j LOG --log-prefix "Stealth scan ? "
    ${IPTABLES} -A INPUT -p tcp ! --syn -m state --state NEW -j DROP
    ${IPTABLES} -A INPUT -j LOG --log-prefix "Dropped, finally (INPUT) "
    ${IPTABLES} -A INPUT -j DROP
   }
   
   # Filtrage et journalisation du trafic sortant
   # À rapprocher de la première remarque concernant la journalisation
   function outbound_filter() {
    ${IPTABLES} -I OUTPUT 1 -m state --state RELATED,ESTABLISHED -j ACCEPT
    ${IPTABLES} -A OUTPUT -j LOG --log-prefix "Dropped, finally (OUTPUT) "
    ${IPTABLES} -A OUTPUT -j DROP
   }
   
   # Filtrage et journalisation du trafic retransmis
   # À rapprocher de la première remarque concernant la journalisation
   function forward_filter() {
    ${IPTABLES} -A FORWARD -j LOG --log-prefix "Dropped, finally (FORWARD) "
    ${IPTABLES} -A FORWARD -j DROP
   }
   
   # Et c'est ici que l'on applique ce que l'on doit ...
   case $1
   in
   
    # Activation du filtrage : on purge les règles en place. ON définit une règle
    # par défaut qui est de tout jeter, puis on valide le trafic sur la boucle
    # locale, le trafic en provenance des adresses listées explicitement, on
    # jette le trafic martien, on bloque le reste du trafic entrant, sortant ou
    # routé en jetant les scans SYN-stealth ou supposés en entrée
    start)
     ${MODPROBE} ip_tables
     flush
     default_policy DROP
     local_ok
     user_ok
     anti_spoof
     inbound_filter
     forward_filter
     outbound_filter
    ;;
   
    # On purge les règles mais on a une règle par défaut qui est de jeter tout ce
    # qui entre ou qui sort. 
    stop)
     flush
     default_policy DROP
    ;;
   
    # On affiche juste les règles en cours
    status)
     status
    ;;
   
    # Particulièrement cool pour un pare-feu :(
    # /!\ On accepte tout /!\
    passoire)
     flush
     default_policy ACCEPT
     status
    ;;
   
    # Un petit message d'info sur ce qu'il est possible de faire
    *)
     echo "Usage : $0 <start|stop|passoire|status>"
     exit 1
    ;;
   
   esac
   
   exit 0

Pour activer le filtrage et en supposant que vous êtes connecté sur une console virtuelle en local (pas par un pseudo-TTY), l'action start de ce script devrait verrouiller l'accès depuis l'extérieur à votre serveur au seul port TCP 22. Les adresses IP autorisées doivent être listées dans un fichier ip.ok.

Ensuite, le script init iptables fourni par Debian fournit une interface très utilisable pour les sauvegardes de tables et de restaurations. Pour définir le jeu de règles activé par défaut, il vous suffira d'exécuter la commande suivante :

   /etc/init.d/iptables save active

Et si l'on a besoin de réaliser une mise à jour de sécurité ?

Pour la plupart des systèmes de mises à jour automatisées, cela implique une connectivité qui n'est pas toujours au rendez-vous (APT, urpmi, up2date peuvent accéder à des mises à jour depuis des serveurs HTTP ou HTTPS).

Pour une Debian, il y aura besoin de récupérer des paquets depuis un serveur externe, ce qui est a priori contraire au principe d'empêcher les connexions initiées par notre serveur.

Dans le pire des cas, il faudra récupérer des fichiers et les copier manuellement. Perspective peu encourageante.

Oui, mais ... avec OpenSSH, il est possible de créer un tunnel, et surtout dans le cas qui nous intéresse, un tunnel à l'envers. Grâce à SSH, il est possible de créer un tunnel qui va permettre d'accéder temporairement à des serveurs web externes. On réalise ce petit miracle en redirigeant le port TCP de la machine distante vers le port d'écoute du serveur web à partir d'une machine hors de la DMZ. « À l'envers » qualifie un tunnel qui autorise la connexion depuis la machine sur laquelle on se connecte.

En pratique, voici deux exemples qui illustrent ce procédé très simplement pour Debian GNU/Linux (en fait, pour des mises à jour via APT).

Connexion HTTP hors DMZ grâce à une redirection de port TCP distante
pour une mise à jour par APT
Connexion HTTP hors DMZ grâce à une redirection de port TCP distante pour une mise à jour par APT

Exemple1 : Le plus transparent

Le fichier /etc/hosts de la machine en DMZ (dans notre cas ma_machine_bastion) contient la ligne :

   127.0.0.1  localhost.localdomain localhost mon_miroir_debian

Le fichier /etc/apt/sources.list contient une déclaration tout à fait standard :

   deb http://mon_miroir_debian/debian stable main contrib non-free
   deb http://mon_miroir_debian/debian-non-US stable/non-US main contrib non-free
   deb http://mon_miroir_debian/debian-security stable/updates main contrib non-free

Pour mettre à jour la distribution, il suffit alors de :

   ssh -l root -R 80:mon_miroir_debian:80 ma_machine_bastion
   apt-get update && apt-get dist-upgrade
   apt-get clean

La première instruction crée un tunnel du port TCP 80 sur ma_machine_bastion vers le port TCP 80 du serveur web contenant les miroirs. On spécifie un compte root pour la connexion parce que le port TCP d'écoute sur ma_machine_bastion est privilégié (les ports TCP et UDP < 1024 sont privilégiés sur les UNIX et Linux).

La deuxième ligne met à jour les listes de paquets et procède à la mise à jour.

La dernière ligne vide les paquets téléchargés qui n'ont plus de raison de rester dans le cache.

Exemple 2 : Le plus général

Si la connexion directe en root n'est pas autorisée, il vous est alors difficile (impossible est l'adjectif exact ;o) de créer le tunnel à l'écoute sur le port 80. Mais il est aussi possible de créer un tunnel depuis un port non privilégié.

L'entrée dans le fichier /etc/hosts est toujours valable.

Le fichier /etc/apt/sources.list contient une déclaration dans laquelle on précise un port TCP (8000) différent du port standard 80 qui est privilégié. Cette astuce permet de créer le tunnel même depuis un compte non privilégié :

   deb http://mon_miroir_debian:8000/debian stable main contrib non-free
   deb http://mon_miroir_debian:8000/debian-non-US stable/non-US main contrib non-free
   deb http://mon_miroir_debian:8000/debian-security stable/updates main contrib non-free

Pour la mise à jour, il suffit de :

   ssh -R 8000:mon_miroir_debian:80 ma_machine_bastion
   sudo apt-get update && sudo apt-get dist-upgrade
   sudo apt-get clean

La première ligne redirige le port 8000 de ma_machine_bastion vers le port 80 de mon_miroir_debian.

On est alors connecté avec un compte non priviligié et c'est pour cette raison que l'on doit utiliser sudo (ou une tout autre moyen pour élever ses privilèges) pour les commandes apt-get.

Si le miroir de mises à jour est accessible en HTTPS, le principe reste le même. Néanmoins, il faudra se baser sur le port TCP 443.

Pour un protocole comme FTP, plusieurs ports TCP sont utilisés. Le port 21 est utilisé pour les commandes, et les réponses pour les ports négociés sont utilisées sur le port TCP 20 (ftp-data). Ensuite, les données transitent sur des port TCP supérieurs à 1024. Cette méthode ne s'applique donc pas, et il faudra se rabattre sur d'autres méthodes (proxy FTP, VPN, etc.).

Mééééh, ça marche pas avec WinSCP ...

Que faire si vous utilisez un client graphique comme WinSCP et que vous utilisez la version chrootée ? Vous butez sur des messages grossiers concernant une fin de fichier ?

Il y a une opération supplémentaire à réaliser qui consiste à compiler un fichier groups.c. Il faut pour cela un gcc, mais il est conseillé de le compiler sur une autre machine pour ne pas avoir à laisser de compilateur sur une machine bastion. Ça se fait simplement avec :

   gcc /usr/share/doc/scponly/groups.c -o groups

Et en copiant le fichier ainsi obtenu dans les répertoires bin ET usr/bin de la cage de l'utilisateur vous évitez normalement ce problème. Le source C révèle uniquement des print ... rien de bien dangereux à priori.

Conclusion

Grâce à quelques logiciels libres et en s'appuyant sur un protocole ouvert et sécurisé, il est possible de configurer une plate-forme d'échange de fichiers à l'aide d'une machine-bastion qui pourrait se trouver sur une DMZ ou directement connectée à internet.

Références

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]