TDs associés au cours sur la sécurité des infrastructures de virtualisation
Pour réaliser ces TDs, nous allons utiliser des machines virtuelles Fedora et Ubuntu. Les instructions de mise en place varient en fonction du contexte.
Mise en place en salle 2SEC à l'INSA-CVL
Pour réaliser ces TDs, nous allons utiliser des machines virtuelles Fedora et Ubuntu. Pour cela, nous allons mettre en place libvirt et Vagrant sur notre système.
Installation de libvirtd & vagrant
Suivre les instructions officielles pour installer Vagrant: https://developer.hashicorp.com/vagrant/install
Installer aussi le démon libvirtd si nécessaire.
Configurer l'accès à libvirtd pour l'utilisateur :
$ nano /etc/libvirt/libvirtd.conf
unix_sock_group = "libvirt"
unix_sock_rw_perms = "0770"
Puis redémarrer le démon libvirtd :
$ systemctl restart libvirtd
Vérifier que libvirtd fonctionne correctement :
$ systemctl status libvirtd
Si nécessaire, ajouter votre utilisateur au groupe libvirt :
$ usermod -G libvirt user
Vérifier l'accès au démon libvirtd en tant qu'utilisateur :
$ virsh --connect=qemu:///system list --all
Récupération des images disque
Monter le partage NFS si il n'est pas déjà monté :
$ mkdir -p /mnt/
$ mount -t nfs -o soft 172.19.3.254:mnt /mnt
Créer un dossier sous votre nom (sans caractères spéciaux ni accents ou espaces) et récupérer la configuration Vagrant et l'image disque virtuelle depuis le server de fichier partagé :
$ mkdir <nomprénom>
$ cd <nomprénom>
$ cp /mnt/nfs/DATASTORE/RAVIER/* ./
$ ls
Vagrantfile
fedora-2su.box
ubuntu-2su.box
sha256sums
Installer le plugin Vagrant libvirt :
$ vagrant plugin install vagrant-libvirt
Lancer la première machine virtuelle :
$ vagrant up ubuntu
Accéder à la première machine virtuelle :
$ vagrant ssh ubuntu
Mise en place avec accès à Proxmox à l'INSA-CVL
Pour réaliser ces TDs, nous allons utiliser des machines virtuelles Fedora et Ubuntu. Nous allons utiliser les machines virtuelles pré-créées dans l'instance Proxmox qui est mise à votre dispositon.
Se connecter à l'instance Proxmox
Utiliser l'adresse IP de l'instance qui vous a été attribuée pour vous connecter à l'interface web de Proxmox : https://172.16.160.XX:8006
La plupart des opérations peuvent être réalisées avec l'interface web.
Gestion des machines virtuelles en ligne de commande
Si dessous un extrait des commandes que vous pouvez aussi utiliser directement lorsque vous êtes connectés en SSH :
$ ssh root@172.16.160.XX
Pour lancer une machine virtuelle :
$ qm start <ID de la machine virtuelle>
Pour arrêter une machine virtuelle normallement :
$ qm shutdown <ID de la machine virtuelle>
Pour forcer l'arrêt d'une machine virtuelle :
$ qm stop <ID de la machine virtuelle>
Pour supprimer une machine virtuelle :
$ qm destroy <ID de la machine virtuelle> --purge
Pour créer une nouvelle machine à partir d'un template :
$ qm clone <ID template> <ID nouvelle machine> --name <nom nouvelle machine>
Mise en place chez vous
Pour réaliser ces TDs, nous allons utiliser des machines virtuelles Fedora et Ubuntu.
Outils de gestion de machine virtuelles
Pour faire fonctionner ces machines virtuelles, je vous recommande d'utiliser les solutions suivantes :
- Sur Windows et macOS, utilisez VirtualBox.
- Sur Linux, utilisez aussi VirtualBox ou virt-manager avec libvirt et QEMU/KVM (suivre les guides pour Fedora, Ubuntu ou Arch Linux).
Vous ne pourrez pas réaliser les exercices sur votre système Linux directement ou sur Windows 10 avec WSL (1 ou 2). Il faut nécessairement utiliser de vraies machines virtuelles.
Préparation à réaliser pour chaque TD
Voici les machines virtuelles que vous devez préparer pour chaque TD.
Comme les manipulations que nous serons amenées à faire pendant les TDs peuvent rendre le système virtualisé non fonctionnel, je vous demande de démarer chaque TD à partir d'une machine virtuelle neuve.
Je vous demande aussi de vous assurer que vous pouvez vous connecter en SSH à ces machines virtuelles pour que vous puissez utiliser plusieurs terminaux simultanéments.
1. Namespaces sous Linux
Pour ce TD, vous devez préparer une machine virtuelle Ubuntu mise à jour dans laquelle vous avez installé le démon Docker et le compilateur Go :
$ sudo apt-get -y -q update
$ sudo apt-get -y -q dist-upgrade
$ sudo apt-get -y -q install tree make docker.io golang debootstrap
$ sudo apt-get -y -q autoremove
$ sudo apt-get -y -q clean
$ sudo reboot
2. systemd
Pour ce TD, vous devez préparer une machine virtuelle Fedora mise à jour dans laquelle vous avez installé l'outils nc :
$ sudo dnf upgrade -y
$ sudo dnf install -y tree bash-completion nmap-ncat
$ sudo dnf clean all
$ sudo reboot
3. SELinux
Pour ce TD, vous devez préparer une machine virtuelle Fedora mise à jour dans laquelle vous avez installé le serveur Web Apache ainsi que des outils pour modifier la politique SELinux :
$ sudo dnf upgrade -y
$ sudo dnf install -y tree bash-completion httpd selinux-policy-devel setools-console rpm-build
$ sudo dnf clean all
$ sudo systemctl enable httpd
$ sudo reboot
4. Conteneurs et sécurité
Pour ce TD, vous devez préparer une machine virtuelle sous Ubuntu et une autre sous Fedora avec les paquets suivants :
Ubuntu
$ sudo apt-get -y -q update
$ sudo apt-get -y -q dist-upgrade
$ sudo apt-get -y -q install tree docker.io
$ sudo apt-get -y -q autoremove
$ sudo apt-get -y -q clean
$ sudo reboot
Fedora
$ sudo dnf upgrade -y
$ sudo dnf install -y tree bash-completion podman buildah skopeo
$ sudo dnf clean all
$ sudo systemctl enable httpd
$ sudo reboot
5. Infrastructures
Pour ce TD, il n'est pas strictement nécessaire d'utiliser une machine virtuelle. Pour plus de facilité, je vous recommande d'utiliser une machine virtuelle Ubuntu ou Fedora si vous utilisez Windows ou macOS comme environnement principal.
Conteneurs avec les namespaces sous Linux
Ce TD est à réaliser dans une machine virtuelle Ubuntu avec le démon Docker installé et activé.
A l'INSA-CVL, se connecter à la machine ubuntu-namespaces.
Objectifs
- Découvrir le principe des conteneurs à l'aide de Docker
Introduction aux conteneurs avec Docker
Installer Docker :
$ sudo apt-get update
$ sudo apt-get -y -q install docker.io
Vérifiez que le démon Docker est bien démarré :
$ sudo systemctl status docker
Lancement d'un conteneur interactif
Pour récupérer directement du registre de conteneurs public proposé par Docker (DockerHub) l'image officielle de conteneur Ubuntu, utilisez :
$ sudo docker pull ubuntu
Vérifiez qu'elle a bien été importée avec la commande :
$ sudo docker images
Par défaut, la dernière version publiée de l'image est téléchargée. Lancez un shell interactif dans un conteneur en utilisant cette image :
$ sudo docker run -it ubuntu bash
Essayez les commandes suivantes : ps aux, id, findmnt, lsns et comparez le résultat obtenu à l'intérieur et à l'extérieur du conteneur.
Quitter le conteneur avant de passer à la suite.
Construction d'un conteneur
Nous allons créer un conteneur qui contiendra un serveur HTTP hébergeant un site Web statique.
Pour commencer, voici un programme en Go qui mets à disposition sur le réseau le contenu du dossier www en HTTP sur le port 8000 :
app.go (télécharger) :
package main
import (
"log"
"net/http"
)
func main() {
fs := http.FileServer(http.Dir("www"))
http.Handle("/", fs)
log.Println("Listening...")
http.ListenAndServe(":8000", nil)
}
Créez un site Web minimal :
$ mkdir www
$ echo "Hello world" > www/index.html
Installer le compilateur Go :
$ sudo apt-get -y -q install golang
Compilez et testez le bon fonctionnement du programme avec les commandes :
$ go build -o app app.go
$ ./app
Testez votre application avec curl (ou avec un navigateur web) :
$ curl http://localhost:8000
Maintenant que notre application est prête, nous pouvons la mettre dans un conteneur. Pour que notre application puisse fonctionner, nous allons lister les bibliothèques dont elle a besoin pour les inclure ensuite dans le conteneur:
$ ldd app
linux-vdso.so.1 (0x00007ffea02a0000)
libc.so.6 => /lib64/libc.so.6 (0x00007f4f9e080000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4f9e275000)
La librairie linux-vdso.so.1 "n'existe pas" car elle est fournie directement par le noyau aux processus (voir vdso(7)). Nous récupérons les autres dépendances:
$ cp /<chemin-complet-bilbiothèques> .
Pour décrire le contenu d'une image de conteneur avec Docker, nous allons utiliser un Dockerfile. Voici un Dockerfile qui crée un conteneur avec notre application et le site web :
Dockerfile (télécharger) :
# Partir d'une image vide
FROM scratch
# Copier les bibliothèques et le binaire de notre application
COPY ld-linux-x86-64.so.2 /<chemin-complet-bibliothèque>
COPY libc.so.6 /<chemin-complet-bibliothèque>
COPY app app
# Copier le répertoire www avec la page web
COPY www www
# L'application utilise le port 8000
EXPOSE 8000
# La commande à lancer pour démarrer ce conteneur
CMD ["./app"]
Créer ensuite l'image du conteneur à partir du Dockerfile :
$ sudo docker build --tag app .
Lancez le conteneur :
$ sudo docker run -ti app
Pour vérifier que nous avons bien accés à notre application, listez tous les conteneurs en cours d'exécution :
$ sudo docker ps
CONTAINER ID IMAGE COMMAND ... PORTS NAMES
79c533003fe8 app "./app" ... 8000/tcp loving_goldwasser
Récupérez l'adresse IP du conteneur qui nous intéresse à l'aide de son container id :
$ sudo docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <container id>
172.17.0.2
Récupérez la page Web avec curl (ou un navigateur web) :
$ curl 172.17.0.2:8000
Références
Conteneurs avec les namespaces sous Linux
Ce TD est à réaliser dans une machine virtuelle Ubuntu.
A l'INSA-CVL, se connecter à la machine ubuntu-namespaces.
Objectifs
- Utiliser les interfaces du noyau Linux pour lancer un conteneur
Création « manuelle » d'un conteneur
Nous allons créer et lancer un conteneur sans utiliser Docker pour comprendre les mécanismes utilisés et les interactions avec le noyau Linux.
Création d'une image système
Nous allons utiliser debootstrap pour créer une racine de système de fichier (le contenu d'une image Docker) pour notre conteneur.
$ sudo apt-get -y -q install debootstrap
Nous utiliserons ici la version jammy (22.04 LTS) de la distribution Ubuntu, dans sa variante minimale :
$ mkdir rootfs
$ sudo debootstrap --variant=minbase jammy rootfs
Utilisation de unshare
Pour lancer un shell Bash dans notre image, nous pouvons utiliser chroot :
$ sudo chroot rootfs /bin/bash
Mais chroot « n'isole » que la vue du système de fichier pour une application :
# A l'extérieur du chroot
$ top
$ lsns
# A l'intérieur du chroot
$ mount -t proc proc /proc
$ top
$ ps aux | grep top
$ lsns
$ umount /proc
Pour isoler la vue des processus, il faut utiliser les namespaces avec unshare :
$ sudo unshare --pid --fork chroot rootfs /bin/bash
$ mount -t proc proc /proc
$ ps aux
$ lsns
Notez que nous utilisons des versions distinctes de la distribution Ubuntu à l'intérieur du conteneur de celle de l'hôte. Dans notre cas, c'est une version stable LTS sur un hôte récent non LTS. En général, on souhaite utiliser une distribution stable bien supportée comme hôte et potentiellement une distribution plus récente dans un conteneur pour bénéficier des dernières évolution d'un langage ou d'un compilateur.
Interface du noyau Linux : namespaces
Nous allons reproduire les commandes ci-dessus à l'aide d'un programme en C faisant directement appel à l'interface du noyau.
$ sudo apt-get -y -q install make
Voici un programme qui utilise l'appel système clone(2) pour créer un processus fils :
main.c (télécharger) :
// Modified from https://lwn.net/Articles/533492/
// Copyright 2013, Michael Kerrisk
// Licensed under GNU General Public License v2 or later
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>
#include <sys/mount.h>
// A simple error-handling function: print an error message based
// on the value in 'errno' and terminate the calling process
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
// Function de départ pour le processus fils cloné
static int childFunc(void *arg)
{
char **argv = arg;
// TODO : Mise en place pour le processus fils
execvp(argv[0], &argv[0]);
errExit("execvp");
}
// Espace mémoire réservé pour la pile du processus fils
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
int main(int argc, char *argv[])
{
int flags = 0;
pid_t child_pid;
// TODO : Mise en place du flag pour le PID namespace
child_pid = clone(childFunc, child_stack + STACK_SIZE, flags | SIGCHLD, &argv[1]);
if (child_pid == -1)
errExit("clone");
printf("%s: PID of child created by clone() is %ld\n", argv[0], (long) child_pid);
// Le processus parent continue ici
// Attent que le processus fils se termine
if (waitpid(child_pid, NULL, 0) == -1)
errExit("waitpid");
printf("%s: terminating\n", argv[0]);
exit(EXIT_SUCCESS);
}
et un Makefile (télécharger) :
.PHONY: all clean
all:
gcc -o main -Wall -Wextra main.c
clean:
rm -f main
Compilez et essayez l'application avec :
$ make
$ sudo ./main ls
$ sudo ./main ps aux
Namespace PID
Nous allons ajouter la mise en place du namespace PID :
- Ajoutez le flag pour créer un processus fils dans un PID namespace distinct
- Faites appel à
chroot(2)etchdir(2)pour utiliser le shell du rootfs obtenu avec debootstrap - Montez
/procdans le rootfs pour obtenir les bonnes informations sur les processus - Validez le résultat avec les commandes :
ps auxls -al /proc/self/nslsns
Namespace réseau
Nous allons ensuite ajouter le support du namespace réseau.
Installer le paquet
iproute2pour obtenir la commandipdans le chroot.Indices
Pour cela vous pouvez utiliser
chroot rootfs /bin/bashpour obtenir un shell puis installer le paquet avecapt updatepuisapt install iproute2.Une fois le processus fils lancé dans le nouveau namespace, créer une paire d'interfaces réseau virtuelles
vethà l'aide de la commandipsuivante dans le processus père:ip link add name veth0 type veth peer name veth1 netns [child pid]Vous pouvez par exemple appeler cette commande à l'aide de la fonction
system().Ajouter un appel à
sleep()dans le processus fils pour qu'il attende que l'interface réseau soit créée par le père dans son namespace avant d'essayer de la configurer.Configurer l'interface réseau à l'aide de
ipdans le namespace du père et du fils.Exécuter la command ping depuis le fils pour communiquer avec le père.
Solution
Une solution pour cette section est proposée dans l'article : Linux – Writing a Simple Container App with Namespaces, Liran B. H.
Références
- Linux Security and Isolation APIs Fundamentals Training, Michael Kerrisk
- Learning Containers From The Bottom Up, Ivan Velichko
- Interactive labs for Red Hat Enterprise Linux
Gestion des ressources avec les cgroups
Ce TD est à réaliser dans une machine virtuelle Ubuntu ou Fedora.
A l'INSA-CVL, se connecter à la machine ubuntu-namespaces.
Objectifs
- Gérer des processus avec les cgroups
Gestion manuelle des cgroups
Le PID controller permet de limiter le nombre de PID disponibles dans un cgroup. Dans la plupart des cas, systemd aura déjà monté cette hiérarchie au démarrage du système :
$ mount | grep cgroup
Dans le cas général, il est nécessaire d'avoir des privilèges pour modifier la configuration des controlleurs cgroups. Il faudra donc exécuter les commandes qui suivent dans un shell root directement (l'exemple ne fonctionnera pas en lançant les commandes avec sudo) :
$ sudo -i
Créez un nouveau groupe dans la hiérarchie des cgroups et y déplacer le shell courant :
# mkdir -p /sys/fs/cgroup/foo/bar
# echo $$
# echo $$ > /sys/fs/cgroup/foo/bar/cgroup.procs
Question 1 : Expliquez le résultat de la commande suivante :
# cat /sys/fs/cgroup/foo/bar/cgroup.procs
24150
27463
Pour lister les controlleurs activés sur l'ensemble du système :
# cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc
Les controlleurs actifs pour un cgroups sont configurés à l'echelle du parent dans le fichier subtree_control. Un sous ensemble des controleurs est activé par défaut par systemd :
# cat /sys/fs/cgroup/cgroup.subtree_control
cpu io memory pids
# cat /sys/fs/cgroup/foo/cgroup.controllers
cpu io memory pids
Par défaut, aucun controleur n'est activé pour les fils des nouveaux cgroups.
# cat /sys/fs/cgroup/foo/cgroup.subtree_control
<vide>
# cat /sys/fs/cgroup/foo/bar/cgroup.controllers
<vide>
Pour mettre en place une limite sur le nombre de PIDs par exemple, il faut activer le controleur pids pour le cgroup que l'on souhaite utiliser en l'ajoutant dans le fichier virtuel cgroup.subtree_control des cgroups parents :
# cat /sys/fs/cgroup/foo/cgroup.controllers
cpu io memory pids
# echo '+pids' > /sys/fs/cgroup/foo/cgroup.subtree_control
# cat /sys/fs/cgroup/foo/cgroup.subtree_control
pids
# cat /sys/fs/cgroup/foo/bar/cgroup.controllers
pids
Il est alors possible de définir une limite en nombre de PIDs :
# echo 20 > /sys/fs/cgroup/foo/bar/pids.max
# for i in $(seq 1 20); do sleep 20 & done
Question 2 : Expliquez le résultat de la commande précédente. Pourquoi est-ce que la boucle s'arrête à 19 ?
Avant de supprimer un groupe, il faut que tous les processus qui lui sont associés se terminent. La suppression du dossier supprimera ensuite le groupe.
# exit
# rmdir /sys/fs/cgroup/foo/bar
# rmdir /sys/fs/cgroup/foo
systemd et journald
Ce TD est à réaliser dans une machine virtuelle Fedora.
A l'INSA-CVL, se connecter à la machine fedora-systemd.
Objectifs
- Découvrir systemd et journald
systemd et journald
Etat des services
Listez l'état des services sur la machine :
$ systemctl
$ systemctl status
Inspectez l'état précis de quelques services :
$ systemctl status <service>
Tuez un service au hasard avec la commande kill et constater les changements de status. Il est possible de filtrer les résultats en n'affichant que les services disponibles (ou lancés ou en échec) :
$ systemctl --state=running
$ systemctl --state=active
$ systemctl --state=exited
$ systemctl --state=failed
Désactivez et arrêtez tous les services que vous pensez non nécessaire au fonctionnement du système :
$ systemctl disable <service>
$ systemctl stop <service>
Démarrage du système
Vérifiez que la « target » par défaut est bien multi-user.target :
$ systemctl get-default
$ systemctl set-default multi-user.target
Etudiez la procédure de démarrage du système avec :
$ systemd-analyze plot > bootchart.svg
Le fichier bootchart.svg peut être affiché dans un navigateur. Quittez votre session SSH et copiez le fichier sur votre système hôte :
$ scp fedora@172.XX.YY.ZZ:bootchart.svg bootchart.svg
- Question 1 : Dans quel ordre démarrent les services ?
- Question 2 : Trouvez quatre services avec des dépendances et retrouvez cette information dans leur fichier d'unit.
Modifier certains paramètres pour un service précis
Modifiez quelques services sans modifier directement leur unit en utilisant :
$ systemctl edit <service>
Vous pouvez par exemple rajouter des variables d'environment à l'unit:
[Service]
Environment=Foo=Bar
Constatez les changements dans le dossier :
/etc/systemd/system/<service>.service.d/*..conf
Indiquez à systemd de recharger la configuration, redémarrer le service et vérifier que la modification a bien été appliquée :
$ systemctl daemon-reload
$ systemctl restart <service>(.service)
$ systemctl show service
$ systemctl status service
$ cat /proc/<PID>/environ
Question 3 : Indiquez qu'un service devra être relancé en cas d'interruption imprévue (chercher l'option Restart dans systemd.service(5)). Testez que cela fonctionne correctement.
Modifiez quelques services en copiant leur fichier d'unit par défaut dans le dossier :
$ cp /usr/lib/systemd/system/<unit>.service /etc/systemd/system/
Les modifications peuvent alors être effectuées directement dans l'unit. Attention, les modification dans le dossier *.service.d/*.conf sont toujours prises en compte. Pour afficher l'ensemble des configurations prises en compte pour un service par systemd :
$ systemctl cat <service>
Naviguer dans le journal
Essayez et expliquez les commandes suivantes :
$ journalctl -e
$ journalctl -b -1
$ journalctl -b -0 _PID=333
$ journalctl -k
$ journalctl -u httpd
$ journalctl -f -n 50
$ journalctl /usr/bin/dbus-broker
Question 4 : Comment peut on afficher tous les logs qui ont été émis par le service Apache (httpd) entre 10h30 et 11h00 ce matin ?
Références
- Pages de man de systemd: http://www.freedesktop.org/software/systemd/man/
- Red Hat Summit 2013 - Getting Ready for Systemd
- systemd by example
Gestion des cgroups avec systemd
Ce TD est à réaliser dans une machine virtuelle Fedora.
A l'INSA-CVL, se connecter à la machine fedora-systemd.
Objectifs
- Configurer des limites de ressources avec systemd
cgroups et systemd
En général, systemd créé et configure la majeur partie des cgroups sur un système. Pour lister l'organisation mise en place par systemd :
$ systemd-cgls
Question 1 : Que constatez vous sur l'organisation des services avec les cgroups ?
Les variables (liées aux cgroups) modifiables à la volée sont listées dans systemd.resource-control(5).
Pour réduire dynamiquement et de façon permanente la valeur de CPUQuota d'un service :
$ sudo systemctl set-property <service> CPUQuota="20%"
Question 2 : Retrouvez les modifications effectuées par systemd dans l'arbre des cgroups (/sys/fs/cgroup/).
Pour effectuer une modification temporaire qui ne sera effective que jusqu'au prochain reboot :
$ sudo systemctl --runtime set-property ...
Activez l'accounting pour quelques services :
$ sudo systemctl set-property <service> MemoryAccounting=true
Surveillez l'utilisation des ressources par ces services avec :
$ systemd-cgtop
Références
- Pages de man de systemd: http://www.freedesktop.org/software/systemd/man/
- Red Hat Summit 2013 - Getting Ready for Systemd
- systemd by example
Durcissement système à l'aide de systemd
Ce TD est à réaliser dans une machine virtuelle Fedora.
A l'INSA-CVL, se connecter à la machine fedora-systemd.
Objectifs
- Durcir la configuration d'un processus du système à l'aide de systemd
Créer son propre service unit
Nous allons prendre l'utilitaire ncat et en faire un démon géré par systemd.
Question 1 : Que fait la commande suivante ?
$ ncat -lke /bin/bash 0.0.0.0 9999
Indice: Lire la page de man: ncat(1).
Question 2 : Où allons nous placer notre fichier d'unit ?
Voici un template contenant les directives de configuation principales pour un service :
[Unit]
Description=...
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=...
[Install]
WantedBy=multi-user.target
Question 3 : Créez un service unit qui lance la commande précédente à l'aide du template ci-dessus.
N'oubliez pas que systemd ne prend pas en compte les nouveaux fichiers d'unit automatiquement. Pour lui indiquer de recharger la configuration il faut utiliser :
$ systemctl daemon-reload
Démarrez votre nouveau service et testez son bon fonctionnement :
$ nc localhost 9999
ls
...
Question 4 : Aidez vous des options de la page de manuel systemd.exec(5) pour confiner ce service en définissant :
- le redémarrage automatique en cas d'erreur non prévue
- un utilisateur et un groupe à choisir judicieusement (à créer si nécessaire)
- des limitations d'accès sur les répertoires du système (
ProtectSystem, etc.) - des limitations sur les appels systèmes disponibles :
- avec une liste d'interdiction pour supprimer les fonctions de debug ou les appels systèmes obsolètes
- ou avec une liste d'autorisation avec uniquement les appels systèmes nécessaires
- voir l'option
SystemCallFilterdans systemd.exec(5)
Question 5 : Vérifiez que toutes les options fonctionnent correctement.
Références
- Pages de man de systemd: http://www.freedesktop.org/software/systemd/man/
- Red Hat Summit 2013 - Getting Ready for Systemd
- Durcissement système à l'aide de systemd, Timothée Ravier, SSTIC 2017
- systemd by example
SELinux
Ce TD est à réaliser dans une machine virtulle Fedora avec le serveur web Apache installé et activé.
A l'INSA-CVL, se connecter à la machine fedora-selinux.
Objectifs
- Découvrir SELinux
- Gestion des contextes et des booléens
État de SELinux
Avant de modifier la politique SELinux, commençons par afficher l'état de SELinux et de la politique actuellement chargée :
$ sestatus -v
$ cat /etc/selinux/config
$ getenforce
$ seinfo
Contextes fichiers et processus
- Question 1 : Sous quel contexte le shell de l'utilisateur courant s'exécute-il ?
- Question 2 : Quel sont les contextes associés aux fichiers ou répertoires suivants :
/usr/bin/etc/shadow/etc/passwd/home/home/fedora/root/var/www/tmp
- Question 3 : Quel sont les contextes associés aux processus suivants :
- démon
sshd; - démon
httpd; init(systemd).
- démon
Indices
Vous pouvez vous aider des commandes :
$ ps -eZ
$ ls -Z
Gestion des booléens
Comme la politique par défaut ne peut pas couvrir l'ensemble des usages possibles de chaque logiciel potentiellement installables sur un système, nous allons mettre en place une configuration de serveur web classique et utiliser les booléens pour autoriser ce fonctionnement dans la politique SELinux.
Nous allons mettre en place un hébergement web statique à la disposition des utilisateurs en utilisant Apache.
Créez le dossier /home/fedora/public_html et un ficher dans ce dossier :
$ mkdir /home/fedora/public_html
$ echo "Hello SELinux" > /home/fedora/public_html/test
Donnez le droit à tout le monde de traverser le dossier /home/fedora/ pour qu'Apache puisse accéder à /home/fedora/public_html` :
$ chmod o+x /home/fedora
$ ls -l /home/fedora
Dans la configuration d'Apache (/etc/httpd/conf.d/userdir.conf) :
- Commentez la directive
UserDir disabled - Décommentez la directive
UserDir public_html
Redémarrez Apache :
$ systemctl restart httpd.service
Essayez de récupérer le contenu du fichier précedement créé avec la commande curl :
$ curl http://localhost/~fedora/test
Cette opération doit normalement échouer.
Consulter les logs pour obtenir des informations :
$ sudo journalctl -e
$ sudo tail /var/log/httpd/error_log
$ sudo tail /var/log/audit/audit.log
Vous devez trouver une entrée de log qui indique que SELinux a bloqué l'accès au fichier par Apache (httpd).
Cherchez si une règle n'existe pas déjà dans la politique pour autoriser cet accès :
$ ls -alZ ~/public_html/
$ sesearch -v --allow -s httpd_t -t httpd_user_content_t
sesearch vous liste les règles existantes dans la politique qui correspondent à votre recherche. Certaines de ces règles sont suffixées par une condition : le booléen qu'il faut activer pour activer la règle.
Affichez toutes les règles ajoutées par le booléen httpd_enable_homedirs :
$ sesearch --allow -b httpd_enable_homedirs
Inspectez l'état de l'ensemble des booléens disponibles dans la politique :
$ getsebool -a
Activez le booléen pour autoriser Apache à lire le contenu du dossier public_html dans les répertoires utilisateurs :
$ sudo setsebool httpd_enable_homedirs on
Essayez à nouveau d'obtenir la page web :
$ curl http://localhost/~fedora/test
Références
Création d'un module SELinux
Ce TD est à réaliser dans une machine virtulle Fedora.
A l'INSA-CVL, se connecter à la machine fedora-selinux.
Objectifs
- Ecriture d'un module pour un démon système
Écrire un module pour un démon système
Nous allons désormais écrire un module de politique SELinux pour confiner une nouvelle application sur notre système. Nous allons lancer le script python pyserver (télécharger) :
#!/usr/bin/env python3
import http.server
import socketserver
PORT = 8000
Handler = http.server.SimpleHTTPRequestHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print("serving at port", PORT)
httpd.serve_forever()
avec l'unit systemd pyserver.service (télécharger) :
[Unit]
Description=Test Python HTTP server for SELinux lab
After=network.target network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/pyserver
[Install]
WantedBy=multi-user.target
Importez le script python pyserver dans le dossier /usr/local/bin/ et l'unit systemd pyserver.service dans le dossier /etc/systemd/system/.
Vérifier que le script est exécutable:
$ ls -al /usr/local/bin/pyserver
...
$ chmod a+x /usr/local/bin/pyserver
Restaurez les contextes SELinux par défaut pour ces fichiers :
$ sudo restorecon -Fv /usr/local/bin/pyserver /etc/systemd/system/pyserver.service
Rechargez la configuration de systemd et démarrer le service :
$ sudo systemctl daemon-reload
$ sudo systemctl start pyserver
Installez le démon setroubleshoot (à ne pas utiliser en production) pour s'aider à comprendre les erreurs SELinux :
$ sudo dnf install -y setroubleshoot-server
Utilisez sepolgen pour générer un caneva de politique SELinux pour notre démon :
$ mkdir ~/module
$ cd ~/module
$ sepolgen --init /usr/local/bin/pyserver
Les erreurs produites par la commande sepolgen ne sont pas importantes.
Regardez le contenu du module généré :
$ cat pyserver.te
$ cat pyserver.fc
$ cat pyserver.if
Désactivez les règles dontaudit pour obtenir tous les logs d'erreur :
$ sudo semodule -DB
Compilez le module SELinux :
$ make -f /usr/share/selinux/devel/Makefile pyserver.pp
Si vous obtenez des erreurs, il faudra les résoudres avant de progresser.
Chargez ensuite le module avec la politique dans le noyau :
$ sudo semodule -i pyserver.pp
Mettez en place le nouveau contexte SELinux pour l'executable pyserver :
$ sudo restorecon -Fv /usr/local/bin/pyserver /etc/systemd/system/pyserver.service
Démarrez ou redémarez le démon avec systemd :
$ sudo systemctl restart pyserver
Vérifiez que le serveur s'execute sous le bon contexte :
$ sudo systemctl status pyserver
$ sudo ps auxZ | grep pyserver
system_u:system_r:pyserver_t:s0 root ... python3 /usr/local/bin/pyserver
Créez du contenu pour votre serveur web :
$ echo "test" | sudo tee /var/www/html/index.html
$ sudo restorecon -Fv /var/www/html/index.html
Testez le serveur :
$ curl http://localhost:8000/
$ curl http://localhost:8000/var/www/html/
Regardez les erreurs dans les logs d'audit :
$ sudo tail -fn 10 /var/log/audit/audit.log
$ sudo journalctl -e _TRANSPORT=audit
Mettez à jour votre module SELinux pyserver.te à l'aide du résultat de la commande audit2allow :
$ sudo cat /var/log/audit/audit.log | audit2allow
Répétez les étapes précédentes jusqu'à ce que la politique soit complète.
Une fois les premières règles ajoutées, regardez les options de la commande audit2allow, notament -l.
Important: Une fois la politique terminée, il ne faut pas oublier de supprimer la directive rendant le domaine permissif pour rendre effective la protection avec SELinux.
Une fois le mode permissif désactivé, vérifiez à nouveau que votre service fonctionne corectement.
Références
- Wiki du projet SELinux
- SELinux Notebook
- Blog de Dan Walsh
- Sources de la politique SELinux de Fedora
- How custom SELinux policies secure servers and containers
Confinement d'un utilisateur avec SELinux
Ce TD est à réaliser dans une machine virtulle Fedora.
A l'INSA-CVL, se connecter à la machine fedora-selinux.
Objectifs
- Confinement des utilisateurs non privilégiés
Confinement d'un utilisateur
Nous allons ajouter un utilisateur non privilégié et lui associer un utilisateur et un rôle SELinux non privilégié et confiné.
Ajoutez un utilisateur toto :
$ sudo adduser toto
$ sudo passwd toto
Listez les associations nom de login -> utilisateur SELinux :
$ sudo semanage login --list
Listez les associations utilisateurs SELinux -> Rôles :
$ sudo semanage user --list
Autoriser plus de catégories pour l'utilisateur user_u:
$ sudo semanage user --modify --range s0-s0:c0.c1023 user_u
Ajoutez une association login -> utilisateur SELinux confiné pour toto :
$ sudo semanage login --add -s user_u -r s0-s0:c0.c1023 toto
Vérifiez l'ajout de l'association :
$ sudo semanage login --list
Se loguer sous cet utilisateur à distance avec SSH, pas avec su.
- Question 1 : Sous quel contexte tourne le shell obtenu ?
- Question 2 : Essayez de passer
root. Que se passe-t-il ?
Références
Compiler la politique SELinux à partir des sources
Ce TD est à réaliser dans une machine virtulle Fedora.
A l'INSA-CVL, se connecter à la machine fedora-selinux.
Objectifs
- Recompiler la politique SELinux complète
Obtenir les sources complètes de la politique SELinux
Pour récupérer les sources de la politique et les recompiler :
$ dnf download --source selinux-policy
$ rpm --install selinux-policy-3.13.1-283.19.fc27.src.rpm 2>/dev/null # erreurs non importantes
$ sudo dnf install -y rpmdevtools
$ sudo dnf builddep -y selinux-policy
$ sudo dnf install -y @development-tools
$ cd ~/rpmbuild/SPECS
$ rpmbuild -bp selinux-policy.spec # erreurs non importantes
$ cd ~/rpmbuild/BUILD/serefpolicy-3.13.1
Références
Confinement des conteneur avec SELinux
Objectifs
- Découvrir Podman et le confinement avec SELinux sous Fedora
- Comparer avec AppArmor sous Ubuntu, et sans SELinux sous Fedora
Podman avec SELinux
Nous allons commencer par regarder comment SELinux est utilisé par podman pour cloisonner les conteneurs sur Fedora.
A l'INSA-CVL, se connecter à la machine fedora-containers.
Récupération d'une image de conteneur
Récupérez directement du registre public l'image officiel de conteneur Fedora :
$ sudo podman pull fedora
Vérifiez qu'elle a bien été importée avec la commande :
$ sudo podman images
Par défault, c'est la version latest de l'image qui est téléchargée.
Confinement avec SELinux
Pour lancer un shell interactif dans un conteneur utilisant cette image :
$ sudo podman run -it fedora bash
Ouvrez un autre terminal, connectez vous à la machine via SSH et cherchez le processus bash qui s'exécute dans le conteneur.
Pour cela vous pouvez chercher l'ID créé par podman pour ce conteneur :
$ sudo podman ps
Et ensuite chercher cet ID dans la liste des cgroups de votre système :
$ systemd-cgls
Une fois le PID du processus bash trouvé, pour pouvez regarder les information pour ce processus à l'aide de ps ou directement dans le fichier /proc/<PID>/staus :
$ ps afuxZ
$ cat /proc/<PID>/status | grep UID
- Question 1 : Sous quel utilisateur tourne ce shell ?
- Question 2 : Est-ce le même à l'intérieur et à l'extérieur du conteneur ?
- Question 3 : Sous quel contexte SELinux et quelles catégories tourne ce processus ?
Si vous avez besoin d'installer des commandes dans votre conteneur et que vous ne connaissez pas le nom du paquet à installer, vous pouvez directement demander à dnf de le chercher pour vous :
$ dnf install /usr/bin/ps
Pour trouver quel paquet inclu un fichier :
$ rpm -qf /usr/bin/ps
Lancez un deuxième shell dans un deuxième conteneur en parallèle.
- Question 4 : Sous quel utilisateur, contexte SELinux et quelles catégories tourne le shell de ce nouveau conteneur ?
Lancez un nouveau shell avec la commande :
$ sudo podman run -it --rm --privileged --volume /:/host fedora bash
- Question 5 : Sous quel utilisateur, contexte SELinux et quelles catégories tourne ce nouveau shell ?
- Question 6 : Quels sont les risques présentés par cette commande ?
Inspirez-vous de la commande suivante pour obtenir des informations sur les accès autorisés par la politique SELinux pour le domaine container_t :
$ sesearch -A -s container_t /etc/selinux/targeted/policy/policy.*
AppArmor sous Ubuntu
Reproduisez les commandes du TD ci dessus dans une machine Ubuntu qui utilise AppArmor. Tous les appels à podman peuvent être remplacés par des appels à Docker.
A l'INSA-CVL, se connecter à la machine ubuntu-containers.
Podman sans SELinux
Reproduisez les commandes du TD ci dessus dans une machine Fedora dans laquelle nous allons désactiver SELinux.
A l'INSA-CVL, se connecter à la machine fedora-containers-noselinux.
Pour désactiver SELinux sur la machine virtuelle Fedora containers-noselinux, voir les instructions dans le fichier de configuration /etc/selinux/config :
$ sudo grubby --update-kernel ALL --args selinux=0
$ sudo reboot
Références
- Why we don't let non-root users run Docker in CentOS, Fedora, or RHEL
- Understanding root inside and outside a container, Scott McCarty, Red Hat Blog
- How containers use PID namespaces to provide process isolation
- Rootless containers using Podman
- The shortcomings of rootless containers
Création d'un conteneur durci
Ce TD est à réaliser dans une machine virtuelle Fedora.
A l'INSA-CVL, se connecter à la machine fedora-containers.
Objectifs
- Créer une image de conteneur durcie en terme de sécurité
Création d'une image de conteneur durcie à l'aide d'un Containerfile/Dockerfile
Récupérez l'exemple de Containerfile/Dockerfile suivant dans un nouveau dossier :
$ mkdir conteneur
$ cd conteneur
Dockerfile (télécharger):
FROM registry.fedoraproject.org/fedora:41
# Installation de nginx
RUN dnf -y install nginx && dnf clean all
# Expose le port 80 par défaut
EXPOSE 80
# Lance nginx
CMD ["nginx", "-g", "daemon off;"]
Construisez un conteneur à partir de ce Dockerfile :
$ sudo podman build --tag nginx .
Lancez intéractivement le conteneur créé :
$ sudo podman run --rm -it --publish 80 localhost/nginx
<vide>
Ouvrez un nouveau terminal, connectez vous au système et cherchez l'ID du conteneur créé :
$ sudo podman ps
...
Récupérez l'adresse IP associée à ce conteneur :
$ sudo podman inspect -f '{{.NetworkSettings.IPAddress}}' 19f063bfff5aa37655a...
10.88.0.8
# Ou avec Docker
$ sudo docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 19f063bfff5aa37655a...
172.17.0.2
Vérifiez que le serveur web fonctionne :
$ curl 10.88.0.8:80
Modifiez le Dockerfile pour obtenir une image conteneur qui :
- est basée sur la dernière version stable de Fedora
- a été mise à jour
- lance le serveur nginx sur le port 8080
- ne tourne pas sous l'utilisateur
root
Le conteneur exécute la commande nginx par défaut, mais vous pouvez forcer le lancement d'un shell dans le conteneur avec la commande :
$ sudo podman run --rm -ti localhost/nginx bash
Précisez ensuite lorsque vous lancer le conteneur que :
- l'image du conteneur est en lecture seule (RO)
- et les données (logs, etc.) sont stockées dans des volumes persistents
Aidez vous des guides suivants :
- la référence sur les
Dockerfiles: https://docs.docker.com/engine/reference/builder/ - les recommandations de l'article : https://www.projectatomic.io/docs/docker-image-author-guidance/
- le guide de bonnes pratiques : https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/
- Volumes avec podman : Quand utiliser les suffix
:zou:Z: Podman volume mounts: When to use the :z or :Z suffix?
Références
- Guidance for Docker Image Authors
- Just say no to root (in containers), Daniel J Walsh, opensource.com
- 10 tips for writing secure, maintainable Dockerfiles
CI/CD avec GitHub et signatures avec cosign
Objectifs
- Construire une image de conteneur automatiquement avec les GitHub Workflows / Actions
- Signer une image de conteneur avec cosign
- Configurer podman pour n'accepter que des images de conteneur signées
CI/CD avec les GitHub Workflows / Actions
Créer un compte GitHub si vous n'en avez pas déjà un
Ajouter un clé SSH à votre compte
Créer un nouveau dépot public sur GitHub
Cloner le dépôt en local sur votre machine
Ajouter un
Containerfilepour créer une image de conteneur :conteneur/Containerfile:FROM registry.fedoraproject.org/fedora:39 RUN touch /insa-cvlAjouter le workflow GitHub suivant :
.github/workflows/conteneur.yml:name: "Build container image" env: NAME: "conteneur" # Si votre identifiant de compte GitHub comporte des majuscules, remplacez # '${{ github.repository_owner }}' par votre identifiant en miniscule uniquement REGISTRY: ghcr.io/${{ github.repository_owner }} # Ce workflow ne sera exécuté que lors d'un 'push' sur la branche 'main' on: push: branches: - main # Authorise : # - Accès en lecture seule au contenu du dépôt Git # - Accès en lecture/écriture au "packages" pour avoir l'accès au registre de # conteneurs de GitHub permissions: contents: read packages: write jobs: build-push-image: runs-on: ubuntu-latest steps: # Récupération du dépôt Git - name: Checkout repo uses: actions/checkout@v4 # Construction de l'image de conteneur avec buildah - name: Build container image uses: redhat-actions/buildah-build@v2 with: context: ${{ env.NAME }} image: ${{ env.NAME }} tags: latest containerfiles: ${{ env.NAME }}/Containerfile layers: false oci: true # Publication de l'image de conteneur dans le regsitre de GitHub avec podman - name: Push to Container Registry uses: redhat-actions/push-to-registry@v2 id: push with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} image: ${{ env.NAME }} tags: latestVous devez obtenir une organisation comme suit :
$ tree -a . ├── conteneur │ └── Containerfile ├── .git │ └── ... └── .github └── workflows └── conteneur.ymlCommiter et poussez vos changements dans votre dépôt distant.
Observer la construction de votre image dans l'onglet Actions.
Vérifier que votre image est bien construite et disponible dans le registre de conteneur de GitHub à l'aide de l'interface Web.
Tester votre image :
$ podman run --rm -ti ghcr.io/<utilisateur GitHub>/conteneur # ls / ... insa-cvl ...
Sigstore et Cosign
Installer cosign. Suivre la documentation en ligne: https://docs.sigstore.dev/system_config/installation/
Générer une clé pivée et publique pour signer les conteneurs avec cosign
$ cosign generate-key-pairConnectez vous au registre de conteneurs GitHub de votre dépôt :
$ podman login ghcr.io/<utilisateur GitHub>Si vous utilisez l'authentification multi-facteurs, suivez la documentation GitHub pour créer un access token à utiliser comme mot de passe dans la commande précédente.
Si nécessaire, copiez les token d'authentification de podman à l'emplacement de ceux de Docker :
$ cp ${XDG_RUNTIME_DIR}/containers/auth.json ~/.docker/config.jsonSigner votre image avec cosign :
$ cosign sign --key cosign.key ghcr.io/<utilisateur GitHub>/conteneurVérifier la signature ajoutée à l'image avec cosign :
$ cosign verify --key cosign.pub ghcr.io/<utilisateur GitHub>/conteneur
Signatures avec cosign dans le workflow GitHub
Nous allons ensuite automatiser cette procédure à l'aide du workflow GitHub :
Ajouter la clé privée
cosign.keycomme secret dans GitHub :- Aller dans les préférences du dépôt, puis "Secrets et variables", puis "Actions"
- Ajouter la clé privée dans les secrets associés au dépôt
- Nommer le secret
COSIGN_PRIVATE_KEY
Ajouter la signature du conteneur à l'aide de cosign dans le workflow GitHub, à la suite du contenu existant :
... # Authentification au registre de conteneur de GitHub - name: Login to Container Registry uses: redhat-actions/podman-login@v1 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Installation de cosign - uses: sigstore/cosign-installer@v3.3.0 # Signature de l'image de conteneur - name: Sign container image run: | cosign sign -y --key env://COSIGN_PRIVATE_KEY ${{ env.REGISTRY }}/${{ env.NAME }}@${{ steps.push.outputs.digest }} env: COSIGN_EXPERIMENTAL: false COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}Attendre qu'une nouvelle version de l'image soit publiée dans le registre de conteneur
Vérifier la signature de la nouvelle image à l'aide de cosign :
$ cosign verify --key cosign.pub ghcr.io/<utilisateur GitHub>/conteneur
Configurer Podman pour vérifier les signatures
Nous allons configurer podman sur notre système pour qu'il vérifie la signature des images de conteneurs et qu'il refuse les images non signées.
Installer la clé publique :
$ sudo mkdir -p /etc/pki/containers $ sudo cp cosign.pub /etc/pki/containers/ $ sudo restorecon -RFv /etc/pki/containersAjouter la configuration du registre pour récupérer les signatures :
$ cat /etc/containers/registries.d/ghcr.io-<utilisateur GitHub>.yamldocker: ghcr.io/<utilisateur GitHub>: use-sigstore-attachments: true$ sudo restorecon -RFv /etc/containers/registries.d/ghcr.io-<utilisateur GitHub>.yamlConfigurer ensuite la politique utilisée par podman. Modifier le fichier
/etc/containers/policy.jsonpour obtenir la configuration suivante. Les...signifient de laisser les valeurs qui se trouvent déjà dans le fichier à ces emplacements :{ "default": [ { "type": "reject" } ], "transports": { "docker": { "ghcr.io/<utilisateur GitHub>": [ { "type": "sigstoreSigned", "keyPath": "/etc/pki/containers/ghcr.io-<utilisateur GitHub>.pub", "signedIdentity": { "type": "matchRepository" } } ], ... "": [ { "type": "insecureAcceptAnything" } ] }, ... } }La page de man qui explique les options de configuration utilisées : containers-policy.json(5)
Vérifier que seul les conteneurs signés sont acceptés :
$ podman rmi --force ghcr.io/<utilisateur GitHub>/conteneur $ podman pull ghcr.io/<utilisateur GitHub>/conteneur $ podman run --rm -ti ghcr.io/<utilisateur GitHub>/conteneurAjouter une copie du workflow initial pour créer un autre conteneur non signé dans le même repo mais avec un autre tag et vérifier qu'il n'est pas accepté par podman
Références
- Exemple complet avec support multi-architectures:
Conteneurs rootless avec podman
Objectifs
- Découvrir la différence entre les conteneur rootfull et rootless avec podman.
Conteneurs rootless avec podman
A l'INSA-CVL, se connecter à la machine fedora-containers.
Reproduisez les commandes des TDs précédents sans être root et sans utiliser sudo :
$ podman pull fedora
$ podman images
$ podman run -it fedora bash
$ podman run -it --rm --privileged --volume /:/host fedora bash
$ podman build --tag httpd .
$ podman run --publish 80 httpd
Question 1 : Détaillez les différences constatées.
Références
- Rootless containers using Podman
- Understanding root inside and outside a container, Scott McCarty, Red Hat Blog
- The shortcomings of rootless containers
- How containers use PID namespaces to provide process isolation
Découvrir Kubernetes
Objectifs
- Découvrir Kubernetes et l'orchestration de conteneurs
Obtenir un cluster Kubernetes avec minikube
Nous allons utiliser minikube pour obtenir un cluster en local pour réaliser ce TD.
Récupérer le binaire minikube (documentation minikube):
$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
$ sudo install -o root -g root -m 0755 minikube-linux-amd64 /usr/local/bin/minikube
Configurer l'utilisation du driver kvm2 avec libvirt et l'usage de cri-o :
$ minikube config set driver kvm2
$ minikube config set container-runtime cri-o
Récupérer l'image minikube mise à disposition en TD :
$ cp /mnt/nfs/DATASTORE/RAVIER/minikube-v1.37.0-amd64.iso* ./
$ sha256sum --check minikube-v1.37.0-amd64.iso.sha256sum
Démarer la machine virtuelle avec l'image fournie :
$ minikube start --iso-url=file://$(pwd)/minikube-v1.37.0-amd64.iso
Installer kubectl :
$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256"
$ echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
$ sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
Activer la completion (avec <TAB>) des commandes kubectl :
$ echo 'source <(kubectl completion bash)' >>~/.bashrc
$ source ~/.bashrc
Vérifier que le noeud minikube est bien fonctionel :
$ kubectl get nodes
Intéragir avec le cluster
Pour intéragir avec l'API exposée par le cluster Kubernetes, nous allons utiliser kubectl.
Le commandes kubectl suivent le format suivant :
$ kubectl <action> <ressource>/(<nom-de-la-ressource)
# ou
$ kubectl <action> <ressource> (<nom-de-la-ressource)
Par exemple pour obtenir la liste des noeuds disponibles dans notre cluster Kubernetes :
$ kubectl get nodes
Pour obtenir plus d'informations, vous pouvez rajouter l'option -o wide :
$ kubectl get nodes -o wide
Pour vérifier la version de kubectl et du cluster :
$ kubectl version --output=yaml
Vous pouvez vous connecter au noeud de notre cluster (notre machine virtuelle) :
$ minikube ssh
Et constater que nous avons les services minimum nécessaires au fonctionnement de Kubernetes installés et lancés :
$ systemctl -t service | grep -E 'crio|kubelet'
Listez les conteneurs qui forment le control plane de Kubernetes :
$ sudo crictl ps
Déployer une application
Pour déployer une application sur un cluster Kubernetes, nous utilisons un déploiement (deployment).
Le déploiement indique à Kubernetes comment créer et mettre à jour des instances d'une application. Nous ne gérons pas les coteneurs directement mais c'est Kubernetes qui se charge de garantir que ce qui tourne sur le cluster correspond à ce que nous lui demandons.
Pour créer un déploiement avec l'interface de ligne de commande kubectl, nous devons préciser un nom et l'image de conteneur qui sera utilisée :
$ kubectl create deployment kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1
Kubernetes va faire les choses suivantes :
- Ecrire dans la base de donnée etcd les informations pour faire tourner notre deploiement
- Le Scheduler va chercher un noeud disponible pour faire tourner notre deploiement et ses pods (nous n'en avons ici qu'un seul)
- Il va alors écrire le ou les noeuds choisis dans la configuration du/des noeuds dans etcd pour programmer le lancement des pods sur ces noeuds
- Les kubelets sur les noeuds vont réaliser qu'il a des nouveaux pods à lancer qui n'existent pas sur le noeud actuellement
Pour lister les déploiements :
$ kubectl get deployments
Pods
Lorsque que nous créons un déploiement, Kubernetes crée un Pod pour regrouper les conteneurs qui forment notre application. Un pod est une abstraction Kubernetes qui représente un groupe de conteneurs qui partagent certains namespaces :
- le stockage partagé (volumes)
- le namespace réseau, avec une adresse IP unique sur tout le cluster
Pour lister les pods déployés :
$ kubectl get pods
Pour obtenir plus d'informations sur les pods et les conteneurs en cours d'exécution :
$ kubectl describe pods
Logs
Pour obtenir les logs des conteneurs qui constituent notre pod, nous pouvons utiliser :
$ kubectl logs kubernetes-bootcamp-f95c5b745-7td7b
Exécuter des commandes dans les conteneurs
Nous pouvons aussi exécuter directement des commandes à l'intérieur des conteneurs :
$ kubectl exec kubernetes-bootcamp-f95c5b745-7td7b -- env
Ou obtenir un shell :
$ kubectl exec -ti kubernetes-bootcamp-f95c5b745-7td7b -- bash
Exposer une application publiquement
Les conteneurs lancés dans un cluster Kubernetes ne sont pas accessibles par défaut depuis l'extérieur. Seules les autres applications du cluster peuvent communiquer avec eux.
De plus, les pods sont ephemères et peuvent être déplacés entre les noeuds du cluster en cas de crash, mise à jour ou redémarrage. Pour y accéder depuis l'extérieur du cluster, nous allons utiliser le concept de Service. Pour lister les Services sur le cluster :
$ kubectl get services
Nous allons ensuite exposer notre application publiquement à l'aide d'un service de type "NodePort", qui va mettre à disposition notre application sur un port d'un noeud du cluster :
$ kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 8080
Nous pouvons obtenir le port sur lequel notre application a été rendu disponible :
$ kubectl get services
$ kubectl describe services/kubernetes-bootcamp
...
NodePort: <unset> 30542/TCP
...
Et ensuite contacter notre application :
# Obtenir l'IP de la machine minikube
$ minikube ip
192.168.39.80
$ curl http://192.168.39.80:30542
Passage à l'échelle
Si notre application gère beaucoup de requettes, il peut devenir nécessaire d'en déployer plusieurs copies pour répartir la charge sur les noeuds du cluster. Pour cela, nous allons utiliser les ReplicaSet. Lorsque nous avons créé notre déploiement, un ReplicaSet a aussi été automatiquement créé :
$ kubectl get replicasets
Par défaut, un seul réplicat est demandé et un seul est en cours d'exécution.
Nous allons désormais en demander 4 :
$ kubectl scale deployments/kubernetes-bootcamp --replicas=4
Constater que le changement a été effectué avec :
$ kubectl get deployments
et constater la création des nouveaux pods avec :
$ kubectl get pods -o wide
Noter que chaque pod a sa propre adresse IP.
Ce changement a aussi été enregistré dans le log des évènements :
$ kubectl describe deployments/kubernetes-bootcamp
Lorsque nous contactons notre application via notre service, nous sommes désormais redirigé vers différentes instances de notre application :
# Répétez la requête jusqu'à obtenir une reponse d'un pod différent
$ curl http://192.168.39.80:30542
Nous pouvons ensuite réduire le nombre de conteneurs déployés :
$ kubectl scale deployments/kubernetes-bootcamp --replicas=2
$ kubectl get deployments
$ kubectl get pods
Déploiment progressif d'une mise à jour
Pour pouvoir déployer une mise à jour de notre application sans créer d'interruption de service, nous allons réaliser une mise à jour progressive (rolling update).
Nous allons indiquer à Kubernetes que nous voulons désormais utiliser une nouvelle version de notre application et celui-ci va se charger de déployer de nouveaux pods avec cette nouvelles version pour progressivement remplacer les anciens pods.
Listons d'abord les pods en cours d'exécution :
$ kubectl get pods
Indiquons que nous voulons désormais utiliser la version 2 de notre application :
$ kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=jocatalin/kubernetes-bootcamp:v2
Constater le remplacement progressif des pods :
$ kubectl get pods
Constater aussi que notre application reste toujours disponible au cours de la mise à jour :
$ curl http://192.168.39.80:31436
Les réponses arrivent progressivement depuis les nouveaux pods.
Nous pouvons aussi valider la progression du déploiment avec :
$ kubectl rollout status deployments/kubernetes-bootcamp
Essayons désormais de réaliser une autre mise à jour avec la version 10 de notre application :
$ kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=gcr.io/google-samples/kubernetes-bootcamp:v10
Observer la (non) progression :
$ kubectl get pods
Pour obtenir plus d'informations sur l'erreur :
$ kubectl describe pods
Et pour annuler le déploiement de la mise à jour :
$ kubectl rollout undo deployments/kubernetes-bootcamp
Références
- Ce TD est fortement inspiré du tutoriel Learn Kubernetes Basics :
- Ensemble des tutoriels de Kubernetes :
- Tutoriels alternatifs :
- Sur GCP : Suivre le tutoriel intégré à la plateforme (Quickstart / Démarrage rapide)
- Sur Azure : Tutorial - Deploy an Azure Kubernetes Service (AKS) cluster (Azure Kubernetes Service (AKS))
- Sur AWS : Getting started with Amazon EKS
- Apprendre Kubernetes à l'aide d'exemples : Kubernetes By Example
- Site de minikube
- Install and Set Up kubectl on Linux
- Kubernetes The Hard Way, Kelsey Hightower
- Découvrez le fonctionnement d'
etcd: http://play.etcd.io/play
Kubernetes et sécurité
Objectifs
- Découvrir comment configurer Kubernetes pour garantir certaines propriétés de sécurité
Kubernetes avec minikube
Suivez la section Kubernetes avec minikube pour obtenir un cluster avec minikube. Si vous enchainez depuis le TD Kubernetes, supprimez votre machine minikube pour repartir d'un cluster vide:
$ minikube delete
$ minikube start --iso-url=file://$(pwd)/minikube-v1.37.0-amd64.iso
Obliger les conteneurs à tourner avec des permissions restreintes
Pour séparer logiquement les déploiements dans notre cluster nous allons utiliser le concept de namespace dans Kubernetes. Attention, ces namespaces sont un concept distinct des namespaces du noyau Linux qui sont utilisés pour créer les conteneurs.
Nous allons utiliser les Pod Security Standards pour contraindre les conteneurs d'un namespace Kubernetes à tourner avec des permissions restreintes (non root, pas d'élévations de privilèges, etc.).
Pour cela, nous allons créer un namespace et lui ajouter un label pour appliquer une politique restrictive. Pour avoir plus de contrôle sur les paramètres utilisés pour créer les objets Kubernetes, nous allons utiliser des descriptions au format YAML que nous allons passer à kubectl via l'entrée standard (stdin ou -) :
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: insacvl
labels:
pod-security.kubernetes.io/enforce: restricted
EOF
Vous pouvez retrouver l'intégralité des options possibles dans les pages de référence de la documentation Kubernetes (exemple pour les namespaces).
Vérifier que le namespace a bien été créé :
$ kubectl describe namespaces insacvl
Essayons maintenant de créer un déploiement qui contient un conteneur tournant en root par défaut dans ce namespace :
$ kubectl apply --namespace insacvl -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
EOF
Noter que nous obtenons un warning mais que Kubernetes accepte la création du déploiement :
Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
deployment.apps/nginx-deployment created
Notre déploiement est créé mais n'est pas prêt et nous n'avons pas de pods :
$ kubectl get deployments --namespace insacvl
$ kubectl get pods --namespace insacvl
Question 1 : Retrouver la raison de l'échec du déploiement :
$ kubectl get deployments --namespace insacvl -o json
Nous allons donc mettre à jour notre déploiement pour appliquer les contraintes demandées :
$ kubectl apply --namespace insacvl -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
- containerPort: 80
EOF
Le status de notre déploiement est désormais "en cours" (Progressing) :
$ kubectl get deployments --namespace insacvl -o json
Mais notre pod n'est toujours pas démarré :
$ kubectl get pods --namespace insacvl
Question 2 : Retrouver la raison de l'échec de la création du pod :
$ kubectl get pods --namespace insacvl nginx-deployment-86d8f64d7-tw8lm -o json
Nous allons donc à nouveau mettre à jour notre déploiement pour appliquer les contraintes demandées :
$ kubectl apply --namespace insacvl -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 8080
EOF
Kubernetes essaie désormais de créer le conteneur mais celui-ci ne fonctionne pas (CrashLoopBackOff) parce qu'il n'est pas prévu pour s'éxécuter sous un utilisateur non-privilégié :
$ kubectl get pods --namespace insacvl
$ kubectl get pods --namespace insacvl nginx-deployment-68dccfb96c-6mwmf -o json
Remplacer l'image du conteneur par une image conçue pour tourner sous un utilisateur non-privilégié :
$ kubectl apply --namespace insacvl -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: quay.io/fedora/nginx-124:latest
command: ["nginx", "-g", "daemon off;"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 80
EOF
Il n'est plus nécessaire de préciser un utilisateur puisque l'image tourne sous un utilisateur non root par défaut. Voir la documentation pour plus de détails.
Notre conteneur fonctionne désormais et notre déploiement est prêt :
$ kubectl get pods --namespace insacvl
$ kubectl get deployments --namespace insacvl
Installation de Kubewarden
Nous allons utiliser le projet Kubewarden pour mettre en place des politiques de sécurité sur notre cluster Kubernetes de façon automatique et plus avancées que les politiques inclues par défaut dans Kubernetes.
La première étape pour installer Kubewarden est d'installer certmanager, un gestionnaire de certificats X.509 pour Kubernetes et OpenShift :
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml
$ kubectl wait --for=condition=Available deployment --timeout=2m -n cert-manager --all
$ kubectl get pods --namespace cert-manager
Ensuite, nous allons installer Helm, qui est un "gestionaire de paquets" pour Kubernetes :
$ curl -LO https://get.helm.sh/helm-v3.14.0-linux-amd64.tar.gz
$ echo "f43e1c3387de24547506ab05d24e5309c0ce0b228c23bd8aa64e9ec4b8206651 helm-v3.14.0-linux-amd64.tar.gz" | sha256sum --check
$ tar xf helm-v3.14.0-linux-amd64.tar.gz
$ sudo install -o root -g root -m 0755 linux-amd64/helm /usr/local/bin/helm
Et nous pouvons ensuite poursuivre l'installation de Kuberwarden :
$ helm repo add kubewarden https://charts.kubewarden.io
$ helm repo update kubewarden
$ helm install --wait -n kubewarden --create-namespace kubewarden-crds kubewarden/kubewarden-crds
$ helm install --wait -n kubewarden kubewarden-controller kubewarden/kubewarden-controller
$ helm install --wait -n kubewarden kubewarden-defaults kubewarden/kubewarden-defaults
Obliger tous les namespaces à avoir les annotations de sécurité
Nous allons désormais appliquer une politique de sécurité sur notre cluster à l'aide de Kuberwarden pour automatiser la mise en place de ces contraintes de sécurité sur les namespaces nouvellement créés ou mis à jour.
Nous allons créer une ClusterAdmissionPolicy qui s'appliquera à tout le cluster (sauf certains namespaces explicitement exclus).
Cette politique (psa-label-enforcer) obligera tous les namespaces à avoir le label pod-security.kubernetes.io/enforce: restricted pour appliquer les restrictions de sécurité aux conteneurs :
$ kubectl apply -f - <<EOF
apiVersion: policies.kubewarden.io/v1
kind: ClusterAdmissionPolicy
metadata:
name: pod-security-admission-enforcer-default-mode
spec:
module: registry://ghcr.io/kubewarden/policies/psa-label-enforcer:v0.1.1
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["namespaces"]
operations:
- CREATE
- UPDATE
mutating: true
namespaceSelector:
matchExpressions:
- key: "kubernetes.io/metadata.name"
operator: NotIn
values: ["kube-system", "kube-public", "kube-node-lease"]
settings:
modes:
enforce: "restricted"
EOF
Une fois la politique crée, nous devons attendre qu'elle soit déployée sur le cluster :
$ kubectl get clusteradmissionpolicies.policies.kubewarden.io pod-security-admission-enforcer-default-mode
$ kubectl get clusteradmissionpolicies.policies.kubewarden.io pod-security-admission-enforcer-default-mode -o json
Pour vérifier que cette politique fonctionne comme prévu, nous allons créer un namespace et y créer des déploiements avec des conteneurs qui tournent en root par défaut comme dans la section précédente :
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: insacvl-kubewarden
EOF
Noter que nous n'avons pas spécifié le label pod-security.kubernetes.io/enforce: restricted mais qu'il a été automatiquement ajouté par Kuberwarden à notre namespace à la création :
$ kubectl get namespace insacvl-kubewarden -o yaml
Essayons maintenant de deployer un conteneur que l'on fait tourner en root:
$ kubectl apply --namespace insacvl-kubewarden -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 80
EOF
Nous pouvons alors constater que le pods n'est pas démarré :
$ kubectl get deployments --namespace insacvl-kubewarden
$ kubectl get pods --namespace insacvl-kubewarden
$ kubectl get pods --namespace insacvl-kubewarden nginx-deployment-86d8f64d7-stzpm -o json
Mettons à jour notre déploiement pour utiliser une image durcie :
$ kubectl apply --namespace insacvl-kubewarden -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: quay.io/fedora/nginx-124:latest
command: ["nginx", "-g", "daemon off;"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 8080
EOF
Vérifier que le conteneur est désormais fonctionnel :
$ kubectl get deployments --namespace insacvl-kubewarden
$ kubectl get pods --namespace insacvl-kubewarden
$ kubectl get pods --namespace insacvl-kubewarden nginx-deployment-86d8f64d7-stzpm -o json
Autoriser uniquement les conteneurs signés
Note : Si vous n'avez pas encore réalisé le TD CI/CD avec GitHub et signatures avec cosign, commencez par celui-ci avant de poursuivre ici.
Nous allons ensuite appliquer une politique de sécurité sur notre cluster à l'aide de Kuberwarden pour valider les signatures des images de conteneurs qui serons utilisées pour lancer les conteneurs.
Nous allons créer une autre ClusterAdmissionPolicy qui s'appliquera à tout le cluster (sauf certains namespaces explicitement exclus).
Cette politique (verify-image-signatures) obligera les images de conteneur qui matchent l'expression quay.io/travier/* à avoir une signature cosign valide :
$ kubectl apply -f - <<EOF
apiVersion: policies.kubewarden.io/v1
kind: ClusterAdmissionPolicy
metadata:
name: verify-image-signatures
spec:
module: registry://ghcr.io/kubewarden/policies/verify-image-signatures:v0.1.7
rules:
- apiGroups: ["", "apps", "batch"]
apiVersions: ["v1"]
resources: ["pods", "deployments", "statefulsets", "replicationcontrollers", "jobs", "cronjobs"]
operations:
- CREATE
- UPDATE
mutating: true
namespaceSelector:
matchExpressions:
- key: "kubernetes.io/metadata.name"
operator: NotIn
values: ["kube-system", "kube-public", "kube-node-lease"]
settings:
signatures:
- image: "quay.io/travier/*"
pubKeys:
- |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEviYlbu3VB0KQ1h97SDbQqndpM04q
Yi/UcUSxoB4ho8gfnw0b61REAtgleIDExQ3+PqEZoXB+nQ+NeAbSDwkHYA==
-----END PUBLIC KEY-----
EOF
Attendre que la politique soit déployée sur le cluster :
$ kubectl get clusteradmissionpolicies.policies.kubewarden.io verify-image-signatures
$ kubectl get clusteradmissionpolicies.policies.kubewarden.io verify-image-signatures -o json
Créer un nouveau namespace:
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: insacvl-cosign
EOF
Créer un déploiement qui utilise une image de conteneur non signée :
$ kubectl apply --namespace insacvl-cosign -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: image-signature-test
labels:
app: cosign-test
spec:
replicas: 1
selector:
matchLabels:
app: template-cosign-test
template:
metadata:
labels:
app: template-cosign-test
spec:
containers:
- name: cosign
image: quay.io/travier/cosign-example:unsigned
command: ["sleep", "10000"]
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
EOF
Vous devriez obtrenir une erreur :
Error from server: error when creating "STDIN": admission webhook "clusterwide-verify-image-signatures.kubewarden.admission" denied the request: Resource image-signature-test is not accepted: verification of image quay.io/travier/cosign-example:unsigned failed: Host error: Callback evaluation failure: no signatures found for image: quay.io/travier/cosign-example:unsigned
Aucun déploiement ou pod n'a été créé :
$ kubectl get deployments --namespace insacvl-cosign
$ kubectl get pods --namespace insacvl-cosign
Modifier notre déploiement pour utiliser une image de conteneur signée :
$ kubectl apply --namespace insacvl-cosign -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: image-signature-test
labels:
app: cosign-test
spec:
replicas: 1
selector:
matchLabels:
app: template-cosign-test
template:
metadata:
labels:
app: template-cosign-test
spec:
containers:
- name: cosign
image: quay.io/travier/cosign-example:latest-cosign
command: ["sleep", "10000"]
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
EOF
Vérifier que notre conteneur est bien lancé :
$ kubectl get deployments --namespace insacvl-cosign
$ kubectl get pods --namespace insacvl-cosign
Lancer une autre image de conteneur :
$ kubectl apply --namespace insacvl-cosign -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: quay.io/fedora/nginx-124:latest
command: ["nginx", "-g", "daemon off;"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 8080
EOF
Question 3 : Que constatez vous ?
Question 4 : Comment pourrions nous modifier la politique pour corriger ce problème ?
Modifiez ensuite le déploiement et la configuration de la politique pour utiliser votre propre image de conteneur signée que vous avez créée dans le TD 4.3.
Références
Kubernetes :
Kubewarden :
CRI-O :
Images de conteneurs durcies : Projet Software Collections
Exemples d'images de conteneurs signées : github.com/travier/cosign-test
Fedora CoreOS
Objectifs
- Découvrir Fedora CoreOS, un système conçu pour les conteneurs
Fedora CoreOS
Découvrez le fonctionnement d'un système conçu pour les conteneurs avec le tutoriel de Fedora CoreOS.
Par défaut, le tutoriel utilise libvirt avec QEMU/KVM (sous Linux uniquement). Pour le réaliser sur une autre platforme :
- Provisioning Fedora CoreOS on Google Cloud Platform
- Provisioning Fedora CoreOS on Azure
- Provisioning Fedora CoreOS on Amazon Web Services
- Provisioning Fedora CoreOS on VirtualBox
Exemples supplémentaires
Voici d'autres exemples, dont certains utilise les Quadlets pour déployer un ensemble de conteneurs :
- Quadlets might make me finally stop using docker-compose
- Deploying a multi-container application using Podman and Quadlet
- Deploy a Matrix homeserver on Fedora CoreOS (nécessite un nom de domaine)
- Obsolete, mais pour l'exemple: Deploy Hashicorp Nomad on Fedora CoreOS
OpenShift / OKD
Objectifs
- Découvrir OpenShift / OKD et les propriétés de sécurité pré-configurées
Obtenir un environement OKD en local
OKD est la distribution soeur d'OpenShift, disponible gratuitement, basée sur Fedora CoreOS / CentOS Stream CoreOS et supportée par la communauté.
Suivre les instructions des liens suivants pour obtenir un cluster OKD en local :
Fonctionnalités de sécurité d'OpenShift / OKD
OpenShift est configuré par défaut avec un ensemble de restrictions et de fonctionnalités de sécurité (voir la documentation, l'article dédié et l'ebook dédié). Ces restrictions améliorent la sécurité de la plateforme mais elles nécessitent parfois des changements dans les applications et conteneurs publiés sur le DockerHub qui ne suivraient pas les recommandations en terme de sécurité.
L'une de ces fonctionnalité est de lancer systématiquement les conteneurs sous un utilisateur différent de root. De nombreuses images ne sont pas capables de gérer ce mode de fonctionnement correctement par défaut (c.f. TD précédent dans lequel vous deviez créer une image capable de fonctionner en non-root).
Par exemple, l'image de conteneur officielle de PostgreSQL ne fonctionne pas par défaut. Voici des examples de deployment pour vérifier ces éléments :
apiVersion: apps/v1
kind: Deployment
metadata:
name: pgsql-deployment
labels:
app: pgsql
spec:
replicas: 3
selector:
matchLabels:
app: pgsql
template:
metadata:
labels:
app: pgsql
spec:
containers:
- name: pgsql
image: postgres:12
env:
- name: POSTGRES_PASSWORD
value: "azerty"
ports:
- containerPort: 80
Red Hat fournit une image de conteneur avec PostgreSQL qui a été adaptée pour s'assurer que l'application ne tourne jamais en root, même si vous ne l'utilisez pas sur OpenShift :
apiVersion: apps/v1
kind: Deployment
metadata:
name: pgsqlrh-deployment
labels:
app: pgsqlrh
spec:
replicas: 3
selector:
matchLabels:
app: pgsqlrh
template:
metadata:
labels:
app: pgsqlrh
spec:
containers:
- name: pgsqlrh
image: registry.redhat.io/rhel8/postgresql-12
env:
- name: POSTGRESQL_ADMIN_PASSWORD
value: "azerty"
ports:
- containerPort: 80
La documentation liste les instructions pour créer des images de conteneurs propres et plus particulièrement les spécificités d'OpenShift. Faire cet effort n'est pas vain même si vous utilisez un cluster Kubernetes classique (non-OpenShift) puisque ne pas faire tourner en root les applications dans les conteneurs est l'un des principe de sécurité de base pour un cluster.
Références
- Openshift
- OpenShift Local
- Try Red Hat OpenShift
- OKD
- Red Hat OpenShift security guide (2021)
- A layered approach to container and Kubernetes security (2021)
- Images de conteneurs durcies : Projet Software Collections
Références et sources recommandées
Pages de manuels
Attention: Ne pas faire confiance aux pages de man de linux.die.net.
Documentation des projets
- Linux Kernel
- systemd:
- Kubernetes
Documentation des distributions
Debian / Ubuntu
- apt Cheat Sheet
- Ubuntu/Debian Linux apt-get package management cheat sheet
- dpkg command Debian/Ubuntu Linux cheat sheet
Fedora
- Documentation Fedora
- Fedora Quick Docs
- Documentation Fedora CoreOS
- dnf Cheat Sheet
- rpm command cheat sheet for Linux
Red Hat
Arch Linux & Gentoo
Licence / License
Ce contenu de Timothée Ravier est publié sous la licence Creative Commons Attribution-ShareAlike 4.0 International License.
This work by Timothée Ravier is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
