Article publié dans Linux Magazine 88, novembre 2006.
Copyright © 2005 - Laurent Gautrot
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
.
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
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 :
scponly
fournit un shell minimaliste qui ne permet que l'exécution des
commandes scp. Il est compatible avec la plupart des clients SCP pour
Microsoft Windows (WinSCP
au moins). Il offre aussi une possibilité de mise en
cage (chroot
) ;
iptables
regroupe les commandes qui permettent de manipuler
Netfilter
(le pare-feu du noyau Linux pour peu que le noyau ait été compilé
avec l'option CONFIG_NETFILTER=Y
) ;
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
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).
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.
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.).
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.
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.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.