Les conteneurs sous Linux

Timothée Ravier (@siosm) - https://tim.siosm.fr/cours

5e année cycle ingénieur, filière STI

Option Sécurité des Systèmes Ubiquitaires

2021-2022

Sommaire

Historique et concepts

Gestion des ressources (cgroups)

Isolation (namespaces)

Capabilities & seccomp

Gestionaires de conteneurs

Virtualisation ?

On parle de virtualisation légère, virtualisation au niveau du système d'exploitation ou de conteneurs par opposition à la virtualisation lourde, complète, totale d'un système d'exploitation.

Virtualisation « lourde »

  • Avantages :
    • Bonne isolation entre machines virtuelles
    • Contrôle complet de l'environnement d'exécution
    • Options pour le partage de périphériques matériel
    • Gestion des ressources disponibles simple
  • Désavantages :
    • Besoin en authentification, journalisation, etc.
    • Gestion de l'ensemble du système d'exploitation
    • Sécurité des éléments matériels partagés ?
    • Ressources disponibles fixes

Résultat : Plusieurs services par machine virtuelle

Cas d'usage des conteneurs

Objectifs :

  • Regrouper logiquement les composants d'une application
  • Isoler chaque applications vis à vis du système et des autres applications
  • Maîtriser de la consommation en ressources
  • Sans nécessairement utiliser la virtualisation
  • Démarrage rapide
  • Durée de vie variable (usage unique, récurrent, long terme, etc.)

Historique

  • 1982 : chroot : exécuter un programme avec un / (root) distinct
  • 2000 : FreeBSD Jails : ajoute des restrictions au niveau des intéractions avec le noyau, le réseau et virtualise le super utilisateur
  • 2001 : Linux Vserver : concept de context et fonctionnalités similaire aux jails pour Linux. Patch noyau uniquement, support très limité
  • 2004 : Solaris Zones : concept de system call translation
  • 2005 : OpenVZ : patch noyau Linux uniquement, support limité. Certaines fonctionnalités ont été intégrées upstream (CRIU)

Conteneurs sous Linux

Attention ! Pas de concept de conteneur dans le noyau Linux !

Il faut combiner « manuellement » :

  • la gestion des ressources et de l'accès à certains périphériques avec les cgroups
  • l'isolation (modulable) avec les namespaces
  • les restrictions de permissions avec les capabilities, les filtres seccomp et les Linux Security Modules (SELinux / AppArmor)

Gestion statique des ressources

rlimits : resource limits

  • Limite les ressources disponibles pour l'ensemble des processus d'un utilisateur ou d'un groupe
  • Affichage / manipulation des limites avec ulimit
  • Deux types de limites : hard et soft
  • Limite soft :
    • limite appliquée par le noyau
    • définissable entre 0 et la limite hard
  • CAP_SYS_RESOURCE pour augmenter les limites hard
  • Réduction des limites hard irréversibles pour !root
  • Limites initiales appliquées lors du login par PAM, configurées dans /etc/security/limits.conf

rlimits : resource limits

  • cpu time (seconds) : unlimited
  • file size (blocks) : unlimited
  • data seg size (kbytes) : unlimited
  • stack size (kbytes) : 8192
  • core file size (blocks) : 0
  • resident set size (kbytes) : unlimited
  • processes : 1024

rlimits : resource limits

  • file descriptors : 1024
  • locked-in-memory size (kbytes) : 64
  • address space (kbytes) : unlimited
  • file locks : unlimited
  • pending signals : 22949
  • bytes in POSIX msg queues : 819200
  • max nice : 0
  • max rt priority : 0

Quota disque

  • Limite les ressources disque par user et par group
  • Limite soft & hard par système de fichier
  • La limite hard ne peut pas être dépassée
  • La limite soft peut être dépassée pour un temps donné
  • Une fois ce temps dépassée, la limite soft devient hard
  • L'utilisateur doit libérer de la place pour redescendre au dessous de la limite soft avant de pouvoir écrire à nouveau des données
  • Quota disque par « projets » avec XFS : voir xfs_quota(8)

Espace disque réservé pour les processus privilégiés

  • Possibilité de réserver de l'espace sur un système de fichier
  • Permet aux processus privilégiés de continuer à fonctionner lorsque le disque est « plein »
  • $ tune2fs -m reserved-blocks-percentage
  • Valeur par défaut : 5 % de la taille du système de fichier

L'overcommit

  • Concept essentiel permettant la co-location des VMs et conteneurs sur un système
  • Chaque processus a l'impression d'être le seul à utiliser le matériel
  • Chaque processus « dispose » de plus de ressources que ce qui est réellement disponible
  • Exemple : l'espace d'adressage virtuel :
    • 2322^{32} \approx{} 4Go en 32bits, pour l'instant 2482^{48} en 64 bits
    • /proc/sys/vm/overcommit_memory & /proc/sys/vm/overcommit_ratio
  • Linux kernel : Overcommit Accounting
  • Linux kernel : Memory Layout on AArch64 Linux

Priorités, IO, et CPU

  • nice level : Défini la priorité d'accès aux ressources CPU pour un processus
  • ionice : Défini la priorité d'accès aux périphériques pour un processus
  • cpu affinity : Oblige un processus à utiliser des cœurs définis

Gestion dynamique : cgroups

Limites du modèle statique

  • Flexibilité très limitée
  • Gestion manuelle des changements de limites :
    • Pas de répartition automatique (entre utilisateurs, services, applications)
  • Peu adapté au modèle de co-location d'un grand nombre de machines virtuelles ou de conteneurs :
    • Besoins variables
    • Un seul utilisateur
  • Peu adapté à la gestion d'un service constitué de plusieurs processus

cgroups

  • Introduit dans le noyau 2.6.24
  • Permets de regrouper des processus en leur associant un label
  • Organisation hiérarchique
  • Les processus fils héritent du cgroup du père
  • Représenté par un système de fichier virtuel : /sys/fs/cgroup/

Exemple : libvirt : Control Groups Resource Management

cgroups : controllers

  • Configuration d'un ou plusieurs controllers pour chaque groupe de tâches
  • Tous les processus d'un groupe se voient imposer les mêmes contraintes
  • cgroups(7) : liste complète ainsi que les versions du noyau à partir desquelles chaque controller est disponible
  • Liste des controllers disponibles sur un système :
$ cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset  0       358     1
cpu     0       358     1
cpuacct 0       358     1
blkio   0       358     1
memory  0       358     1
devices 0       358     1
...

cgroups : controllers disponibles

  • cpu (& cpuacct v1) : mesure et limite l'utilisation CPU
  • cpuset : répartition sur les CPUs / nœuds NUMA
  • memory : mesure et limite l'utilisation de la mémoire
  • blkio/io : mesure et limite l'utilisation des périphériques de type block
  • pids : limite le nombre de PID disponibles
  • freezer : pause l'exécution des processus du groupe
  • perf_event : groupes spécifiques au sous-système perf
  • rdma : limite les accès RDMA et InfiniBand
  • hugetlb : limite l'utilisation des Huge Page
  • devices : restreint l'accès aux devices disponibles
  • net_cls & net_prio (v1) : classe et définit des priorités pour les paquets réseaux

cgroups v1 & v2

Deux versions des cgroups :

  • cgroups v1 :
    • chaque controller a sa propre hiérarchie
    • pose des problèmes de gestion, performance, complexité
    • diverses limites liées au design
  • cgroups v2 :
    • une seule hiérarchie commune à tous les controllers
    • configuration différente pour certains controllers
    • disponibilité des controllers v2 équivalente à v1 à partir du noyau 5.6.

Linux kernel : cgroups v2

cgroups v1 ou v2 ?

  • Privilégier la version 2 lorsque c'est possible
  • Désormais supporté par tous les gestionnaires de conteneurs
  • Disponible par défaut :
    • Fedora 31
    • CentOS 9 Stream & RHEL 9
    • Debian 11
    • Ubuntu 21.10

Isolation avec les namespaces

Isolation avec les namespaces (1)

Principe général :

  • Crée un ou plusieurs espaces de nom (namespaces ou ns) pour séparer le contexte d'exécution d'un processus par rapport aux autres
  • Isole les objets créés dans un namespace vis à vis des autres namespaces
  • Lorsque tous les processus d'un namespace ont terminé leur exécution, les objets restant sont détruits ou renvoyés dans le namespace parent

Isolation avec les namespaces (2)

  • Fonctionnalité ancienne (début dans la branche 2.4!) mais étendue « récemment »
  • Nouveaux flags ajoutés à l'appel système clone :
    int clone(int (*fn)(void *), void *child_stack,
            int flags, void *arg, ...
            /* pid_t *ptid, void *newtls, pid_t *ctid */ );
    
  • CLONE_NEWNET, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWUTS, CLONE_NEWIPC, CLONE_NEWUSER, CLONE_NEWCGROUP, etc.
  • Nouveaux appels système pour modifier le processus courant :
    int unshare(int flags);
    int setns(int fd, int nstype);
    
  • Voir namespaces(7), unshare(2), clone(2), setns(2)

Outils en ligne de commande

  • unshare : lancer un programme en utilisant de nouveaux namespaces
  • nsenter : lancer un programme avec les namespaces d'un programme existant
  • Visualiser les namespaces associés à un programme :
$ ls -l /proc/1006/ns/*
... /proc/1006/ns/cgroup -> 'cgroup:[4026531835]'
... /proc/1006/ns/ipc -> 'ipc:[4026531839]'
... /proc/1006/ns/mnt -> 'mnt:[4026531840]'
... /proc/1006/ns/net -> 'net:[4026531993]'
... /proc/1006/ns/pid -> 'pid:[4026531836]'
... /proc/1006/ns/pid_for_children -> 'pid:[4026531836]'
... /proc/1006/ns/user -> 'user:[4026531837]'
... /proc/1006/ns/uts -> 'uts:[4026531838]'

CLONE_NEWNS

Crée un namespace pour les points de montage :

  • Présent depuis le noyau 2.4.19 (2002)
  • Permet au fils de monter/démonter des systèmes de fichier sans impacter le namespace parent
$ ls /mnt
a b
$ unshare --mount
[newns]# umount /mnt
[newns]# ls /mnt
[newns]# exit
$ ls /mnt
a b

CLONE_NEWUTS

Crée un namespace UTS :

  • Présent depuis le noyau 2.6.19 (2006)
  • Contient le domain name et le host name
  • Permets au fils de changer ces informations sans impacter le namespace parent
$ hostname
foo.bar
$ unshare --uts
[newns]# hostname toto
[newns]# hostname
toto
[newns]# exit
$ hostname
foo.bar

CLONE_NEWIPC

Crée un namespace pour les IPC :

  • Introduit dans le noyau 2.6.19, complété dans le 2.6.30 (2009)
  • Vue isolée des IPC System V : seuls les processus de ce namespace peuvent accéder à ces objets
  • Destruction des objets à la fin du namespace
$ ipcs --summary
...
segments allocated 4
...
$ unshare --ipc
[newns]# ipcs --summary
...
segments allocated 0
...

CLONE_NEWNET

Crée un namespace pour les interface réseaux :

  • Introduit dans le noyau 2.6.24, complété dans le 2.6.29 (2009)
  • Le fils dispose de sa propre stack réseau : interfaces, stacks IPv4/IPv6, tables de routage IP, règles de parfeu, sockets, etc.
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
...
2: enp0s31f6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
...
$ unshare --net
[newns]# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

CLONE_NEWNET

  • Une interface physique ne peut être que dans un seul namespace à la fois
  • Rattachée au namespace initial à la destruction du namespace
  • Utilisation de paires d'interfaces virtuelles (veth) pour lier les namespaces :
$ ip link add ... type veth
...

CLONE_NEWPID

Crée un namespace pour les PID :

  • Présent depuis le noyau 2.6.24 (2008);
  • Les PIDs recommencent à 1 dans ce namespace
  • Les processus du namespace parent voient les processus du namespace fils avec des PIDs différents
  • Les processus dans un namespace ne peuvent voir que ceux du même namespace et des namespaces créés par des processus de leur namespace;

CLONE_NEWPID

$ ps aux
root           1  0.0  0.0 172424 17216 ?        Ss   Jan29   0:03 /usr/lib/systemd/systemd ...
root           2  0.0  0.0      0     0 ?        S    Jan29   0:00 [kthreadd]
...
tim        85096  0.4  0.2 25759272 87136 ?      Sl   20:51   0:00 /usr/bin/google-chrome ...
tim        85127  0.0  0.0 235748  5044 pts/3    R+   20:51   0:00 ps aux
$ unshare --mount --pid --fork
[newns]# mount proc -t proc /proc/
[newns]# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0 233184  5928 pts/3    S    22:52   0:00 -bash
root          30  0.0  0.0 234384  3652 pts/3    R+   22:52   0:00 ps aux

CLONE_NEWUSER

Crée un namespace pour les UID/GID :

  • Introduit dans le noyau 2.6.24, finalisé dans le 3.8 (2013)
  • Associe un ensemble d'UID/GID dans le nouveau namespace au couple UID/GID initial
  • root dans un user namespace sans l'être en dehors
  • Le noyau s'assure qu'il n'y as pas d'élévation de privilèges par rapport au couple UID/GID initial
  • Nécessite la mise en place d'une correspondance entre les UIDs à l'intérieur et à l'extérieur du user namespace

CLONE_NEWCGROUP

Crée un namespace pour les hiérarchies cgroups :

  • Introduit dans le noyau 4.6 (2016)
  • Abstraction des hiérarchies cgroups pour les conteneurs
$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-....scope
$ sudo unshare --cgroup
[newns]# cat /proc/self/cgroup
0::/

Privilèges requis ?

  • CLONE_NEWNET, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWUTS, CLONE_NEWIPC :
    • Nécessite CAP_SYS_ADMIN
  • CLONE_NEWUSER :
    • Nécessite parfois CAP_SYS_ADMIN
    • Souvent désactivé par défaut, comportement variable suivant les distributions
    • Mode unprivileged disponible depuis le noyau 3.8
    • Sinon CAP_SYS_ADMIN, CAP_SETUID et CAP_SETGID
  • CLONE_NEWCGROUP :
    • Non privilégié

Capabilities

Capabilities (1)

  • Utilisateur root tout puissant sur un système Linux
  • Tous les autres utilisateurs non privilégiés par défaut
  • Capabilities Linux : donner une partie des privilèges de root à des programmes non privilégiés
  • Exemple :
    • CAP_NET_BIND_SERVICE : Bind un socket sur un port < 1024
    • CAP_NET_ADMIN : Configurer les interfaces réseaux
    • CAP_SETUID : Définir arbitrairement les UIDs d'un processus
    • CAP_SYS_ADMIN : Effectuer un grand nombre d'opération privilégiées
    • Voir capabilities(7) pour la liste complète

Capabilities (2)

  • Peuvent être associées à des exécutables pour être ainsi transmises aux processus résultant
  • Chaque thread possède son propre ensemble de capabilities (comme pour les UID/GID)
  • Leur état est stocké dans plusieurs variables (nommés sets) :
    • Effective : le set utilisé pour les contrôles dans le noyau;
    • Permitted : limite actuelle des capabilities disponible
    • Inheritable : préservées lors d'un appel à execve et conservées si le fichier exécuté les possède
    • Ambient : préservées lors d'un appel à execve

Capabilities (3)

  • Capabilities Bounding Set : limite définitive des capabilities disponibles pour un thread
  • Ensemble de régles complexes pour déterminer quelles capabilities sont conservées, perdues ou transmises lors des appels à execve, setuid, etc.
  • Voir capabilities(7)

Capabilities & root

seccomp & seccomp-bpf

seccomp

  • Mode strict qui n'autorise un processus à ne faire usage qu'aux appels système read, write et _exit

seccomp-bpf

  • Extension du modèle initial qui filtre les appels système disponibles pour un processus
  • Filtre décrit sous forme de programme BPF (Berkeley Packet Filter)
  • Si le processus utilise un appel système filtré, le noyau peut retourner un code d'erreur, envoyer un signal ou tuer le processus
  • En pratique : utilisation de la libseccomp, des options de systemd ou du gestionnaire de conteneurs
  • Possibilité de créer des listes de refus / authorisation

Implémentations sous Linux

Création d'un conteneur sous Linux

Eléments nécessaires :

  • gestion des ressources et de l'accès à certains périphériques avec les cgroups
  • isolation (modulable) avec les namespaces
  • restrictions de permissions avec les capabilities, les filtres seccomp et les Linux Security Modules (SELinux / AppArmor)
  • image du conteneur (arborescence propre au conteneur : contenu du /)
  • accès au réseau

Container runtime & Container engine

Attention : Nomenclature et limite floue, souvent mélangé !

Container runtime

Programme responsable de la création d'un conteneur et de la configuration de l'environnement d'exécution :

  • mise en place des cgroups et limites UNIX
  • mise en place des namespaces
  • configuration des capabilities, filtres seccomp, LSM

Container runtime & Container engine

Container engine

Programme responsable de la gestion du cycle de vie d'un conteneur :

  • gestion des images de conteneurs (création, récupération à distance, envoi, instanciation, etc.)
  • gestion des points de montage, volumes persistants, etc.
  • mise en place des interfaces réseau (virtuelles)
  • suivit des conteneurs en cours d'exécution.

Utilise un container runtime pour lancer les conteneurs.

Historique des implémentations

  • 2008 : LXC/LXD
  • 2010 : systemd-nspawn
  • 2013 : Docker / Moby engine
  • 2014 : rkt
  • 2015 : runc et containerd & Clear Containers et Kata Containers
  • 2016 : Minijail
  • 2017 : railcar & CRI-O
  • 2018 : Podman, Buildah et Skopeo & gVisor & Nabla containers
  • 2019 : crun

Implémentations les plus utilisées

  • LXC/LXD
  • Docker / Moby engine (avec runc et containerd)
  • Podman, Buildah et Skopeo avec crun
  • Kata Containers et gVisor
  • CRI-O

LXC (runtime) / LXD (engine)

  • Objectif : faire tourner un système complet dans un conteneur
  • Image : simple tar.gz d'un / (obtenu avec debootstrap, dnf --install-root=... ou équivalent)
  • Création de conteneurs à partir de templates
  • LXD : gestionnaire de conteneurs LXC
  • Avantages :
    • Fonctionnement similaire à un système classique
  • Désavantages :
    • Duplication des services (journalisation, monitoring, etc.)
    • Plus de gestion nécessaire (plus proche de la virtualisation « classique  »)
    • LXD disponible principalement sur Ubuntu

Docker (Moby engine) (1)

  • Objectif : une application par conteneur
  • Avantages :
    • Simplificité (récupération des images, utilisation)
    • Consommation en ressources (mémoire et disque) réduite
    • Gestion d'une application et non plus d'un système
  • Désavantages :
    • Chaque conteneur doit être maintenu à jour
    • Les applications classiques ne sont pas prévues pour fonctionner en tant que PID 1 dans un conteneur : processus zombie, etc.
    • Aucun service support disponible dans le conteneur

Docker (Moby engine) (2)

  • Dockerfile : sorte de script pour créer des conteneurs
FROM ubuntu
RUN apt-get install nginx
CMD ['nginx']
  • DockerHub : Container registry
    • Hébergement d'images de conteneurs
    • Construction automatique d'images de conteneurs
    • Registre par défaut avec Docker

Docker : architecture

center https://docs.docker.com/engine/docker-overview

Podman, Buildah, Skopeo

  • Ré-écriture de Docker sans utiliser de démon système
  • Buildah : Construire des images de conteneur
    • à partir d'un Dockerfile
    • à l'aide de commandes arbitraires
  • Skopeo : Manipulation d'images de conteneur
    • téléchargement, envoie, metadata, etc.

Podman, Buildah, Skopeo vs Docker ?

  • Avantages :
    • Pas de démon central
    • Bonne intégration avec systemd
    • Fonctionnement et ligne de commande équivalente à Docker
    • Séparation des tâches \Rightarrow meilleure sécurité
    • Rootless : conteneurs non privilégiés avec utilisateur « root »
  • Désavantages :
    • Légères différences de fonctionnement par rapport à Docker

Podman : architecture

center

https://www.redhat.com/en/blog/why-red-hat-investing-cri-o-and-podman

rkt (~Podman) vs Docker

center

https://coreos.com/rkt/docs/latest/rkt-vs-other-projects.html

Standards et compatibilité

Standards et compatibilité

  • La prolifération des outils de gestion de conteneurs a poussé à la standardisation
  • Création de l'Open Container Initiative
  • Images de conteneur (archive tar + metadata json) :
    • Docker Format v2 (standard de facto)
    • OCI Image Format Specification
  • OCI Runtime Specification (runc)
  • OCI Distribution Specification

https://cloud.redhat.com/blog/podcast-podctl-3-making-sense-of-container-standards

Conteneurs et applications graphiques

Conteneurs et applications graphiques

  • Sandboxing d'applications graphiques :
    • Flatpak (voir Flathub)
    • Firejail
    • Snapcraft (voir Snaps, principalement sous Ubuntu)
  • Attention : Wayland nécesssaire. Le serveur d'affichage X.org ne peut pas être configuré de façon sécurisée
  • A l'échelle d'un système d'exploitation :
    • Fedora Silverblue / Fedora Kinoite
    • Endless OS
    • Subgraph OS

Suite

systemd

[2.1_containers]()

[2.2_dispo]()

[2.3_namespace]()

[2.4_capa-seccomp]()

[2.5_container-managers]()

# Cas d'usage des conteneurs En pratique : - Matériel : abstraction fournie par le noyau souvent suffisante (pas besoin d'émulation (QEMU), même architecture, etc.) ; - Noyau : la version du noyau n'importe pas pour la plupart des applications ; - Complexité moindre (moins d'éléments) pour debugger ; - Partage des services système entre les conteneurs (journalisation, etc.) ; - Temps de démarrage, taille et consommation en ressource moindre. ---

<https://coreos.com/blog/making-sense-of-standards.html>