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

Mon héritage

Ce livre est l'héritage de mes connaissances, en fonction du temps que j'ai pour l'écrire, et ce que je pense à écrire.

Mon expérience

Même si j'ai connu MS-DOS, Windows 3.1, Windows 95, ce n'est qu'à la découverte de GNU/Linux que mon aventure informatique démarre rééllement.

Et même si j'ai testé beaucoup de distros, il est possible de se contenter de cette timeline grossière:

AnnéeDistro/OS
1997Slackware
1998Redhat
2000Debian
2005Gentoo
2008Archlinux
2024NixOS

Solutions à des problèmes

Hardening

Docker

Filesystem

Empêcher une application de saturer le disque système, par exemple via l'écriture de logs.

Services

Restreindre les droits d'un service

Mon environnement

En tant que dinosaure, je suis encore principalement avec des outils qui s'executent dans un terminal.

Cela correspond à ma philosophie :

  • Si c'est simple, et fait le boulot, ça suffit.
  • Les interfaces graphiques coopèrent mal ensemble.
  • C'est moins gourmand en ressources.
  • Je suis un gros nostalgique d'une époque maintenant phantasmée: les années 80/90.

Ma stack

Globalement, mes outils necessaires sont dans mon dépôt gfriloux/nix-cli.

Je vais lister ceux qui pour moi offrent une synergie interessante:

OutilDescription
fishShell en rust, qui à l'usage m'est beaucoup plus utile que bash.
atuinHistorique shell tellement plus moderne que history.
batAlternative à cat. J'ai un alias batcat.
btopAlternative à top/htop.
deltaAlternative à diff.
fzfFuzzy Finder dont les usages sont tellements nombreux...
gitflow-toolkitOutil d'aide au formattage des messages de commit.
git-workspaceSync les dépôt git sur gitlab/github.
glowLecteur Markdown.
gumPermet de créér des interfaces dans le terminal.
justAlternative à make.
lsdAlternative à lsd. J'ai un alias lsdls.
microÉditeur texte léger, en alternative à nano.
ncduAlternative à du.
oh-my-poshPrompt customisable, avec différents thêmes disponibles sur le site.
prettypingAlternative à prettyping. J'ai un alias prettypingping.
pwgenGénérateur de mots de passe.
rsyncPour les transferts de fichiers.
sshtuiInterface pour les configs ssh en utilisant tv.
tvAlternative à fzf.
xcpAlternative à cp. J'ai un alias xcpcp.
zellijAlternative à screen.

ZFS

ZFS est un système de fichier incroyable, qui résoud un ensemble de problèmes "du quotidien" de manière élégante et fiable.

Ce système de fichier a tellement bousculé l'univers des systèmes de fichiers qu'il a créé une concurrence, qui du coté du monde linux s'appelle Btrfs (mais qui n'est pas au niveau).

Ces dernières années, j'ai eu de nombreuses occasions de sortir ce fabuleux gif, suite à des incidents chronophages qui n'auraient pas existés, ou bien auraient étés résolus bien plus rapidement, si nous avions utilisé ZFS:

On a pas utilisé ZFS

En effet, lorsque les bras vous en tombent, d'avoir à résoudre manuellement des problèmes basiques, parce que nous n'avons pas pus étudier la mise en place d'autres solutions, il ne vous reste que la création de gifs.

Histoire

ZFS est un système de fichiers initialement développé par Sun Microsystems en 2005.

Il se distingue à l'époque sur plusieurs aspects clairement novateurs:

  1. Résilient aux pannes, nous avons à l'époque des conférences des ingénieurs Sun qui nous montrent qu'ils peuvent percer les disques d'un raid ZFS actuellement en cours d'utilisation, sans impact sur la disponibilité du raid.
  2. Prévu pour le futur avec des limites sur le nombre de fichiers, leur taille maximale, la taille maximale du zpool...
  3. La possibilité de créér des datasets (re)configurables contrairement aux classiques partitions.
  4. La possibilité de créér des snapshots sans coût, instantannés, et une gestion très fine des données entre les snapshots.

Mon expérience

J'ai utilisé ZFS sur du perso dès 2009.
Je l'ai utilisé sur de la prod dès 2012.

J'ai géré jusqu'à ~400 serveurs de prod avec du ZFS on root, ce qui me permet d'être solide sur mon expérience avec ce système de fichier, et donc, de savoir les gains qu'il apporte, les problèmes qu'il permet d'éviter.

À ce jour (2025), mon PC perso est toujours sur ZFSEat your own dog food.

Sources

  1. ZFS - Wikipédia.
  2. GNU/Linux Magazine - article de 2009, qui m'a lancé dans l'aventure ZFS.
  3. ZFS for Dummies

Les cas pratiques

Datasets

Les datasets sur ZFS sont des volumes qui sont créés sur un zpool existant, et qui vont porter un ensemble de propriétées qui leur seront propres ou héritées.

Chaque dataset est le fils d'un dataset ou directement du zpool.

Ce dataset sera mount comme un volume classique (par ZFS).

Les datasets peuvent être créés et détruits à tous moments. Il est donc possible, en cas de nouveau besoin, d'adapter le système de fichiers à celui-ci (par exemple un dataset par stack applicative ou par utilisateur).

Pourquoi c'est pratique

Réservation disque

En configurant une réservation disque à l'avance, vous pourrez vous assurer que cet espace lui soit dès le début attribué, même s'il n'en fait pas l'usage immédiat.

C'est pour simplifier de la planification.

Exemple:

# zfs set reservation=5G zroot/root/home/web-user
# zfs list
NAME                        USED  AVAIL  REFER  MOUNTPOINT
zroot/root/home             5.00G  33.5G  8.50K  /home
zroot/root/home/web-user    15.0K  33.5G  8.50K  /home/web-user

L'espace est donc directement consommé sur zroot/root/home (qui est lui aussi un dataset), même si zroot/root/home/web-user ne contient encore aucunes données.

Sources:

Configurer un quota

Il est possible de définir un usage disque max sur un dataset.
Sela permet d'empêcher une application, un utilisateur, ou un élément système de consommer l'intégralité de l'espace du zpool en cas de défaut.

Le cas le plus classique étant la création de logs qui viennent saturer le système.

Même si journalctl est configurable pour disposer d'une taille max des logs, les applications n'utilisent pas toutes journalctl.

Exemple:

# zfs set quota=10G zroot/root/var/log
# zfs get quota zroot/root/var/log
NAME                PROPERTY  VALUE  SOURCE
zroot/root/var/log  quota     10G    local

Sources:

Compresser les données

Si vous crééz un dataset dont vous savez à l'avance que les données se compressent bien, alors vous pouvez activer.

Il est possible de choisir l'algorithme de compression, ainsi qu'obtenir des stats sur son efficacité.

Il est évident que cela va surtout très bien s'appliquer pour un dataset qui va contenir des logs.

Exemple:

# zfs set compression=on zroot/root/var/log
# zfs get compression zroot/root/var/log
NAME               PROPERTY     VALUE      SOURCE
zroot/root/var/log compression  on         local

Snapshots

Supposons un site web PHP classique avec une db MariaDB sous forme de stack docker compose.
Cette stack serait déclarée dans un dossier /srv/docker/victim.org.
Le nom du dataset sera zroot/srv/docker/victim.org.

Création d'un snapshot

zfs snap zroot/srv/docker/victim.org@$(date +%Y-%m-%d-%H-%M-%S)

Liste des derniers snapshots

$  /s/d/victim.org  zfs list -H -rt snapshot -o name zroot/srv/docker/victim.org | tail -n5                                                                                                                                                                                                                                    4359ms  mar. 08 août 2023 16:14:41
zroot/srv/docker/victim.org@2023-08-04-00-00-08
zroot/srv/docker/victim.org@2023-08-05-00-00-13
zroot/srv/docker/victim.org@2023-08-06-00-00-04
zroot/srv/docker/victim.org@2023-08-07-00-00-04
zroot/srv/docker/victim.org@2023-08-08-00-00-06

Fichiers modifiés depuis le dernier snapshot

$  /s/d/victim.org  just diff                                                                                                                                                                                                                                                         mar. 08 août 2023 16:13:49
zfs diff $(zfs list -H -rt snapshot -o name zroot/srv/docker/victim.org | tail -n1)
M   /srv/docker/victim.org/db/ibdata1
M   /srv/docker/victim.org/db/ib_logfile0
M   /srv/docker/victim.org/db/ib_logfile1
M   /srv/docker/victim.org/db/victim/wp_options.ibd
M   /srv/docker/victim.org/code/wp-content
M   /srv/docker/victim.org/db/ibtmp1
+   /srv/docker/victim.org/backup/1691460000_2023-08-08_victim.sql
M   /srv/docker/victim.org/backup

Pour voir le contenu du snapshot 2023-08-08-00-00-06 de notre stack, nous pouvons aller dedans (read-only):

cd /srv/docker/victim.org/.zfs/snapshots/2023-08-08-00-00-06

Rollback la stack à un snapshot

zfs rollback -r zroot/srv/docker/victim.org@2023-08-08-00-00-06

Exporter la stack

Grâce à zfs send, il est possible d'exporter un snapshot comme bon nous semble.

Si notre stack docker compose ne déclare que des volumes de type bind mount, situés dans le même dossier, alors déplacer toute la stack devient trivial!

zfs send pourra être invoqué ainsi:

zfs send zroot/srv/docker/victim.org@2023-08-08-00-00-06

Exporter en tant que fichier:

zfs send zroot/srv/docker/victim.org@2023-08-08-00-00-06 >/root/backup.victim.org.raw

Exporter via SSH:

zfs send zroot/srv/docker/victim.org@2023-08-08-00-00-06 | ssh $host "zfs recv zroot/srv/docker/victim.org"

Outils

httm

httm permet de faciliter l'utilisation de zfs pour avoir une visualisation de ce qui s'est passé dans le temps, au niveau des datasets.

Parmis les différentes possibilitées, les 2 cas d'usage les plus basiques sont ci dessous.

Trouver les versions d'un fichier

Supposons notre stack /srv/docker/victim.org vue sur la page Snapshots, pour visualiser toutes les modifications de docker-compose.yml dans le temps, nous pouvons faire:

 $  /s/d/victim.org  httm docker-compose.yml
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Sun Nov 02 17:04:30 2025 UTC  2.9 KiB  "/srv/docker/victim.org/.zfs/snapshot/zrepl_20251225_060837_000/docker-compose.yml"
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Tue Dec 30 09:03:29 2025 UTC  2.9 KiB  "/srv/docker/victim.org/docker-compose.yml"
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Et observer que ce fichier a été modifié depuis le dernier snapshot!

Trouver les fichiers supprimés

Exemple avec une stack netdata:

 $  /s/d/_base  httm -n --no-live -d -R . | head -n10
⠠
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000049.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000050.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000051.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000052.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000053.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000054.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000055.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000056.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-10-00-00-05/netdata/cache/dbengine-tier2/datafile-1-0000000057.ndf
/srv/docker/_base/.zfs/snapshot/2023-07-11-00-00-02/netdata/cache/dbengine-tier2/datafile-1-0000000057.ndf

zfs-prune-snapshots

zfs-prune-snapshots permet de gérer le nombre de snapshots à conserver.

zrepl

zrepl permet de répliquer des datasets entre plusieurs serveurs, que ce soit en mode push ou pull.

Même s'il peut être difficile de démarrer avec zrepl (j'ai trouvé la documentation peu claire), le résultat est vraiment convaincant.

Nix

Nix

Nix c'est:

  1. Un langage de programmation.
  2. Un gestionnaire de packages.
  3. Un système de build.

Pour cela, il mobilise plusieurs concepts:

  1. C'est un langage de programmation purement fonctionnel.
  2. Il est déclaratif.
  3. Il est paresseux
  4. Le résultat de son travail est reproductible
  5. Le résultat de son travail est portable

NixOS

NixOS c'est:

  1. Une distribution GNU/Linux initiée en 2006.
  2. Une première version stable en 2013.
  3. Un OS entièrement créé de manière déclarative, et reproductible

NixOS est clairement un OVNI dans le monde GNU/Linux.
Son seul concurrent dans son domaine est GNU/Guix.

Sources

Histoire

Nix est un projet démarré en 2003 par Eelco Dolstra, qui prendra du temps à se mettre en place, car il bouscule tout l'existant.

Mon expérience

Bien que j'ai pris connaissance de NixOS en 2017 via une brève sur LinuxFR.org, mon intérêt était focalisé sur d'autres projets, et j'étais encore bien trop lié à Archlinux (depuis 2008).

J'ai commencé Nix en 2023 via Devbox, qui a été convaincant, ce que m'a motivé a essayer home-manager.

1 an plus tard, je passai mon PC perso sous NixOS et commençait à écrire des flakes.

Il me semble, à ce jour, que c'est un excellent moyen de s'embarquer dans l'aventure Nix, par étapes.

Sources

Devbox

Devbox permet de créér des environnements reproductibles entre différentes machines.

Ce projet se distingue des autres par son extrême simplicité:
vous n'avez pas besoin de savoir utiliser nix pour utiliser devbox.

Il s'agit d'une surcouche à nix et aux flakes, afin de rendre le tout immédiatement utilisable pour créér des environnements de travail sur des projets!

Exemple Ansible

En local sous archlinux

Depuis un PC sous Archlinux avec nix + Devbox, j'ai ansible qui a été installé depuis les packages archlinux:

 kuri@Nomad  ansible  16:33  which ansible
/usr/sbin/ansible
 kuri@Nomad  ansible  16:33  ansible --version
ansible [core 2.20.0]
  config file = None
  configured module search path = ['/home/kuri/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.13/site-packages/ansible
  ansible collection location = /home/kuri/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/sbin/ansible
  python version = 3.13.11 (main, Dec  7 2025, 13:01:45) [GCC 15.2.1 20251112] (/usr/bin/python)
  jinja version = 3.1.6
  pyyaml version = 6.0.3 (with libyaml v0.2.5)

Nous avons ansible + jinja + python ainsi que diverses librairies python afin que notre installation d'ansible soit fonctionnelle.

Pour cela, sous archlinux, j'ai fait pacman -S ansible.

Le problème c'est les autres

Si j'ai 3 collègues, sous les OS suivants:

  1. Windows (WSL)
  2. macOS
  3. Ubuntu

Je vais devoir leur dire de se débrouiller, pour voir comment installer ansible sur leurs machines.

La solution c'est devbox

Ou alors, je passe à la méthode Devbox:

 kuri@Nomad  ansible  16:35  devbox init
 kuri@Nomad  ansible  16:39  devbox add ansible
Info: Adding package "ansible@latest" to devbox.json
 kuri@Nomad  ansible  16:39  devbox shell
Info: Ensuring packages are installed.
✓ Computed the Devbox environment.
Starting a devbox shell...
Linux Nomad 6.17.9-arch1-1 x86_64
 16:40:06  up   8:34,  1 user,  load average: 0,38, 0,24, 0,28
(devbox)  kuri@Nomad  ansible  16:40  ansible --version
ansible [core 2.20.0]
  config file = None
  configured module search path = ['/home/kuri/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /nix/store/06p0i8w8zqrinjwldnypkva8s6ivz0r0-python3.13-ansible-core-2.20.0/lib/python3.13/site-packages/ansible
  ansible collection location = /home/kuri/.ansible/collections:/usr/share/ansible/collections
  executable location = /nix/store/06p0i8w8zqrinjwldnypkva8s6ivz0r0-python3.13-ansible-core-2.20.0/bin/ansible
  python version = 3.13.9 (main, Oct 14 2025, 13:52:31) [GCC 14.3.0] (/nix/store/3lll9y925zz9393sa59h653xik66srjb-python3-3.13.9/bin/python3.13)
  jinja version = 3.1.6
  pyyaml version = 6.0.3 (with libyaml v0.2.5)

Si ce dossier se trouve être un dépôt git, dans lequel je commit les fichiers devbox.{json,lock}, tout personne qui le clone va travailler avec les mêmes versions de packages que ceux définis dans devbox.lock.
Et donc, à moins d'un bug spécifique à l'OS, tout le reste fonctionnera à l'identique (y compris sous ARM).

Installer ansible-core 2.16

Que ce soit via devbox search ansible ou Nixhub, je peux lister d'autres versions d'ansible disponibles.

Dans le cas de gestion de serveurs d'une autre époque (article écrit en décembre 2025), par exemple:

  1. CentOS 7
  2. Amazon Linux 2
  3. Debian 10

La version 2.17 casse le support de ces versions, et nous devons donc utiliser la version 2.16, qui n'est globalement plus packagée.

Dans un monde ancien, nous aurions sous le coude une VM sous une version périmée de Debian (genre la 10), qui pointe sur le dépôt des archives, et qui aurait ansible 2.16 + toutes les deps à la bonne version, et on bichonnerait cette VM comme le saint graal.

Ou, encore pire, on considère que des outils de gestion de config/conformité, c'est mal, car les dernières versions ne supportent plus les dinos (le vrai problème étant qu'on ne sait pas MAJ les serveurs).

Maintenant, réglons ce problème à la manière de Devbox:

 kuri@Nomad  ansible  17:22  devbox rm ansible
 kuri@Nomad  ansible  17:22  devbox add ansible@2.16.5
Info: Adding package "ansible@2.16.5" to devbox.json
Info: Installing the following packages to the nix store: ansible@2.16.5
 kuri@Nomad  ansible  17:23  devbox shell
Info: Ensuring packages are installed.
✓ Computed the Devbox environment.
Starting a devbox shell...
Linux Nomad 6.17.9-arch1-1 x86_64
 17:23:16  up   9:18,  1 user,  load average: 0,95, 0,50, 0,65
(devbox)  kuri@Nomad  ansible  17:23  ansible --version
ansible [core 2.16.5]
  config file = None
  configured module search path = ['/home/kuri/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /nix/store/a5k2lpbxj419kzn2pyz55gql35pbg8xx-python3.12-ansible-core-2.16.5/lib/python3.12/site-packages/ansible
  ansible collection location = /home/kuri/.ansible/collections:/usr/share/ansible/collections
  executable location = /nix/store/a5k2lpbxj419kzn2pyz55gql35pbg8xx-python3.12-ansible-core-2.16.5/bin/ansible
  python version = 3.12.4 (main, Jun  6 2024, 18:26:44) [GCC 13.3.0] (/nix/store/04gg5w1s662l329a8kh9xcwyp0k64v5a-python3-3.12.4/bin/python3.12)
  jinja version = 3.1.4
  libyaml = True

Que je sois sous Archlinux, NixOS, Ubuntu, macOS, Windows, et peu importe les versions, tant que j'ai devbox (et donc nix), j'ai cette reproductibilité sur des milliers de packages, ainsi que les anciennes versions de ces packages, dans des envs isolé et reproductibles!

Sources

flakes

les nix flakes sont un moyen d'écrire des expressions nix dans le but de:

  • Packager un outil.
  • Créér une VM.
  • Installer/Configurer son système.
  • Installer/Configurer un système distant.
  • Créér un environnement de développement.

Bien qu'extrêmement puissant, il s'agit d'un outil fort compliqué à utiliser, car il requiert de savoir écrire dans le langage nix.

C'est pourquoi je ne recommande pas de se lancer dans l'aventure nix en partant directement sur nixos + des flakes pour décrire intégralement son OS et ses environnements de travail.

Il faut savoir s'approprier les outils par étapes, pour ne pas s'en dégouter ou se dévaloriser suite à un effort trop complexe pour être fait en une seule fois.

Devbox est un moyen beaucoup plus rapide pour commencer à créér des environnements de travail, tout en effectuant un premier pas vers nix!

Modules

Il est possible d'écrire des flakes sous forme de modules, qui pourront être utilisés par d'autres flakes, afin de ne pas toujours ré-inventer la roue.

Sources

direnv

direnv est un outil qui se greffe sur votre shell afin de pouvoir automatiquement installer/charger des outils, et executer des commandes, lorsque vous entrez dans un répertoire (cd).

Se reposant sur les gains que permet nix (et surtout les flakes), il permettra de configurer automatiquement des envs de travail, en faisant au maximum abstraction de l'OS que l'on utilise.

Sans avoir à connaitre par avance les spécificitées d'un projet (ses dépendances), il sera possible de commencer à travailler dessus.

Exemples d'usages:

  • Installer les outils rust+cargo+cc pour compiler un projet rust.
  • Installer mdBooks ou mkdocs pour build/visualiser de la documentation.
  • Installer les bonnes versions d'ansible ou terraform pour être compatible avec les contraintes d'une infra.

Sources

home-manager

home-manager est du code nix sous forme de flakes qui vont vous permettre de gérer des environnements utilisateurs.

Classiquement, sous GNU/Linux, vous installez des outils au niveau système, et tous ces outils sont disponibles à tous les utilisateurs.

Avec home-manager, vous allez pouvoir séparer les outils, c'est à dire que vous continuerez d'avoir des outils disponibles au niveau système, mais vous pourrez enrichir les environnements de certains utilisateurs.

l'idée est donc de garder la partie système la plus light possible, et de seulement charger les utilisateurs qui ont besoins d'outils.

Il va aussi permettre de gérer les dotfiles, mais à un niveau inégalé par les différents outils de gestion de dotfiles existants.

Utilisateur GNU/Linux depuis 1997, le sujet des dotfiles d'une machine à l'autre, ou à force de réinstalls, je l'ai clairement poncé.

Absolument rien n'a été plus pratique que home-manager qui hérite des gains de nix, des flakes, et donc va non seulement permettre d'installer des packages en espace utilisateur, mais va créér des services systemd en espace utilisateur, et permettre de générer les dotfiles de façon reproductible grâce à nix!

home-manager est Multi-OS, je m'en suis d'abord servit sous Archlinux, vous pouvez vous en servir sous macOS... Il n'est pas necessaire d'être sous NixOS.

Intégration nix

Lorsque vous serez plus à l'aise avec nix, que vous en serez à gérer des portions du système avec nix, ou bien tout simplement basculez sur NixOS, vous pourrez intégrer vos configs home-manager dans vos flakes.

Ce n'est donc pas une perte de temps de démarrer nix avec uniquement home-manager.

Sources

Les cas pratiques

  • sshtui - Packager un script bash.
  • m365-refresh - Packager un service systemd en python (oneshot).

sshtui

sshtui

sshtui est un petit script qui permet de naviguer dans nos configs openssh afin de trouver le serveur sur lequel on souhaite se connecter.

Ses caractéristiques:

  • Il se base sur tv pour le rendu graphique.
  • Il créé un tv channel pour permettre à tv de savoir comment traiter nos configs openssh.
  • Il dépend des outils suivants:
    • bash
    • bat
    • awk
    • xargs

Ses flakes déclarent un module pour home-manager, permettant de facilement l'intégrer dans nos configurations home-manager.

C'est en fait un excellent projet pour apprendre à faire cela, car il a la simplicité permettant de se focaliser sur comment intégrer dans home-manager un package que l'on créé nous-même.

Afin encore d'éviter de se créér un ensemble de problèmes, nous allons utiliser snowfall-lib qui "standardise" la manière d'écrire nos flakes, afin qu'ils soient bien organisés (ce qui est très bien pour des débutants).

Création de notre module nix

Dans cet exemple, nous crééons le dépôt sshtui sur github.

flake.nix

dans notre flake.nix, la partie importante est celle-ci:

inputs.snowfall-lib.mkFlake {
  inherit inputs;
  systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
  src = ./.;

  snowfall = {
    namespace = "sshtui";
  };
  alias = {
    packages.default = "sshtui";
  };
};

C'est ce qui va permette à snowfall-lib de générer tout le code du flake avec les bons output pour réutiliser ce module nix

package sshtui

Nous pouvons dès maintenant déclarer notre package dans packages/sshtui/default.nix:

{ lib, pkgs, writeShellApplication, ... }:

writeShellApplication {
  name = "sshtui";
  text = (builtins.readFile ../../sshtui);
  runtimeInputs = with pkgs; [
    bash
    television
    bat
    gawk       # awk
    findutils  # xargs
  ];

  meta  = with lib; {
    description = "Simple SSH TUI";
    licence = licences.gpl;
    platforms = platforms.all;
    mainProgram = "sshtui";
  };
}

C'est globalement aussi simple que ça! Nous demandons à nix de build un script shell dont l'emplacement est à la racine du projet : ../../sshtui

Et nous déclarons dans runtimeInputs les packages dont il dépend! C'est tout!

module home-manager

Afin de pouvoir utiliser ce nix module depuis home-manager, nous avons besoin de déclarer programs.sshtui dans modules/home/sshtui/default.nix:

{ config, lib, pkgs, ... }:
let
  cfg = config.programs.sshtui;
in
{
  options.programs.sshtui = {
    enable = lib.mkEnableOption "sshtui";

    package = lib.mkOption {
      type = lib.types.package;
      default = pkgs.sshtui;
      description = "sshtui package to use";
    };
  };

  config = lib.mkIf cfg.enable {
    xdg.configFile."television/cable/sshtui.toml".source = ../../../sshtui.toml;
    home.packages = [
      cfg.package
    ];
  };
}

ainsi, si l'on déclare programs.sshtui.enable=true; dans notre config home-manager, nous aurons:

  • L'installation du script sshtui.
  • L'installation de sshtui.toml qui est notre tv channel pour tv.

overlay sshtui

Maintenant, pour que ce module s'intègre bien, nous allons déclarer un snowfall-lib-overlay afin de pouvoir utiliser pkgs.sshtui.
On créé donc overlays/sshtui/default.nix:

{ channels, inputs, ... }:

final: prev: {
  sshtui = inputs.self.packages.${final.system}.sshtui;
}

Utilisation du module

Maintenant que tout est fait, pour l'utiliser il y'a 3 choses à faire dans le dossier contenant nos flakes et notre config home-manager

Ajout du input dans flake.nix

sshtui = {
  url = "github:gfriloux/sshtui";
  inputs.nixpkgs.follows = "nixpkgs";
};

Déclaration du module home-manager

Toujours dans flake.nix (normalement), sous home-manager.lib.homeManagerConfiguration, nous ajoutons ce module:

modules = [
  sshtui.homeModules.sshtui
];

Installation du program

Dans home.nix (normalement), nous ajoutons:

programs.sshtui.enable = true;

Et c'est tout! Normalement vous devriez avoir la commande sshtui disponible!

m365-refresh

m365-refresh est un script python qui permet de refresh les {access,refresh} tokens pour l'auth OAUTH2, si vous avez un compte mail chez microsoft.

C'est une reprise de UvA-FNWI/M365-IMAP, pour le faire fonctionner en mode service.

Ce mini projet nous permet de facilement valider quelques technos:

  • Packager un script python ultra basique.
  • Créér un service systemd dans votre espace utilisateur.
  • Créér un timer systemd dans votre espace utilisateur.

Création de notre module nix

flake.nix

Notre flake.nix ne change pas vraiment par rapport à sshtui:

inputs.snowfall-lib.mkFlake {
  inherit inputs;
  systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
  src = ./.;

  snowfall = {
    namespace = "m365";
  };
  alias = {
    packages.default = "m365";
  };
};

Le package s'appelle m365 car à la base je pensai embarquer plusieurs scripts, dont seul m365-refresh.py serait un service.

packages/m365/default.nix

{ lib, pkgs, python3Packages, ... }:

python3Packages.buildPythonApplication {
  pname = "m365";
  version = "1.0.0";
  src = ./src;

  propagatedBuildInputs = with python3Packages; [
    msal
  ];

  format = "other";

  installPhase = ''
    mkdir -p $out/bin
    install -m755 m365-refresh.py $out/bin/m365-refresh.py
  '';

  meta = with lib; {
    description = "Refresh oauth2 access token using MSAL";
    platforms = platforms.linux;
  };
}

buildPythonApplication va s'occuper de créér un wrapper pour surcharger le shebang du script, et bien être en isolation par rapport au système.

Il va gérer l'installation du package python msal.

Le script python en lui-même : src/m365-refresh.py

modules/m365/default.nix

{ lib, config, pkgs, ...}:

let
  cfg = config.services.m365-refresh;
in
{
  options.services.m365-refresh = {
    enable = lib.mkEnableOption "m365 MSAL shit";
    schedule = lib.mkOption {
      type = lib.types.str;
      default = "hourly";
      description = "Systemd timer schedule";
    };
    config = lib.mkOption {
      type = lib.types.str;
      description = "path to configuration file";
    };
  };

  config = lib.mkIf cfg.enable {
  	systemd.user.services.m365-refresh = {
  	  Unit = {
  	  	Description = "m365 MSAL shit";
  	  };
  	  Service = {
  	  	Type = "oneshot";
  	    ExecStart = "${pkgs.m365}/bin/m365-refresh.py --config ${cfg.config}";
  	  };
  	};

  	systemd.user.timers.m365-refresh = {
  	  Unit = {
  	  	Description = "Timer for m365-refresh";
  	  };
  	  Timer = {
  	  	OnCalendar = cfg.schedule;
  	  	Persistent = true;
  	  };
  	  Install = {
  	  	WantedBy = [ "timers.target" ];
  	  };
  	};
  };
}

C'est ce module qui va nous permettre, dans notre config home-manager de déclarer le service, avec la config dont on a besoin config.py.

Le bloc options.services.m365-refresh nous permet de définir les options qui seront configuration dans notre config home-manager (notamment l'emplacer du fichier config.py).

Ensuite, il s'agit de la déclaration du service systemd et son timer qui le déclenche.

overlays/m365/default.nix

{ channels, inputs, ... }:

final: prev: {
  m365 = inputs.self.packages.${final.system}.m365;
}

Cet overlay ne mérite pas vraiment d'attention, il nous permet juste d'ajouter le package m365 dans pkgs.

Utilisation du module

Ajout du input dans flake.nix

sshtui = {
  url = "github:gfriloux/nix-m365";
  inputs.nixpkgs.follows = "nixpkgs";
};

Déclaration du module home-manager

modules = [
  nix-m365.homeModules.m365
];

Installation du service

services = {
  m365-refresh = {
    enable = true;
    schedule = "hourly";
    config = "/home/kuri/.config/m365/config.py";
  };
};

Vérification

Service systemd

 kuri@Nomad  ~  11:29  systemctl --user status m365-refresh
○ m365-refresh.service - m365 MSAL shit
     Loaded: loaded (/home/kuri/.config/systemd/user/m365-refresh.service; linked; preset: enabled)
     Active: inactive (dead) since Wed 2026-01-07 11:00:46 CET; 29min ago
 Invocation: 76d591c4f9cb4fd3b528e3de7b590869
TriggeredBy: ● m365-refresh.timer
    Process: 59019 ExecStart=/nix/store/i09rwnf3d1abavcmn0f4kz3gs5k370ax-m365-1.0.0/bin/m365-refresh.py --config /home/kuri/.config/m365/config.py (code=exited, status=0/SUCCESS)
   Main PID: 59019 (code=exited, status=0/SUCCESS)
   Mem peak: 49.1M
        CPU: 227ms

janv. 07 11:00:45 Nomad systemd[5477]: Starting m365 MSAL shit...
janv. 07 11:00:46 Nomad systemd[5477]: Finished m365 MSAL shit.

Timer systemd

 kuri@Nomad  ~  11:29  systemctl --user list-timers
NEXT                         LEFT LAST                           PASSED UNIT               ACTIVATES           
Wed 2026-01-07 12:00:00 CET 29min Wed 2026-01-07 11:00:45 CET 29min ago m365-refresh.timer m365-refresh.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.

Systemd

systemd est un ensemble d'outils servant de socle pour un système GNU/Linux.
Il est aujourd'hui utilisé sur la plupart des distributions GNU/Linux.

Il est donc important de s'interesser a ses composants, dont l'un des plus utilisé est systemctl.

Histoire

Systemd est un projet débuté en 2010.
Il se pose alors en alternative à System V, dont il tente de résoudre les problèmes connus.

1 an après la première release, nous avons déjà un ensemble de distributions qui basculent dessus, comme Fedora ou Archlinux.

Bien que Systemd soit fortement critiqué par un ensemble de personnes, notamment sur son coté non KISS, il faut reconnaitre qu'il a très rapidement été adopté sur la plupart des distributions GNU/Linux.

Sources

systemctl

systemctl est le programme principal de gestion de systemd.

Nous n'allons globalement nous interesser qu'à la partie gestion de services.
Il ne s'agit pas ici d'apprendre les bases, mais d'aller un peu plus loin.

Hardening

systemd-analyze security

Cette commande permet d'obtenir un score pour chaque service.
L'idéal est de connaitre les besoins du service, et d'interdire tout accès qui ne correspond pas aux besoins.

Il ne s'agit pas de restreindre le fonctionnement normal de l'application, mais bien d'encadrer son exploitation ou un bug.

Configurations

L'intérêt est donc pour chaque service que l'on déploie, d'ajouter un override (par exemple via systemctl edit) qui va ajouter des restrictions.

Exemples:

  • PrivateTmp=yes: Créé un namespace pour que les accès du service dans /tmp/ soient isolés dans un dossier temporaire.
  • NoNewPrivileges=true: Permet d'empêcher le service d'obtenir de nouveaux privilèges.
    Par exemple pour empêcher php-fpm ou un de ses fils de pouvoir passer root.
  • ProtectSystem=strict: Permet de passer tout / en read-only.
    Il convient ensuite d'utiliser ReadWritePaths pour autoriser les écritures dans certains dossiers.
  • InaccessiblePaths: Permet de rendre certains dossiers inaccessibles, y compris en lecture.

Exemples de service

Ces fichiers de configuration ont étés testés sur Debian.
Ils ne sont pas parfaits, et doivent être adaptés à vos usages.

Redis

Cette configuration permet d'obtenir un score de 2.5 sur systemd-analyze security.
Il force l'utilisation des sockets unix (dans /var/run/redis/) pour la connexion, au lieu de sockets tcp.

redis.conf:

[Service]
PrivateTmp=true
NoNewPrivileges=true
PrivateDevices=true
DevicePolicy=closed
ProtectSystem=strict
ProtectHome=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
LockPersonality=true
ProtectClock=true
ProtectHostname=true
ProtectKernelLogs=true
PrivateUsers=true
RemoveIPC=true
CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE
ReadOnlyPaths=/
NoExecPaths=/
ExecPaths=-/usr/bin/redis-server
ExecPaths=-/usr/lib
ExecPaths=-/lib
ReadWritePaths=-/var/run/redis
ReadWritePaths=-/run/redis
ReadWritePaths=-/var/log/redis
ReadWritePaths=-/var/lib/redis
RestrictAddressFamilies=AF_UNIX
UMask=007
LimitNOFILE=65535
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~ @privileged @resources

MariaDB

Cette configuration permet d'obtenir un score de 4.5 sur systemd-analyze security.

mariadb.conf:

[Service]
PrivateTmp=true
NoNewPrivileges=true
PrivateDevices=true
DevicePolicy=closed
ProtectSystem=strict
ProtectHome=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
LockPersonality=true
ProtectClock=true
ProtectHostname=true
ProtectKernelLogs=true
PrivateUsers=true
RemoveIPC=true
CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE
ReadOnlyPaths=/
NoExecPaths=/
ExecPaths=-/usr/sbin/mariadbd
ExecPaths=-/usr/lib
ExecPaths=-/lib
ReadWritePaths=-/var/run/mysqld
ReadWritePaths=-/run/mysqld
ReadWritePaths=-/var/lib/mysql/
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
UMask=007
LimitNOFILE=65535
SystemCallArchitectures=native

PHP-FPM

Cette configuration permet d'obtenir un score de 4.6 sur systemd-analyze security.

php-fpm.conf:

[Service]
PrivateTmp=true
NoNewPrivileges=true
PrivateDevices=true
DevicePolicy=closed

# This option will make any JIT techniques to fail.
MemoryDenyWriteExecute=true

LockPersonality=true
UMask=007
LimitNOFILE=65535

RestrictNamespaces=~mnt
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6

ProtectSystem=strict
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectClock=true
ProtectHostname=true
ProtectKernelLogs=true

SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=~@mount
SystemCallFilter=~@clock
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@module
SystemCallFilter=~@obsolete
SystemCallFilter=~@debug
SystemCallFilter=~@reboot

CapabilityBoundingSet=~CAP_SYS_PTRACE
CapabilityBoundingSet=~CAP_SYS_ADMIN
CapabilityBoundingSet=~CAP_MAC_*
CapabilityBoundingSet=~CAP_LINUX_IMMUTABLE
CapabilityBoundingSet=~CAP_SYS_BOOT
CapabilityBoundingSet=~CAP_SYS_CHROOT
CapabilityBoundingSet=~CAP_BLOCK_SUSPEND
CapabilityBoundingSet=~CAP_NET_ADMIN

ReadOnlyPaths=/

ReadWritePaths=-/var/log/alternatives.log
ReadWritePaths=-/var/lock
ReadWritePaths=-/run/php
ReadWritePaths=-/etc/alternatives
ReadWritePaths=-/var/lib/dpkg/alternatives
ReadWritePaths=-/var/log/php8.4-fpm.log
ReadWritePaths=-/var/www/html

NoExecPaths=/
ExecPaths=-/usr/sbin
ExecPaths=-/bin
ExecPaths=-/usr/bin
ExecPaths=-/usr/lib
ExecPaths=-/lib

InaccessiblePaths=-/boot
InaccessiblePaths=-/lost+found
InaccessiblePaths=-/etc/default
InaccessiblePaths=-/etc/apache2
InaccessiblePaths=-/etc/apt
InaccessiblePaths=-/etc/shadow
InaccessiblePaths=-/etc/sudoers
InaccessiblePaths=-/etc/sysctl.conf
InaccessiblePaths=-/etc/sysctl.d
InaccessiblePaths=-/var/backups
InaccessiblePaths=-/var/mail
InaccessiblePaths=-/var/spool
InaccessiblePaths=-/var/local
InaccessiblePaths=-/var/cache
InaccessiblePaths=-/var/opt

Apache2

Cette configuration permet d'obtenir un score de 4.9 sur systemd-analyze security.

apache2.conf:

[Service]
PrivateTmp=true
NoNewPrivileges=true
PrivateDevices=true
DevicePolicy=closed
ProtectSystem=strict
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
LockPersonality=true
ProtectClock=true
ProtectHostname=true
ProtectKernelLogs=true
ReadOnlyPaths=/
NoExecPaths=/
ExecPaths=-/usr/sbin
ExecPaths=-/bin
ExecPaths=-/usr/bin
ExecPaths=-/usr/local/bin
ExecPaths=-/usr/lib
ExecPaths=-/lib
ExecPaths=-/usr/lib/apache2/modules/
ReadWritePaths=-/var/log/apache2
ReadWritePaths=-/var/cache/apache2
ReadWritePaths=-/var/lib/apache2
ReadWritePaths=-/var/lock
ReadWritePaths=-/run/apache2
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
UMask=007
LimitNOFILE=65535
SystemCallArchitectures=native
CapabilityBoundingSet=~CAP_SYS_PTRACE

Sources

portable services

Depuis la v239 (2018) de Systemd, est introduit la notion de Portable Services.

En quoi cela va nous interesser?

Isolation

  • Si le service est compromis, les accès au système sont restreints.
    En effet, le service est chrooté dans son image.
  • Il est possible de donner accès à une partie du système de fichiers du host via BindPaths= et BindReadOnlyPaths=, pour y stocker des datas dites "dynamiques", ressemblant aux volumes sur Docker.
    Pour le reste, le service est chroot.
  • Pour les mises à jour, on detach l'image .raw de la version précédente du service, et on attach la nouvelle version!
    C'est tout!
    Pas besoin de MAJ le système, votre démon est indépendant du système sur lequel il s'execute!. Vous pouvez MAJ le système sans impact sur le service, et inversement!

Il va donc être possible d'avoir par exemple un dossier /srv/startpage/ contenant:

.r--r--r-- root root 5.7 MB Thu Jan  1 01:00:01 1970  startpage_1.1.0.raw
.r--r--r-- root root 5.7 MB Thu Jan  1 01:00:01 1970  startpage_1.2.0.raw
.r--r--r-- root root 5.7 MB Thu Jan  1 01:00:01 1970  startpage_1.3.0.raw

et pouvoir upgrade le service dans le temps, et rollback sur l'image d'avant en cas de problème.

Reproductibilité

  • On package notre démon dans une image .raw. Cette image embarque le service et toutes ses dépendances. Peu importe la distribution, il se comportera de la même manière.
  • Nix sait construire des images Portable Services via pkgs.portableService.

Léger

  • C'est plus léger que docker (pas de surcouche / isolation réseau).

Pour quelles limites?

Lorsque l'on commence à vouloir faire discuter plusieurs services, docker compose (ou similaire) va s'imposer naturellement pour continuer de bosser en isolation, avec reproductibilité.

Sources

Les cas pratiques

Startpage

startpage-preview-green

Ce projet est un exemple de comment nous pouvons créér un portable service très simple:

  • Embarque un serveur web minimal (que l'on compile nous-même pour la demo).
  • Embarque le site web de la startpage.
  • Ajoute la database (links.json) des bookmarks.
  • Tout le service est isolé dans un fichier .raw.
  • Pas d'interactions avec d'autres services.
  • Pas de BindPaths.

La construction de l'image se fait donc en 3 étapes:

  1. Packager littleweb.
  2. Packager retro-crt-startpage.
  3. Packager le service file.

Étapes de construction

littleweb

Projet rust utilisant Actix Web pour servir des fichiers statiques.

A l'heure de l'écriture de cette page, le packaging de ce projet (compilation d'un binaire statique) se fait dans ce bloc:

littleweb = pkgs.rustPlatform.buildRustPackage (finalAttrs: {
  pname = "littleweb";
  version = "1.4.0";
  src = pkgs.fetchFromGitHub {
    owner = "gfriloux";
    repo = "littleweb";
    rev = "v1.4.0";
    sha256 = "sha256-Y2u2z/N73S5kJnsojNjY5OHTncZujyd8pLjcVSX/Cv4=";
  };
  cargoHash = "sha256-B9iAE5ua1I7kIfX9tBnnp2ewAs4j5oD8ttQqeorF5Xo=";
});

retro-crt-startpage

La startpage retro-crt-startpage que l'on souhaite servir en HTTP.

Réalisé par ce simple bloc:

website = pkgs.stdenv.mkDerivation rec {
  title = "retro-crt-startpage";
  description = "HTML5-based layout for a personalized retro CRT startpage.";
  name = "retro-crt-startpage";
  version = "1.3.1";
  src = pkgs.fetchzip {
    url = "https://github.com/scar45/retro-crt-startpage/releases/download/v1.3.1/retro-crt-startpage-v1.3.1-release.zip";
    hash = "sha256-UmYyfEy2BVMavAdEqlEYNT5A6dPXuxViAZ18n1fxCfc=";
  };
  nativebuildInputs = [ pkgs.zip ];
  installPhase = ''
    mkdir -p $out
    cp -r css fonts images js *.png *.html *.xml *.txt *.mp3 $out/
    cp ${./links.json} $out/links.json
  '';
};

On note que l'on ajoute notre fichier links.json local au projet, qui n'en contient pas par défaut.

Portable service

Construit l'image .raw compatible portable service. Elle référence le package unit que je ne paste pas ici, il s'agit du service file du service, que vous pouvez trouver ici.

oci-systemd = pkgs.portableService {
  pname = "retro-startpage";
  inherit ( packages.website ) version;
  units = [ packages.unit ];
  contents = with pkgs; [ packages.website ];
  homepage = "https://github.com/gfriloux/retro-startpage";
};

Construction du projet

nix build .#oci-systemd

Résultat

Nous avons une image retro-startpage_1.3.1.raw de 4.9MB.
Le service consomme ~10MB de RAM. systemd-analyze security nous donne un score de 1.2, ce qui est excellent.

Du fait d'utiliser littleweb, que l'on compile statiquement, nous avons une image type distroless, chroot, avec du systemd hardening, qu'il sera a priori très difficile d'exploiter!

Un seul binaire executable: littleweb.

En cas d'exploitation réussie, le service n'est pas root, n'accède pas au système de fichiers de l'hôte, c'est une coquille vide...

Cette image est censée pouvoir s'executer sur toute distribution linux disposant de systemd v239 x86_64, c'est pas mal niveau portabilité!

Et biensûr, tout se passe dans flake.nix!

Docker

Docker est une solution de gestion de conteneurs.
Il s'agit aujourd'hui de la solution dominante, car il est très facile d'accès, un peu comme l'est devbox pour créér des environnements de travail.

Docker + Docker Hub permet de démarrer un service en mode conteneur (donc avec de l'isolation réseau/process...) sans rien connaitre des technologies sous-jacentes.

C'est ce qui a créé sa popularité, mais aussi les réactions d'une partie du monde IT (reprochant l'usage "savant fou" rendu possible).

Il a aussi fait l'objet de critiques venant de "puristes" de types d'installations plus traditionnelles, préférant utiliser les packages système de leur distribution.

Histoire

traefik

Traefik est un proxy qui s'intègre très bien à des stacks docker car il peut se configurer au travers des Docker labels.

En effet, il est possible de déclarer un vhost au travers des Docker labels afin d'obtenir sa création et destruction selon si le conteneur associé est démarré ou non.

Son autre usage le plus populaire est de l'utiliser pour générer des certificats Let's Encrypt de manière automatisée (et donc, toujours sur création du conteneur associé au vhost).

Routers

Les routers nous permettent de décrire le traitement des requêtes, et donc, de définir les vhosts.

Rien de très complexe, un routers nextcloud se déclarera ainsi:

- traefik.http.routers.nextcloud.rule=Host(`nextcloud.victim.org`)

Middlewares

Les middlewares permettent de manipuler les requêtes et les réponses traitées.

Filtrage IP

Nous pouvons déclarer le middlewares interne suivant:

labels:
  - "traefik.http.middlewares.interne.ipwhitelist.sourcerange=192.168.0.0/16"

Cela aura pour effet de filtrer les requêtes pour n'accepter que celles venant du CIDR 192.168.0.0/16.

Pour l'utiliser, il faut attacher ce middleware à notre router dédié à notre conteneur docker (qui s'appelle nextcloud dans cet exemple):

labels:
  - "traefik.http.routers.nextcloud.middlewares=interne"

Attention, ce filtrage cassera de fait la création de certificats Let's Encrypt.
Il conviendra donc de garder /.well-known/ accessible publiquement, par exemple en créant 2 router nextcloud et nextcloud-le:

labels:
  - traefik.http.routers.nextcloud.rule=Host(`nextcloud.victim.org`)
  - traefik.http.routers.nextcloud.tls=true
  - traefik.http.routers.nextcloud.tls.certresolver=lets-encrypt
  - traefik.http.routers.nextcloud.middlewares=galilee-interne
  - traefik.http.routers.nextcloud-le.rule=(Host(`nextcloud.victim.org`) && PathPrefix(`/.well-known/`))
  - traefik.http.routers.nextcloud-le.tls=true
  - traefik.http.routers.nextcloud-le.tls.certresolver=lets-encrypt

Ainsi, toute requête vers nextcloud.victim.org/.well-known/ sera traitée par le router interne-le, sans restriction IP, alors que le reste y sera soumis.

Best practices

Isoler le conteneur

Le mieux est de pouvoir créér un ou plusieurs Docker network dont fera parti Traefik.

Pour chaque conteneur qui aura effectivement besoin d'être proxifié par Traefik, il conviendra de l'associer à ce(s) network(s).

Le but étant, sur une stack docker compose, de n'avoir que ce conteneur qui fasse parti du même Docker network que Traefik, les autres n'étant donc pas directement accessibles par Traefik.

Sources

watchtower

Watchtower est un outil de gestion de mises à jour des conteneurs docker.

Je m'en sers depuis des années sans problèmes.

Il m'est notamment utile car si l'on spécifie des notifications mattermost/slack, alors il va décrire ce qu'il fait.

Projet archivé

Au moment de l'écriture de cette page, ils viennent d'annoncer l'abandon du projet.

Plusieurs forks existent, dont nicholas-fedor/watchtower qui est activement maintenu pour le moment, avec plus de 1000 commits ajoutés.

Donc à voir!

Les cas pratiques

startpage

Startpage est notre petit projet demo de création d'un site web de gestion de bookmarks (statique).

Veuillez vous référer à systemd/startpage pour comprendre la stack.

En effet, ici, nous ne parlerons que de la création de l'image docker!

Construction du projet

Nix

Grâce à nix, nous allons pouvoir générer une image Docker la plus légère possible, sans fioritures, distroless.

Cela se fait en utilisant pkgs.dockerTools:

oci-docker = pkgs.dockerTools.buildLayeredImage {
  name = "retro-startpage";
  tag = "latest";
  config = {
    Cmd = ["${packages.littleweb}/bin/littleweb" "--host" "0.0.0.0" "--path" "${packages.website}/"];
    User = "1000:1000";
  };
};

On lance le build:

nix build .#oci-docker

Pourquoi utiliser nix?

Tout simplement car en utilisant nix plutôt qu'écrire un Dockerfile, nous ajoutons ses concepts, notamment, dans ce contexte:

  • Les phases de build se font sans accès réseaux, il y'a une phase de téléchargement des fichiers, une phase de construction.
  • Nous avons enfin des builds reproductibles, contrairement à des Dockerfile qui cessent de build du jour au lendemain.

Nous allons aussi éviter la facilité introduite par Docker:

Écrire des Dockerfile avec FROM debian car c'est pratique, au cas où on ait besoin d'un outil à un moment donné.
Ça ne semble pas problèmatique si l'on n'y pense pas vraiment, mais partir d'une distribution "de base", fait qu'on embarque un ensemble de librairies, un shell, un package manager... qui pourront un jour jouer contre notre service, en terme de sécurité (un shellcode de base execute /bin/sh).

Résultat

Nous pouvons charger l'image avec la commande docker load < result.

docker images

 kuri@Nomad  retro-startpage  main  16:02  docker images
IMAGE                    ID             DISK USAGE   CONTENT SIZE   EXTRA
retro-startpage:latest   1df45e5206f5       13.2MB             0B

dive

dive nous permet d'inspecter le contenu de notre image, afin de vérifier que nous avons bien quelque chose de léger, distroless:

┃ ● Current Layer Contents ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Permission     UID:GID       Size  Filetree                                                                                                                    
drwxr-xr-x   1000:1000      13 MB  └── nix                                                                                                                     
drwxr-xr-x   1000:1000      13 MB      └── store                                                                                                               
dr-xr-xr-x   1000:1000     9.9 MB          ├── mk1nrzsfngfj5hipmgq1a51cysr89x43-littleweb-static-x86_64-unknown-linux-musl-1.4.0                               
dr-xr-xr-x   1000:1000     9.9 MB          │   └── bin                                                                                                         
-r-xr-xr-x   1000:1000     9.9 MB          │       └── littleweb                                                                                               
dr-xr-xr-x   1000:1000     3.2 MB          └── nga39cm902ckqjwffp8r2f63kkb72jbc-retro-crt-startpage-x86_64-unknown-linux-musl                                  
-r--r--r--   1000:1000      33 kB              ├── apple-touch-icon.png                                                                                        
-r--r--r--   1000:1000     8.7 kB              ├── ascii-headers.html                                                                                          
-r--r--r--   1000:1000      445 B              ├── browserconfig.xml                                                                                           
-r--r--r--   1000:1000      611 B              ├── crossdomain.xml                                                                                             
dr-xr-xr-x   1000:1000      23 kB              ├─⊕ css                                                                                                         
-r--r--r--   1000:1000      33 kB              ├── favicon.png                                                                                                 
dr-xr-xr-x   1000:1000     398 kB              ├─⊕ fonts                                                                                                       
-r--r--r--   1000:1000      229 B              ├── humans.txt                                                                                                  
dr-xr-xr-x   1000:1000     2.4 MB              ├─⊕ images                                                                                                      
-r--r--r--   1000:1000     3.3 kB              ├── index.html                                                                                                  
dr-xr-xr-x   1000:1000     264 kB              ├─⊕ js                                                                                                          
-r--r--r--   1000:1000     2.0 kB              ├── links.json                                                                                                  
-r--r--r--   1000:1000      23 kB              ├── power_off.mp3                                                                                               
-r--r--r--   1000:1000      35 kB              ├── power_on.mp3                                                                                                
-r--r--r--   1000:1000       61 B              └── robots.txt          

docker run

 kuri@Nomad  retro-startpage  main  16:07  docker run -p 80:8080 retro-startpage
[2025-12-29T15:07:16Z INFO  littleweb] Serving /nix/store/nga39cm902ckqjwffp8r2f63kkb72jbc-retro-crt-startpage-x86_64-unknown-linux-musl/ on port 8080
[2025-12-29T15:07:16Z INFO  actix_server::builder] starting 2 workers
[2025-12-29T15:07:16Z INFO  actix_server::server] Actix runtime found; starting in Actix runtime
[2025-12-29T15:07:16Z INFO  actix_server::server] starting service: "actix-web-service-0.0.0.0:8080", workers: 2, listening on: 0.0.0.0:8080

Il est désormais possible d'accéder au service via http://localhost:80

vaultwarden

Vaultwarden est une implémentation libre du serveur de Bitwarden.

Nous allons discuter de comment nous implémentons ce service dans un écosystème disposant de traefik et watchtower.

Pas de nixfection (pour le moment!).

Séparation des déclarations

Bien que cela semble ridicule pour cet exemple du fait que la stack soit très légère, cela nous permet justement de mieux appréhender le concept.

Sur une infra plus complexe, nous y trouverions aussi MariaDB, Redis...

docker-compose.yml

Dans docker-compose.yml nous n'allons déclarer que les éléments essentiels à la gestion de la stack.

Ce fichier pourrait tout simplement être donné à un client, un collègue...
A l'inverse, il peut vous être fourni par les développeurs de l'application, et vous le gardez inchangé pour mieux voir les diffs dans le temps (pour certains projets, votre stack est un git clone d'un dépôt, et donc vous ne souhaitez pas créér de diff sur ce fichier indexé par git).

Il ne contient rien de spécifique.

services:
  bitwarden:
    image: vaultwarden/server:latest
    restart: always
    environment:
      - EXPERIMENTAL_CLIENT_FEATURE_FLAGS=ssh-key-vault-item,ssh-agent
    volumes:
      - ./data:/data

docker-compose.override.yml

Dans docker-compose.override.yml nous allons écrire les spécifités de notre stack, notamment:

  • Notre conteneur doit être sur le réseau web (dans lequel nous avons notre traefik).
  • Notre conteneur est mis à jour par watchtower avec les règles par défaut.
  • Notre conteneur dispose de labels pour traefik afin de déclarer le vhost et gérer automatiquement le certificat Let's Encrypt.
services:
  bitwarden:
    labels:
      - traefik.http.routers.vaultwarden.rule=Host(`vaultwarden.victim.org`)
      - traefik.http.routers.vaultwarden.tls=true
      - traefik.http.routers.vaultwarden.tls.certresolver=lets-encrypt
      - com.centurylinklabs.watchtower.enable=true
    networks:
      - web

networks:
  web:
    external: true

Rien à faire pour que Docker prenne en compte ce fichier, vous pouvez directement utiliser docker compose up -d.