Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,30 @@ repos:
pass_filenames: false
always_run: true

# Un setup et son cleanup doivent se répondre : ce que le setup garde
# (marqueur `creates:`) ou crée (comptes), le cleanup doit le rendre.
# Sinon le lab ne se réinitialise qu'une fois, ou occupe un nom pour tous
# les suivants — et c'est le lab d'APRÈS qui échoue, jamais le fautif.
# Lecture de fichiers seulement : ni VM ni réseau.
- id: marqueurs-setup-cleanup
name: un cleanup rend ce que son setup garde ou crée
entry: uv run --no-project --with pytest --with pyyaml pytest tests/test_marqueurs_setup_cleanup.py -q
language: system
pass_filenames: false
always_run: true

# Un YAML valide ne prouve pas qu'Ansible charge les tâches : une
# apostrophe dans un bloc `shell` casse le découpage des arguments, et le
# playbook ne se voit refusé qu'à l'exécution, en rc=4 sans une seule
# tâche jouée. On ne le lance que si un playbook change : `--syntax-check`
# sur les 129 fichiers prend une quarantaine de secondes.
- id: playbooks-syntaxe
name: les setup/cleanup se chargent (ansible-playbook --syntax-check)
entry: uv run --no-project --with pytest --with pyyaml pytest tests/test_playbooks_syntaxe.py -q
language: system
pass_filenames: false
files: 'labs/.*/(setup|cleanup)\.yaml$'

# Garde-fou d'outillage. Dans le dépôt Ansible jumeau, deux fois pendant
# une même session, une modification a disparu sans trace git : un test de
# catalogue, puis un hook. Plusieurs processus écrivent dans ces dépôts,
Expand Down
95 changes: 95 additions & 0 deletions CHANGELOG.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,103 @@ suit le [versionnage sémantique](https://semver.org/lang/fr/).

## [Non publié]

### Corrigé — la première campagne de validation complète

Les 84 labs ont été rejoués sur KVM avec contrôle négatif (rouge sans la
solution, vert avec). Neuf défauts sont remontés, tous invisibles jusque-là :
les tests d'un lab ne tournent que si quelqu'un le joue, et personne ne les
enchaînait.

- **`. /root/xxx.env 2>/dev/null || exit 0` ne protégeait rien.** En `sh` POSIX
non interactif, un `.` sur un fichier absent tue le shell **avant** le `||`.
Six `cleanup.yaml` en dépendaient (`l2-autofs-ondemand`,
`l2-disk-space-troubleshoot`, `l2-filesystem-create-xfs`, `l2-partition-gpt`,
`l2-storage-performance`, `l3-fs-readonly-recover`) : leur `dsoxlab reset`
échouait, donc l'apprenant restait bloqué. Le test est désormais fait avant
le source.
- **Trois labs laissaient une partition sur le disque partagé** en n'effaçant
que la signature de la partition, jamais la table du disque. Ce n'était pas
eux qui échouaient, mais le **lab suivant**.
- **`l2-lvm-extend-persist` échouait sur un `rmdir`** : `set +e` n'empêche pas
le module `shell` de rendre le code de la dernière commande. Un `exit 0`
explicite ferme le sujet.
- **`l2-filesystem-create-xfs` n'était rejouable qu'une fois.** Son `setup` se
garde avec `creates: /root/.xfs-lab-ready`, que son `cleanup` n'effaçait pas :
disque nettoyé, marqueur conservé, donc partition jamais recréée et solution
en échec sur une partition inexistante.
- **`l2-luks-encryption` : le test était faux**, pas la solution. Il cherchait
le « 2 » de `Version:` dans une tranche de 8 caractères, alors que
l'alignement de `cryptsetup` le place en 9ᵉ position. Le volume était en LUKS2
depuis le début. On lit maintenant la valeur du champ.
- **`l3-journald-persist` : la solution écrivait dans un répertoire absent.**
Le module `copy` ne crée pas les parents, et `cleanup` supprimait
`/etc/systemd/journald.conf.d`. La solution le crée désormais.
- **`l4-reverse-proxy-lb` et `l4-ldap-integration` étaient infaisables** : leur
`setup` démarrait le service sur le second nœud sans **ouvrir le port dans
son pare-feu**. HAProxy rendait un `503 No server is available`, et SSSD ne
résolvait rien (`No route to host` sur le port 389). L'ouverture fait partie
de l'état de départ : le sujet de ces labs est le proxy et SSSD, pas le
pare-feu du serveur.
- **`drill-firewall` laissait `firewalld` désactivé** après son nettoyage, ce
qui cassait `rhcsa-mock-exam` (« FirewallD is not running »). Un `cleanup`
rend le système neutre, il ne laisse pas derrière lui l'état de départ du
drill.
- **`rhcsa-mock-exam` gardait son VG sur le disque partagé.** Son `umount`
échouait parce que l'export NFS tenait encore `/data/share` à ce moment,
`exportfs -ua` venant après.

### Ajouté — les garde-fous

Chacun est né d'un défaut réel de cette campagne, et chacun a été vérifié en le
faisant échouer :

- **`tests/test_playbooks_syntaxe.py`** : `ansible-playbook --syntax-check` sur
les 129 `setup.yaml`/`cleanup.yaml`. Un YAML valide ne prouve pas qu'Ansible
charge les tâches : une apostrophe française dans un bloc `shell` suffit à
casser le découpage des arguments, et le lab ne se voyait qu'à l'exécution,
sous la forme d'un `reset` en `rc=4` sans une seule tâche jouée.
- **`tests/test_marqueurs_setup_cleanup.py`** : tout marqueur `creates:` d'un
`setup` doit être effacé par son `cleanup`, sinon le lab ne se réinitialise
qu'une fois. Jouable sans VM, donc en CI. Il a trouvé le défaut de
`l2-filesystem-create-xfs` et un second cas, `l4-ldap-integration`, qui s'est
avéré délibéré : il est exempté explicitement, avec son motif.
- **Contrôle du disque partagé** dans `verify-solutions.py` : après chaque lab
`vm`, le `cleanup` est joué et l'état du disque **comparé à celui d'avant**.
Un lab n'est accusé que de ce qu'il ajoute, et il est nommé au moment où il
salit plutôt que de laisser échouer le suivant.

### Corrigé

- **La séquence l1 exigeait d'éditer un fichier sans jamais l'avoir enseigné.**
Les trois premiers labs demandaient de compléter un fichier de réponses, et le
challenge de `l1-first-terminal` interdit même de le recréer par redirection,
alors qu'aucun cours l1 ne montrait comment ouvrir, écrire, sauvegarder et
quitter un éditeur. La seule occurrence de `vim` dans toute la section était
la **valeur** de la variable `EDITOR` au lab 19. Un débutant devait donc
sortir de la formation dès le premier challenge, ce qu'un retour d'apprenant a
confirmé.
- `l1-first-terminal` gagne une section « Écrire dans un fichier sans quitter
le terminal » (FR et EN) : `nano` (Ctrl+O, Ctrl+X), la survie sous `vi`
(Échap, `:wq`, `:q!`), la variable `EDITOR`, et les entrées de dépannage
correspondantes. Les exemples portent des noms étrangers au challenge, pour
ne pas en donner la réponse.
- `l1-first-terminal` passe **en tête** de la section l1, devant
`l1-discover-linux-map`, `l1-choose-distro` et `l1-prepare-vm`.

### Ajouté

- **`scripts/verify-solutions.py`** : rejoue les solutions de référence
chiffrées et prouve qu'elles passent encore les tests. Une montée de version
peut casser une solution sans que personne ne s'en aperçoive, puisque les
tests d'un lab ne tournent que si quelqu'un le joue. Le verdict est consigné
dans `solution/verified-with.json`.
- `--negative` ajoute le contrôle qui manque le plus : après un
`dsoxlab reset`, les tests doivent **échouer** sans la solution. Un test qui
passe dans les deux sens ne prouve rien.
- Les labs `vm` sans infrastructure provisionnée sont comptés **ignorés**,
jamais en échec : un harnais absent n'est pas une régression de contenu.
- Le script refuse de tourner avec un interpréteur sans `pytest`, plutôt que
de faire échouer les 84 labs d'un coup et de faire croire à une régression.
- Catalogue de labs initial pour la formation sécurité Linux / DevSecOps
(RHCSA + LFCS) :
- 9 labs **L1** de fondamentaux (shell), chacun validé contre l'**état réel**
Expand Down
90 changes: 90 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,98 @@ based on [Keep a Changelog](https://keepachangelog.com/), and the project follow

## [Unreleased]

### Fixed — the first full validation campaign

All 84 labs were replayed on KVM with a negative control (red without the
solution, green with it). Nine defects surfaced, all previously invisible: a
lab's tests only run when someone plays it, and nobody was chaining them.

- **`. /root/xxx.env 2>/dev/null || exit 0` protected nothing.** In a
non-interactive POSIX `sh`, sourcing a missing file kills the shell **before**
the `||`. Six `cleanup.yaml` relied on it (`l2-autofs-ondemand`,
`l2-disk-space-troubleshoot`, `l2-filesystem-create-xfs`, `l2-partition-gpt`,
`l2-storage-performance`, `l3-fs-readonly-recover`): their `dsoxlab reset`
failed, leaving the learner stuck. The file is now tested before being
sourced.
- **Three labs left a partition on the shared disk**, wiping only the
partition's signature and never the disk's table. They did not fail
themselves: the **next** lab did.
- **`l2-lvm-extend-persist` failed on an `rmdir`**: `set +e` does not stop the
`shell` module from returning the last command's exit code. An explicit
`exit 0` settles it.
- **`l2-filesystem-create-xfs` was replayable only once.** Its `setup` guards
itself with `creates: /root/.xfs-lab-ready`, which its `cleanup` never
removed: disk wiped, marker kept, so the partition was never recreated and
the solution failed on a partition that did not exist.
- **`l2-luks-encryption`: the test was wrong**, not the solution. It looked for
the `2` of `Version:` within an 8-character slice, while `cryptsetup`'s
alignment puts it at position 9. The volume had been LUKS2 all along. The
field's value is now read properly.
- **`l3-journald-persist`: the solution wrote into a missing directory.** The
`copy` module does not create parents, and `cleanup` removed
`/etc/systemd/journald.conf.d`. The solution now creates it.
- **`l4-reverse-proxy-lb` and `l4-ldap-integration` were impossible**: their
`setup` started the service on the second node without **opening the port in
its firewall**. HAProxy returned `503 No server is available`, and SSSD
resolved nothing (`No route to host` on port 389). Opening it belongs to the
starting state: these labs are about the proxy and SSSD, not the server's
firewall.
- **`drill-firewall` left `firewalld` disabled** after cleaning up, which broke
`rhcsa-mock-exam` ("FirewallD is not running"). A `cleanup` returns the system
to neutral; it does not leave the drill's starting state behind.
- **`rhcsa-mock-exam` kept its VG on the shared disk.** Its `umount` failed
because the NFS export still held `/data/share` at that point, `exportfs -ua`
coming afterwards.

### Added — the guards

Each one came from a real defect in this campaign, and each was verified by
making it fail:

- **`tests/test_playbooks_syntaxe.py`**: `ansible-playbook --syntax-check` over
the 129 `setup.yaml`/`cleanup.yaml`. Valid YAML does not prove Ansible can
load the tasks: a French apostrophe inside a `shell` block is enough to break
argument splitting, and the breakage only showed at run time, as a `reset`
exiting `rc=4` without running a single task.
- **`tests/test_marqueurs_setup_cleanup.py`**: every `creates:` marker in a
`setup` must be removed by its `cleanup`, otherwise the lab resets only once.
Runs without a VM, so it runs in CI. It found the `l2-filesystem-create-xfs`
defect and a second case, `l4-ldap-integration`, which turned out to be
deliberate: it is explicitly exempted, with its rationale.
- **Shared-disk check** in `verify-solutions.py`: after each `vm` lab the
`cleanup` is played and the disk state **compared against the state before**.
A lab is only blamed for what it adds, and it is named the moment it dirties
the disk instead of letting the next one fail.

### Fixed

- **The l1 sequence required editing a file it never taught how to edit.** The
first three labs asked learners to fill in an answer file, and the
`l1-first-terminal` challenge even forbids recreating it with a redirection,
while no l1 course ever showed how to open, write, save and quit an editor.
The only occurrence of `vim` in the whole section was the **value** of the
`EDITOR` variable in lab 19. A beginner therefore had to leave the course at
the very first challenge, as a learner report confirmed.
- `l1-first-terminal` gains a "Writing to a file without leaving the terminal"
section (FR and EN): `nano` (Ctrl+O, Ctrl+X), `vi` survival (Esc, `:wq`,
`:q!`), the `EDITOR` variable, and the matching troubleshooting rows. Its
examples use names foreign to the challenge, so they do not give away the
answer.
- `l1-first-terminal` moves to the **front** of the l1 section, ahead of
`l1-discover-linux-map`, `l1-choose-distro` and `l1-prepare-vm`.

### Added

- **`scripts/verify-solutions.py`**: replays the encrypted reference solutions
and proves they still pass the labs' tests. A version bump can break a
solution unnoticed, since a lab's tests only run when someone plays it. The
verdict is recorded in `solution/verified-with.json`.
- `--negative` adds the missing check: after a `dsoxlab reset`, the tests must
**fail** without the solution. A test that passes both ways proves nothing.
- `vm` labs with no provisioned infrastructure are counted as **skipped**,
never as failures: a missing harness is not a content regression.
- The script refuses to run under an interpreter without `pytest`, rather than
failing all 84 labs at once and looking like a massive regression.
- Initial lab catalog for the Linux security / DevSecOps track (RHCSA + LFCS):
- 9 **L1** fundamentals labs (shell), each validated against the **real state**
of the machine (no fill-in-the-blank worksheets).
Expand Down
14 changes: 7 additions & 7 deletions README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,17 @@ peux les faire sur ton poste, tout de suite.

```bash
dsoxlab use l1 # section de départ
dsoxlab next # → l1-discover-linux-map
dsoxlab next # → l1-first-terminal
```

Puis, pour ce lab comme pour tous les autres, le même cycle en quatre temps :

```bash
dsoxlab course l1-discover-linux-map # 1. le contexte, puis le cours
dsoxlab challenge l1-discover-linux-map # 2. ce qui t'est demandé
dsoxlab run l1-discover-linux-map # 3. prépare ton espace de travail
dsoxlab course l1-first-terminal # 1. le contexte, puis le cours
dsoxlab challenge l1-first-terminal # 2. ce qui t'est demandé
dsoxlab run l1-first-terminal # 3. prépare ton espace de travail
# (challenge/work/) et t'y place
dsoxlab check l1-discover-linux-map # 4. valide et note
dsoxlab check l1-first-terminal # 4. valide et note
```

`run` est l'étape que l'on oublie : c'est elle qui crée les fichiers sur
Expand Down Expand Up @@ -235,10 +235,10 @@ ci-dessous est générée à partir des vrais `lab.yaml` : lance

| Lab (id) | Titre | Niveau | Certif | Runtime | Guide compagnon |
|---|---|---|---|---|---|
| `l1-first-terminal` | Premiers pas dans le terminal | l1 | RHCSA · LFCS | shell | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/fondamentaux/decouvrir-linux/prompt-terminal/) |
| `l1-discover-linux-map` | Cartographier Linux : noyau, distribution et répertoires clés | l1 | RHCSA · LFCS | shell | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/fondamentaux/decouvrir-linux/notions/) |
| `l1-choose-distro` | Choisir sa distribution Linux de référence | l1 | RHCSA · LFCS | shell | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/fondamentaux/decouvrir-linux/distributions-serveur/) |
| `l1-prepare-vm` | Identifier sa machine Linux | l1 | RHCSA · LFCS | shell | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/fondamentaux/decouvrir-linux/installer-vm/) |
| `l1-first-terminal` | Premiers pas dans le terminal | l1 | RHCSA · LFCS | shell | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/fondamentaux/decouvrir-linux/prompt-terminal/) |
| `l1-read-a-command` | Lire et décoder une commande | l1 | RHCSA · LFCS | shell | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/fondamentaux/decouvrir-linux/anatomie-commande/) |
| `l1-get-help` | Obtenir de l'aide en ligne de commande | l1 | RHCSA · LFCS | shell | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/fondamentaux/decouvrir-linux/obtenir-aide/) |
| `l1-linux-filesystem` | Hiérarchie du système de fichiers Linux (FHS) | l1 | RHCSA · LFCS | shell | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/fondamentaux/se-reperer-fichiers/arborescence-fhs/) |
Expand Down Expand Up @@ -350,7 +350,7 @@ ci-dessous est générée à partir des vrais `lab.yaml` : lance
| `rhcsa-mock-exam` | Examen blanc RHCSA EX200 — 20 tâches sur 2 VMs | l2 | RHCSA | vm | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/certifications/rhcsa/) |
| `lfcs-mock-exam` | Examen blanc LFCS — 17 tâches sur Ubuntu 24.04 | l2 | LFCS | vm | [guide](https://blog.stephane-robert.info/docs/admin-serveurs/linux/certifications/lfcs/) |

_84 labs table générée par `scripts/gen_catalog.py`._
_84 labs, table générée par `scripts/gen_catalog.py`._
<!-- LABS:END -->

## Contribuer et licence
Expand Down
Loading