Update tuto2

This commit is contained in:
nemunaire 2021-09-23 02:55:18 +02:00
parent 5f097b4221
commit 2c5317f4f9
35 changed files with 3588 additions and 472 deletions

View file

@ -1,6 +1,6 @@
include ../pandoc-opts.mk
SOURCES = tutorial.md oci.md runc.md linuxkit.md rendu.md
SOURCES = tutorial.md oci.md runc.md linuxkit.md linuxkit-admin.md linuxkit-content.md rendu.md
all: tutorial.pdf

View file

@ -0,0 +1,20 @@
[`Clair`](https://github.com/coreos/clair/) est un projet de CoreOS, similaire
à Trivy : il va rechercher parmi sa base de vulnérabilités lesquels concernent
les images qu'on lui donne.
Contrairement à Trivy, il ne va pas tenter d'analyser les dépendances
supplémentaires des applications métiers (Trivy est capable d'analyser les
dépendances PHP, Python, Ruby, Go, ...).
La mise en œuvre de Clair est un peu plus complexe car il s'agit d'un *daemon*
qui s'exécute en permanence, et auquel on peut transmettre via une API, nos
images. Dans un contexte d'intégration continue, ou d'un registre d'images qui
teste régulièrement ses images hébergées, c'est un outil idéal. Néanmoins il
faut le configurer un minimum pour qu'il soit opérationnel. Consultez [l'annexe
dédiée](https://virli.nemunai.re/tutorial-2-clair.pdf) si vous souhaitez opter
pour cette solution.
On notera tout de même que les outils sont donnés pour générer des rapports
HTML avec des graphiques explicites :
![Rapport d'analyse statique des vulnérabilités par Clair](paclair.png)

View file

@ -1,53 +1,20 @@
\newpage
### Clair
Une vision plus Clair de la sécurité
====================================
Nous avons vu, au travers de nos TPs jusqu'à présent, que Docker nous apportait
un certain degré de sécurité d'emblée au lancement du conteneur. Cela peut sans
doute paraître quelque peu rassurant pour la personne chargée d'administrer la
machine hébergeant les conteneurs, car cela lui apporte des garanties quant à
l'effort de cloisonnement mis en place.
Mais doit-on pour autant s'arrêter là et considérer que nous avons réglé
l'ensemble des problématiques de sécurité liées aux conteneurs ?
Évidemment, non : une fois nos services lancés dans des conteneurs, il ne sont
pas moins exposés aux bugs et autres failles applicatives ; qu'elles soient
dans notre code ou celui d'une bibliothèque, accessible par rebond, ...
Il est donc primordial de ne pas laisser ses conteneurs à l'abandon une fois
leur image créée et envoyée en production. Nos conteneurs doivent être
regénérés sitôt que leur image de base est mise à jour (une mise à jour d'une
image telle que Debian, Ubuntu ou Redhat n'apparaît que pour cela) ou bien
lorsqu'un des programmes ou bibliothèques que l'on a installé ensuite.
Convaincu ? Cela sonne encore comme des bonnes pratiques difficile à mettre en
œuvre, pouvant mettre en péril tout un système d'information. Pour s'en
protéger, nous allons avoir besoin de réaliser à intervalles réguliers une
analyse statique de nos conteneurs.
![Rapport d'analyse statique des vulnérabilités d'un conteneur](paclair.png)
## Clair
Le principal outil pour indexer et chercher des vulnérabilités est
Un outil complet indexer et chercher des vulnérabilités est
[`Clair`](https://github.com/coreos/clair/), du projet CoreOS. À partir des
informations mises à disposition par les équipes de sécurités des principales
distributions, cela alimente en continu une base de données qui sera accéder au
moment de l'analyse.
L'outil se présente sous la forme d'un serveur autonome dans la récupération
de ses données sources, auquel nous pourrons interagir au moyen d'une API :
de ses données sources, auquel nous pourrons interagir au moyen d'une API :
pour lui envoyer des images et lui demander une analyse. Les clients de cette
API seront soit les registres directement, soit un programme dédié.
![Scan de vulnérabilités sur le Docker Hub](hubvuln.png)
---
Commençons par lancer notre propre instance de `Clair`, à l'aide d'un
`docker-compose.yml` :
`docker-compose.yml` :
<div lang="en-US">
```yml
@ -77,10 +44,10 @@ services:
```
</div>
Prenez quelques minutes pour comprendre ce `docker-compose.yml` : notez la
présence de la variable d'environnement `POSTGRES_PASSWORD`, non définie : ce
Prenez quelques minutes pour comprendre ce `docker-compose.yml` : notez la
présence de la variable d'environnement `POSTGRES_PASSWORD`, non définie : ce
sera la variable présente dans votre environnement, au moment du
`docker-compose up` qui sera utilisée. N'oubliez pas de la définir :
`docker-compose up` qui sera utilisée. N'oubliez pas de la définir :
<div lang="en-US">
```bash
@ -99,7 +66,7 @@ pas de changer le nom d'hôte et le mot de passe pour se connecter au conteneur
de base de données.**
Une fois lancé, la base nécessite d'être initialisée. L'opération peut prendre
plusieurs minutes. Vous pouvez suivre l'avancement de l'ajout via :
plusieurs minutes. Vous pouvez suivre l'avancement de l'ajout via :
<div lang="en-US">
```bash
@ -109,11 +76,11 @@ curl http://localhost:6060/v1/namespaces/debian:9/vulnerabilities?limit=10
</div>
## PAClair
### PAClair
Afin de pouvoir réaliser à la demande et sans registre privé, l'analyse de
conteneur, nous allons utiliser le programme
[`paclair`](https://github.com/yebinama/paclair) :
[`paclair`](https://github.com/yebinama/paclair) :
<div lang="en-US">
```bash
@ -121,7 +88,7 @@ pip3 install paclair
```
</div>
Il nécessite un fichier de configuration pour être utilisé, essayez :
Il nécessite un fichier de configuration pour être utilisé, essayez :
<div lang="en-US">
```yml
@ -134,7 +101,7 @@ Plugins:
</div>
Pour obtenir un rapport d'analyse, on commence par envoyer les couches de
l'image à `Clair` :
l'image à `Clair` :
<div lang="en-US">
```bash
@ -142,7 +109,7 @@ paclair --conf conf.yml Docker nemunaire/fic-admin push
```
</div>
Puis on lui demande la génération d'un rapport `html` :
Puis on lui demande la génération d'un rapport `html` :
<div lang="en-US">
```bash
@ -150,7 +117,7 @@ paclair --conf conf.yml Docker nemunaire/fic-admin analyse --output-format html
```
</div>
Si l'on souhaite uniquement avoir des statistiques dans la console :
Si l'on souhaite uniquement avoir des statistiques dans la console :
<div lang="en-US">
```bash
@ -161,10 +128,3 @@ Medium: 5
High: 4
```
</div>
## Exercice {-}
Déterminez le nombre de vulnérabilités dans les principales images officielles
du [Docker Hub](https://hub.docker.com/explore), notamment `nginx`, `golang`,
`redis`, ...

View file

@ -0,0 +1,11 @@
Si vous vous rappelez du cours d'AdLin[^adlin] en début d'années, toutes les
VMs que vous avez utilisées reposaient entièrement sur `linuxkit`. En fait,
chaque conteneur représentait alors une machine différente : un conteneur
postgres d'un côté, un conteneur `miniflux` directement issu du DockerHub, un
conteneur `unbound` pour faire office de serveur DNS, et les machines clientes
étaient de simples conteneurs exécutant un client DHCP.
[^adlin]: toutes les sources des machines sont dans ce dépôt :
<https://git.nemunai.re/srs/adlin>. Les fichiers de construction
LinuxKit sont `challenge.yml`, `server.yml`, `tuto2.yml`,
`tuto3.yml`

View file

@ -0,0 +1,243 @@
## Prérequis
Si vous n'avez pas déjà le binaire `linuxkit`, vous pouvez télécharger ici :\
<https://github.com/linuxkit/linuxkit/releases/latest>.
Notez qu'étant donné qu'il est écrit en Go, aucune dépendance n'est nécessaire
en plus du binaire[^lollibc] ;-)
[^lollibc]: à condition tout de même que vous utilisiez une libc habituelle.
Vous aurez également besoin de QEMU pour tester vos créations.
## Structure d'un fichier `linuxkit.yml`
Le fichier utilisé pour construire notre image se décompose en plusieurs
parties :
- `kernel` : il est attendu ici une image OCI contenant le nécessaire pour
pouvoir utiliser un noyau : l'image du noyau, ses modules et un initramfs ;
- `init` : l'ensemble des images OCI de cette liste seront fusionnés pour
donner naissance au *rootfs* de la machine. On n'y place normalement qu'un
gestionnaire de conteneur, qui sera chargé de lancer chaque conteneur au bon
moment ;
- `onboot`, `onshutdown` et `services` : il s'agit de conteneurs qui seront
lancés par le système disponible dans l'`init`, au bon moment. Les conteneurs
indiqués dans `onboot` seront lancés **séquentiellement** au démarrage de la
machine, ceux dans `onshutdown` seront lancés lors de l'arrêt de la
machine. Les conteneurs dans `services` seront lancés simultanément une fois
que le dernier conteneur de `onboot` aura rendu la main ;
- `files` : des fichiers supplémentaires à placer dans le rootfs.
Le format est documenté
[ici](https://github.com/linuxkit/linuxkit/blob/master/docs/yaml.md).
## Hello?
L'image la plus simple que l'on puisse réaliser pourrait être :
<div lang="en-US">
```yaml
kernel:
image: linuxkit/kernel:4.14.80
cmdline: "console=tty0 console=ttyS0"
init:
- linuxkit/init:c563953a2277eb73a89d89f70e4b6dcdcfebc2d1
- linuxkit/runc:83d0edb4552b1a5df1f0976f05f442829eac38fe
- linuxkit/containerd:326b096cd5fbab0f864e52721d036cade67599d6
onboot:
- name: dhcpcd
image: linuxkit/dhcpcd:v0.6
command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
services:
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
env:
- INSECURE=true
trust:
org:
- linuxkit
```
</div>
L'image `getty` est très pratique pour déboguer, car elle permet d'avoir un
shell sur la machine !
On notera cependant que, positionné dans `services`, le shell que nous
obtiendrons sera lui-même exécuté dans un conteneur, nous n'aurons donc pas un
accès entièrement privilégier. Pour déboguer, il faut placer cette image dans
la partie `init`, elle sera alors lancé comme un équivalent de
`init=/bin/sh`.[^infogetty]
[^infogetty]: Plus d'infos à
<https://github.com/linuxkit/linuxkit/blob/master/pkg/getty/README.md#linuxkit-debug>
## `namespaces`
Chaque nouveau conteneur est donc lancé dans un espace distinct où il ne pourra
pas interagir avec les autres, ou déborder s'il s'avérait qu'il expose une
faille exploitable.
Néanmoins, contrairement à Docker qui va de base nous dissocier du maximum de
*namespaces*, `linuxkit` ne le fait pas pour les *namespaces* `net`, `ipc` et
`uts`. C'est-à-dire que, par défaut, la pile réseau est partagée entre tous les
conteneurs, tout comme les IPC et le nom de la machine.
Il reste possible de se dissocier également de ces namespaces, en précisant :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
net: new
```
</div>
Ou inversement, pour persister dans le namespace initial :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
pid: host
```
</div>
### Partage de `namespace`
Dans le cas où l'on souhaite que deux conteneurs partagent le même *namespace*,
il faut passer le chemin vers la structure du noyau correspondante.
On commence donc d'abord par créer le nouveau *namespace*, en prenant soin de
*bind mount* la structure du noyau à un emplacement connu :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
net: new
runtime:
bindNS: /run/netns/mynewnetns
```
</div>
À la création du *namespace* `net`, le lien vers la structure du noyau
correspondante sera *bind mount* sur `/run/netns/synchro`. On pourra alors
réutiliser plus tard ce chemin, en remplacement du mot clef `new` :
<div lang="en-US">
```yaml
- name: xxxx
image: linuxkit/xxxx:v0.6
net: /run/netns/mynewnetns
```
</div>
## Construction et démarrage
Toute la puissance de `linuxkit` repose dans son système de construction et
surtout de lancement. En effet, il peut construire des images pour un grand
nombre de plate-forme, mais il est également possible d'utiliser les API de ces
plates-formes pour aller y lancer des instances de cette image !
Pour construire l'image faite précédemment :
<div lang="en-US">
```bash
linuxkit build hello.yml
```
</div>
Cela va générer plusieurs fichiers dont un noyau (extrait de l'image de la
partie `kernel`) ainsi qu'une image. Exactement ce qu'attend QEMU ! Pour
tester, n'attendons pas davantage pour lancer :
<div lang="en-US">
```bash
linuxkit run qemu -gui hello
```
</div>
## Ajouter un service
Maintenant que notre machine fonctionne, que nous pouvons interagir avec elle,
tentons de se passer de l'interface de QEMU (option `-gui`) en ajoutant un
serveur SSH aux `services` :
<div lang="en-US">
```yaml
- name: sshd
image: linuxkit/sshd:c4bc89cf0d66733c923ab9cb46198b599eb99320
```
</div>
Comme nous n'avons définis aucun mot de passe, il va falloir utiliser une clef
SSH pour se connecter. Voilà un bon début d'utilisation de la section `files` :
<div lang="en-US">
```yaml
- path: root/.ssh/authorized_keys
source: ~/.ssh/id_rsa.pub
mode: "0600"
```
</div>
Ceci va aller chercher votre clef RSA sur votre machine, pour la placer dans
Notons qu'il est inutile d'ajouter un *bind mount* du dossier `.ssh` ainsi
recopié, car le *package* `linuxkit/sshd` défini déjà cela :
[pkg/sshd/build.yml#L5](https://github.com/linuxkit/linuxkit/blob/master/pkg/sshd/build.yml#L5).
## Interface réseau virtuelle
Lorsque l'on souhaite se dissocier d'un *namespace* `net` afin de s'isoler,
mais que l'on veut tout de même pouvoir communiquer, il est nécessaire de créer
une interface `virtual ethernet` :
<div lang="en-US">
```yaml
- name: db
image: mariadb:latest
net: new
runtime:
bindNS:
net: /run/netns/db
interfaces:
- name: vethin-db
add: veth
peer: veth-db
```
</div>
## Exercice {-}
Réalisez une recette `vault.yml` démarrant une instance du gestionnaire de
secrets [Hashicorp Vault](https://www.vaultproject.io/), utilisant une [base de
données au
choix](https://www.vaultproject.io/docs/configuration/storage/index.html)
(Consul, Etcd, MySQL, Cassandra, ...).
Au démarrage, Vault devra déjà être configuré pour parler à sa base de données,
qui devra se trouver dans un conteneur isolé et non accessible d'internet. Il
faudra donc établir un lien `virtual ethernet` entre les deux conteneurs ; et
ne pas oublier de le configurer (automatiquement au *runtime*, grâce à un
[`poststart`
*hook*](https://github.com/opencontainers/runtime-spec/blob/master/config.md#posix-platform-hooks)
ou bien à un conteneur issu du *package*
[`ip`](https://github.com/linuxkit/linuxkit/tree/master/pkg/ip)).
Les permissions étant généralement très strictes, vous aurez sans doute besoin
de les assouplir un peu en ajoutant des *capabilities* autorisées à vos
conteneurs, sans quoi vos conteneurs risquent d'être tués prématurément.
En bonus, vous pouvez gérer la [persistance des
données](https://github.com/linuxkit/linuxkit/blob/master/examples/swap.yml)
stockées dans Vault.

View file

@ -13,258 +13,3 @@ des images pour les différents fournisseurs de cloud !
Bien entendu, au sein de ce système, tout est fait de conteneur ! Alors quand
il s'agit de donner une IP publique utilisable par l'ensemble des conteneurs,
il faut savoir jouer avec les *namespaces* pour arriver à ses fins !
Si vous vous rappelez du cours d'AdLin[^adlin] en début d'années, toutes les
VMs que vous avez utilisées reposaient entièrement sur `linuxkit`. En fait,
chaque conteneur représentait alors une machine différente : un conteneur
mattermost d'un côté, un conteneur `unbound` pour faire office de serveur DNS,
et les machines clientes étaient de simples conteneurs exécutant un client
dhcp.
[^adlin]: toutes les sources des machines sont dans ce dépôt :
<https://git.nemunai.re/?p=lectures/adlin.git;a=tree>
## Prérequis
Si vous n'avez pas déjà le binaire `linuxkit`, vous pouvez télécharger ici :
<https://github.com/linuxkit/linuxkit/releases/latest>.
Notez qu'étant donné qu'il est écrit en Go, aucune dépendance n'est nécessaire
en plus du binaire[^lollibc] ;-)
[^lollibc]: à condition tout de même que vous utilisiez une libc habituelle.
Vous aurez également besoin de QEMU pour tester vos créations.
## Structure d'un fichier `linuxkit.yml`
Le fichier utilisé pour construire notre image se décompose en plusieurs
parties :
- `kernel` : il est attendu ici une image OCI contenant le nécessaire pour
pouvoir utiliser un noyau : l'image du noyau, ses modules et un initramfs ;
- `init` : l'ensemble des images OCI de cette liste seront fusionnés pour
donner naissance au *rootfs* de la machine. On n'y place normalement qu'un
gestionnaire de conteneur, qui sera chargé de lancer chaque conteneur au bon
moment ;
- `onboot`, `onshutdown` et `services` : il s'agit de conteneurs qui seront
lancés par le système disponible dans l'`init`, au bon moment. Les conteneurs
indiqués dans `onboot` seront lancés **séquentiellement** au démarrage de la
machine, ceux dans `onshutdown` seront lancés lors de l'arrêt de la
machine. Les conteneurs dans `services` seront lancés simultanément une fois
que le dernier conteneur de `onboot` aura rendu la main ;
- `files` : des fichiers supplémentaires à placer dans le rootfs.
Le format est documenté
[ici](https://github.com/linuxkit/linuxkit/blob/master/docs/yaml.md).
## Hello?
L'image la plus simple que l'on puisse réaliser pourrait être :
<div lang="en-US">
```yaml
kernel:
image: linuxkit/kernel:4.14.80
cmdline: "console=tty0 console=ttyS0"
init:
- linuxkit/init:c563953a2277eb73a89d89f70e4b6dcdcfebc2d1
- linuxkit/runc:83d0edb4552b1a5df1f0976f05f442829eac38fe
- linuxkit/containerd:326b096cd5fbab0f864e52721d036cade67599d6
onboot:
- name: dhcpcd
image: linuxkit/dhcpcd:v0.6
command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"]
services:
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
env:
- INSECURE=true
trust:
org:
- linuxkit
```
</div>
L'image `getty` est très pratique pour déboguer, car elle permet d'avoir un
shell sur la machine !
On notera cependant que, positionné dans `services`, le shell que nous
obtiendrons sera lui-même exécuté dans un conteneur, nous n'aurons donc pas un
accès entièrement privilégier. Pour déboguer, il faut placer cette image dans
la partie `init`, elle sera alors lancé comme un équivalent de
`init=/bin/sh`.[^infogetty]
[^infogetty]: Plus d'infos à
<https://github.com/linuxkit/linuxkit/blob/master/pkg/getty/README.md#linuxkit-debug>
## `namespaces`
Chaque nouveau conteneur est donc lancé dans un espace distinct où il ne pourra
pas interagir avec les autres, ou déborder s'il s'avérait qu'il expose une
faille exploitable.
Néanmoins, contrairement à Docker qui va de base nous dissocier du maximum de
*namespaces*, `linuxkit` ne le fait pas pour les *namespaces* `net`, `ipc` et
`uts`. C'est-à-dire que, par défaut, la pile réseau est partagée entre tous les
conteneurs, tout comme les IPC et le nom de la machine.
Il reste possible de se dissocier également de ces namespaces, en précisant :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
net: new
```
</div>
Ou inversement, pour persister dans le namespace initial :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
pid: host
```
</div>
### Partage de `namespace`
Dans le cas où l'on souhaite que deux conteneurs partagent le même *namespace*,
il faut passer le chemin vers la structure du noyau correspondante.
On commence donc d'abord par créer le nouveau *namespace*, en prenant soin de
*bind mount* la structure du noyau à un emplacement connu :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
net: new
runtime:
bindNS: /run/netns/mynewnetns
```
</div>
À la création du *namespace* `net`, le lien vers la structure du noyau
correspondante sera *bind mount* sur `/run/netns/synchro`. On pourra alors
réutiliser plus tard ce chemin, en remplacement du mot clef `new` :
<div lang="en-US">
```yaml
- name: xxxx
image: linuxkit/xxxx:v0.6
net: /run/netns/mynewnetns
```
</div>
## Construction et démarrage
Toute la puissance de `linuxkit` repose dans son système de construction et
surtout de lancement. En effet, il peut construire des images pour un grand
nombre de plate-forme, mais il est également possible d'utiliser les API de ces
plates-formes pour aller y lancer des instances de cette image !
Pour construire l'image faite précédemment :
<div lang="en-US">
```bash
linuxkit build hello.yml
```
</div>
Cela va générer plusieurs fichiers dont un noyau (extrait de l'image de la
partie `kernel`) ainsi qu'une image. Exactement ce qu'attend QEMU ! Pour
tester, n'attendons pas davantage pour lancer :
<div lang="en-US">
```bash
linuxkit run qemu -gui hello
```
</div>
## Ajouter un service
Maintenant que notre machine fonctionne, que nous pouvons interagir avec elle,
tentons de se passer de l'interface de QEMU (option `-gui`) en ajoutant un
serveur SSH aux `services` :
<div lang="en-US">
```yaml
- name: sshd
image: linuxkit/sshd:c4bc89cf0d66733c923ab9cb46198b599eb99320
```
</div>
Comme nous n'avons définis aucun mot de passe, il va falloir utiliser une clef
SSH pour se connecter. Voilà un bon début d'utilisation de la section `files` :
<div lang="en-US">
```yaml
- path: root/.ssh/authorized_keys
source: ~/.ssh/id_rsa.pub
mode: "0600"
```
</div>
Ceci va aller chercher votre clef RSA sur votre machine, pour la placer dans
Notons qu'il est inutile d'ajouter un *bind mount* du dossier `.ssh` ainsi
recopié, car le *package* `linuxkit/sshd` défini déjà cela :
[pkg/sshd/build.yml#L5](https://github.com/linuxkit/linuxkit/blob/master/pkg/sshd/build.yml#L5).
## Interface réseau virtuelle
Lorsque l'on souhaite se dissocier d'un *namespace* `net` afin de s'isoler,
mais que l'on veut tout de même pouvoir communiquer, il est nécessaire de créer
une interface `virtual ethernet` :
<div lang="en-US">
```yaml
- name: db
image: mariadb:latest
net: new
runtime:
bindNS:
net: /run/netns/db
interfaces:
- name: vethin-db
add: veth
peer: veth-db
```
</div>
## Exercice {-}
Réalisez une recette `vault.yml` démarrant une instance du gestionnaire de
secrets [Hashicorp Vault](https://www.vaultproject.io/), utilisant une [base de
données au
choix](https://www.vaultproject.io/docs/configuration/storage/index.html)
(Consul, Etcd, MySQL, Cassandra, ...).
Au démarrage, Vault devra déjà être configuré pour parler à sa base de données,
qui devra se trouver dans un conteneur isolé et non accessible d'internet. Il
faudra donc établir un lien `virtual ethernet` entre les deux conteneurs ; et
ne pas oublier de le configurer (automatiquement au *runtime*, grâce à un
[`poststart`
*hook*](https://github.com/opencontainers/runtime-spec/blob/master/config.md#posix-platform-hooks)
ou bien à un conteneur issu du *package*
[`ip`](https://github.com/linuxkit/linuxkit/tree/master/pkg/ip)).
Les permissions étant généralement très strictes, vous aurez sans doute besoin
de les assouplir un peu en ajoutant des *capabilities* autorisées à vos
conteneurs, sans quoi vos conteneurs risquent d'être tués prématurément.
En bonus, vous pouvez gérer la [persistance des
données](https://github.com/linuxkit/linuxkit/blob/master/examples/swap.yml)
stockées dans Vault.

View file

@ -7,7 +7,7 @@ Formée en juin 2015, l'Open Container Initiative (OCI) a pour but d'établir le
standard commun aux programmes de contenerisation, afin d'éviter une
fragmentation de l'écosystème.
Trois spécifications ont été écrites :
Trois spécifications ont été écrites :
- [`runtime-spec`](https://github.com/opencontainers/runtime-spec/blob/master/spec.md#platforms): définit les paramètres du démarrage d'un conteneur ;
- [`image-spec`](https://github.com/opencontainers/image-spec/blob/master/spec.md): définit la construction, le transport et la préparation des images ;
@ -24,8 +24,8 @@ d'avoir un fichier de configuration `config.json` ainsi qu'un dossier où l'on
peut trouver la racine de notre conteneur.
Le plus gros de la spécification concerne le format de ce fichier de
configuration : il contient en effet l'ensemble des paramètres avec lesquels il
faudra créer le conteneur : tant d'un point de vue isolement (on peut par
configuration : il contient en effet l'ensemble des paramètres avec lesquels il
faudra créer le conteneur : tant d'un point de vue isolement (on peut par
exemple choisir de quel *namespace* on souhaite se dissocier, ou le(s)quel(s)
rejoindre), quelles *capabilities* resteront disponibles, quels nouveaux points
de montages, ... Voir [la
@ -45,8 +45,8 @@ fichiers, d'une configuration ainsi qu'un index d'image optionnel.
Le
[manifest](https://github.com/opencontainers/image-spec/blob/master/manifest.md)
est toujours le point d'entrée d'une image : il référence l'emplacement où
trouver les différents éléments : configuration et couches. Lorsqu'une même
est toujours le point d'entrée d'une image : il référence l'emplacement où
trouver les différents éléments : configuration et couches. Lorsqu'une même
image a des variation en fonction de l'architecture du processeur, du système
d'exploitation, ... dans ce cas [l'index
d'image](https://github.com/opencontainers/image-spec/blob/master/image-index.md)
@ -54,7 +54,7 @@ est utilisé pour sélectionner le bon manifest.
Le format des [couches de système de
fichiers](https://github.com/opencontainers/image-spec/blob/master/layer.md)
sont spécifiées : il est nécessaire de passer par des formats standards (comme
sont spécifiées : il est nécessaire de passer par des formats standards (comme
les tarballs), contenant éventuellement des fichiers et dossiers spéciaux
contenant les modifications, suppressions, ... éventuelles de la couche
représentée.
@ -64,14 +64,14 @@ La
contient l'ensemble des métadonnées qui serviront par exemple à construire le
`config.json` attendu par `runc` lorsqu'il faudra lancer l'image (c'est
là-dedans que finissent toutes les métadonnées que l'on inscrit dans nos
`Dockerfile` : `CMD`, `VOLUME`, `PORT`, ...). On y retrouve également l'ordre
`Dockerfile` : `CMD`, `VOLUME`, `PORT`, ...). On y retrouve également l'ordre
des couches du système de fichiers, ainsi que l'historique de l'image.
## `distribution-spec`
Dernière née de l'organisme, cette spécification fédère la notion de
*registre* : une API REST sur HTTP où l'on peut récupérer des images, mais
*registre* : une API REST sur HTTP où l'on peut récupérer des images, mais
aussi en envoyer.
@ -79,7 +79,7 @@ aussi en envoyer.
Si maintenant `docker` fait appel à un programme externe pour lancer
effectivement nos conteneurs, c'est que l'on peut changer cette implémentation
? la réponse dans l'article :
? la réponse dans l'article :
<https://ops.tips/blog/run-docker-with-forked-runc/>
Et `containerd` dans l'histoire ?

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View file

@ -3,14 +3,14 @@
Registres
=========
**Outils nécessaires :** `curl`, `gunzip`, `jq`, `tar`.
**Outils nécessaires :** `curl`, `gunzip`, `jq`, `tar`.
* * * * *
Nous allons appréhender le fonctionnement d'un registre OCI,
et préparer le *rootfs* d'une image de base (Debian, Ubuntu, hello, ...) : en
et préparer le *rootfs* d'une image de base (Debian, Ubuntu, hello, ...) : en
nous préoccupant simplement de la couche la plus basse (qui ne contient pas de
modification ou de suppression : chaque fichier est normal).
modification ou de suppression : chaque fichier est normal).
## Authentification
@ -18,7 +18,7 @@ modification ou de suppression : chaque fichier est normal).
L'authentification est facultative et est laissée à l'appréciation du
fournisseur de service. Étant donné que nous allons utiliser le [Docker
Hub](https://hub.docker.com/), le registre par défaut de `docker`, nous allons
devoir nous plier à leur mécanisme d'authentification : chaque requête au
devoir nous plier à leur mécanisme d'authentification : chaque requête au
registre doit être effectuée avec un jeton, que l'on obtient en s'authentifiant
auprès d'un service dédié. Ce service peut délivrer un jeton sans authentifier
l'interlocuteur, en restant anonyme ; dans ce cas, on ne pourra accéder qu'aux
@ -26,8 +26,8 @@ images publiques. Ça tombe bien, c'est ce qui nous intéresse aujourd'hui !
Il n'en reste pas moins que le jeton est forgé pour un service donné (dans
notre cas `registry.docker.io`) et avec un objectif bien cerné (pour nous, on
souhaite récupérer le contenu du dépôt[^quiddepot] `hello-world` :
<span lang="en-US">`repository:hello-world:pull`</span>). Ce qui nous donne :
souhaite récupérer le contenu du dépôt[^quiddepot] `hello-world` :
<span lang="en-US">`repository:hello-world:pull`</span>). Ce qui nous donne :
[^quiddepot]: Dans un registre, les fichiers qui composent l'image forment un
dépôt (*repository*).
@ -50,7 +50,7 @@ souhaite récupérer le contenu du dépôt[^quiddepot] `hello-world` :
C'est le `token` qu'il faudra fournir lors de nos prochaines requêtes au
registre.
Avec `jq`, on peut l'extraire grâce à :
Avec `jq`, on peut l'extraire grâce à :
<div lang="en-US">
```bash
@ -58,7 +58,7 @@ Avec `jq`, on peut l'extraire grâce à :
```
</div>
**Attention :** le token expire ! Pensez à le renouveler régulièrement.
**Attention :** le token expire ! Pensez à le renouveler régulièrement.
En cas d'erreur inexplicable, vous pouvez ajouter un `-v` à la ligne de
commande `curl`, afin d'afficher les en-têtes. Prêtez une attention toute
@ -68,7 +68,7 @@ particulière à `Www-Authenticate`.
## Lecture de l'index d'images
Une fois en possession de notre jeton, nous pouvons maintenant demander l'index
d'images à notre registre :
d'images à notre registre :
<div lang="en-US">
```bash
@ -87,7 +87,7 @@ somme de contrôle.
## Lecture du manifest
Demandons maintenant le manifest correspondant à notre matériel et à notre
système d'exploitation :
système d'exploitation :
<div lang="en-US">
```bash
@ -109,7 +109,7 @@ répertoire `blobs`, il ne s'agit en effet plus de manifest. Si les manifests
sont toujours stockés par le registre lui-même, les blobs peuvent être délégués
à un autre service, par exemple dans le cloud, chez Amazon S3, un CDN, etc.
Pour récupérer la configuration de l'image :
Pour récupérer la configuration de l'image :
<div lang="en-US">
```bash
@ -120,7 +120,7 @@ curl -s --location \
</div>
Enfin, armé du `digest` de notre couche, il ne nous reste plus qu'à la demander gentiment :
Enfin, armé du `digest` de notre couche, il ne nous reste plus qu'à la demander gentiment :
<div lang="en-US">
```bash
@ -134,7 +134,7 @@ wget --header "Authorization: Bearer ${TOKEN}" \
Le type indiqué par le manifest pour cette couche était
`application/vnd.docker.image.rootfs.diff.tar.gzip`, il s'agit donc d'une
tarball compressée au format gzip :
tarball compressée au format gzip :
<div lang="en-US">
```bash
@ -143,7 +143,7 @@ tar xzf ${DL_LAYER} -C rootfs
```
</div>
Et voilà, nous avons extrait notre première image, nous devrions pouvoir :
Et voilà, nous avons extrait notre première image, nous devrions pouvoir :
<div lang="en-US">
```bash
@ -156,7 +156,7 @@ Hello from Docker!
## Exercice {-}
Réalisez un script pour automatiser l'ensemble de ces étapes :
Réalisez un script pour automatiser l'ensemble de ces étapes :
<div lang="en-US">
```bash

View file

@ -26,7 +26,7 @@ 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 (vous pourriez avoir des fichiers supplémentaires,
cela dépendra de votre avancée dans le projet) :
cela dépendra de votre avancée dans le projet) :
<div lang="en-US">
```

View file

@ -4,7 +4,7 @@
======
`runc` est le programme qui est responsable de la création effective du
conteneur : c'est lui qui va mettre en place toute la machinerie, les points de
conteneur : c'est lui qui va mettre en place toute la machinerie, les points de
montages ou volumes, ... Attention, son rôle reste limité à la mise en place de
l'environnement conteneurisé, ce n'est pas lui qui télécharge l'image, ni fait
l'assemblage des couches de système de fichiers, entre autres.
@ -21,17 +21,17 @@ essayer de lancer un shell `alpine` avec un volume dans notre home.
## Prérequis
Vous devriez avoir le binaire `runc` ou `docker-runc`. Si ce n'est pas le cas,
vous pouvez télécharger la dernière version :
vous pouvez télécharger la dernière version :
<https://github.com/opencontainers/runc/releases>. La 1.0.0-rc92 est Ok.
## Extraction du rootfs
À l'aide du script d'extraction de registre réalisé dans le TP 3, extrayons le
rootfs d'alpine : `library/alpine` dans le registre Docker.
rootfs d'alpine : `library/alpine` dans le registre Docker.
Si vous n'avez pas eu le temps de terminer le script d'extraction, vous pouvez
utiliser :
utiliser :
<div lang="en-US">
```bash
@ -44,7 +44,7 @@ docker image save alpine | tar xv -C rootfs
L'écriture complète d'un fichier `config.json` pour `runc` est plutôt
fastidieux et répétitif, nous allons donc gagner du temps et utiliser la
commande suivante, qui nous créera un modèle que nous adapterons un peu :
commande suivante, qui nous créera un modèle que nous adapterons un peu :
<div lang="en-US">
```bash
@ -52,7 +52,7 @@ runc spec
```
</div>
Pour savoir à quoi correspondent tous ces éléments, vous pouvez consulter :
Pour savoir à quoi correspondent tous ces éléments, vous pouvez consulter :
<https://github.com/opencontainers/runtime-spec/blob/master/config.md>
Nous verrons dans les prochains TP, plus en détails tout ce qui porte sur les
@ -61,7 +61,7 @@ aujourd'hui.
## Test brut
Voici comment nous pouvons tester le fonctionnement de notre *bundle* :
Voici comment nous pouvons tester le fonctionnement de notre *bundle* :
<div lang="en-US">
```
@ -75,7 +75,7 @@ rootfs/ config.json
Quelques informations sont disponibles, mais il ne faut pas s'attendre à
retrouver tout l'écosystème de `docker` ; ici il n'y a pas de gestion des
journaux, etc. :
journaux, etc. :
<div lang="en-US">
```bash
@ -92,7 +92,7 @@ virli1 12345 running /tmp/work/runctest 2012-12-12T12:12:12.123456789Z root
## Attacher notre `home`
Dans le modèle de `config.json`, il y a déjà de nombreux systèmes de fichiers
qui sont montés. Nous pouvons les filtrer avec :
qui sont montés. Nous pouvons les filtrer avec :
<div lang="en-US">
```bash
@ -110,7 +110,7 @@ qui sont montés. Nous pouvons les filtrer avec :
</div>
Pour avoir notre équivalent du `-v /home:/home` de `docker`, il va donc falloir
ajouter un élément à cette liste, demandant de *bind* :
ajouter un élément à cette liste, demandant de *bind* :
<div lang="en-US">
```json
@ -143,8 +143,8 @@ stocker les photos (dossier `/srv/images`)[^chmod].
simple pour l'instant serait d'attribuer les permissions `0777` à la
source, temporairement.
Pour ce TP, considérez que vous avez réussi si vous voyez s'afficher :
Pour ce TP, considérez que vous avez réussi si vous voyez s'afficher :
> `Ready, listening on :8080`
> `Ready, listening on :8080`
Il faudra attendre les TP suivants pour avoir du réseau dans notre conteneur.

View file

@ -0,0 +1,12 @@
## Exercice {-}
Déterminez le nombre de vulnérabilités dans les principales images officielles
du [Docker Hub](https://hub.docker.com/explore), notamment `nginx`, `golang`,
`redis`, ...
En utilisant l'outil de votre choix, exporter un rapport HTML (ou la sortie
standard de l'outil s'il n'exporte pas en HTML) de l'image **officielle**
contenant les vulnérabilités les plus inquiétantes. Vous placerez ce rapport
dans un fichier `${IMAGE}:${TAG}.html` ou `${IMAGE}:${TAG}.txt`.
Veillez à utiliser une image __différente__ de `mysql`, utilisée en exemple.

View file

@ -0,0 +1,259 @@
\newpage
Analyse de vulnérabilité
========================
Nous avons vu jusqu'à présent que Docker nous apportait un certain degré de
sécurité d'emblée, au lancement de nos conteneurs. Cela peut sans doute
paraître quelque peu rassurant pour une personne chargée d'administrer une
machine hébergeant des conteneurs, car cela lui apporte des garanties quant à
l'effort de cloisonnement mis en place.
Mais doit-on pour autant s'arrêter là et considérer que nous avons réglé
l'ensemble des problématiques de sécurité liées aux conteneurs ?
Évidemment, non : une fois nos services lancés dans des conteneurs, il ne sont
pas moins exposés aux bugs et autres failles applicatives ; qu'elles soient
dans notre code ou celui d'une bibliothèque, accessible par rebond, ...
Il est donc primordial de ne pas laisser ses conteneurs à l'abandon une fois
leur image créée et envoyée en production. Nos conteneurs doivent être
regénérés sitôt que leur image de base est mise à jour (une mise à jour d'une
image telle que Debian, Ubuntu ou Redhat n'apparaît que pour cela) ou bien
lorsqu'un des programmes ou l'une des bibliothèques que l'on a installés
ensuite est mise à jour.
Convaincu ? Cela sonne encore comme des bonnes pratiques difficiles à mettre en
œuvre, pouvant mettre en péril tout un système d'information. Pour s'en
protéger, nous allons avoir besoin de réaliser à intervalles réguliers une
analyse statique de nos conteneurs.
![Scan de vulnérabilités sur le Docker Hub](hubvuln.png){ width=90% }
Selon une étude[^dockerhubvulnerability] réalisée sur les images du Docker Hub,
elles présenteraient en moyenne 180 vulnérabilités, beaucoup ne sont pas mises
à jour depuis trop longtemps et les vulnérabilités ont surtout tendance à se
propager à cause de l'usage d'une image parente pas à jour.
[^dockerhubvulnerability]: <https://www.enck.org/pubs/shu-codaspy17.pdf>
Une mesure efficace consiste à reconstruire régulièrement (et surtout
automatiquement) les images que l'on publie sur un registre public, sans
oublier de mettre à jour l'image de base.
D'ailleurs, avez-vous vérifié qu'une mise à jour de l'image `nemunaire/youp0m`
n'était pas disponible depuis que vous avez commencé à l'utiliser ? Docker ne
vérifie jamais si une mise à jour des images que vous avez précédemment
téléchargées. Pensez donc régulièrement à appeler :
<div lang="en-US">
```
42sh$ docker pull IMAGE
```
</div>
## Docker Scan
Pour faire face à ces problèmes de sécurité qui prennent de l'ampleur, le
Docker Hub, dans son modèle payant, permet d'analyser régulièrement ses images,
pour avoir une idée sur la nécessité de les reconstruire.
Un plugin existe pour réaliser des scans d'images présentes sur votre machine,
à travers les données du Docker Hub. Cela nécessite d'avoir un compte Docker,
si vous n'en avez pas, nous verrons dans la section suivante `trivy` qui permet
de réaliser ses scans directement sur notre machine, sans passer par un
intermédiaire.
#### Attention {-}
Par cette méthode, vous êtes limité à 10 scans par mois.
### Installation du plugin
#### Windows et MacOS {-}
Avec Docker Desktop, le plugin est déjà installé. Il faut que vous vous soyez
préalablement connecté à votre compte Docker avec la commande `docker login`.
#### Linux {-}
Comme `docker scan` est un plugin, suivant la méthode d'installation que vous
avez suivie, il n'a pas forcément été installé. Si vous obtenez un message
d'erreur en lançant la commande, [voici comment récupérer le plugin et
l'installer manuellement :](https://github.com/docker/scan-cli-plugin#on-linux)
<div lang="en-US">
```
mkdir -p ~/.docker/cli-plugins
curl https://github.com/docker/scan-cli-plugin/releases/latest/download/docker-scan_linux_amd64 \
-L -s -S -o ~/.docker/cli-plugins/docker-scan
chmod +x ~/.docker/cli-plugins/docker-scan
```
</div>
### Utilisation
Une fois le plugin installé et la licence du service acceptée, nous pouvons
commencer notre analyse :
<div lang="en-US">
```
42sh$ docker scan nemunaire/fic-admin
Testing nemunaire/fic-admin...
Package manager: apk
Project name: docker-image|nemunaire/fic-admin
Docker image: nemunaire/fic-admin
Platform: linux/amd64
Base image: alpine:3.14.2
✓ Tested 16 dependencies for known vulnerabilities, no vulnerable paths found.
According to our scan, you are currently using the most secure version of the selected base image
```
</div>
<div lang="en-US">
```
$ docker scan mysql
Testing mysql...
✗ Low severity vulnerability found in util-linux/libuuid1
[...]
✗ High severity vulnerability found in gcc-8/libstdc++6
Description: Insufficient Entropy
Info: https://snyk.io/vuln/SNYK-DEBIAN10-GCC8-469413
Introduced through: apt@1.8.2.3, mysql-community/mysql-community-client@8.0.26-1debian10, mysql-community/mysql-community-server-core@8.0.26-1debian10, mecab-ipadic@2.7.0-20070801+main-2.1, meta-common-packages@meta
From: apt@1.8.2.3 > gcc-8/libstdc++6@8.3.0-6
From: mysql-community/mysql-community-client@8.0.26-1debian10 > gcc-8/libstdc++6@8.3.0-6
From: mysql-community/mysql-community-server-core@8.0.26-1debian10 > gcc-8/libstdc++6@8.3.0-6
and 7 more...
Image layer: Introduced by your base image (mysql:8.0.26)
Package manager: deb
Project name: docker-image|mysql
Docker image: mysql
Platform: linux/amd64
Base image: mysql:8.0.26
Tested 135 dependencies for known vulnerabilities, found 79 vulnerabilities.
According to our scan, you are currently using the most secure version of the selected base image
```
</div>
Ce dernier exemple est sans appel : `mysql` est une image officielle, et sa
dernière version à l'écriture de ses lignes contient pas moins de 79
vulnérabilités dont 11 *high*.
## Trivy
Le principal outil pour chercher des vulnérabilités connues dans les images
Docker est [`trivy`](https://www.aquasec.com/products/trivy/), édité par Aqua
security.
À partir des informations mises à disposition par les équipes de sécurité des
principales distributions, `trivy` va générer un rapport, pour chacune de nos
images, indiquant les vulnérabilités connues au sein de l'image.
L'outil se présente sous la forme d'un binaire ou d'une image Docker, prenant
un certain nombre d'arguments, notamment le nom de l'image à analyser.
### Utilisation
Tentons à nouveau d'analyser l'image `mysql` :
<div lang="en-US">
```
42sh$ docker run --rm aquasec/trivy mysql
2021-09-22T10:27:46.509Z INFO Need to update DB
2021-09-22T10:27:46.509Z INFO Downloading DB... 100.00% 14.41 MiB p/s 2s
2021-09-22T10:27:56.556Z INFO Detected OS: debian
2021-09-22T10:27:56.556Z INFO Detecting Debian vulnerabilities...
2021-09-22T10:27:56.579Z INFO Number of language-specific files: 0
mysql (debian 10.10)
====================
Total: 158 (UNKNOWN: 5, LOW: 19, MEDIUM: 64, HIGH: 61, CRITICAL: 9)
```
</div>
Les résultats sont un peu différents qu'avec `docker scan`, mais on constate
que l'image `mysql` contient vraiment de nombreuses vulnérabilités. Même si
elles ne sont heureusement pas forcément exploitable directement.
Voyons maintenant s'il y a des différentes avec l'image `nemunaire/fic-admin` :
<div lang="en-US">
```
42sh$ docker run --rm aquasec/trivy nemunaire/fic-admin
2021-09-22T10:29:48.091Z INFO Need to update DB
2021-09-22T10:29:48.091Z INFO Downloading DB... 100.00% 15.98 MiB p/s 1s
2021-09-22T10:29:51.902Z INFO Detected OS: alpine
2021-09-22T10:29:51.902Z INFO Detecting Alpine vulnerabilities...
2021-09-22T10:29:51.903Z INFO Number of language-specific files: 1
2021-09-22T10:29:51.903Z INFO Detecting gobinary vulnerabilities...
nemunaire/fic-admin (alpine 3.14.2)
===================================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
srv/admin (gobinary)
====================
Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
```
</div>
Nous pouvons remarque que Trivy, en plus de faire l'analyse statique des
vulnérabilités de l'image, a aussi fait une analyse des dépendances du binaire
`/srv/admin`.
Trivy est en effet capable de rechercher des vulnérabilités par rapport aux
dépendances connues de certains langages : Python, PHP, Node.js, .NET, Java,
Go, ...
### Usage du cache
Pour éviter de surcharger les serveurs de distributions de la base de données
de vulnérabilités, nous devrions utiliser un cache pour faire nos
analyses. Préférez lancer `trivy` avec les options suivantes :
<div lang="en-US">
```
42sh$ docker run --rm -v /tmp/trivy-cache:/tmp/trivy/ aquasec/trivy --cache-dir /tmp/trivy
```
</div>
### Format de génération du rapport
Lorsque nous appelons `trivy` directement, il génère un rapport au format texte
lisible directement dans notre terminal. Il peut être pourtant pratique de
pouvoir l'exporter pour l'afficher dans un navigateur (par exemple pour le
mettre à disposition des développeurs, lors d'une analyse automatique).
Pour ce faire, on peut ajouter les options suivantes à la ligne de commande de
notre conteneur :
<div lang="en-US">
```bash
--quiet --format template --template "@contrib/html.tpl"
```
</div>
En redirigeant la sortie standard vers un fichier, vous pourrez l'ouvrir dans
votre navigateur favori.
---
![Scan de vulnérabilités sur le registre Quay.io](quay-vulns.png){ width=90% }
## Clair