tuto: Prepare for 2022

This commit is contained in:
nemunaire 2021-09-12 10:31:36 +02:00
parent 37f0ba4b3d
commit af860b40a0
40 changed files with 246 additions and 1974 deletions

View file

@ -1,11 +1,11 @@
include ../pandoc-opts.mk
SOURCES = tutorial.md installation.md chroot.md pseudofs.md capabilities.md cgroups.md oom.md seccomp.md project-intro.md project-body.md project-rendu.md
SOURCES_TUTO = tutorial.md setup.md mount.md namespaces.md cmpns.md docker-exec.md networkns.md pidns.md mountns.md userns.md rendu.md
all: tutorial.pdf
tutorial.pdf: ${SOURCES}
tutorial.pdf: ${SOURCES_TUTO}
pandoc ${PANDOCOPTS} -o $@ $+
clean::

View file

@ -1,202 +0,0 @@
\newpage
Les *capabilities*
==================
## Présentation
Historiquement, dans la tradition UNIX, on distinguait deux catégories de
processus :
* les processus *privilégiés* : dont l'identifiant numérique de son utilisateur
est 0 ;
* les processus *non-privilégiés* : dont l'identifiant numérique de son
utilisateur n'est pas 0.
Lors des différents tests de permission fait par le noyau, les processus
privilégiés outrepassaient ces tests, tandis que les autres devaient passer les
tests de l'effective UID, effective GID, et autres groupes supplémentaires...
Depuis Linux 2.2 (en 1998), les processus privilégiés peuvent activer ou
désactiver des *capabilities*, chacune donnant accès à un groupe d'actions
privilégiées au sein du noyau.
On trouve par exemple :
* `CAP_CHOWN` : permet de modifier le propriétaire d'un fichier de manière
arbitraire ;
* `CAP_KILL` : permet de tuer n'importe quel processus ;
* `CAP_SYS_BOOT` : permet d'arrêter ou de redémarrer la machine ;
* `CAP_SYS_MODULE` : permet de charger et décharger des modules ;
* et beaucoup d'autres, il y en a environ 39 en tout (ça dépend de la
version du noyau) !
### `ping`
Pour émettre un ping, il est nécessaire d'envoyer des paquets ICMP. À la
différence des datagrammes UDP ou des segments TCP, il n'existe pas d'interface
exposée par le noyau aux utilisateurs pour envoyer des paquets ICMP. Pour le
faire, il est nécessaire de pouvoir écrire directement sur l'interface ; ça,
seul le super-utilisateur peut le faire.
Pour permettre à tous les utilisateurs de pouvoir envoyer des ping, le
programme est donc généralement *Setuid root*. Cela permet à n'importe quel
utilisateur de prendre les droits du super-utilisateur, le temps de l'exécution
du programme.
Les problèmes surviennent lorsque l'on découvre des vulnérabilités dans les
programmes *Setuid root*. En effet, s'il devient possible pour un utilisateur
d'exécuter du code arbitraire, ce code sera exécuté avec les privilèges de
l'utilisateur *root* ! Dans le cas de `ping`, on se retrouverait alors à
pouvoir lire l'intégralité de la mémoire, alors que l'on avait juste besoin
d'écrire sur une interface réseau.
C'est donc à ce moment que les *capabilities* entrent en jeu : un processus (ou
même un thread) privilégié peut décider, généralement à son lancement, de
réduire ses *capabilities*, pour ne garder que celles dont il a réellement
besoin. Ainsi, `ping` pourrait se contenter de `CAP_NET_RAW`.
## Les attributs de fichier étendus
Une grosse majorité des systèmes de fichiers (ext[234], XFS, btrfs, ...)
permet d'enregistrer, pour chaque fichier, des attributs (dits attributs
*étendus*, par opposition aux attributs *réguliers* qui sont réservés à l'usage
du système de fichiers).
Sous Linux, les attributs sont regroupés dans des espaces de noms :
* *security* : espace utilisé par les modules de sécurité du noyau, tel que
SELinux, ... ;
* *system* : espace utilisé par le noyau pour stocker des objets système, tels
que les ACL POSIX ;
* *trusted*: espace dont la lecture et l'écriture est limité au
super-utilisateur ;
* *user* : modifiable sans restriction, à partir du moment où l'on est le
propriétaire du fichier.
Par exemple, on peut définir un attribut sur un fichier comme cela :
<div lang="en-US">
```bash
42sh$ echo 'Hello World!' > toto
42sh$ setfattr -n user.foo -v bar toto
42sh$ getfattr -d toto
# file: toto
user.foo="bar"
```
</div>
Encore plus fort, vous pouvez utiliser les ACL POSIX :
<div lang="en-US">
```bash
42sh$ sudo chown root:root toto && sudo chmod o-r toto
42sh$ cat toto
cat: toto: Permission denied
42sh$ sudo setfattr -m u:$USER:r toto
42sh$ cat toto
Hello World!
```
</div>
Bien que les droits UNIX traditionnels ne vous donnent pas accès au fichier,
les ACL POSIX vous autorisent à le lire.
Vous pouvez voir ces attributs avec la commande :
<div lang="en-US">
```bash
42sh$ getfattr -d -m "^system" toto
# file: toto
system.posix_acl_access=0sgAAEAD/////AgAEOgDAEAA/////xAABAD////8=
```
</div>
### `ping`
De la même manière que l'on peut définir de façon plus fine les droits d'accès
par utilisateur, un attribut de l'espace de nom *security* peut être défini
pour accroître les *capabilities* d'un processus lorsqu'il est lancé par un
utilisateur non-privilégié. On peut alors voir le Setuid root comme
l'utilisation de cet attribut auquel on accroîtrait l'ensemble des
*capabilities*.
Si votre distribution profite de ces attributs étendus, vous devriez obtenir :
<div lang="en-US">
```bash
42sh$ getfattr -d -m "^security" $(which ping)
# file: bin/ping
security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=
```
</div>
Ou, dans sa version plus lisible :
<div lang="en-US">
```bash
42sh$ getcap $(which ping)
/bin/ping = cap_net_raw+ep
```
</div>
## Exercice : visualisateur de capabilities d'un processus {-}
Écrivons maintenant un programme permettant de voir les *capabilities*
d'un processus :
<div lang="en-US">
```
42sh$ ./view_caps 1
cap_user_header_t
-----------------
Version: 20080522
PID: 1
cap_user_data_t
---------------
effective: 0x3fffffffff
CAP_AUDIT_CONTROL
CAP_AUDIT_READ
[...]
CAP_SYS_TIME
CAP_SYS_TTY_CONFIG
CAP_SYSLOG
CAP_WAKE_ALARM
permitted: 0x3fffffffff
CAP_AUDIT_CONTROL
CAP_AUDIT_READ
[...]
CAP_SYS_TIME
CAP_SYS_TTY_CONFIG
CAP_SYSLOG
CAP_WAKE_ALARM
inheritable: 0x0
```
</div>
Astuces : `capget(2)`, X-macros, ...
## Pour aller plus loin {-}
Je vous recommande la lecture des *man* suivants :
* `capabilities(7)` : énumérant tous les capabilities, leur utilisation, etc. ;
* `xattrs(7)` : à propos des attributs étendus.
Et de ces quelques articles :
* [Secure Your Containers with this One Weird Trick](https://www.redhat.com/en/blog/secure-your-containers-one-weird-trick)
* [Guidelines for extended attributes](https://www.freedesktop.org/wiki/CommonExtendedAttributes/)
* [File-based capabilities](https://lwn.net/Articles/211883/)
* [A bid to resurrect Linux capabilities](https://lwn.net/Articles/199004/)
* [False Boundaries and Arbitrary Code Execution](https://forums.grsecurity.net/viewtopic.php?f=7&t=2522#p10271)
Pour revenir à Docker, par défaut, un certain nombre de *capabilities* sont
désactivées par défaut ; vous pouvez en ajouter et en retirer via les arguments
`--cap-add` et `--cap-drop` du `docker container run`.

View file

@ -1,287 +0,0 @@
\newpage
Utiliser les *cgroup*s
======================
Les *cgroup*s (pour *Control Group*s) permettent de collecter des statistiques
sur des groupes de processus (appelés tâches) et de leur attribuer des
propriétés. Par exemple, il est possible leur imposer des limites d'utilisation
de ressources ou d'altérer leur comportement.
## Premiers tests
Nous allons commencer par faire quelques tests avec le *cgroup* *freezer*, qui
permet d'interrompre l'exécution d'un groupe de processus et de la reprendre.
### Montage du *cgroup*
En fonction de la configuration de votre système, il est possible que les
*cgroup*s ne soient pas montés au démarrage dans `/sys/fs/cgroup/`. Si vous n'avez
pas de dossier `freezer` ou si celui-ci est vide, montez-le en suivant la
procédure suivante :
<div lang="en-US">
```bash
mkdir /sys/fs/cgroup/freezer/
mount -t cgroup -o freezer none /sys/fs/cgroup/freezer/
```
</div>
Cette dernière commande monte l'arborescence de groupes relative à ce *cgroup*
*freezer*. Tous les dossiers contenus dans cette racine sont donc des
sous-groupes.
### Création d'un nouveau groupe
La première étape dans l'utilisation d'un *cgroup* est de créer un groupe.
Pour ce faire, il suffit de créer un nouveau dossier dans un groupe existant,
par exemple la racine :
<div lang="en-US">
```bash
mkdir /sys/fs/cgroup/freezer/virli/
ls /sys/fs/cgroup/freezer/virli/
```
</div>
Nous avons maintenant un nouveau groupe de processus `virli` dans le *cgroup*
*Freezer*. Comme il s'agit d'une hiérarchie, le groupe `virli` hérite des
propriétés de son (ses) père(s).
### Rattachement de processus
Pour le moment, ce nouveau groupe ne contient aucune tâche.
Ouvrons un nouveau terminal (c'est lui que l'on va geler), et récupérons son
PID : `echo $$`.
La liste des processus rattachés à un *cgroup* se trouve dans le fichier `task`
du groupe. Pour ajouter une tâche à ce groupe, cela se passe de cette manière :
<div lang="en-US">
```bash
echo $PID > /sys/fs/cgroup/freezer/virli/tasks
```
</div>
Il faut ici remplacer `$PID` par le PID du shell que l'on a relevé juste avant.
En validant cette commande, nous avons déplacé le processus dans ce groupe, il
n'est alors plus dans aucun autre groupe (pour ce *cgroup*, il ne bouge pas
dans les autres *cgroup*s).
Malgré l'utilisation de la redirection `>` (et non `>>`), il s'agit belle et
bien d'un ajout au fichier et non d'un écrasement. Il faut garder en tête que
le système de fichier est entièrement simulé et que certains comportements sont
adaptés.
### Consultation de l'état
En affichant le contenu du dossier `virli`, nous pouvions constater que
celui-ci contenait déjà un certain nombre de fichiers. Certain d'entre-eux sont
en lecture seule et permettent de lire des statistiques instantanées sur le
groupe ; tandis que d'autres sont des propriétés que nous pouvons modifier.
Nous pouvons consulter l'état de gel du groupe en affichant le contenu du
fichier\newline `/sys/fs/cgroup/freezer/virli/freezer.state`.
Pour plus d'information sur les différents fichiers présents dans ce *cgroup*,
consultez
[la documentation associée](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/freezer-subsystem.html).
### Changement d'état
Faisons exécuter à notre interpréteur une commande pour voir effectivement
l'exécution s'arrêter. Si vous manquez d'inspiration, utilisez :
<div lang="en-US">
```bash
for i in $(seq 9999); do echo -n $i; sleep .1; echo -n " - "; sleep .1; done
```
</div>
Maintenant, nous avons donné l'ordre au noyau de ne plus allouer de temps de
calcul à notre shell et ses fils :
<div lang="en-US">
```bash
echo FROZEN > /sys/fs/cgroup/freezer/virli/freezer.state
```
</div>
À cet instant, vous devriez voir votre compteur s'arrêter. Pour reprendre
l'exécution :
<div lang="en-US">
```bash
echo THAWED > /sys/fs/cgroup/freezer/virli/freezer.state
```
</div>
## Exercice : script de monitoring
À nous maintenant de concevoir un script qui va enregistrer vers une base de
données des statistiques issues des *cgroup*s.
### Rappel d'InfluxDB
Commençons par lancer le conteneur Docker d'InfluxDB (pour éviter de
l'installer sur notre machine) :
<div lang="en-US">
```bash
docker container run --name mytsdb -d -p 8086:8086 influxdb
```
</div>
Il nous faut ensuite créer une base de données pour y stocker nos
métriques. Voici comment on s'était débrouillé dans un précédent TP pour
interagir avec InfluxDB :
<div lang="en-US">
```bash
docker container exec -i mytsdb influx <<EOF
CREATE DATABASE metrics;
SHOW DATABASES;
EOF
```
</div>
Vérifiez que la base de données `metrics` a bien été créée.
### Monitoring instantané vers la console
Dans un premier temps, commençons par afficher dans la console, la quantité de
mémoire utilisée par le groupe monitoré.
Vous pouvez utiliser un programme comme
[`memhog`](https://virli.nemunai.re/memhog.c) pour remplir rapidement votre
mémoire.
<div lang="en-US">
```
42sh# mkdir /sys/fs/cgroup...
42sh$ echo $$ | sudo tee /sys/fs/cgroup.../tasks
42sh# ./monitor group_name memhog 500
~~~ 13595 ~~~ Current memory usage: 75194368/550502400 (13%)
~~~ 13595 ~~~ Current memory usage: 150290432/550502400 (27%)
~~~ 13595 ~~~ Current memory usage: 223690752/550502400 (40%)
~~~ 13595 ~~~ Current memory usage: 296828928/550502400 (53%)
~~~ 13595 ~~~ Current memory usage: 368001024/550502400 (66%)
~~~ 13595 ~~~ Current memory usage: 438517760/550502400 (79%)
~~~ 13595 ~~~ Current memory usage: 480329728/550502400 (87%)
~~~ 13595 ~~~ Current memory usage: 155648/550502400 (0%)
```
</div>
Si vous n'avez pas le *cgroup* *memory*, il est possible qu'il ne soit pas
activé par défaut par votre système. Si vous êtes dans ce cas, essayez
d'ajouter `cgroup_enable=memory` à la ligne de commande de votre noyau.
### Monitoring vers InfluxDB
Maintenant, envoyons nos données vers la base
<https://docs.influxdata.com/influxdb/v1.6/guides/writing_data/> :
<div lang="en-US">
```bash
curl -i -XPOST 'http://localhost:8086/write?db=metrics' --data-binary \
"$my_cgroup_name memory.usage_in_bytes=$(cat .../my_cgroup_name/memory.usage_in_bytes)"
```
</div>
Pour vérifier que les données ont bien été ajoutées, nous pouvons effectuer la
requête suivante dans notre client `influx` :
<div lang="en-US">
```sql
SELECT * from "$my_cgroup_name";
```
</div>
### Monitorer davantage de données
Liste non exhaustive de données à monitorer :
* Nombre d'IOs effectué ;
* nombre d'octets lu/écrit sur les disques ;
* temps de calcul utilisé ;
* trafic réseau généré ;
* ...
Tous les cgroups existants dans le dernier noyau publié ont leur documentation
accessible ici :
<https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/index.html>
### Permettre à l'utilisateur de monitorer des processus
Maintenant, séparons notre script en deux parties afin qu'un utilisateur normal
(non-root) puisse utiliser la partie monitoring de notre script.
Un premier script doit s'occuper de créer le(s) *cgroup*s et lui attribuer les
bons droits, tandis que le deuxième va effectuer le monitoring, sans privilèges
particuliers.
#### Exemple
<div lang="en-US">
```
42sh$ sudo ./monitor_init my_cgroup_name
42sh$ ./monitor my_cgroup_name memhog 500
```
</div>
## Fixer des limites
Au delà de la simple consultation, les *cgroup*s peuvent servir à limiter la
quantité de ressources mise à disposition à un groupe de processus.
Pour définir une limite, nous allons écrire la valeur dans le fichier
correspondant à une valeur limite, comme par exemple
`memory.max_usage_in_bytes`, qui limite le nombre d'octets que notre groupe de
processus va pouvoir allouer au maximum :
<div lang="en-US">
```
42sh$ cat /sys/fs/cgroup/memory/virli/memory.max_usage_in_bytes
0
# 0 = Aucune limite
42sh$ echo 4M > /sys/fs/cgroup/memory/virli/memory.max_usage_in_bytes
# Maintenant, la limite est à 4MB, vérifions...
42sh$ cat /sys/fs/cgroup/memory/virli/memory.max_usage_in_bytes
4194304
```
</div>
Chaque *cgroup*s défini de nombreux indicateurs et possède de nombreux
limiteurs, n'hésitez pas à consulter la documentation associée à chaque
*cgroup*.
## Pour aller plus loin {-}
Pour tout connaître en détails, [la série d'articles de Neil Brown sur les
Control groups](https://lwn.net/Articles/604609/) est excellente !
Depuis les noyaux 4.5, il est possible d'utiliser la nouvelle version du
pseudo système de fichiers des *CGroup*s. Le principal changement vient du
regroupement au sein d'une seule hiérarchie des différents *CGroup*s que l'on
avait dans la v1. Davantage d'informations sont disponibles :
* [Understanding the new control groups API](https://lwn.net/Articles/679786/)
;
* [Kernel Document about Control Group v2](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html).

View file

@ -1,46 +0,0 @@
#!/bin/sh
note_init() {
NOTE=0
echo -n $@
}
note() {
NOTE=$(($NOTE + $1))
echo -n ,$@
}
for LOGIN in $@
do
note_init $LOGIN
if [ -f "$LOGIN" ]; then
QUESTIONSF="$LOGIN"
else
QUESTIONSF="$LOGIN/questions.txt"
fi
# Questions
if grep -E -i "(grsec)" "${QUESTIONSF}" 2> /dev/null > /dev/null; then
note 1
else
note 0
fi
if grep -E -i "(/proc/.*/oom_|score)" "${QUESTIONSF}" 2> /dev/null > /dev/null; then
note 1
else
note 0
fi
if grep -E -i "root" "${QUESTIONSF}" 2> /dev/null > /dev/null; then
note 1
else
note 0
fi
echo #" = $NOTE"
done

View file

@ -1,111 +0,0 @@
\newpage
L'isolation ... du pauvre
=========================
Depuis les premières versions d'Unix, il est possible de changer le répertoire
vu comme étant la racine du système de fichiers. En anglais : *change root*:
`chroot`. Le processus effectuant cette action ainsi que tous ses fils, verront
donc une racine différente du reste du système.
## Mise en place de l'environnement
Pour se créer un environnement afin de changer notre racine, il va falloir
commencer par créer le dossier de notre nouvelle racine :
<div lang="en-US">
```bash
mkdir newroot
```
</div>
### `busybox`
Queques mots, pour commencer, à propos du projet Busybox : c'est un programme
*linké* statiquement, c'est-à-dire qu'il ne va pas chercher ni charger de
bibliothèque dynamique à son lancement. Il se suffit donc à lui-même dans un
*chroot*, car il n'a pas de dépendances. Nous pouvons donc tester notre
première isolation\ :
<div lang="en-US">
```bash
cp $(which busybox) newroot/
chroot newroot /busybox ash
```
</div>
Jusque là ... ça fonctionne, rien de surprenant ! Mais qu'en est-il pour
`bash` :
<div lang="en-US">
```bash
42sh$ cp $(which bash) newroot/
42sh# chroot newroot /bash
chroot: failed to run command bash: No such file or directory
```
</div>
De quel fichier est-il question ici ?
### `debootstrap`
`debootstrap` est le programme utilisé par l'installeur des distributions
Debian et ses dérivées. Il permet d'installer dans un dossier (en général, ce
dossier correspond au point de montage de la nouvelle racine choisie par
l'utilisateur lors de l'installation) le système de base.
<div lang="en-US">
```bash
debootstrap buster newroot/ http://httpredir.debian.org/debian/
```
</div>
`pacstrap` est le programme équivalent pour Archlinux.
### *stage3*
Les distributions *à l'ancienne* proposent encore de télécharger leur système
de base sous forme de tarball\ :
<div lang="en-US">
```bash
wget http://gentoo.mirrors.ovh.net/gentoo-distfiles/releases/amd64/autobuilds/current-stage3-amd64/stage3-amd64-20201104T214503Z.tar.xz
tar xpf stage3-amd64-*.tar.xz -C newroot/
```
</div>
L'avantage de télécharger l'archive de Gentoo est que l'on a déjà `gcc` dans un
environnement qui tient dans 300 MB.
## Exercice {-}
Écrivons maintenant un programme dont le seul but est de s'échapper du `chroot` :
<div lang="en-US">
```bash
make escape
echo bar > ../foo
chroot .
```
</div>
Dans le nouvel environnement, vous ne devriez pas pouvoir faire :
<div lang="en-US">
```bash
cat ../foo
```
</div>
Mais une fois votre programme `escape` exécuté, vous devriez pouvoir !
<div lang="en-US">
```
(chroot) 42sh# ./escape
bash# cat /path/to/foo
```
</div>

58
tutorial/4/cmpns.md Normal file
View file

@ -0,0 +1,58 @@
## Exercice : comparaison de *namespace*
Les *namespaces* d'un programme sont exposés sous forme de liens symboliques
dans le répertoire `/proc/<PID>/ns/`.
Deux programmes qui partagent un même *namespace* auront un lien vers la même
structure de données.
Écrivons un script ou un programme, `cmpns`, permettant de déterminer si deux
programmes s'exécutent dans les mêmes *namespaces*.
### Exemples {.unnumbered}
<div lang="en-US">
```
42sh$ ./cmpns $(pgrep influxdb) $(pgrep init)
- cgroup: differ
- ipc: differ
- mnt: differ
- net: differ
- pid: differ
- user: same
- uts: same
```
</div>
<div lang="en-US">
```
42sh$ ./cmpns $(pgrep init) self
- cgroup: same
- ipc: same
- mnt: same
- net: same
- pid: same
- user: same
- uts: same
```
</div>
Ici, `self` fait référence au processus actuellement exécuté (comme il existe
un dossier `/proc/self/`, vous n'avez pas besoin de gérer de cas particulier
pour ça !).
Et pourquoi pas :
<div lang="en-US">
```
42sh# unshare -m ./cmpns $$ self
- cgroup: same
- ipc: same
- mnt: differ
- net: same
- pid: same
- user: same
- uts: same
```
</div>

46
tutorial/4/docker-exec.md Normal file
View file

@ -0,0 +1,46 @@
Exercice : `docker exec`
------------------------
Après voir lu la partie concernant les *namespaces*, vous avez dû comprendre
qu'un `docker exec`, n'était donc rien de plus qu'un `nsenter(1)`.
Réécrivons, en quelques lignes, la commande `docker exec` !
Pour savoir si vous avez réussi, comparez les sorties des commandes :
- `ip address` ;
- `hostname` ;
- `mount` ;
- `pa -aux` ;
- ...
### Tests {-}
<div lang="en-US">
```
42sh$ docker run --name mywebserver -d -p 80:80 nginx
d63ceae863956f8312aca60b7a57fbcc1fdf679ae4c90c5d9455405005d4980a
42sh$ docker container inspect --format '{{ .State.Pid }}' mywebserver
234269
42sh# ./mydocker_exec mywebserver ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
13: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.1/16 scope global eth0
valid_lft forever preferred_lft forever
42sh# hostname
koala.zoo.paris
42sh# ./mydocker_exec mywebserver hostname
d63ceae86395
42sh# ./mydocker_exec mywebserver mount
42sh# ./mydocker_exec mywebserver ps aux
...
```
</div>

View file

@ -1,102 +0,0 @@
\newpage
Prérequis
=========
## Noyau Linux
Ce TP requiert un noyau Linux, dans sa version 3.8 au minimum. Il doit de plus
être compilé avec les options suivantes (lorsqu'elles sont disponibles pour
votre version) :
<div lang="en-US">
```
General setup --->
[*] Control Group support --->
[*] Freezer cgroup subsystem
[*] PIDs cgroup subsystem
[*] Device controller for cgroups
[*] Cpuset support
[*] Simple CPU accounting cgroup subsystem
[*] Memory Resource Controller for Control Groups
[*] Group CPU scheduler --->
[*] Group scheduling for SCHED_OTHER
[*] Group scheduling for SCHED_RR/FIFO
<*> Block IO controller
[*] Networking support --->
Networking options --->
[*] Network priority cgroup
[*] Network classid cgroup
```
</div>
### Vérification via `menuconfig`
L'arbre ci-dessous correspond aux options qui seront *built-in* (signalées par
une `*`) ou installées en tant que module (signalées par un `M`). En effet,
chaque noyau Linux peut être entièrement personnalisé en fonction des options
et des pilotes que l'on voudra utiliser.
Pour parcourir l'arbre des options du noyau, il est nécessaire d'avoir les
sources de celui-ci. Les dernières versions stables et encore maintenues sont
disponibles sur la page d'accueil de <https://kernel.org>.
Dans les sources, on affiche la liste des options avec la commande :
<div lang="en-US">
```bash
make menuconfig
```
</div>
### Vérification via `/boot/config-xxx`
Les distributions basées sur Debian ont pour habitude de placer le fichier de
configuration ayant servi à compiler le noyau et ses modules dans le dossier
`/boot`, aux côtés de l'image du noyau `vmlinuz-xxx`, de l'éventuel système de
fichiers initial (`initramfs-xxx`) et des symboles de débogage
`System.map-xxx`.
Ce fichier répertorie toutes les options qui ont été activées. Par rapport à
l'arbre présenté ci-dessus, vous devriez trouver :
<div lang="en-US">
```
CONFIG_CGROUPS=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_PIDS=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CPUSETS=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_MEMCG=y
CONFIG_CGROUP_SCHED=y
CONFIG_BLK_CGROUP=y
CONFIG_NET=y
CONFIG_CGROUP_NET_PRIO=y
CONFIG_CGROUP_NET_CLASSID=y
```
</div>
### Vérification via `/proc/config.gz`
Dans la plupart des autres distributions, la configuration est accessible à
travers le fichier `/proc/config.gz`. Comme vous ne pouvez pas écrire dans
`/proc` pour décompresser le fichier, utilisez les outils `zcat`, `zgrep`, ...
Vous devez retrouver les mêmes options que celles de la section précédente.
## Présence des en-têtes
Si vous utilisez un noyau standard fourni par votre distribution, les options
requises seront a priori déjà sélectionnées et vous n'aurez donc pas à compiler
votre propre noyau. Néanmoins, durant ce TP, nous allons nous interfacer avec
le noyau, il est donc nécessaire d'avoir les en-têtes de votre noyau.
Sous Debian, vous pouvez les installer via le paquet au nom semblable à
`linux-headers`. Le paquet porte le même nom sous Arch Linux et ses dérivés.

12
tutorial/4/lesson.md Normal file
View file

@ -0,0 +1,12 @@
---
title: Virtualisation légère -- Linux Internals partie 2
subtitle: Support de cours
author: Pierre-Olivier *nemunaire* [Mercier]{.smallcaps}
institute: EPITA
date: Mercredi 7 novembre 2018
abstract: |
Le but de cette seconde partie sur les mécanismes internes du noyau
va nous permettre d'utiliser les commandes et les appels systèmes
relatifs aux espaces de noms du noyau Linux ainsi que d'appréhender
la complexité des sytèmes de fichiers.
...

302
tutorial/4/mount.md Normal file
View file

@ -0,0 +1,302 @@
\newpage
Des particularités de `mount` {#mount}
=============================
Petite parenthèse avant de parler des *namespaces* ...
## Les points de montage
Au premier abord, les points de montage dans l'arborescence d'un système de
fichiers n'ont pas l'air d'être remplis de notions complexes : un répertoire
peut être le point d'entrée d'un montage vers la partition d'un disque
physique... ou d'une partition virtuelle, comme nous l'avons vu dans le TP
précédent.
Mais avez-vous déjà essayé de monter la même partition d'un disque physique à
deux endroits différents de votre arborescence ?
Si pour plein de raisons on pouvait se dire que cela ne devrait pas être
autorisé, ce problème s'avère être à la base de beaucoup de fonctionnalités
intéressantes. Le noyau va finalement décorréler les notions de montage,
d'accès et d'accroches dans l'arborescence : et par exemple, une partition ne
sera plus forcément démontée après un appel à `umount(2)`, mais le sera
seulement lorsque cette partition n'aura plus d'accroches dans aucune
arborescence.
La commande `findmnt(1)`, des
[`util-linux`](https://www.kernel.org/pub/linux/utils/util-linux/) nous permet
d'avoir une vision arborescente des points de montage en cours d'utilisation.
<div lang="en-US">
```
TARGET SOURCE FSTYPE OPTIONS
/ /dev/sda1 ext4 rw,relatime,data=ordered
├─/proc proc proc rw,nosuid,nodev,noexec,relatime
├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/security securityfs securityfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/firmware/efi/efivars efivarfs efivarfs ro,relatime
│ └─/sys/fs/cgroup cgroup_root tmpfs rw,nosuid,nodev,noexec,relatime,size=10240k,mode=755
│ ├─/sys/fs/cgroup/unified none cgroup2 rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/cgroup/cpuset cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset
│ ├─/sys/fs/cgroup/cpu cpu cgroup rw,nosuid,nodev,noexec,relatime,cpu
│ ├─/sys/fs/cgroup/cpuacct cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpuacct
│ ├─/sys/fs/cgroup/blkio blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio
│ ├─/sys/fs/cgroup/memory memory cgroup rw,nosuid,nodev,noexec,relatime,memory
│ ├─/sys/fs/cgroup/devices devices cgroup rw,nosuid,nodev,noexec,relatime,devices
│ ├─/sys/fs/cgroup/freezer freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer
│ ├─/sys/fs/cgroup/net_cls net_cls cgroup rw,nosuid,nodev,noexec,relatime,net_cls
│ ├─/sys/fs/cgroup/perf_event perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event
│ ├─/sys/fs/cgroup/net_prio net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_prio
│ └─/sys/fs/cgroup/pids pids cgroup rw,nosuid,nodev,noexec,relatime,pids
├─/dev devtmpfs devtmpfs rw,nosuid,size=10240k,nr_inodes=486250,mode=755
│ ├─/dev/pts devpts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000
│ ├─/dev/shm tmpfs tmpfs rw
│ └─/dev/mqueue mqueue mqueue rw,nosuid,nodev,noexec,relatime
├─/home /dev/sda3 ext4 rw,nosuid,nodev,relatime,data=ordered
├─/run tmpfs tmpfs rw,nosuid,nodev,noexec,mode=755
└─/tmp tmpfs tmpfs rw,nosuid,nodev,noexec,relatime
```
</div>
## `bind`
Lorsque l'on souhaite monter à un deuxième endroit (ou plus) une partition, on
utilise le *bind mount* :
<div lang="en-US">
```bash
mount --bind olddir newdir
```
</div>
Lorsque l'on souhaite `chroot` dans un système complet (par exemple lorsqu'on
l'installe ou qu'on le répare via un *live CD*), il est nécessaire de dupliquer
certains points de montage, tels que `/dev`, `/proc` et `/sys`.
Sans monter ces partitions, vous ne serez pas en mesure d'utiliser le système
dans son intégralité : vous ne pourrez pas monter les partitions indiquées par
le `/etc/fstab`, vous ne pourrez pas utiliser `top` ou `ps`, `sysctl` ne pourra
pas accorder les paramètres du noyau, ...
Pour que tout cela fonctionne, nous aurons besoin, au préalable, d'exécuter les
commandes suivantes :
<div lang="en-US">
```bash
cd newroot
mount --bind /dev dev
mount --bind /proc proc
mount --bind /sys sys
```
</div>
En se `chroot`ant à nouveau dans cette nouvelle racine, tous nos outils
fonctionneront comme prévu.
Tous ? ... en fait non. Si l'on jette un œil à `findmnt(1)`, nous constatons
par exemple que `/sys/fs/cgroup` dans notre nouvelle racine est vide, alors que
celui de notre machine hôte contient bien les répertoires de nos *cgroups*.
`--bind` va se contenter d'attacher le système de fichiers (ou au moins une
partie de celui-ci) à un autre endroit, sans se préoccuper des points de
montages sous-jacents. Pour effectuer cette action récursivement, et donc
monter au nouvel emplacement le système de fichier ainsi que tous les points
d'accroche qu'il contient, il faut utiliser `--rbind`. Il serait donc plus
correct de lancer :
<div lang="en-US">
```bash
cd newroot
mount --rbind /dev dev
mount -t proc none proc
mount --rbind /sys sys
```
</div>
## Les montages parfumés
On distingue quatre variétés de répercution des montages pour un sous-arbre :
partagé, esclave, privé et non-attachable.
Chacun va agir sur la manière dont seront propagées les nouvelles accroches au
sein d'un système de fichiers attaché à plusieurs endroits.
### partagé -- *shared mount*
Dans un montage partagé, une nouvelle accroche sera propagée parmi tous les
systèmes de fichiers de ce partage (on parle de *peer group*). Voyons avec un
exemple :
<div lang="en-US">
```bash
# Création de notre répertoire de travail
mkdir /mnt/test-shared
# On s'assure que le dossier que l'on va utiliser pour nos tests utilise bien la politique shared
mount --make-shared /tmp
# Duplication de l'accroche, sans s'occuper des éventuels sous-accroches
mount --bind /tmp /mnt/test-shared
```
</div>
Si l'on attache un nouveau point de montage dans `/tmp` ou dans
`/mnt/test-shared`, avec la politique `shared`, l'accroche sera propagée :
<div lang="en-US">
```bash
mkdir /mnt/test-shared/toto
mount -t tmpfs none /mnt/test-shared/toto
```
</div>
Un coup de `findmnt` nous montre l'existence de deux nouveaux points de
montage. À `/mnt/test-shared/toto`, mais également à `/tmp/toto`.
### esclave -- *slave mount*
De la même manière que lorsque la propagation est partagée, cette politique
propagera, mais seulement dans un sens. Le point de montage déclaré comme
esclave ne propagera pas ses nouveaux points de montage à son *maître*.
<div lang="en-US">
```bash
# Suite de l'exemple précédent
cd /mnt/test-slave
# Duplication de l'accroche, sans s'occuper des éventuels sous-accroches
mount --bind /mnt/test-shared /mnt/test-slave
# On rend notre dossier esclave
mount --make-slave /mnt/test-slave
```
</div>
Si l'on effectue un montage dans `/mnt/test-shared` :
<div lang="en-US">
```bash
mkdir /mnt/test-shared/foo
mount -t tmpfs none /mnt/test-shared/foo
```
</div>
Le point de montage apparaît bien sous `/mnt/test-slave/foo`. Par contre :
<div lang="en-US">
```bash
mkdir /mnt/test-slave/bar
mount -t tmpfs none /mnt/test-slave/bar
```
</div>
Le nouveau point de montage n'est pas propagé dans `/mnt/test-shared/bar`.
### privé -- *private mount*
C'est le mode le plus simple : ici les points de montage ne sont tout
simplement pas propagés.
Pour forcer un point d'accroche à ne pas propager et à ne pas recevoir de
propagation, on utilise l'option suivante :
<div lang="en-US">
```bash
mount --make-private mountpoint
```
</div>
### non-attachable -- *unbindable mount*
Ce mode interdira toute tentative d'attache à un autre endroit.
<div lang="en-US">
```bash
mount --make-unbindable /mnt/test-slave
```
</div>
Il ne sera pas possible de faire :
<div lang="en-US">
```bash
mkdir /mnt/test-unbindable
mount --bind /mnt/test-slave /mnt/test-unbindable
```
</div>
### Parfums récursifs
Les options que nous venons de voir s'appliquent sur un point de montage. Il
existe les mêmes options pour les appliquer en cascade sur les points d'attache
contenus dans leur sous-arbre :
<div lang="en-US">
```bash
mount --make-rshared mountpoint
mount --make-rslave mountpoint
mount --make-rprivate mountpoint
mount --make-runbindable mountpoint
```
</div>
## `bind` de dossiers et de fichiers
Il n'est pas nécessaire que le point d'accroche que l'on cherche à dupliquer
pointe sur un point de montage (c'est-à-dire, dans la plupart des cas : une
partition ou un système de fichiers virtuel). Il peut parfaitement pointer sur
un dossier, et même sur un simple fichier, à la manière d'un *hardlink*, mais
que l'on pourrait faire entre plusieurs partitions et qui ne persisterait pas
au redémarrage (le *hardlink* persiste au redémarrage, mais doit se faire au
sein d'une même partition).
Nous verrons dans la partie [*namespace* réseau](#net-ns), une utilisation
d'attache sur un fichier.
## Déplacer un point de montage
À tout moment, il est possible de réorganiser les points de montage, en les
déplaçant. Comme cela se fait sans démonter de partition, il est possible de le
faire même si un fichier est en cours d'utilisation. Il faut cependant veiller
à ce que les programmes susceptibles d'aller chercher un fichier à l'ancien
emplacement soient prévenus du changement.
Pour déplacer un point de montage, on utilise l'option `--move` de `mount(8)` :
<div lang="en-US">
```bash
mount --move olddir newdir
```
</div>
Par exemple :
<div lang="en-US">
```bash
mount --move /dev /newroot/dev
```
</div>
Cette possibilité s'emploie notamment lorsque l'on souhaite changer la racine
de notre système de fichiers : par exemple pour passer de l'*initramfs* au
système démarré, de notre système hôte au système d'un conteneur, ...
## Aller plus loin {-}
Voici quelques articles qui valent le détour, en lien avec les points de
montage :
* [Shared subtree](https://lwn.net/Articles/159077) et la
[documentation du noyau associée](https://kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) ;
* [Mount namespaces and shared subtrees](https://lwn.net/Articles/689856) ;
* [Mount namespaces, mount propagation, and unbindable mounts](https://lwn.net/Articles/690679).

137
tutorial/4/mountns.md Normal file
View file

@ -0,0 +1,137 @@
\newpage
Le *namespace* `mount`
======================
L'espace de noms `mount` permet d'isoler la vision du système de fichiers
qu'ont un processus et ses fils.
Peut-être que l'on peut trouver avec ça, un moyen de faire un `chroot` plus sûr ?
## Préparation du changement de racine
### Avant propos
Nous allons essayer de changer la racine de notre système de fichier. À la
différence d'un `chroot(2)`, changer de racine est quelque chose d'un peu plus
sportif car il s'agit de ne plus avoir aucune trace de l'ancienne racine. Au
moins ici, il ne sera certainement pas possible de revenir en arrière dans
l'arborescence\ !
Pour l'instant, votre système utilise sans doute la partition d'un disque
physique comme racine de son système de fichier. Le changement de racine, va
nous permettre d'utiliser un autre système.
Bien sûr, nous n'allons pas changer la racine de votre système hôte, nous
allons faire cela dans un *namespace* qui nous permet d'avoir des points de
montage virtuels. Le changement de racine sera donc effectif uniquement dans
cet espace de noms.
### L'environnement
Pour pouvoir changer de racine, il est nécessaire que la nouvelle racine soit
la racine d'un point de montage, comme l'explique `pivot_root(2)`. En effet, il
serait encore possible hypothétiquement de remonter dans l'arborescence si l'on
ne se trouvait pas à la racine d'une partition au moment du basculement.
Si vous n'avez pas de partition à disposition, vous pouvez utiliser un `tmpfs` :
<div lang="en-US">
```bash
42sh# mkdir /mnt/newroot
42sh# mount -t tmpfs none /mnt/newroot
```
</div>
Placez ensuite dans cette nouvelle racine le système de votre choix (cf. le TP
précédent pour les différentes méthodes et liens).
## Changer de racine
Voici les grandes étapes du changement de racine :
1. S'isoler dans les *namespaces* adéquats ;
2. Démonter ou déplacer toutes les partitions de l'ancienne racine vers la
nouvelle racine ;
3. `pivot_root` !
### S'isoler
Notre but étant de démonter toutes les partitions superflues, nous allons
devoir nous isoler sur :
* les points de montages, ça semble évident ;
* les PIDs : car on ne pourra pas démonter une partition en cours
d'utilisation. S'il n'y a pas de processus, il n'y a personne pour nous
empêcher de démonter une partition !
* les autres *namespaces* ne sont pas forcément nécessaires.
Isolons-nous :
<div lang="en-US">
```bash
42sh# unshare -p -m -f --mount-proc
```
</div>
### Dissocier la propagation des démontages
Attention ! avant de pouvoir commencer à démonter les partitions, il faut
s'assurer que les démontages ne se propagent pas via une politique de *shared
mount*.
Commençons donc par étiqueter tous nos points de montage (de ce *namespace*),
comme esclave :
<div lang="en-US">
```bash
42sh# mount --make-rslave /
```
</div>
### Démonter tout !
À vous maintenant de démonter vos points d'attache. Il ne devrait vous rester
après cette étape que : `/`, `/dev`, `/sys`, `/proc`, `/run` et leurs fils.
### Switch !
À ce stade, dans votre console, vous avez plusieurs solutions : utiliser
`switch_root(8)` ou `pivot_root(8)`. La première abstrait plus de choses que la
seconde.
#### `switch_root`
Cette commande s'occupe de déplacer les partitions restantes pour vous, et lance
la première commande (*init*) de votre choix.
#### `pivot_root`
Cette commande, plus proche du fonctionnement de l'appel système
`pivot_root(2)`, requiert de notre part que nous ayons préalablement déplacé
les partitions systèmes à leur place dans la nouvelle racine.
L'appel de la commande sert à intervertir les deux racines ; elle prend en argument :
* le chemin de la nouvelle racine,
* le chemin dans la nouvelle racine où placer l'ancienne.
Une fois le pivot effectué, on peut démonter l'ancienne racine.
Pour lancer la première commande dans la nouvelle racine, on passe généralement
par :
<div lang="en-US">
```bash
42sh# exec chroot / command
```
</div>

318
tutorial/4/namespaces.md Normal file
View file

@ -0,0 +1,318 @@
\newpage
Les espaces de noms -- *namespaces* {#namespaces}
===================================
## Introduction
Les espaces de noms du noyau, les *namespaces*, permettent de
dupliquer certaines structures, habituellement considérées uniques
pour le noyau, dans le but de les isoler d'un groupe de processus à un
autre.
On en dénombre sept (le dernier ayant été ajouté dans Linux 4.6) : `cgroup`,
`IPC`, `network`, `mount`, `PID`, `user` et `UTS`.
La notion d'espace de noms est relativement nouvelle et a été intégrée
progressivement au sein du noyau Linux. Aussi, toutes les structures
ne sont pas encore *containerisables* :
[le document fondateur](https://www.kernel.org/doc/ols/2006/ols2006v1-pages-101-112.pdf)
parle ainsi d'isoler les périphériques, ou encore l'horloge. Pour ce
dernier,
[un patch a même déjà été proposé](https://lwn.net/Articles/766089/).
### L'espace de noms `mount` {#mount-ns}
Depuis Linux 2.4.19.
Cet espace de noms isole la liste des points de montage.
Chaque processus appartenant à un *namespace mount* différent peut monter,
démonter et réorganiser à sa guise les points de montage, sans que cela n'ait
d'impact sur les processus hors de cet espace de noms. Une partition ne sera
donc pas nécessairement démontée après un appel à `umount(2)`, elle le sera
lorsqu'elle aura effectivement été démontée de chaque *namespace mount* dans
lequel elle était montée.
Attention il convient cependant de prendre garde aux types de liaison existant
entre vos points de montage (voir la partie sur
[les particularités des points de montage](#mount)), car les montages et
démontages pourraient alors être répercutés dans l'espace de noms parent.
Une manière rapide pour s'assurer que nos modifications ne sortiront pas de
notre *namespace* est d'appliquer le type esclave à l'ensemble de nos points de
montage, récursivement, dès que l'on est entré dans notre nouvel espace de
noms.
<div lang="en-US">
```bash
mount --make-rslave /
```
</div>
### L'espace de noms `UTS` {#uts-ns}
Depuis Linux 2.6.19.
Cet espace de noms isole le nom de machine et son domaine NIS.
### L'espace de noms `IPC` {#ipc-ns}
Depuis Linux 2.6.19.
Cet espace de noms isole les objets IPC et les files de messages POSIX.
Une fois le *namespace* attaché à un processus, il ne peut alors plus parler
qu'avec les autres processus de son espace de noms (lorsque ceux-ci passent par
l'API IPC du noyau).
### L'espace de noms `PID`
Depuis Linux 2.6.24.
Cet espace de noms isole la liste des processus et virtualise leurs numéros.
Une fois dans un espace, le processus ne voit que le sous-arbre de processus
également attachés à son espace. Il s'agit d'un sous-ensemble de l'arbre global
de PID : les processus de tous les PID *namespaces* apparaissent donc dans
l'arbre initial.
Pour chaque nouvel espace de noms de processus, une nouvelle numérotation est
initiée. Ainsi, le premier processus de cet espace porte le numéro 1 et aura
les mêmes propriétés que le processus `init` usuel\ ; entre autre, si un
processus est rendu orphelin dans ce *namespace*, il devient un fils de ce
processus, et non un fils de l'`init` de l'arbre global.
### L'espace de nom `network`
Depuis Linux 2.6.29.
Cet espace de noms fournit une isolation pour toutes les ressources associées
aux réseaux : les interfaces, les piles protocolaires IPv4 et IPv6, les tables
de routage, règles pare-feu, ports numérotés, etc.
Une interface réseau (`eth0`, `wlan0`, ...) ne peut se trouver que dans un seul
espace de noms à la fois. Il est par contre possible de les déplacer.
Lorsque le *namespace* est libéré (généralement lorsque le dernier processus
attaché à cet espace de noms se termine), les interfaces qui le composent sont
ramenées dans l'espace initial/racine (et non pas dans l'espace parent, en cas
d'imbrication).
### L'espace de noms `user`
Depuis Linux 3.8.
Cet espace de noms isole la liste des utilisateurs, des groupes, leurs
identifiants, les *capabilities*, la racine et le trousseau de clefs du noyau.
La principale caractéristique est que les identifiants d'utilisateur et de
groupe pour un processus peuvent être différents entre l'intérieur et
l'extérieur de l'espace de noms. Il est donc possible, alors que l'on est un
simple utilisateur à l'extérieur du *namespace*, d'avoir l'UID 0 dans le
conteneur.
### L'espace de noms `cgroup` {#cgroup-ns}
Depuis Linux 4.6.
Cet espace de noms filtre l'arborescence des *Control Group* en changeant la
racine de l'arborescence des cgroups. Au sein d'un *namespace*, la racine vue
correspond en fait à un sous-groupe de l'arborescence globale.
Ainsi, un processus dans un `CGroup` *namespace* ne peut pas voir le contenu
des sous-groupes parents (pouvant laisser fuiter des informations sur le reste
du système). Cela peut également permettre de faciliter la migration de
processus (d'un système à un autre) : l'arborescence des cgroups n'a alors
plus d'importance car le processus ne voit que son groupe.
## S'isoler dans un nouveau *namespace*
### Avec son coquillage
De la même manière que l'on peut utiliser l'appel système `chroot(2)` depuis un
shell via la commande `chroot(1)`, la commande `unshare(1)` permet de faire le
nécessaire pour lancer l'appel système `unshare(2)`, puis, tout comme
`chroot(1)`, exécuter le programme passé en paramètre.
En fonction des options qui lui sont passées, `unshare(1)` va créer le/les
nouveaux *namespaces* et placer le processus dedans.
Par exemple, nous pouvons modifier sans crainte le nom de notre machine, si
nous sommes passés dans un autre *namespace* `UTS` :
<div lang="en-US">
```
42sh# hostname --fqdn
koala.zoo.paris
42sh# sudo unshare -u /bin/bash
bash# hostname --fqdn
koala.zoo.paris
bash# hostname lynx.zoo.paris
bash# hostname --fqdn
lynx.zoo.paris
bash# exit
42sh# hostname --fqdn
koala.zoo.paris
```
</div>
Nous avons pu ici modifier le nom de la machine, sans que cela n'affecte notre
machine hôte.
### Les appels systèmes
L'appel système par excellence pour contrôler l'isolation d'un nouveau
processus est `clone(2)`.
L'isolement ou non du processus est faite en fonction des `flags` qui sont
passés à la fonction :
* `CLONE_NEWNS`,
* `CLONE_NEWUTS`,
* `CLONE_NEWIPC`,
* `CLONE_NEWPID`,
* `CLONE_NEWNET`,
* `CLONE_NEWUSER`,
* `CLONE_NEWCGROUP`.
On peut bien entendu cumuler un ou plusieurs de ces `flags`, et les combiner
avec d'autres `flags` attendu par la fonction.
Les mêmes `flags` sont utilisés lors des appels à `unshare(2)` ou `setns(2)`.
Pour créer un nouveau processus qui sera à la fois dans un nouvel espace de
noms réseau et dans un nouveau *namespace* `cgroup`, on écrirait un code
similaire à :
<div lang="en-US">
```c
#include <sched.h>
#define STACKSIZE (1024 * 1024)
static char child_stack[STACKSIZE];
int clone_flags = CLONE_CGROUP | CLONE_NEWNET | SIGCHLD;
pid_t pid = clone(do_execvp, // First function executed by child
child_stack + STACKSIZE, // Assume stack grows downward
clone_flags, // clone specials flags
args); // Arguments to pass to do_execvp
```
</div>
Dans cet exemple, le processus fils créé disposera d'un nouvel espace de noms
pour les *CGroups* et disposera d'une nouvelle pile réseau.
Un exemple complet d'utilisation de `clone(2)` et du *namespace* `UTS` est
donné dans le `man` de l'appel système.
## Rejoindre un *namespace*
Rejoindre un espace de noms se fait en utilisant l'appel système `setns(2)`,
auquel on passe le *file descriptor* d'un des liens du dossier
`/proc/<PID>/ns/` :
<div lang="en-US">
```c
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdlib.h>
// ./a.out /proc/PID/ns/FILE cmd args...
int
main(int argc, char *argv[])
{
int fd = open(argv[1], O_RDONLY);
if (fd == -1)
{
perror("open");
return EXIT_FAILURE;
}
if (setns(fd, 0) == -1)
{
perror("setns");
return EXIT_FAILURE;
}
execvp(argv[2], &argv[2]);
perror("execve");
return EXIT_FAILURE;
}
```
</div>
Dans un shell, on utilisera la commande `nsenter(1)` :
<div lang="en-US">
```bash
42sh# nsenter --uts=/proc/42/ns/uts /bin/bash
```
</div>
## Durée de vie d'un *namespace* {#ns-lifetime}
Le noyau tient à jour un compteur de références pour chaque *namespace*. Dès
qu'une référence tombe à 0, l'espace de noms est automatiquement libéré, les
points de montage sont démontés, les interfaces réseaux sont réattribués à
l'espace de noms initial, ...
Ce compteur évolue selon plusieurs critères, et principalement selon le nombre
de processus qui l'utilise. C'est-à-dire que, la plupart du temps, le
*namespace* est libéré lorsque le dernier processus s'exécutant dedans se
termine.
Lorsque l'on a besoin de référencer un *namespace* (par exemple pour le faire
persister après le dernier processus), on peut utiliser un `mount bind` :
<div lang="en-US">
```bash
42sh# touch /tmp/ns/myrefns
42sh# mount --bind /proc/<PID>/ns/mount /tmp/ns/myrefns
```
</div>
De cette manière, même si le lien initial n'existe plus (si le `<PID>` s'est
terminé), `/tmp/ns/myrefns` pointera toujours au bon endroit.
On peut très bien utiliser directement ce fichier pour obtenir un descripteur
de fichier valide vers le *namespace* (pour passer à `setns(2)`).
### Faire persister un *namespace*
Il n'est pas possible de faire persister un espace de noms d'un reboot à
l'autre.
Même en étant attaché à un fichier du disque, il s'agit d'un pointeur vers une
structure du noyau, qui ne persistera pas au redémarrage.
## Aller plus loin {-}
Je vous recommande la lecture des *man* suivants :
* `namespaces(7)` : introduisant et énumérant les *namespaces* ;
Pour tout connaître en détails, [la série d'articles de Michael Kerrisk sur
les *namespaces*](https://lwn.net/Articles/531114/) est excellente ! Auquel il
faut ajouter [le petit dernier sur le `cgroup`
*namespace*](https://lwn.net/Articles/621006/).
[Cet article de Michael Crosby montrant l'utilisation de clone(2)](https://web.archive.org/web/20190206073558/http://crosbymichael.com/creating-containers-part-1.html)
est également des plus intéressants, pour ce qui concerne la programmation
plus bas-niveau.

241
tutorial/4/networkns.md Normal file
View file

@ -0,0 +1,241 @@
\newpage
Le *namespace* `network` {#net-ns}
========================
## Introduction
L'espace de noms `network`, comme son nom l'indique permet de virtualiser tout
ce qui est en lien avec le réseau : les interfaces, les ports, les routes, les
règles de filtrage, etc.
En entrant dans un nouvel espace de nom `network`, on se retrouve dans un
environnement qui n'a plus qu'une interface de *loopback* :
<div lang="en-US">
```
42sh# unshare -n ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
```
</div>
Bien que portant le même nom que l'interface de *loopback* de notre
environnement principal, il s'agit bien de deux interfaces isolées l'une de
l'autre.
Qui dit nouvelle pile réseau, dit également que les ports qui sont assignés
dans l'espace principal, ne le sont plus dans le conteneur : il est donc
possible de lancer un serveur web sans qu'il n'entre en conflit avec celui d'un
autre espace de noms.
## Premiers pas avec `ip netns`
La suite d'outils `iproute2` propose une interface simplifiée pour utiliser le
*namespace* `network` : `ip netns`.
Nous pouvons tout d'abord créer un nouvel espace de nom :
<div lang="en-US">
```bash
42sh# ip netns add virli
```
</div>
La technique utilisée ici pour avoir des *namespaces* nommés est la même que
celle que nous avons vue dans
[la première partie sur les *namespaces*](#ns-lifetime) : via un `mount --bind`
dans le dossier `/var/run/netns/`. Cela permet de faire persister le namespace
malgré le fait que plus aucun processus ne s'y exécute.
Maintenant que notre *namespace* est créé, nous pouvons regarder s'il contient
des interfaces :
<div lang="en-US">
```
42sh# ip netns exec virli ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
```
</div>
Cette commande ne nous montre que l'interface de *loopback*, car nous n'avons
pour l'instant pas encore attaché la moindre interface.
D'ailleurs, cette interface est rapportée comme étant désactivée, activons-là
via la commande :
<div lang="en-US">
```bash
42sh# ip netns exec virli ip link set dev lo up
```
</div>
À ce stade, nous pouvons déjà commencer à lancer un `ping` sur cette interface:
<div lang="en-US">
```
42sh# ip netns exec virli ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.038 ms
...
```
</div>
## *Virtual Ethernet*
Étant donné qu'une interface réseau ne peut être présente que dans un seul
espace de noms à la fois, il n'est pas bien pratique d'imposer d'avoir une
interface physique par conteneur, d'autant plus si l'on a plusieurs
centaines de conteneurs à gérer.
Une technique couramment employée consiste à créer une interface virtuelle de
type `veth` :
<div lang="en-US">
```
42sh# ip link add veth0 type veth peer name veth1
```
</div>
Une interface `veth` se comporte comme un tube bidirectionnel : tout ce qui
entre d'un côté sort de l'autre et inversement. La commande précédente a donc
créé deux interfaces `veth0` et `veth1` : les paquets envoyés sur `veth0` sont
donc reçus par `veth1` et les paquets envoyés à `veth1` sont reçus par `veth0`.
Dans cette configuration, ces deux interfaces ne sont pas très utiles, mais si
l'on place l'une des deux extrêmités dans un autre *namespace* `network`, il
devient alors possible de réaliser un échange de paquets entre les deux.
Pour déplacer `veth1` dans notre *namespace* `virli` :
<div lang="en-US">
```bash
42sh# ip link set veth1 netns virli
```
</div>
Il ne reste maintenant plus qu'à assigner une IP à chacune des interfaces :
<div lang="en-US">
```bash
42sh# ip netns exec virli ip a add 10.10.10.42/24 dev veth1
42sh# ip a add 10.10.10.41/24 dev veth0
```
</div>
Dès lors[^linkdown], nous pouvons `ping`er chaque extrêmité :
[^linkdown]: Il peut être nécessaire d'activer chaque lien, via `ip link set
vethX up`.
<div lang="en-US">
```
42sh# ping 10.10.10.42
- et -
42sh# ip netns exec virli ping 10.10.10.41
```
</div>
Il ne reste donc pas grand chose à faire pour fournir Internet à notre
conteneur : via un peu de NAT ou grâce à un pont Ethernet.
## Les autres types d'interfaces
Le bridge ou le NAT obligera tous les paquets à passer à travers de nombreuses
couches du noyau. Utiliser les interfaces *veth* est plutôt simple et disponible
partout, mais c'est loin d'être la technique la plus rapide ou la moins
gourmande.
### VLAN
Il est possible d'attribuer juste une interface de VLAN, si l'on a un switch
supportant la technologie [802.1q](https://fr.wikipedia.org/wiki/IEEE_802.1Q)
derrière notre machine.
<div lang="en-US">
```
42sh# ip link add link eth0 name eth0.100 type vlan id 100
42sh# ip link set dev eth0.100 up
42sh# ip link set eth0.100 netns virli
```
</div>
### MACVLAN
<!-- https://hicu.be/bridge-vs-macvlan -->
Lorsque l'on n'a pas assez de carte ethernet et que le switch ne supporte pas
les VLAN, le noyau met à disposition un routage basé sur les adresses MAC : le
MACVLAN. S'il est activé dans votre noyau, vous allez avoir le choix entre l'un
des quatre modes : *private*, VEPA, *bridge* ou *passthru*.
Quelque soit le mode choisi, les paquets en provenance d'autres machines et à
destination d'un MAC seront délivrés à l'interface possédant la MAC. Les
différences entre les modes se trouvent au niveau de la communication entre les
interfaces.
#### VEPA
Dans ce mode, tous les paquets sortants sont directement envoyés sur
l'interface Ethernet de sortie, sans qu'aucun routage préalable n'ait été
effectué. Ainsi, si un paquet est à destination d'un des autres conteneurs de
la machine, c'est à l'équipement réseau derrière la machine de rerouter le
paquet vers la machine émettrice (par exemple un switch
[802.1Qbg](http://www.ieee802.org/1/pages/802.1bg.html)).
Pour construire une nouvelle interface de ce type :
<div lang="en-US">
```
42sh# ip link add link eth0 mac0 type macvlan mode vepa
```
</div>
#### *Private*
À la différence du mode *VEPA*, si un paquet émis par un conteneur à
destination d'un autre conteneur est réfléchi par un switch, le paquet ne sera
pas délivré.
Dans ce mode, on est donc assuré qu'aucun conteneur ne pourra parler à un autre
conteneur de la même machine.
<div lang="en-US">
```
42sh# ip link add link eth0 mac1 type macvlan mode private
```
</div>
#### *Bridge*
À l'inverse des modes *VEPA* et *private*, les paquets sont routés selon leur
adresse MAC : si jamais une adresse MAC est connue, le paquet est délivré à
l'interface MACVLAN correspondante ; dans le cas contraire, le paquet est
envoyé sur l'interface de sortie.
Pour construire une nouvelle interface de ce type :
<div lang="en-US">
```
42sh# ip link add link eth0 mac2 type macvlan mode bridge
```
</div>
## Aller plus loin {-}
Pour approfondir les différentes techniques de routage, je vous
recommande cet article :
[Linux Containers and Networking](https://blog.flameeyes.eu/2010/09/linux-containers-and-networking).
Appliqué à Docker, vous apprécirez cet article : [Understanding Docker
Networking Drivers and their use
cases](https://www.docker.com/blog/understanding-docker-networking-drivers-use-cases/).

View file

@ -1,52 +0,0 @@
\newpage
Gestion de la mémoire
=====================
Linux a une gestion de la mémoire bien particulière[^vm-overcommit] : par
défaut, `malloc(3)` ne retournera jamais `NULL`. En se basant sur l'euristique
qu'un bloc mémoire demandé ne sera pas utilisé directement et que de nombreux
process ne feront pas un usage total des blocks qu'ils ont alloués, le noyau
permet d'allouer plus de mémoire qu'il n'y en a réellement disponible. La
mémoire est donc utilisée de manière plus efficace.
[^vm-overcommit]: Dépendant de la valeur de `/proc/sys/vm/overcommit_memory`,
généralement 0. Voir
<https://www.kernel.org/doc/html/latest/vm/overcommit-accounting.html>.
Mais évidemment, cela peut donner lieu à des situations où le noyau n'est plus
en mesure de trouver de bloc physiquement disponible, alors qu'ils avaient
effectivement été alloués au processus. Pour autant, ce n'est pas une raison
pour tuer ce processus, car il est peut-être vital pour le système (peut-être
est-ce `init` qui est en train de gérer le lancement d'un nouveau daemon). On
dit alors que le noyau est *Out-Of-Memory*.
Pour se sortir d'une telle situation, et après avoir tenté de vider les caches,
il lance son arme ultime, pour retrouver au plus vite de la mémoire :
l'*Out-Of-Memory killer*.
## OOM killer
Selon un algorithme dont on raconte qu'il ne serait pas basé entièrement sur
l'aléatoire[^oom-algo], un processus est tiré au sort (plus un processus occupe
de mémoire et plus il a de chance d'être tiré au sort) par l'OOM killer. Le
sort qui lui est réservé est tout simplement une mort brutale. Pour permettre
au système de disposer à nouveau de mémoire disponible. Si cela n'est pas
suffisant, un ou plusieurs autres processus peuvent être tués à tour de rôle,
jusqu'à ce que le système retrouve sa sérénité.
[^oom-algo]: <https://linux-mm.org/OOM_Killer>
## Esquiver l'OOM killer
Au sein d'un *cgroup* *memory*, le fichier `memory.oom_control` peut être
utilisé afin de recevoir une notification du noyau avant que l'OOM-killer
ne s'attaque à un processus de ce groupe.
Grâce à cette notification, il est possible de figer le processus pour
l'envoyer sur une autre machine. Et ainsi libérer la mémoire avant que l'OOM
killer ne passe.
Jetez un œil à [cet article parru sur LWN](https://lwn.net/Articles/590960/) à
ce sujet.

107
tutorial/4/pidns.md Normal file
View file

@ -0,0 +1,107 @@
\newpage
Le *namespace* `PID` {#pid-ns}
=====================
## Introduction {#pid-ns-intro}
L'espace de noms `PID` est celui qui va nous permettre d'isoler un sous-arbre
de processus en créant un nouvel arbre, qui aura son propre processus considéré
comme l'`init`.
Contrairement aux autres *namespaces* où l'on peut demander à se séparer du
*namespace* en question à n'importe quel moment de l'exécution du processus,
via `unshare(2)` ou `setns(2)` par exemple, ici, le changement ne sera valable
qu'après le prochain `fork(2)` (ou similaire).
En effet, l'espace de noms n'est pas changé, afin que le processus ne change
pas de PID en cours de route, puisqu'il dépend du *namespace* dans lequel il se
trouve.
## Isolons !
Première étape s'isoler :
<div lang="en-US">
```bash
42sh# unshare --pid --fork /bin/bash
```
</div>
Nous utilisons ici l'option `-f`, pour que le passage dans le nouvel espace de
noms des PID soit effectif (cf. [Introduction](#pid-ns-intro)).
Un coup d'œil à `top` ou `ps aux` devrait nous montrer que l'on est maintenant
seul processus ... pourtant, il n'en est rien, ces deux commandes continuent
d'afficher la liste complète des processus de notre système.
Cela est dû au fait que ces deux programmes, sous Linux, se basent sur le
contenu de `/proc`. D'ailleurs, si l'on affiche le PID du processus courant
`echo $$`, on obtient bien 1.
En l'état, beaucoup d'informations sont divulguées. Mais il n'est pas possible
de monter le bon `/proc` car il serait également monté pour les processus de
notre système initial. Pour s'en sortir, il est nécessaire de s'isoler dans un
*namespace* `mount` séparé.
### Double isolation : ajout du *namespace* `mount`
Voici la nouvelle ligne de commande que l'on va utiliser :
<div lang="en-US">
```bash
42sh# unshare --pid --mount --fork --mount-proc /bin/bash
```
</div>
Avec l'option `--mount-proc`, `unshare` va s'occuper de monter le nouveau
`/proc`.
Cette fois, `top` et `ps` nous rapportent bien que l'on est seul dans notre
*namespace*.
## Arborescence à l'extérieur du *namespace*
Lors de notre première tentative de `top`, lorsque `/proc` était encore monté
sur le `procfs` de l'espace de noms initial : notre processus (au PID 1 dans
son nouveau *namespace*) était présent dans l'arborescence de l'espace initial
avec un PID dans la continuité des autres processus, étonnant !
En fait, l'isolation consiste en une virtualisation des numéros du processus :
la plupart des processus du système initial ne sont pas accessibles, et ceux qui
font partie de l'espace de noms créé disposent d'une nouvelle numérotation. Et
c'est cette nouvelle numérotation qui est montrée au processus.
Si l'on veut interagir avec ce processus depuis un de ses espaces de noms
parent, il faut le faire avec son identifiant de processus du même *namespace*
que le processus appelant.
## Processus orphelins et `nsenter`
Au sein d'un *namespace*, le processus au PID 1 est considéré comme le
programme `init`, les mêmes propriétés s'appliquent donc.
Si un processus est orphelin, il est donc affiché comme étant fils du PID 1
dans son *namespace*[^PR_SET_CHILD_SUBREAPER] ; il n'est pas sorti de l'espace
de nom.
[^PR_SET_CHILD_SUBREAPER]: en réalité, ce comportement est lié à la propriété
`PR_SET_CHILD_SUBREAPER`, qui peut être définie pour n'importe quel processus
de l'arborescence. Le processus au PID 1 hérite forcément de cette propriété\ ;
il va donc récupérer tous les orphelins, si aucun de leurs parents n'ont la
propriété définie.
Lorsque l'on lance un processus via `nsenter(1)` ou `setns(2)`, cela crée un
processus qui n'est sans doute pas un fils direct du processus d'`init` de
notre conteneur. Malgré tout, même s'il est affiché comme n'étant pas un fils à
l'extérieur du conteneur, les propriétés d'`init` sont biens appliquées à
l'intérieur pour conserver un comportement cohérent.
## Aller plus loin {-}
N'hésitez pas à jeter un œil à la page de manuel consacré à cet espace de
noms : `pid_namespaces(7)`.

View file

@ -1 +0,0 @@
../../subject/2/project-part1.md

View file

@ -5,20 +5,20 @@ Projet et rendu
## Sujet
Vous allez commencer aujourd'hui un projet qui s'étendra au prochain TP et qui
consistera à réaliser la partie d'isolation de la moulinette des ACUs !
**Ce projet, étalé sur ce TP et le TP précédent, constitue le cœur de la
notation de ce cours.**
Vous allez continuer aujourd'hui le projet qui s'étendra depuis le TP précédent
et qui consistera à réaliser la partie d'isolation de la moulinette des ACUs !
Cette semaine, il faudra faire en sorte de restreindre un groupe de processus
pour qu'il ne puisse pas faire de déni de service sur notre machine.
pour qu'il s'exécute indépendemment de votre système.
Il n'y a pas de restriction sur le langage utilisé, vous pouvez tout aussi bien
utiliser du C, du C++, du Python, etc.
utiliser du C, du C++, du Python, du shell, etc.
L'usage de bibliothèques **non relatives** au projet est autorisé : le but de
ce sujet est d'évaluer votre compréhension et votre utilisation de la
tuyauterie bas-niveau du noyau liée à la virtualisation légère. À partir du
moment où vous n'utilisez pas une bibliothèque qui abstrait complètement cette
plomberie, n'hésitez pas à l'utiliser !
Gardez en tête que ce projet sera à continuer au prochain TP, où il sera
principalement question de faire des appels systèmes.

View file

@ -1,39 +1,35 @@
## Modalité de rendu
Modalités de rendu
==================
En tant que personnes sensibilisées à la sécurité des échanges électroniques,
vous devrez m'envoyer vos rendus signés avec votre clef PGP.
Un service automatique s'occupe de réceptionner vos rendus, de faire les
vérifications nécessaires et de vous envoyer un accusé de réception (ou de
rejet).
Ce service écoute sur l'adresse <virli@nemunai.re>, c'est donc à cette adresse
Ce service écoute sur l'adresse <virli@nemunai.re>. C'est donc à cette adresse
et exclusivement à celle-ci que vous devez envoyer vos rendus. Tout rendu
envoyé à une autre adresse et/ou non signé et/ou reçu après la correction ne
sera pas pris en compte.
Par ailleurs, n'oubliez pas de répondre à
[l'évaluation du cours](https://virli.nemunai.re/quiz/6).
Pour différencier le rendu du TP, du rendu du projet, ajoutez une balise
`[PROJET]` au sujet de votre courriel, afin qu'il soit traité comme tel.
N'hésitez pas à indiquer dans le corps du courriel votre
ressenti et vos difficultés ou bien alors écrivez votre meilleure histoire
drôle si vous n'avez rien à dire.
Tarball
-------
## Tarball
Le projet à rendre pour ce cours est à placer dans une tarball (pas d'archive
ZIP, RAR, ...).
Tous les fichiers identifiés comme étant à rendre pour ce TP sont à
placer dans une tarball (pas d'archive ZIP, RAR, ...).
Voici une arborescence type (adaptez les extensions et les éventuels
fichiers supplémentaires associés au langage que vous aurez choisi
pour chaque exercice) :
Voici une arborescence type:
<div lang="en-US">
```
login_x-TP3/
login_x-TP3/escape.c
login_x-TP3/procinfo.sh
login_x-TP3/suspend_schedule.sh
login_x-TP3/view_caps.c
login_x-TP3/monitor.sh
login_x-TP3/monitor_init.sh
login_x-TP3/syscall_filter.c
login_x-mymoulette/README
login_x-mymoulette/...
```
</div>
Les premières étapes du projet ne sont pas à rendre et feront l'objet
d'un rendu à part.

View file

@ -1,252 +0,0 @@
\newpage
Pseudos systèmes de fichiers
============================
## Rappels sur les points de montage
Les systèmes Unix définissent le système de fichiers comme étant un arbre
unique partant d'une racine[^FHS] et où l'on peut placer au sein de son arborescence
des points de montage. Ainsi, l'utilisateur définit généralement deux points de
montage :
[^FHS]: Consultez
<https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard> pour
plus de détails sur l'arboresence.
<div lang="en-US">
```
/dev/sda1 on / type ext4 (rw,relatime,data=ordered)
/dev/sda3 on /home type ext4 (rw,relatime,data=ordered)
```
</div>
Dans ce schéma, la racine correspond à la première partition du premier disque,
et les fichiers des utilisateurs sont sur la troisième partition du premier
disque.
## Présentation des pseudos systèmes de fichiers
D'autres points de montage sont utilisés par le système : `/dev`, `/proc`,
`/tmp`, ... Ces points de montage vont, la plupart du temps, être montés par le
programme d'initialisation en utilisant des systèmes de fichiers virtuels, mis
à disposition par le noyau.
Ces systèmes sont virtuels, car ils ne correspondent à aucune partition d'aucun
disque : l'arborescence est créée de toute pièce par le noyau pour trier les
informations mises à disposition, mais il n'est pas toujours possible d'y
apporter des modifications.
Linux emploie de nombreux systèmes de fichiers virtuels :
- `/proc` : contient, principalement, la liste des processus (`top` et ses
dérivés se contentent de lire les fichiers de ce point de montage) ;
- `/proc/sys` : contient la configuration du noyau ;
- `/sys` : contient des informations à propos du matériel (utilisées notamment
par `udev` pour peupler `/dev`) et des périphériques (taille des tampons,
clignottement des DELs, ...) ;
- `/sys/firmware/efi/efivars` : pour accéder et modifier les variables de
l'UEFI ;
- ...
Tous ces systèmes de fichiers sont généralement exclusivement stockés en
RAM. Pour rendre une modification persistante, il est nécessaire de modifier un
fichier de configuration qui sera chargé par le programme d'initialisation. Par
exemple, pour modifier les paramètres du noyau, on passe par le fichier
`/etc/sysctl.conf` et du programme `sysctl`.
### Consultation et modification
La consultation d'un élément se fait généralement à l'aide d'un simple `cat` :
<div lang="en-US">
```
42sh$ cat /sys/power/state
freeze mem
```
</div>
La modification d'un élément se fait avec `echo`, comme ceci :
<div lang="en-US">
```bash
42sh# echo mem > /sys/power/state
```
</div>
Vous devriez constater l'effet de cette commande sans plus attendre !
## Exercices
### `procinfo`
Explorons le pseudo système de fichiers `/proc` pour écrire un script qui va
afficher des informations sur un processus donné :
<div lang="en-US">
```
42sh$ ./procinfo $$
PID: 4242
Path: /bin/bash
Command line: bash
Working directory: /home/nemunaire/virli/
Root: /
State: S (sleeping)
Threads: 1
CGroups
=======
12:pids:/
11:net_prio:/
10:perf_event:/
9:net_cls:/
8:freezer:/
7:devices:/
6:memory:/
5:blkio:/
4:cpuacct:/
3:cpu:/
2:cpuset:/
1:name=openrc:/
Namespaces
==========
cgroup:[4026531835]
ipc:[4026531839]
mnt:[4026531840]
net:[4026531969]
pid:[4026531836]
user:[4026531837]
uts:[4026531838]
```
</div>
### `batinfo.sh`, `cpuinfo.sh`
Explorons le pseudo système de fichiers `/sys` pour écrire un script
qui va, en fonction de ce que vous avez de disponible :
* nous afficher des statistiques sur notre batterie ;
* nous afficher des statistiques la fréquence de notre CPU.
#### `batinfo.sh`
Voici un exemple d'utilisation :
<div lang="en-US">
```
42sh$ ./batinfo.sh
BAT0 Discharging
====
Capacity: 83% (Normal)
Voltage: 11.972000 V (minimal: 11.400000 V)
Energy: 18.290000/21.830000 Wh
Power: 7.937000 W
Remaining time: 2.304 h
BAT1 Unknown
====
Capacity: 83% (Normal)
Voltage: 11.972000 V (minimal: 11.400000 V)
Energy: 18.290000/21.830000 Wh
Power: 0.0 W
Remaining time: N/A
```
</div>
Pour les détails sur l'organisation de ce dossier, regardez :
<https://www.kernel.org/doc/Documentation/power/power_supply_class.txt>.
#### `cpuinfo.sh`
Voici un exemple d'utilisation :
<div lang="en-US">
```
42sh$ ./cpuinfo.sh
cpu0
====
Current frequency: 2100384 Hz
Current governor: powersave
Allowed frequencies: between 500000 - 2100000 Hz
Thermal throttle count: 0
cpu1
====
Current frequency: 2099871 Hz
Current governor: powersave
Allowed frequencies: between 500000 - 2100000 Hz
Thermal throttle count: 0
```
</div>
N'hésitez pas à rajouter toute sorte d'information intéressantes !
### `rev_kdb_leds.sh`, `suspend_schedule.sh`
Maintenant que vous savez lire des informations dans `/sys`, tentons d'aller
modifier le comportement de notre système. Au choix, réaliser l'un des scripts
suivant, en fonction du matériel dont vous disposez :
* inverser l'état des diodes de notre clavier ;
* mettre en veille votre machine, en ayant programmé une heure de réveil.
#### `rev_kdb_leds.sh`
Si vous avez :
* numlock On,
* capslock Off,
* scrolllock Off ;
Après avoir exécuté le script, nous devrions avoir :
* numlock Off,
* capslock On,
* scrolllock On.
Voici un exemple d'utilisation :
<div lang="en-US">
```
42sh# ./rev_kdb_leds.sh input20
```
</div>
`input20` correspond à l'identifiant de votre clavier, sous
`/sys/class/input/`.
#### `suspend_schedule.sh`
Votre script prendra en argument l'heure à laquelle votre machine doit être
réveillée, avant de la mettre effectivement en veille.
Bien sûr, vous ne devez utiliser que des écritures (et des lectures) dans le
système de fichiers `/sys`. Il n'est pas question de faire appel à un autre
programme (vous pourriez cependant avoir besoin de `date(1)` pour faire les
calculs horaires).
Voici un exemple d'utilisation :
<div lang="en-US">
```
42sh# ./suspend_schedule.sh 15:42
```
</div>
Vous aurez besoin de définir une alarme au niveau de votre RTC, via le
fichier : `/sys/class/rtc/rtcX/wakealarm`.
Attention au fuseau horaire utilisé par votre RTC, si votre système principal
est Windows, elle utilisera sans doute le fuseau horaire courant. Sinon, ce
sera UTC.
Un article très complet sur le sujet est disponible ici :
<https://www.linux.com/tutorials/wake-linux-rtc-alarm-clock/>

49
tutorial/4/rendu.md Normal file
View file

@ -0,0 +1,49 @@
\newpage
Projet et rendu
===============
Projet
------
[Le sujet complet du projet est disponible ici](https://virli.nemunai.re/project-3.pdf). Il
n'est pas à rendre en même temps que le TP. Consultez ses modalités de rendu
pour plus d'informations.
Modalités de rendu
------------------
Un service automatique s'occupe de réceptionner vos rendus, de faire des
vérifications élémentaires et de vous envoyer un accusé de réception (ou de
rejet).
Ce service écoute sur l'adresse <virli@nemunai.re>. C'est donc à cette adresse
et exclusivement à celle-ci que vous devez envoyer vos rendus. Tout rendu
envoyé à une autre adresse et/ou non signé et/ou reçu après la correction ne
sera pas pris en compte.
Afin d'orienter correctement votre rendu, ajoutez une balise `[TP5]` au sujet
de votre courriel. N'hésitez pas à indiquer dans le corps du courriel votre
ressenti et vos difficultés ou bien alors écrivez votre meilleure histoire
drôle si vous n'avez rien à dire.
Par ailleurs, n'oubliez pas de répondre à
[l'évaluation du cours](https://virli.nemunai.re/quiz/7).
Tarball
-------
Tous les exercices de ce TP sont à placer dans une tarball (pas d'archive ZIP,
RAR, ...).
Voici une arborescence type :
<div lang="en-US">
```
login_x-TP5/cmpns.sh
login_x-TP5/mydocker_exec.sh
login_x-TP5/myswitch_root.sh
```
</div>

View file

@ -1,92 +0,0 @@
\newpage
Secure Computing Mode
=====================
Plus connue sous l'acronyme *seccomp*, cette fonctionnalité du noyau Linux
permet de restreindre les appels systèmes qu'un processus est autorisé à
utiliser. En cas d'appel non autorisé, le processus fautif est directement tué
(`SIGKILL`) par le noyau, ou, lorsque c'est précisé, un code `errno`
particulier peut être renvoyé au programme.
Depuis la version 3.17 du noyau, l'appel système `seccomp(2)` permet de faire
entrer le processus courant dans ce mode. En effet, c'est le processus lui-même
qui déclare au noyau qu'il peut désormais se contenter d'une liste réduite
d'appel système ; à l'inverse des politiques de sécurité comme SELinux ou
AppArmor, qui encapsulent les programmes pour toute la durée de leur exécution.
*Seccomp* est particulièrement utile lorsqu'un processus a terminé son
initialisation (ne dépendant en général pas de données sujettes à
l'exploitation de vulnérabilité) et doit commencer à entrer dans des portions
de code promptes aux vulnérabilités : c'est notamment le cas des moteurs de
rendus des navigateurs Firefox et Chrome.
## Prérequis
L'utilisation de `seccomp` nécessite d'avoir un noyau compilé avec l'option
`CONFIG_SECCOMP`.
Vous aurez également besoin de la bibliothèque `libseccomp` car l'appel système
`seccomp(2)` n'a pas de *wrapper* dans la libc, vous devrez donc passer par
cette bibliothèque.
## `MODE_STRICT`
Le mode traditionnel de *seccomp* est de ne permettre uniquement l'utilisation
des appels système `read(2)`, `write(2)` (sur les descripteurs de fichier déjà
ouvert), `_exit(2)` et `sigreturn(2)`.
Historiquement, avant la création de l'appel système `seccomp(2)`, on activait
ce mode via :
<div lang="en-US">
```c
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
```
</div>
Une fois passé cet appel système, toute entrée dans un appel système non
autorisé conduit à un `SIGKILL` du processus.
## `MODE_FILTER`
Plus modulable que le mode strict, le mode de filtrage permet une grande
amplitude en permettant au programmeur de définir finement quels appels
systèmes le programme est autorisé à faire ou non, et quel sentence est
exécutée en cas de faute.
Notons que les processus fils issus (`fork(2)` ou `clone(2)`) d'un processus
auquel est appliqué un filtre `seccomp`, héritent également de ce filtre.
La construction de ce filtre est faite de manière programatique, via
des règles BPF (`Berkeley Packet Filter`). On passe ensuite ce filtre BPF en
argument de l'appel système :
<div lang="en-US">
```c
struct sock_filter filter[];
struct sock_fprog prog = {
.len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog);
```
</div>
### Exercice {-}
Écrivez un programme filtrant un appel système, à l'aide de `seccomp` :
<div lang="en-US">
```
42sh$ ./syscall_filter sleep 5
sleep: cannot read realtime clock: Operation not permitted
42sh$
```
</div>
Dans cet exemple, l'appel système filtré est `nanosleep(2)`.

113
tutorial/4/setup.md Normal file
View file

@ -0,0 +1,113 @@
\newpage
Mise en place
=============
Noyau Linux
-----------
Ce TP requiert un noyau Linux, dans sa version 3.12 au minimum. Il doit de plus
être compilé avec les options suivantes (lorsqu'elles sont disponibles pour
votre version) :
<div lang="en-US">
```
General setup --->
[*] Control Group support --->
-*- Namespaces support
[*] UTS namespace
[*] IPC namespace
[*] User namespace
[*] PID Namespaces
[*] Network namespace
[*] Networking support --->
Networking options --->
<M> 802.1d Ethernet Bridging
Device Drivers --->
[*] Network device support --->
<M> MAC-VLAN support
<M> Virtual ethernet pair device
```
</div>
Les variables de configuration correspondantes sont :
<div lang="en-US">
```
CONFIG_CGROUPS=y
CONFIG_NAMESPACES=y
CONFIG_UTS_NS=y
CONFIG_IPC_NS=y
CONFIG_USER_NS=y
CONFIG_PID_NS=y
CONFIG_NET_NS=y
CONFIG_NET=y
CONFIG_BRIDGE=m
CONFIG_NETDEVICES=y
CONFIG_MACVLAN=m
CONFIG_VETH=m
```
</div>
Référez-vous, si besoin, au TP précédent pour la marche à suivre.
Paquets
-------
Nous allons utiliser des programmes issus des
[`util-linux`](https://www.kernel.org/pub/linux/utils/util-linux/), de
[`procps-ng`](https://gitlab.com/procps-ng/procps) ainsi que ceux de la
[`libcap`](https://sites.google.com/site/fullycapable/).
Sous Debian et ses dérivés, ces paquets sont respectivement :
* `util-linux`
* `procps`
* `libcap2-bin`
À propos de la sécurité de l'espace de nom `user`
-------------------------------------------------
La sécurité du *namespace* `user` a souvent été remise en cause et on lui
attribue de nombreuses vulnérabilités. Je vous laisse consulter à ce sujet :
* [Security Implications of User Namespaces](https://blog.araj.me/security-implications-of-user-namespaces/) ;
* [Anatomy of a user namespaces vulnerability](https://lwn.net/Articles/543273/) ;
* <http://marc.info/?l=linux-kernel&m=135543612731939&w=2> ;
* <http://marc.info/?l=linux-kernel&m=135545831607095&w=2>.
De nombreux projets ont choisi de ne pas autoriser l'utilisation de cet espace
de noms sans disposer de certaines *capabilities*[^userns-caps].
[^userns-caps]: Sont nécessaires, conjointement, `CAP_SYS_ADMIN`, `CAP_SETUID` et `CAP_SETGID`.
De nombreuses distributions ont par exemple choisi d'utiliser un paramètre du
noyau pour adapter le comportement.
### Debian et ses dérivées {.unnumbered}
Si vous utilisez Debian ou l'un de ses dérivés, vous devrez autoriser
explicitement cette utilisation non-privilégiée :
<div lang="en-US">
```bash
42sh# sysctl -w kernel.unprivileged_userns_clone=1
```
</div>
### Grsecurity {.unnumbered}
D'autres patchs, tels que
[*grsecurity*](https://forums.grsecurity.net/viewtopic.php?f=3&t=3929#p13904) ont
fait le choix de désactiver cette possibilité sans laisser d'option pour la
réactiver éventuellement à l'exécution. Pour avoir un comportement identique à
celui de Debian, vous pouvez
[appliquer ce patch](https://virli.nemunai.re/grsec-enable-user-ns.patch), sur
vos sources incluant le patch de *grsecurity*.

View file

@ -1,20 +1,19 @@
---
title: Virtualisation légère -- TP n^o^ 4
subtitle: Linux Internals partie 1
subtitle: Linux Internals partie 2
author: Pierre-Olivier *nemunaire* [Mercier]{.smallcaps}
institute: EPITA
date: Jeudi 4 novembre 2020
date: Jeudi 7 octobre 2021
abstract: |
Ce premier TP consacré aux Linux Internals va nous permettre
d'appréhender les notions de pseudos systèmes de fichiers, de
cgroups ainsi que de capabilities.
Le but de ce second TP sur les mécanismes internes du noyau va nous
permettre d'utiliser les commandes et les appels systèmes relatifs
aux *namespaces* ainsi que d'appréhender la complexité des systèmes
de fichiers.
\vspace{1em}
Certains éléments de ce TP sont à rendre à <virli@nemunai.re> au
plus tard le jeudi 12 novembre 2020 à 12 h 42. Consultez la
dernière section de chaque partie pour plus d'information sur les
éléments à rendre.
Tous les exercices de ce TP sont à rendre à <virli@nemunai.re> au
plus tard le jeudi 19 novembre 2020 à 12 h 42.
En tant que personnes sensibilisées à la sécurité des échanges électroniques,
vous devrez m'envoyer vos rendus signés avec votre clef PGP. Pensez à

File diff suppressed because it is too large Load diff

116
tutorial/4/userns.md Normal file
View file

@ -0,0 +1,116 @@
\newpage
Le *namespace* `user` {#user-ns}
=====================
## Introduction
L'espace de noms `user` est plutôt pratique car il permet de virtualiser la
liste et les droits des utilisateurs.
Par exemple, on va pouvoir entrer dans un conteneur en tant que
super-utilisateur à partir d'un compte d'un simple utilisateur. Il nous sera
alors possible d'effectuer toutes les actions privilégiées dont nous pourrions
avoir besoin à l'intérieur de cet espace de noms, sans que cela ne réduise la
sécurité des composants à l'extérieur de cet espace.
## Comportement vis-à-vis des autres *namespaces*
Alors qu'il est normalement nécessaire d'avoir des privilèges pour créer de
nouveaux espaces de noms, en commençant par demander un *namespace*
utilisateurs, on obtient les privilèges requis pour créer tous les autres types
de *namespaces*.
Grâce à cette technique, il est possible de lancer des conteneurs en tant que
simple utilisateur ; le projet [Singularity](https://sylabs.io/) repose
entièrement sur cela.
## Correspondance des utilisateurs et des groupes
Comme pour les autres espaces de noms, le *namespace* `user` permet de ne
garder dans le nouvel espace, que les utilisateurs et les groupes utiles au
processus, en les renumérotant au passage si besoin.
### L'utilisateur -2 : *nobody*
Lorsque l'on arrive dans un nouvel espace, aucun utilisateur ni groupe n'est
défini. Dans cette situation, tous les identifiants d'utilisateur et de groupe,
renvoyés par le noyau sont à -2 ; valeur qui correspond par convention à
l'utilisateur *nobody* et au groupe *nogroup*.
-1 étant réservé pour indiqué une erreur dans le retour d'une commande, ou la
non-modification d'un paramètres passé en argument d'une fonction.
### `uid_map` et `gid_map`
#### `uid_map`
Pour établir la correspondance, une fois que l'on a créé le nouveau
*namespace*, ces deux fichiers, accessibles dans `/proc/self/`, peuvent être
écrits une fois.
Sur chaque ligne, on doit indiquer :
- L'identifiant marquant le début de la plage d'utilisateurs, pour le processus
en question.
- L'identifiant marquant le début de la plage d'utilisateurs, pour le processus
affichant le fichier.
- La taille de la plage.
Par exemple, le *namespace* `user` initial défini la correspondance suivante :
<div lang="en-US">
```
42sh$ cat /proc/self/uid_map
0 0 4294967295
```
</div>
Cela signifie que les utilisateurs dont l'identifiant court de 0 à `MAX_INT -
2` inclu, dans cet espace de noms, correspondent aux utilisateurs allant de 0 à
`MAX_INT - 1` inclu, pour le processus affichant ce fichier.
Lorsque l'on crée un *namespace* `user`, généralement, la correspondance vaut :
<div lang="en-US">
```
42sh$ cat /proc/self/uid_map
0 1000 1
```
</div>
Dans cette situation, on comprend que notre processus considère que
l'utilisateur root, dans le conteneur équivaut à l'utilisateur 1000 hors de
l'espace de noms.
#### `gid_map`
Le principe est identique pour ce fichier, mais agit sur les correspondances
des groupes au lieu des utilisateurs.
## Utilisation de l'espace de noms
<div lang="en-US">
```bash
42sh$ unshare --mount --pid --mount-proc --fork --net --user --map-root-user /bin/bash
```
</div>
Un `capsh --print` nous montre que l'on est bien `root` et que l'on possède
toutes les *capabilities*. Cependant, cela ne signifie pas que l'on a tous les
droits sur le système ; il y a plusieurs niveaux de validation qui entrent en
jeu. L'idée étant que l'on a été désigné root dans son conteneur, on devrait
pouvoir y faire ce que l'on veut, tant que l'on n'agit pas en dehors.
## Aller plus loin {-}
N'hésitez pas à jeter un œil à la page du manuel consacré à ce *namespace* :
`user_namespaces(7)`.