Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 :

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 :

  1. Ajoutez le flag pour créer un processus fils dans un PID namespace distinct
  2. Faites appel à chroot(2) et chdir(2) pour utiliser le shell du rootfs obtenu avec debootstrap
  3. Montez /proc dans le rootfs pour obtenir les bonnes informations sur les processus
  4. Validez le résultat avec les commandes :
    • ps aux
    • ls -al /proc/self/ns
    • lsns

Namespace réseau

Nous allons ensuite ajouter le support du namespace réseau.

  1. Installer le paquet iproute2 pour obtenir la command ip dans le chroot.

    Indices

    Pour cela vous pouvez utiliser chroot rootfs /bin/bash pour obtenir un shell puis installer le paquet avec apt update puis apt install iproute2.

  2. Une fois le processus fils lancé dans le nouveau namespace, créer une paire d'interfaces réseau virtuelles veth à l'aide de la command ip suivante 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().

  3. 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.

  4. Configurer l'interface réseau à l'aide de ip dans le namespace du père et du fils.

  5. 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

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>

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

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

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 SystemCallFilter dans systemd.exec(5)

Question 5 : Vérifiez que toutes les options fonctionnent correctement.

Références

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).
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

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

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 :

Références

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 Containerfile pour créer une image de conteneur :

    conteneur/Containerfile :

    FROM registry.fedoraproject.org/fedora:39
    
    RUN touch /insa-cvl
    
  • Ajouter 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: latest
    
  • Vous devez obtenir une organisation comme suit :

    $ tree -a
    .
    ├── conteneur
    │   └── Containerfile
    ├── .git
    │   └── ...
    └── .github
        └── workflows
            └── conteneur.yml
    
  • Commiter 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-pair
    
  • Connectez 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.json
    
  • Signer votre image avec cosign :

    $ cosign sign --key cosign.key ghcr.io/<utilisateur GitHub>/conteneur
    
  • Vé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.key comme 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/containers
    
  • Ajouter la configuration du registre pour récupérer les signatures :

    $ cat /etc/containers/registries.d/ghcr.io-<utilisateur GitHub>.yaml
    
    docker:
      ghcr.io/<utilisateur GitHub>:
        use-sigstore-attachments: true
    
    $ sudo restorecon -RFv /etc/containers/registries.d/ghcr.io-<utilisateur GitHub>.yaml
    
  • Configurer ensuite la politique utilisée par podman. Modifier le fichier /etc/containers/policy.json pour 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>/conteneur
    
  • Ajouter 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

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

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

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

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 :

Exemples supplémentaires

Voici d'autres exemples, dont certains utilise les Quadlets pour déployer un ensemble de conteneurs :

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

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

Documentation des distributions

Debian / Ubuntu

Fedora

Red Hat

Arch Linux & Gentoo

Licence / License

Creative Commons 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.