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ée | Distro/OS |
|---|---|
| 1997 | Slackware |
| 1998 | Redhat |
| 2000 | Debian |
| 2005 | Gentoo |
| 2008 | Archlinux |
| 2024 | NixOS |
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:
| Outil | Description |
|---|---|
| fish | Shell en rust, qui à l'usage m'est beaucoup plus utile que bash. |
| atuin | Historique shell tellement plus moderne que history. |
| bat | Alternative à cat. J'ai un alias bat → cat. |
| btop | Alternative à top/htop. |
| delta | Alternative à diff. |
| fzf | Fuzzy Finder dont les usages sont tellements nombreux... |
| gitflow-toolkit | Outil d'aide au formattage des messages de commit. |
| git-workspace | Sync les dépôt git sur gitlab/github. |
| glow | Lecteur Markdown. |
| gum | Permet de créér des interfaces dans le terminal. |
| just | Alternative à make. |
| lsd | Alternative à lsd. J'ai un alias lsd → ls. |
| micro | Éditeur texte léger, en alternative à nano. |
| ncdu | Alternative à du. |
| oh-my-posh | Prompt customisable, avec différents thêmes disponibles sur le site. |
| prettyping | Alternative à prettyping. J'ai un alias prettyping → ping. |
| pwgen | Générateur de mots de passe. |
| rsync | Pour les transferts de fichiers. |
| sshtui | Interface pour les configs ssh en utilisant tv. |
| tv | Alternative à fzf. |
| xcp | Alternative à cp. J'ai un alias xcp → cp. |
| zellij | Alternative à 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:

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:
- Résilient aux pannes, nous avons à l'époque des conférences des
ingénieurs
Sunqui nous montrent qu'ils peuvent percer les disques d'un raidZFSactuellement en cours d'utilisation, sans impact sur la disponibilité du raid. - Prévu pour le futur avec des limites sur le nombre de fichiers, leur
taille maximale, la taille maximale du
zpool... - La possibilité de créér des datasets (re)configurables contrairement
aux classiques
partitions. - 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 ZFS → Eat your own dog food.
Sources
- ZFS - Wikipédia.
- GNU/Linux Magazine - article de 2009, qui m'a lancé dans l'aventure
ZFS. - 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
Naviguer dans un mount du snapshot
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:
- Un langage de programmation.
- Un gestionnaire de packages.
- Un système de build.
Pour cela, il mobilise plusieurs concepts:
- C'est un langage de programmation purement fonctionnel.
- Il est déclaratif.
- Il est paresseux
- Le résultat de son travail est reproductible
- Le résultat de son travail est portable
NixOS
NixOS c'est:
- Une distribution GNU/Linux initiée en 2006.
- Une première version stable en 2013.
- 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.
- Integrating Software Construction and Software Deployment - 2003
- Nix: A Safe and Policy-Free System for Software Deployment - 2004
- The Purely Functional Software Deployment Model - 2006
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
- L'heure du test - épisode 1 - NixOS.
- Journal NixOS ou comment j'ai rendu mes machines interchangeables et ennuyeuses.
- Donnez moi un NixOS à ronger.
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:
- Windows (WSL)
- macOS
- 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:
- CentOS 7
- Amazon Linux 2
- 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
- Flakes - Official NixOS Wiki
- Paranoid NixOS Setup
- Nix Impermanence
- oddlama/nix-config - Exemple de
flakespour gérer plusieurs hôtes sousNixOS. - Is NixOS the best OS for servers?
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+ccpour compiler un projetrust. - Installer
mdBooksoumkdocspour build/visualiser de la documentation. - Installer les bonnes versions d'
ansibleouterraformpour être compatible avec les contraintes d'une infra.
Sources
- direnv.net.
- Effortless dev environments with Nix and direnv.
- Automatic environment activation with direnv.
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
- Home Manager Manual.
- Tutorial: Getting started with Home Manager for Nix.
- Evertras/simple-homemanager A practical guide to getting started with home manager with flakes and all that 2024 goodness.
- Home Manager - Option Search.
Les cas pratiques
- sshtui - Packager un script bash.
- m365-refresh - Packager un service systemd en python (oneshot).
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:
bashbatawkxargs
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.tomlqui 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
- systemd - Wikipédia
- The Biggest Myths (2013)
- systemd : l’init martyrisé, l’init bafoué, mais l’init libéré ! (2015)
- Systemd sur Debian : la guerre de clochers tourne aux menaces (2014)
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êcherphp-fpmou un de ses fils de pouvoir passerroot. - ProtectSystem=strict: Permet de passer tout
/enread-only.
Il convient ensuite d'utiliserReadWritePathspour 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
- systemd.exec
- Maitriser la gestion des services Linux avec systemd
- alegrey91/systemd-service-hardening
- Systemd Hardneing - NixOS Wiki
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=etBindReadOnlyPaths=, pour y stocker des datas dites "dynamiques", ressemblant aux volumes surDocker.
Pour le reste, le service estchroot. - Pour les mises à jour, on
detachl'image.rawde la version précédente du service, et onattachla 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

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:
- Packager littleweb.
- Packager retro-crt-startpage.
- 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 - Créér une image docker
distrolessde startpage - vaultwarden - Bien gérer notre fichier
docker-compose.yml
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
Dockerfilequi 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
vhostet gérer automatiquement le certificatLet'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.