Confinement de processus sous FreeBSD (Jail) et Linux (OpenVZ)

Magazine
Marque
GNU/Linux Magazine
Numéro
118
Mois de parution
juillet 2009


Résumé

La virtualisation est omniprésente dans tous les secteurs de l'informatique. Elle se présente sous plusieurs formes [1] (virtualisation complète, para-virtualisation, etc.). Cependant, est-il toujours nécessaire de déployer un hyperviseur pour isoler les services partageant une même machine physique ? Nous allons voir deux méthodes de confinement de processus : Jail et OpenVZ respectivement sous FreeBSD et Linux.


Body

1. Historique

Au commencement était chroot. Chroot est un appel système : il prend comme unique argument un répertoire et y fixe la racine du système de fichiers pour le processus qui l'invoque. Il existe deux moyens pour une application de se chrooter : soit elle le fait nativement (argument sur la ligne de commande), soit elle est lancée avec la commande chroot.

Illustrons le cas d'un programme qui effectue nativement un chroot : named (un composant de la suite DNS BIND). La page de manuel de ce service mentionne un option -t qui attend un répertoire où se chrooter. S’il est utilisé dans la commande, l'argument est passé à la fonction ns_os_chroot qui contient le code suivant :

void
ns_os_chroot(const char *root) {
[...]
if (root != NULL) {
#ifdef HAVE_CHROOT
if (chroot(root) < 0) {
isc__strerror(errno, strbuf, sizeof(strbuf));
ns_main_earlyfatal("chroot(): %s", strbuf);
}
#else
ns_main_earlyfatal("chroot(): disabled");
#endif
[...]
}

Cette fonction effectue donc le chroot et traite les erreurs. Si on poursuit la lecture du man, un point intéressant est soulevé : « This option should be used in conjunction with the -u option, as chrooting a process running as root doesn't enhance security on most systems; the way chroot() is defined allows a process with root privileges to escape a chroot Jail ». Sur tous les systèmes (même les plus mauvais), un programme tourne sous une certaine identité associée à un jeu de privilèges. Par défaut, named tourne avec l'identité de root pour utiliser le port 53 (les ports inférieurs à 1024 sont privilégiés et ne peuvent être ouverts que par root). Or, un programme qui tourne en root dans un chroot, cela ne doit tout simplement pas arriver. Être root dans un chroot, c'est être root sur le système complet. Sur les UNIX, un appel système nommé setuid fixe l'identité (passée en paramètre) sous laquelle doit tourner le programme qui l'invoque. Justement dans le main.c de named, la fonction qui suit le ns_os_chroot est ns_os_minprivs. Cette fonction appelle dans son code source une autre fonction ns_os_changeuser qui effectue le setuid (et le setgid pour le groupe) : 

void
ns_os_changeuser(void) {
[...]

if (setgid(runas_pw->pw_gid) < 0) {

isc__strerror(errno, strbuf, sizeof(strbuf));
ns_main_earlyfatal("setgid(): %s", strbuf);
}

if (setuid(runas_pw->pw_uid) < 0) {

isc__strerror(errno, strbuf, sizeof(strbuf));
ns_main_earlyfatal("setuid(): %s", strbuf);
}
[...]

Un compte utilisateur doit donc être associé à l'exécutable named. En utilisant la combinaison des deux arguments (-t racine et -u utilisateur) au lancement de la commande, on a un processus chrooté qui tourne sous un compte banalisé. Est-ce suffisant ? Dans le cas de named, oui, car le programme est développé avec un minimum de soin (ou pas selon certains [2]). Un commentaire lors de l'appel de ns_os_changeuser par ns_os_minprivs dit « Call setuid() before threads are started ». Pourquoi est-ce si important ? Le changement d'identité doit s'opérer le plus tôt possible dans l'exécution d'un programme. Sinon, une attaque de type « race condition » [3] risque de compromettre les privilèges de root (et donc l'étanchéité du chroot).

Une commande chroot existe sur les systèmes UNIX. Elle prend comme argument un répertoire et éventuellement une commande (avec ses arguments). Voyons le code source de cette commande : 

int chroot_main(int argc, char **argv)
{
[...]

if (chroot(*argv) || (chdir("/"))) {

fatalError("chroot: cannot change root directory to %s: %s\n",
*argv, strerror(errno));
}
argc--;
argv++;
if (argc >= 1) {
prog = *argv;

execvp(*argv, argv);

} else {
prog = getenv("SHELL");
if (!prog)
prog = "/bin/sh";

execlp(prog, prog, NULL);

}
[...]
}

On voit donc l'appel à l'appel système chroot (comme avec named) et aussi que la partie commande est traitée par les fonctions execvp si un programme est fourni en argument ou execlp sinon. Ces deux fonctions sont des surcouches de l'appel système execve. Le programme passé en paramètre va donc recouvrir les segments de textes, les données et la pile du programme appelant. Il hérite cependant du PID (identifiant de processus) et des descripteurs en cours d'utilisation (un descripteur est un fichier ouvert). Les signaux en attente sont effacés. Le changement d'identité n'est donc pas effectué par la commande chroot : il est de la responsabilité du programme appelé.

Nous avons survolé les aspects système du chroot : voyons maintenant les implications au niveau administration système. Nous savons qu'un programme exécutable utilise des bibliothèques (les fameux .so des répertoires /lib). Pour connaître les bibliothèques utilisées par un programme, il faut utiliser ldd :

garnett@airjordan:~$ ldd /usr/bin/vi
 Linux-gate.so.1 => (0xb7f50000)
 libncurses.so.5 => /lib/libncurses.so.5 (0xb7ef3000)
 libseLinux.so.1 => /lib/libseLinux.so.1 (0xb7ed9000)
 libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7d7a000)
 libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0xb7d76000)
 /lib/ld-Linux.so.2 (0xb7f36000)

Cet utilitaire donne les bibliothèques utilisées par vi. Si on voulait chrooter vi, il faudrait inclure ces fichiers ainsi que les fichiers de configuration au même endroit relatif dans le chroot. Si on ajoute à cela le fait qu'il va falloir mettre à jour le chroot, la situation devient assez compliquée. Après une mise à jour, un programme risque d'avoir besoin de nouvelles bibliothèques. D'autres, par contre, sont peut être obsolètes. En tout cas, il faut reconsidérer le chroot à chaque mise à jour. Cet aspect est traité dans de nombreux howto. Il existe même des solutions plus ou moins automatiques : happy googling !

Cette introduction à chroot n'a traité que d'un point : le confinement dans un emplacement du système de fichiers. Chroot n'est fait que pour cela. Comme le rappelle Alan Cox : « chroot is not and never has been a security tool ». Voilà c'est dit : chroot n'est pas orienté sécurité. Mis à part les éléments du système de fichiers extérieurs au chroot, le programme a accès à tout : réseau, noyau et IPC sans restrictions particulières. Ces moyens d'interactions offrent quelques biais pour sortir du chroot. Quelques possibilités de renforcement existent bien sous Linux. GRSecurity [4] se présente sous la forme d'un patch noyau et propose (entre autres) de modifier le comportement de l'appel système chroot pour y inclure un certain nombre de contrôles [5] (double chroot, envoi de signaux depuis le chroot vers l'extérieur, pivot de racine etc.). D'autres systèmes existent pour réaliser un confinement plus efficace et simple à administrer. Nous allons étudier Jail sous FreeBSD et OpenVZ sous Linux.

2. Jail

Jail est fourni avec le système FreeBSD depuis la version 4. Cet utilitaire se présente sous la forme d'un appel système pour la partie noyau et d'un jeu de commandes pour la partie utilisateur (jail, jls et jexec). À la création du jail, on a deux choix : on crée un « thin jail » ou un « fat jail ». Un « thin jail » contient juste une application (avec ses dépendances) un peu à la manière d'un chroot classique. Un « fat jail » contient un système complet. Quoi qu'il en soit, un jail est défini par la structure suivante :

struct jail {
u_int32_t version;
char *path;
char *hostname;
u_int32_t ip_number;
};

Cette structure est utilisée par l'appel système jail afin d'initialiser la prison. L'attribut version contient la version de l'API utilisée par Jail (à zéro pour l'instant), le path donne la racine de la prison au niveau système de fichiers. Enfin, hostname et ip_number fixent les paramètres réseau. Déjà, on voit que, par rapport à chroot, un jail dispose de son propre environnement réseau. À la fin de son exécution, l'appel système jail renvoie un entier. Cet entier est ce que l'on appelle le JID (Jail Identifier). Ce numéro est unique pour chaque jail en cours d'exécution. La commande jls permet de les lister (on retrouve bien les informations de la structure) :

$ jls
JID IP Address Hostname Path
3 192.168.89.2 jailtest.garnett.fr /jail/jailtest
2 192.168.89.3 jailfoo.garnett.fr /jail/jailfoo
1 194.168.89.4 jailbar.garnett.fr /jail/jailbar

Le système de base accueillant les jails possède le JID 0. Il lui est réservé. Si on veut faire exécuter un programme, il faut utiliser la commande jexec. Cette commande prend comme arguments un JID et un programme à exécuter dans le jail.

$ jexec 3 ps -auxwww
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
root 1181 0.0 0.0 3184 920 ?? SsJ Fri08AM 0:00.42 /usr/sbin/syslogd -s -s
root 1266 0.0 0.1 3212 1064 ?? IsJ Fri08AM 0:00.65 /usr/sbin/cron -s
root 8953 0.0 0.6 17356 11884 ?? SsJ 11:42AM 0:00.48 /usr/local/sbin/httpd -DNOHTTPACCEPT
www 11665 0.0 0.6 17356 11896 ?? IJ 5:03PM 0:00.00 /usr/local/sbin/httpd -DNOHTTPACCEPT
www 11687 0.0 0.6 17356 11896 ?? IJ 5:08PM 0:00.00 /usr/local/sbin/httpd -DNOHTTPACCEPT
www 11693 0.0 0.6 17356 11896 ?? IJ 5:09PM 0:00.00 /usr/local/sbin/httpd -DNOHTTPACCEPT
www 11780 0.0 0.6 17356 11896 ?? IJ 5:13PM 0:00.00 /usr/local/sbin/httpd -DNOHTTPACCEPT
www 11797 0.0 0.6 17356 11896 ?? IJ 5:16PM 0:00.00 /usr/local/sbin/httpd -DNOHTTPACCEPT
root 54636 0.0 0.1 5752 2360 ?? IsJ Fri05PM 0:00.00 /usr/sbin/sshd

On a la liste des processus (ps -auxwww) s'exécutant dans le jail ayant pour JID 3. On voit que là-dedans tournent un serveur SSH et un serveur Apache. Nous allons voir comment arriver à ce résultat.

2.1 Création du jail

Nous allons ici traiter uniquement des fat jails. Ils sont faciles à mettre en œuvre et à mettre à jour (tout ce que l'on attend d'un environnement confiné). L'utilisation des thin jails, bien qu'optimale, nous ferait retomber dans les mêmes difficultés que le chroot. À ce stade, un petit rappel s'impose. FreeBSD est composé de trois éléments : le noyau, le système de base et les ports. Le noyau gère (entre autres) les interactions avec le matériel. Le système de base comprend tous les outils de base du système (shell, compilateurs, sendmail, commandes système, bibliothèques de base, etc.). Enfin, les ports sont des portages de logiciels (souvent issus du monde UNIX) vers FreeBSD. Nous allons initialiser le jail avec le système de base. Les sources du système de base sont dans /usr/src. Si vous ne les avez pas dans ce répertoire, il faut faire un coup de cvsup [4].

2.1.1 Installation du jail (système confiné)

Nous allons commencer par rechercher les services à l'écoute sur toutes les interfaces. Pour que les services du système hôte n'interfèrent pas avec ceux du système confiné, il faut explicitement lier les services avec la ou les adresses du système de base. La commande suivante :

$ sockstat | grep "\*:[0-9]"

donne tous les services liés à *, c'est-à-dire sur toutes les interfaces. Nous avions juste SSHD dans ce cas. Une documentation en ligne assez complète liste les différents cas et les actions à effectuer [5]. Une fois les services attachés à l'IP de l'hôte et les sources récupérées, nous pouvons passer à la compilation et installation du système confiné :

$ mkdir -p /jail/jailtest
$ make world DESTDIR=/jail/jailtest

La racine du jail est créée dans /jail/jailtest. La commande suivante compile le système de base. La compilation se base sur le fichier /etc/make.conf qui va donner des éléments sur ce qu'il faut compiler. On peut faire un compromis entre le thin jail et le fat jail à ce niveau. Cependant, un allègement du système de base peut avoir comme conséquence un mauvais fonctionnement des applications emprisonnées. Si vous souhaitez vous lancer dans l'optimisation du make.conf, allez voir du côté de [5].

$ make distribution DESTDIR=/jail/jailtest

Cette commande installe le système de base fraîchement compilé dans /jail/jailtest.

On notera qu'une installation binaire du système confiné est possible. Pour cela, il faut se procurer un CD contenant le système FreeBSD. Supposons que le CD soit monté dans /cdrom. Les commandes suivantes installent un système dans /jail/jailtest :

$ cd /cdrom/base
$ /bin/sh install.sh DESTDIR=/jail/jailtest
$ mount -t devfs devfs /jail/jailtest /dev

On monte le /dev à l'intérieur du jail.

$ touch /jail/jailtest /fstab
$ cd /jail/jailtest
$ ln -s dev/null kernel
$ cp /etc/resolv.conf /jail/jailtest /etc/

Ces dernières commandes créent un fstab vide, un faux kernel et importent le resolv.conf de l'hôte. Il ne reste plus qu'à commenter la ligne adjkerntz dans /etc/crontab (le temps est synchronisé sur le système hôte) et à créer un rc.conf minimal (à l'intérieur du jail) :

$ cat /jail/jailtest/etc/rc.conf
hostname="jailtest.garnett.fr"
keymap="fr.iso.acc"
sshd_enable="YES"
syslogd_flags="-s -s"
sendmail_enable="NONE"

Le rc.conf initialise les variables par défaut du système de base, ainsi que les services à activer au démarrage. Ici, on initialise le hostname du jail et sa disposition clavier. On active aussi le serveur SSH et Syslog (uniquement en local avec -s -s). On désactive Sendmail. La configuration locale au jail s'arrête ici. Il faut maintenant configurer le système hôte.

2.1.2 Configuration du système hôte

La première étape est d'initialiser ce qu'il faut côté système hôte pour accueillir le jail. Nous allons créer un fat jail sur la machine hôte ayant l'IP 192.168.89.1. Nous rappelons que le répertoire accueillant le fat jail est /jail/jailtest. Ce fat jail aura l'IP 192.168.89.2. Pour la partie réseau, la création d'un alias sur l'interface réseau est nécessaire. Cet alias sera utilisé par notre jail pour communiquer sur le réseau. Dans le /etc/rc.conf de l'hôte, ajoutez :

$ cat /etc/rc.conf
ifconfig_bge0="inet 192.168.89.1 netmask 255.255.255.0"
ifconfig_bge0_alias0="inet 192.168.89.2 netmask 255.255.255.0"

Ensuite, il faut indiquer au système hôte les emplacements des différents jails, ainsi que la manière de les démarrer. On a deux sections pour initialiser les jails : une section globale pour l'ensemble des systèmes confinés et une section locale spécifique à chacun. Ces deux sections sont renseignées dans le rc.conf. Pour la section globale, on a les paramètres minimaux suivants :

jail_enable="YES"

Cette directive active Jail sur le système hôte.

jail_list="jailtest"

liste des jails à démarrer. Ce nom sert de préfixe aux options associées à ce jail. Par exemple, pour déclarer la variable option1 à la valeur valeur1 sur jailtest, on ajoute la ligne jailtest_option1='valeur1' au rc.conf.

jail_exec_start="/bin/sh /etc/rc"

Cette instruction clôt la section globale en précisant la commande à exécuter au démarrage du jail. Ici, on fait exécuter le script /etc/rc au shell /bin/sh. Cette commande démarre les services installés dans le système confiné conformément aux directives du fichier /jail/jailtest/etc/rc.conf. Sous FreeBSD, le démarrage des composants de base est réalisé par le script /etc/rc. Pour le démarrage des services installés par les ports, cela se passe dans le répertoire /usr/local/etc/rc.d (si on a préalablement activé le service dans /etc/rc.conf). Nous allons maintenant voir les options minimales de la section spécifique à chaque système confiné.

jail_tractest_rootdir="/jail/tractest"
jail_tractest_hostname="tractest.univ-orleans.fr"
jail_tractest_ip="192.168.89.2"

Ces trois directives fixent les paramètres locaux de chaque système confiné. Nous avons respectivement le répertoire racine, le FQDN et l'adresse IP. À ce stade, le système confiné devrait démarrer au prochain reboot du système hôte. Avant le reboot, il faut tester que notre jail fonctionne en se mettant à l'intérieur. On en profitera pour faire quelques ajustements. Nous allons donc utiliser la commande jail :

$ jail /jail/jailtest jailtest.garnett.fr 192.168.89.3 /bin/sh

Cette commande lance un shell /bin/sh à l'intérieur du jail situé dans /jail/jailtest. On doit aussi fixer les paramètres réseau (IP et FQDN). Une fois dans le jail, on affecte un mot de passe à root et on crée un utilisateur habilité à passer root (pour l'accès SSH).

$ passwd
$ pw useradd garnett -m -G wheel -w no
$ passwd garnett

La première commande donne un mot de passe à root. La seconde crée un utilisateur appartenant au groupe wheel (sous FreeBSD, seuls les membres de ce groupe peuvent faire un su pour passer root) avec un compte bloqué (-w no). La dernière commande affecte un mot de passe à l'utilisateur créé pour le débloquer. Nous avons bien rempli le rc.conf du jail. Donc, on peut rebooter la machine pour tester qu'il se lance bien au démarrage (on peut aussi utiliser le script /etc/rc.d/jail qui est fait pour manipuler les jails). Si le jail est bien lancé (un coup de jls), alors on peut passer à la post-configuration par SSH (la classe quoi !).

2.1.3 Post-configuration et exploitation

Pour exploiter notre système confiné, il faut commencer par installer les ports dessus. Plusieurs écoles pour réaliser cette opération. Nous avons retenu deux méthodes : montage NFS en read only et installation des ports dans chaque jail. Nous avons choisi la seconde version, car le stockage de quelques dizaines de Mo par jail n'est pas un problème. De plus, avoir un arbre des ports spécifique à chaque machine permet de travailler avec des versions différentes. Connectons-nous donc en SSH et installons l'arbre des ports :

$ portsnap fetch extract

Cette commande installe l'arbre des ports dans /usr/ports. Nous allons enchaîner par l'installation des outils portinstall et portupgrade :

$ cd /usr/ports/ports-mgmt/portupgrade

$ make clean install clean

Cette commande télécharge les sources de portupgrade, les compile, installe le logiciel sur le système et complète la base de données des ports installés. BSD ou comment combiner le meilleur du monde des gestionnaires de paquets avec la réactivité (et la finesse de configuration) des sources. Sous Gentoo, le système de ports est très similaire. Pour finir, installons un « vrai » service sur la machine. Apache fera l'affaire :

$ portinstall www/apache22

Activons ensuite Apache dans le rc.conf du système confiné :

$ vi /etc/rc.conf

hostname="jailtest.garnett.fr"

keymap="fr.iso.acc"

sshd_enable="YES"

syslogd_flags="-s -s"

sendmail_enable="NONE"

apache22_enable="YES"

$ /usr/local/etc/rc.d/apache22 start

Et voilà, vous avez un Apache dans un jail FreeBSD. Vous pouvez ajouter d'autres services à volonté. Il nous reste maintenant le côté exploitation. À savoir :

  • mises à jour du système de base (confiné) ;
  • mises à jour des ports installés ;
  • audit des vulnérabilités de sécurité sur les services installés.

De plus, nous souhaitons planifier ces tâches depuis le système de base. Commençons par les mises à jour du système de base. Nous allons utiliser la commande freebsd-update qui récupère (et installe) les mises à jour du système de base. Connectons-nous au système hôte (192.168.89.1). Nous pouvons l'exécuter depuis le système de base et le faire opérer sur le système confiné par la commande suivante :

$ freebsd-update fetch install -b /jail/jailtest

Les commandes fetch et install vont respectivement chercher les mises à jour et les installer. Le paramètre -b donne à freebsd-update la racine à partir de laquelle travailler. On peut automatiser le tout dans cron avec envoi automatique aux administrateurs (paramètres -t) si des mises à jour sont disponibles. Dans ce cas, on utilisera la commande cron à la place de fetch qui télécharge les mises à jour au bout d'un temps aléatoire par rapport au lancement de la commande pour que tous les serveurs ne demandent pas la mise à jour au même moment. Ajoutons dans la crontab :

$ vi /etc/crontab
0 1 * * * root /usr/sbin/freebsd-update cron -t admin@garnett.fr

Round 2, la mise à jour des ports. Pour cela, il faut déjà mettre à jour l'arbre des ports du système confiné. On va utiliser portsnap. Un update classique des ports se fait comme ça :

$ portsnap fetch update

À l'instar de freebsd-update, portsnap dispose d'un argument cron que nous allons utiliser dans le crontab des systèmes confinés.

$ vi /jail/jailtest/etc/crontab
0 2 * * * root /usr/sbin/portsnap cron update

Pour vérifier que les ports de notre système sont à jour, on va utiliser portversion (issu du port ports-mgmt/portupgrade).

$ export PKG_DBDIR=/jail/jailtest/var/db/pkg
$ /usr/local/sbin/portversion -v -l '<'

La première commande donne l'emplacement de la base de données des paquets à examiner par l'intermédiaire de la variable d'environnement PKG_DBDIR. La commande portversion est ensuite utilisée pour relever les paquets nécessitant une mise à jour. S’il faut en mettre à jour, on utilisera portupgrade en SSH sur le système confiné :

$ portupgrade -ar

Cette commande réalise la mise à jour de tous les paquets (-a). Les logiciels dépendant de ces paquets sont également recompilés. Enfin, auditons les potentielles vulnérabilités du système. FreeBSD propose un outil, portaudit, qui analyse les ports installés sur le système et les confronte à une base de données propre à FreeBSD de vulnérabilités connues. Cette base est maintenue par la FreeBSD Security Team. Pour réaliser ces audits depuis le système de base, il faut d'abord installer portaudit :

$ portinstall portaudit

On doit aussi récupérer la base connue des vulnérabilités (et scanner la machine hôte en passant) :

$ /usr/local/sbin/portaudit -Fad

Ensuite, pour le lancer sur un système, il faut d'abord créer (un peu artificiellement) une liste des paquets du système à auditer. Une simple liste du contenu de /var/db/pkg du système confiné suffit :

$ /bin/ls -l /jail/jailtest/var/db/pkg > /tmp/jailtest.pkg

Un coup de portaudit sur le fichier (-f) réalise l'audit :

$ /usr/local/sbin/portaudit -f /tmp/jailtest.pkg

Pour finir sur le volet exploitation, vous pensez sans doute qu'il serait judicieux d'automatiser tout cela dans un script ? C'est parfaitement jouable en utilisant le rc.conf dans votre script. Ajoutez la ligne suivante :

. /etc/rc.conf

Et vous aurez accès à un grand nombre de variables dans votre script comme $jail_list qui contient la liste des jails. Quelques exemples à mettre dans vos scripts :

for jail in $jail_list; do
eval jaildir=\"\$jail_${jail}_rootdir\"
echo "" >> $logfile
echo "=> Portaudit of $jail" >> $logfile
/bin/ls -l $jaildir/var/db/pkg > $tmpdir/$jail.pkg
/usr/local/sbin/portaudit -f $tmpdir/$jail.pkg >> $logfile

Pour faire les portaudit sur les machines. On a le même pour les portversion :

for jail in $jail_list; do
eval jaildir=\"\$jail_${jail}_rootdir\"
echo "" >> $logfile
echo "=> Ports update(s) of $jail" >> $logfile
export PKG_DBDIR=$jaildir/var/db/pkg
/usr/local/sbin/portversion -v -l '<' 2>>$logfile 1>&2
unset PKG_DBDIR

Le dernier point concerne l'aspect sécurité. D'autres paramètres existent pour affiner la configuration des jails rendant leur exécution encore plus sécurisée. Nous allons les lister dans un tableau avec une explication.

Paramètre

Explication

jail_set_hostname_allow = « NO »

Empêche le root du jail de modifier le nom du système confiné

jail_socket_unixiproute_only = « YES »

On ne fait que du TCP/IP dans le jail. Pas d'autres protocoles.

jail_sysvipc_allow = « NO »

Pas d'IPC système V dans le jail.

jail_devfs_enable = « YES »

jail_devfs_ruleset = « devfsrules_jail »

On définit un /dev minimal adapté. On a juste les terminaux, /dev/null, /dev/zero, /dev/random, /dev/urandom, /dev/crypto.

jail_procfs_enable = « NO »

On désactive /proc

jail_mount_enable = « NO »

Une autre raison de ne pas utiliser NFS pour les ports : on peut désactiver la commande mount.

3. OpenVZ

OpenVZ prend la forme d'un patch pour le noyau Linux. Il est livré sous forme de paquets pour diverses distributions. Nous allons utiliser la version fournie pour Debian lenny. À l'instar de Jail, OpenVZ propose d'associer les processus à des environnements confinés nommés VE (Virtual Environments) ou VPS (Virtual Private Servers). De même qu'un jail FreeBSD ne peut accueillir que des systèmes confinés FreeBSD, un Linux en OpenVZ ne peut avoir que des VE Linux (éventuellement sous d'autres distributions). La grosse différence par rapport à Jail (à part l'OS sous-jacent) est que OpenVZ est très orienté « fat jail ». Dans le même esprit, on trouve aussi Linux Vserver qui a déjà fait l'objet d'articles dans GLMF [6]. Chaque VE est composé des éléments suivants :

  • Fichiers : initialisation de la racine au répertoire accueillant le VE.
  • Utilisateurs et groupes : chaque VE possède ses propres utilisateurs et groupes. Le root du VE est confiné au VE.
  • Processus : chaque processus s'exécute dans l'espace de son VE. Il n'est pas possible pour des processus de VE différents d'interagir entre eux. Tout ce qui est IPC est également confiné au VE.
  • Réseau : les VE disposent chacun d'une interface virtuelle. Dès règles iptables ou un routage spécifique au VE pourront être associés sur cette interface.
  • Périphériques : chaque VE dispose de son propre /dev. Les fichiers spéciaux pourront être créés au besoin (on n’a pas de configuration minimale comme le devfsrules_jail fourni sous FreeBSD).

Au vu de cette introduction, OpenVZ semble être un jail like pour Linux. Il offre cependant des fonctionnalités supplémentaires telles que :

  • Quota d'espace disque pour un VE : le seul moyen de limiter l'espace occupé par un jail est de travailler au niveau des partitions. OpenVZ propose de limiter l'espace occupé par un VE en termes d'inodes (fichiers) ou de taille selon le principe bien connu des « soft & hard limits » [7]. À l'intérieur du VE, les outils classiques de gestion de quotas sont utilisables.
  • Gestion du CPU : une valeur cpuunits peut être associée à chaque VE. Cette valeur détermine le pourcentage du (ou des) processeurs utilisables par le VE. À l'intérieur du VE, les algorithmes classiques du noyau Linux sont utilisés.
  • Répartition des E/S : la priorité des VE par rapport aux entrées sorties type accès au(x) disque(s) dur(s) sont configurables.
  • Quotas divers : OpenVZ permet de quantifier les limites pour différentes entités du système (RAM, fichiers ouverts, taille de la mémoire partagée, etc.). Il y a plus d'une vingtaine de paramètres.
  • Last but not least, il est possible de migrer un VE d'une machine physique à une autre à chaud [8] moyennant que les systèmes source et cible soient équivalents au niveau données.

On notera quand même que l'utilisation d'OpenVZ oblige à se passer de certains patchs de sécurité du noyau type GRSecurity [9]. Nous allons dérouler l'installation d'une instance OpenVZ dans une Debian Lenny [10] (maintenant stable).

3.1 Configuration du système de base

Nous allons commencer par récupérer le noyau fournissant le support de OpenVZ :

$ apt-get install binutils debootstrap rsync binutils-doc quota
$ apt-get install vzctl vzquota
Paramétrage de vzquota (3.0.11-1) ...
Paramétrage de vzctl (3.0.22-14) ...
$ apt-get install linux-image-openvz-686

A pour effet le rajout dans /boot/grub/menu.lst :

title Debian GNU/Linux, kernel 2.6.26-1-openvz-686
root (hd0,0)
kernel /boot/vmlinuz-2.6.26-1-openvz-686 root=/dev/sda1 ro quiet
initrd /boot/initrd.img-2.6.26-1-openvz-686

Configurons /etc/sysctl.conf pour paramétrer le noyau (IPV4 - IPV6 – forwarding – sécurité). Par défaut, tout est commenté. Nous activerons les lignes suivantes :

net.ipv4.conf.default.rp_filter=1
net.ipv4.conf.all.rp_filter=1

Redémarrons la machine et vérifions que OpenVZ est bien actif :

$ uname -r
2.6.26-1-openvz-686

OpenVZ se présente sous la forme d'un module noyau :

$ ps ax | grep vz
2956 ? S 0:00 [vzmond]
4655 pts/0 R+ 0:00 grep vz

ifconfig fait apparaître une nouvelle interface :

$ ifconfig
venet0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
UP BROADCAST POINTOPOINT RUNNING NOARP MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 lg file transmission:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

3.2 Création d'un VE

Il reste à créer nos VE (environnements virtuels équivalents des machines virtuelles ou serveurs privés virtuels VPS). Pour cela, il existe des gabarits (templates) pour chaque système d'exploitation notamment pour les Debian que l'on installe :

$ apt-get install vzctl-ostmpl-debian-5.0-i386-minimal

Réalise l'installation du template de la Lenny dans /var/lib/vz/template/cache/ :

$ ls -al /var/lib/vz/template/cache/
total 60880
-rw-r--r-- 1 root root 62274628 jan 21 13:28 debian-5.0-i386-minimal.tar.gz

Ce template est l'arborescence de la Lenny qui sera utilisée comme VE. Il apparaît que toutes les VE et la configuration de VZ vont se trouver dans /var/lib/vz. Il convient de dédier une partition pour vos VE. Nous monterons cette partition dans /vz (/vz est un lien de /var/lib/vz).

Création d'un VE :

$ vzctl create 101 --ostemplate debian-5.0-i386-minimal

L'id doit être un nombre et non un nom. Cela pose quelques soucis de lisibilité. Cette commande a pour effet de créer un répertoire contenant l'arborescence de la distribution :

$ ll /vz/private/101/
total 72
drwxr-xr-x 2 root root 4096 jan 8 19:26 bin
drwxr-xr-x 2 root root 4096 déc 4 10:22 boot
drwxr-xr-x 4 root root 4096 déc 24 18:07 dev
drwxr-xr-x 42 root root 4096 avr 14 16:02 etc
drwxr-xr-x 2 root root 4096 déc 4 10:22 home
drwxr-xr-x 10 root root 4096 jan 21 12:58 lib
drwxr-xr-x 2 root root 4096 déc 24 18:07 media
drwxr-xr-x 2 root root 4096 déc 4 10:22 mnt
drwxr-xr-x 2 root root 4096 déc 24 18:07 opt
drwxr-xr-x 2 root root 4096 déc 4 10:22 proc
drwx------ 2 root root 4096 jan 21 13:06 root
drwxr-xr-x 2 root root 4096 jan 21 12:58 sbin
drwxr-xr-x 2 root root 4096 sep 16 2008 selinux
drwxr-xr-x 2 root root 4096 déc 24 18:07 srv
drwxr-xr-x 2 root root 4096 aoû 12 2008 sys
drwxrwxrwt 4 root root 4096 jan 21 12:58 tmp
drwxr-xr-x 11 root root 4096 déc 24 18:34 usr
drwxr-xr-x 13 root root 4096 déc 24 18:07 var

3.3 Configuration du VE

Il n'y a pas d'interface réseau de défini au VE 101 :

$ cat /vz/private/101/etc/network/interfaces
# loopback
auto lo
iface lo inet loopback

Commençons par assigner une adresse IP à notre VE 101 :

$ vzctl set 101 --ipadd 192.168.89.2 --save
Saved parameters for CT 101

Associons-lui aussi un serveur de nom :

$ vzctl set 101 --nameserver <ip_du_dns> --save
Saved parameters for CT 101

Lancement du VE :

$ vzctl start 101
Starting container ...
Container is mounted
Adding IP address(es): 192.168.89.2
Setting CPU units: 1000
Configure meminfo: 65536
File resolv.conf was modified
Container start in progress...

Voyons la configuration réseau de notre VE 101 :

$ cat /vz/private/101/etc/network/interfaces
auto lo
iface lo inet loopback
auto venet0
iface venet0 inet manual
 up ifconfig venet0 0
 up route add -net 192.0.2.1 netmask 255.255.255.255 dev venet0
 up route add default gw 192.0.2.1
auto venet0:0
iface venet0:0 inet static
 address 192.168.89.2
 netmask 255.255.255.255
 broadcast 0.0.0.0

3.4 Utilisation du VE

À l'instar de Jail, les commandes sont directement exécutables dans le VE par vzctl :

$ vzctl exec 101 ps ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 init [2]
274 ? Sl 0:00 /usr/sbin/rsyslogd -c3
287 ? Ss 0:00 /usr/sbin/sshd
302 ? Ss 0:00 /usr/sbin/cron
321 ? Rs 0:00 ps ax

Nous pouvons accéder au VE directement depuis le système hôte :

$ vzctl enter 101
entered into CT 101
root@localhost:/#

Une fois connecté au VE, on peut initialiser le mot de passe root, faire sa configuration IPTABLES ou encore installer ses packages. OpenSSH serait utile pour se connecter directement à la machine. Pour arrêter le VE :

$ vzctl stop 101

3.5 Adapter le VE à ses besoins (quotas disque, mémoire et CPU)

Comme dit dans l'introduction, OpenVZ propose de fixer un certain nombre de quotas. Commençons par les quotas sur l'espace disque. La syntaxe de vzctl est la suivante :

$ vzctl set CTID --diskspace $SoftLimit$:$HardLimit$ --save

Cette commande fixe une limite souple et dure des quotas. Voyons comment cela fonctionne.

Avant la mise en place du quota :

$ vzctl exec 101 df -h
Filesystem Size Used Avail Use% Mounted on
simfs 1.0G 159M 866M 16% /
tmpfs 4.0G 0 4.0G 0% /lib/init/rw
tmpfs 4.0G 0 4.0G 0% /dev/shm

Fixons une limite souple à 6G et dure à 7G :

$ vzctl set 101 --diskspace 6G:7G --save
Saved parameters for CT 101
$ vzctl exec 101 df -h
Filesystem Size Used Avail Use% Mounted on
simfs 6.0G 159M 5.9G 3% /
tmpfs 4.0G 0 4.0G 0% /lib/init/rw
tmpfs 4.0G 0 4.0G 0% /dev/shm

Le / est alors limité à 6 Go. Voyons maintenant la manipulation de la mémoire allouée. Par défaut, le VE a 256 Mo :

$ vzctl exec 101 free -k
total used free shared buffers cached
Mem: 262144 28300 233844 0 0 0
-/+ buffers/cache: 28300 233844
Swap: 0 0 0

Les changements à faire concernent plusieurs paramètres et le choix dépend d'un calcul savant en fonction des possibilités de la machine physique :

$ vzctl exec 101 cat /proc/user_beancounters
Version: 2.5
uid resource held maxheld barrier limit failcnt
101: kmemsize 543203 1515702 11055923 11377049 0
lockedpages 0 0 256 256 0
privvmpages 7051 8156 65536 69632 0
shmpages 0 336 21504 21504 0
dummy 0 0 0 0 0
numproc 7 19 240 240 0
physpages 636 1533 0 2147483647 0
vmguarpages 0 0 33792 2147483647 0
oomguarpages 636 1533 26112 2147483647 0
numtcpsock 2 3 360 360 0
numflock 1 3 188 206 0
numpty 0 1 16 16 0
numsiginfo 0 2 256 256 0
tcpsndbuf 17920 0 1720320 2703360 0
tcprcvbuf 32768 0 1720320 2703360 0
othersockbuf 0 17280 1126080 2097152 0
dgramrcvbuf 0 0 262144 262144 0
numothersock 15 19 360 360 0
dcachesize 46725 65100 3409920 3624960 0
numfile 162 351 9312 9312 0
dummy 0 0 0 0 0
dummy 0 0 0 0 0
dummy 0 0 0 0 0
numiptent 10 10 128 128 0

Augmentons la limite mémoire :

$ vzctl set 101 --privvmpages $((65536*2)):$((69632*2)) --save
UB limits were set successfully
Configure meminfo: 131072
Saved parameters for CT 101

Nous avons alors doublé la mémoire allouée :

$ vzctl exec 101 free -k
total used free shared buffers cached
Mem: 524288 28300 495988 0 0 0
-/+ buffers/cache: 28300 495988
Swap: 0 0 0

Pour finir, limitons le CPU (La mesure est 100% par CPU, donc, si vous avez deux CPU, vous avez 200%...) :

$ vzctl set 101 --cpulimit 50 --save

Conclusion sur le confinement

Cet article a dressé un panorama des différentes solutions de confinement disponibles. Nous avons commencé par le plus ancien : chroot. Retenons que l'utilisation de chroot dans un contexte sécurité est à proscrire (malgré le fait qu'un patch de sécurité nommé GRSecurity en augmente considérablement la sécurité). Sous FreeBSD, on reposera plutôt sur Jail qui propose de réaliser des « thin jails » pour confiner un programme particulier ou des « fat jails » pour confiner un système complet. Sous Linux, on pourra utiliser OpenVZ très orienté confinement de système complet. L'avantage de Jail par rapport à OpenVZ est qu'il est intégré dans le système de base de FreeBSD. En revanche, OpenVZ propose des possibilités de quotas et de migration en direct absentes de Jail. Les développeurs d'OpenVZ font beaucoup d'efforts pour rendre leur système compatible avec la libvirt [11]. Ces fonctionnalités rapprochent OpenVZ d'un système comme XEN. En tout cas, le confinement peut s'avérer une alternative intéressante à la virtualisation pour la mise à disposition de machines homogènes.

Références

[1] « La virtualisation : vecteur de vulnérabilités ou de sécurité ? », MISC 32.

[2] http://cr.yp.to/djbdns/other.html (BIND = Buggy Internet Name Daemon)

[3] http://www.cgsecurity.org/Articles/SecProg/Art5/index-fr.html

[4] http://www.freebsd.org/doc/en/books/handbook/cvsup.html

[5] http://www.section6.net/wiki/index.php/Creating_a_FreeBSD_Jail

[6] GLMF, Numéros 90 et 92.

[7] http://wiki.openvz.org/Checking_disk_quota

[8] http://wiki.openvz.org/Checkpointing_and_live_migration

[9] http://wiki.openvz.org/Grsecurity

[10] http://wiki.openvz.org/Installation_on_Debian

[11] http://libvirt.org/

À propos des auteurs

Nicolas Grenèche (aka Garnett ou Poussin), 29 ans, RSSI au Centre de Ressources Informatiques de l'Université d'Orléans et Doctorant au sein du projet SDS (Sécurité et Distribution des Systèmes). Adepte de [Open|Free]BSD.

Pascal Pautrat, 39 ans, administrateur systèmes et réseaux au Centre de Ressources Informatiques de l'Université d'Orléans. Adepte de Linux Debian et de Xen.



Article rédigé par

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous