ajoute le portail Laravel : connexion prof/élève, session unique, gestion de classe, salle d'attente

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-08 02:01:58 +02:00
parent aa31b6112f
commit 30e14b1c54
191 changed files with 25324 additions and 0 deletions

18
portail/.editorconfig Normal file
View file

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[{compose,docker-compose}.{yml,yaml}]
indent_size = 4

65
portail/.env.example Normal file
View file

@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
portail/.gitattributes vendored Normal file
View file

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

28
portail/.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
*.log
.DS_Store
.env
/database/database.sqlite
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.codex
/.cursor/
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/fonts-manifest.dev.json
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
_ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db

2
portail/.npmrc Normal file
View file

@ -0,0 +1,2 @@
ignore-scripts=true
audit=true

58
portail/README.md Normal file
View file

@ -0,0 +1,58 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
## Agentic Development
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
```bash
composer require laravel/boost --dev
php artisan boost:install
```
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View file

@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\Eleve;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;
class ConnexionController extends Controller
{
public function connexionProf(Request $request): RedirectResponse
{
$donnees = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
$this->appliquerLimiteTentatives($request, 'prof:'.$donnees['email']);
if (! Auth::guard('web')->attempt($donnees)) {
RateLimiter::hit($this->cleLimite($request, 'prof:'.$donnees['email']));
throw ValidationException::withMessages([
'email' => 'Identifiants incorrects.',
]);
}
RateLimiter::clear($this->cleLimite($request, 'prof:'.$donnees['email']));
$request->session()->regenerate();
/** @var User $prof */
$prof = Auth::guard('web')->user();
$avaitDejaUneSession = $prof->jeton_session_actif !== null;
$request->session()->put('jeton_session_web', $prof->demarreNouvelleSession());
return redirect()->route('mes-classes.index')
->with('session_precedente_fermee', $avaitDejaUneSession);
}
public function connexionEleve(Request $request): RedirectResponse
{
$donnees = $request->validate([
'identifiant' => ['required', 'string'],
'code' => ['required', 'string'],
]);
$this->appliquerLimiteTentatives($request, 'eleve:'.$donnees['identifiant']);
$succes = Auth::guard('eleve')->attempt([
'identifiant' => $donnees['identifiant'],
'password' => $donnees['code'],
]);
if (! $succes) {
RateLimiter::hit($this->cleLimite($request, 'eleve:'.$donnees['identifiant']));
throw ValidationException::withMessages([
'identifiant' => 'Identifiant ou code secret incorrect.',
]);
}
RateLimiter::clear($this->cleLimite($request, 'eleve:'.$donnees['identifiant']));
$request->session()->regenerate();
/** @var Eleve $eleve */
$eleve = Auth::guard('eleve')->user();
$eleve->forceFill(['derniere_connexion_a' => now()])->save();
$request->session()->put('jeton_session_eleve', $eleve->demarreNouvelleSession());
return redirect()->route('eleve');
}
/**
* Connexion directe par QR (à usage unique, voir App\Services\GenerateurCompteEleve).
*/
public function connexionParQr(Request $request, string $jeton): RedirectResponse
{
$eleve = Eleve::where('jeton_qr', $jeton)->first();
if ($eleve === null) {
abort(404);
}
$eleve->forceFill(['jeton_qr' => null, 'derniere_connexion_a' => now()])->save();
Auth::guard('eleve')->login($eleve);
$request->session()->regenerate();
$request->session()->put('jeton_session_eleve', $eleve->demarreNouvelleSession());
return redirect()->route('eleve');
}
public function deconnexion(Request $request): RedirectResponse
{
if (Auth::guard('web')->check()) {
Auth::guard('web')->user()->terminerSession();
Auth::guard('web')->logout();
}
if (Auth::guard('eleve')->check()) {
Auth::guard('eleve')->user()->terminerSession();
Auth::guard('eleve')->logout();
}
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('accueil');
}
private function cleLimite(Request $request, string $identifiant): string
{
return 'connexion:'.$identifiant.'|'.$request->ip();
}
private function appliquerLimiteTentatives(Request $request, string $identifiant): void
{
$cle = $this->cleLimite($request, $identifiant);
if (RateLimiter::tooManyAttempts($cle, 5)) {
$secondes = RateLimiter::availableIn($cle);
throw ValidationException::withMessages([
'identifiant' => "Trop de tentatives. Réessaie dans {$secondes} secondes.",
]);
}
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers;
use App\Models\Classe;
use App\Models\Eleve;
use App\Services\GenerateurCompteEleve;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\View\View;
class ClasseController extends Controller
{
public function __construct(private readonly GenerateurCompteEleve $generateur) {}
public function index(): View
{
$classes = Auth::guard('web')->user()->classes()->withCount('eleves')->latest()->get();
return view('mes-classes.index', ['classes' => $classes]);
}
public function show(Classe $classe): View
{
$this->verifierProprietaire($classe);
return view('mes-classes.show', [
'classe' => $classe->load('eleves'),
]);
}
public function storeEleve(Request $request, Classe $classe): RedirectResponse
{
$this->verifierProprietaire($classe);
$donnees = $request->validate([
'prenom' => ['required', 'string', 'max:100'],
'nom' => ['nullable', 'string', 'max:100'],
]);
$fiche = $this->creerEleve($classe, $donnees['prenom'], $donnees['nom'] ?? null);
return redirect()->route('mes-classes.show', $classe)
->with('fiches', [$fiche]);
}
public function importerCsv(Request $request, Classe $classe): RedirectResponse
{
$this->verifierProprietaire($classe);
$request->validate([
'fichier' => ['required', 'file', 'mimes:csv,txt'],
]);
$lignes = file($request->file('fichier')->getRealPath(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$fiches = [];
foreach ($lignes as $ligne) {
$colonnes = str_getcsv($ligne);
$prenom = trim($colonnes[0] ?? '');
if ($prenom === '' || Str::lower($prenom) === 'prenom') {
continue; // ligne vide ou en-tête
}
$nom = trim($colonnes[1] ?? '') ?: null;
$fiches[] = $this->creerEleve($classe, $prenom, $nom);
}
return redirect()->route('mes-classes.show', $classe)
->with('fiches', $fiches)
->with('nombre_importes', count($fiches));
}
public function regenererCode(Classe $classe, Eleve $eleve): RedirectResponse
{
$this->verifierProprietaire($classe);
abort_if($eleve->classe_id !== $classe->id, 404);
$codeSecret = $this->generateur->genererCodeSecret();
$jetonQr = Str::random(32);
$eleve->forceFill([
'code_secret' => Hash::make($codeSecret),
'jeton_qr' => $jetonQr,
])->save();
return redirect()->route('mes-classes.show', $classe)
->with('fiches', [[
'identifiant' => $eleve->identifiant,
'prenom' => $eleve->prenom,
'nom' => $eleve->nom,
'code_secret' => $codeSecret,
'jeton_qr' => $jetonQr,
]]);
}
/**
* @return array{identifiant: string, prenom: string, nom: ?string, code_secret: string, jeton_qr: string}
*/
private function creerEleve(Classe $classe, string $prenom, ?string $nom): array
{
$identifiant = $this->generateur->genererIdentifiant($prenom, $nom);
$codeSecret = $this->generateur->genererCodeSecret();
$jetonQr = Str::random(32);
$classe->eleves()->create([
'prenom' => $prenom,
'nom' => $nom,
'identifiant' => $identifiant,
'code_secret' => Hash::make($codeSecret),
'jeton_qr' => $jetonQr,
]);
return [
'identifiant' => $identifiant,
'prenom' => $prenom,
'nom' => $nom,
'code_secret' => $codeSecret,
'jeton_qr' => $jetonQr,
];
}
private function verifierProprietaire(Classe $classe): void
{
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class EleveTableauDeBordController extends Controller
{
public function show(): View
{
$eleve = Auth::guard('eleve')->user()->load('classe');
return view('eleve', ['eleve' => $eleve]);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers;
use App\Models\Classe;
use App\Models\Etablissement;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class InscriptionController extends Controller
{
public function store(Request $request): RedirectResponse
{
$donnees = $request->validate([
'prenom' => ['required', 'string', 'max:100'],
'nom' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'etablissement_nom_liste' => ['required', 'string'],
'etablissement_nom' => ['required_if:etablissement_nom_liste,autre', 'nullable', 'string', 'max:150'],
'etablissement_ville' => ['required_if:etablissement_nom_liste,autre', 'nullable', 'string', 'max:100'],
'etablissement_uai' => ['nullable', 'string', 'max:20'],
'formation' => ['nullable', 'string', 'max:100'],
'classe' => ['required', 'string', 'max:100'],
]);
[$prof, $classe] = DB::transaction(function () use ($donnees) {
$etablissement = $donnees['etablissement_nom_liste'] === 'autre'
? Etablissement::firstOrCreate(
['nom' => $donnees['etablissement_nom']],
['ville' => $donnees['etablissement_ville'], 'uai' => $donnees['etablissement_uai'] ?? null],
)
: Etablissement::firstOrCreate(['nom' => $donnees['etablissement_nom_liste']]);
$prof = User::create([
'name' => trim($donnees['prenom'].' '.$donnees['nom']),
'email' => $donnees['email'],
'password' => Hash::make($donnees['password']),
'etablissement_id' => $etablissement->id,
]);
$classe = Classe::create([
'etablissement_id' => $etablissement->id,
'user_id' => $prof->id,
'nom' => $donnees['classe'],
'formation' => $donnees['formation'] ?? null,
]);
return [$prof, $classe];
});
Auth::guard('web')->login($prof);
$request->session()->regenerate();
$request->session()->put('jeton_session_web', $prof->demarreNouvelleSession());
return redirect()->route('mes-classes.show', $classe)
->with('classe_creee', true);
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers;
use App\Models\Classe;
use App\Models\LabSession;
use App\Models\Seance;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class SeanceController extends Controller
{
public function store(Request $request, Classe $classe): RedirectResponse
{
abort_if($classe->user_id !== Auth::guard('web')->id(), 403);
$donnees = $request->validate([
'lab_id' => ['required', 'exists:labs,id'],
]);
$seance = $classe->seances()->create([
'lab_id' => $donnees['lab_id'],
'statut' => Seance::SALLE_ATTENTE,
'demarree_a' => now(),
]);
return redirect()->route('seances.attente', $seance);
}
public function attente(Seance $seance): View
{
abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403);
return view('seances.attente', [
'seance' => $seance->load(['classe', 'lab', 'labSessions.eleve']),
]);
}
public function admettre(Seance $seance, LabSession $labSession): RedirectResponse
{
abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403);
abort_if($labSession->seance_id !== $seance->id, 404);
$labSession->update(['etat' => LabSession::ADMIS]);
return redirect()->route('seances.attente', $seance);
}
/**
* Un élève rejoint la séance : il patiente jusqu'à ce que le prof l'admette.
*/
public function rejoindre(Request $request, Seance $seance): RedirectResponse
{
$eleve = Auth::guard('eleve')->user();
abort_if($eleve->classe_id !== $seance->classe_id, 403);
LabSession::firstOrCreate(
['seance_id' => $seance->id, 'eleve_id' => $eleve->id],
['lab_id' => $seance->lab_id, 'etat' => LabSession::EN_ATTENTE],
);
return redirect()->route('seances.eleve.attente', $seance);
}
public function attenteEleve(Seance $seance): View|RedirectResponse
{
$eleve = Auth::guard('eleve')->user();
abort_if($eleve->classe_id !== $seance->classe_id, 403);
$labSession = LabSession::where('seance_id', $seance->id)
->where('eleve_id', $eleve->id)
->first();
if ($labSession === null) {
return redirect()->route('eleve');
}
return view('seances.attente-eleve', [
'seance' => $seance->load('lab'),
'labSession' => $labSession,
]);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
/**
* Une seule session active par compte (étape 8). Si le jeton que porte cette
* session ne correspond plus à celui enregistré sur le compte, c'est qu'une
* connexion plus récente a eu lieu ailleurs : on déconnecte et on le dit.
*/
class VerifieSessionUnique
{
public function handle(Request $request, Closure $next, string $garde = 'web'): Response
{
$utilisateur = Auth::guard($garde)->user();
if ($utilisateur !== null) {
$cleSession = "jeton_session_{$garde}";
if (! $utilisateur->sessionEstEncoreValide($request->session()->get($cleSession))) {
Auth::guard($garde)->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')
->with('session_coupee', 'Vous avez été déconnecté : une nouvelle connexion a eu lieu sur votre compte.');
}
}
return $next($request);
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Classe extends Model
{
use HasFactory;
protected $fillable = ['etablissement_id', 'user_id', 'nom', 'formation', 'code_classe'];
protected $casts = [
'archivee_a' => 'datetime',
];
protected static function booted(): void
{
static::creating(function (Classe $classe) {
$classe->code_classe ??= static::genererCodeUnique();
});
}
public static function genererCodeUnique(): string
{
do {
$code = Str::upper(Str::random(6));
} while (static::where('code_classe', $code)->exists());
return $code;
}
public function etablissement(): BelongsTo
{
return $this->belongsTo(Etablissement::class);
}
public function prof(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function eleves(): HasMany
{
return $this->hasMany(Eleve::class);
}
public function seances(): HasMany
{
return $this->hasMany(Seance::class);
}
public function estArchivee(): bool
{
return $this->archivee_a !== null;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Models\Concerns;
use Illuminate\Support\Str;
/**
* Une seule session active par compte (étape 8) : la précédente est coupée
* et son porteur en est averti à sa prochaine requête (voir
* App\Http\Middleware\VerifieSessionUnique).
*/
trait GereSessionUnique
{
public function demarreNouvelleSession(): string
{
$jeton = Str::random(40);
$this->forceFill(['jeton_session_actif' => $jeton])->save();
return $jeton;
}
public function sessionEstEncoreValide(?string $jeton): bool
{
return $jeton !== null && $jeton === $this->jeton_session_actif;
}
public function terminerSession(): void
{
$this->forceFill(['jeton_session_actif' => null])->save();
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Models;
use App\Models\Concerns\GereSessionUnique;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Eleve extends Model implements AuthenticatableContract
{
use Authenticatable, GereSessionUnique, HasFactory;
protected $fillable = ['classe_id', 'prenom', 'nom', 'identifiant', 'code_secret', 'jeton_qr'];
protected $hidden = ['code_secret', 'jeton_session_actif', 'jeton_qr'];
protected $casts = [
'derniere_connexion_a' => 'datetime',
];
public function getAuthPasswordName(): string
{
return 'code_secret';
}
public function classe(): BelongsTo
{
return $this->belongsTo(Classe::class);
}
public function labSessions(): HasMany
{
return $this->hasMany(LabSession::class);
}
public function nomAffiche(): string
{
return $this->nom
? $this->prenom.' '.mb_substr($this->nom, 0, 1).'.'
: $this->prenom;
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Etablissement extends Model
{
use HasFactory;
protected $fillable = ['nom', 'ville', 'uai'];
public function profs(): HasMany
{
return $this->hasMany(User::class);
}
public function classes(): HasMany
{
return $this->hasMany(Classe::class);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Lab extends Model
{
use HasFactory;
protected $fillable = ['identifiant', 'titre', 'niveau', 'duree_min'];
public function seances(): HasMany
{
return $this->hasMany(Seance::class);
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LabSession extends Model
{
use HasFactory;
public const EN_ATTENTE = 'en_attente';
public const ADMIS = 'admis';
public const ACTIF = 'actif';
public const TERMINE = 'termine';
public const EXPIRE = 'expire';
protected $fillable = ['seance_id', 'eleve_id', 'lab_id', 'stack_id', 'etat', 'expire_a'];
protected $casts = [
'expire_a' => 'datetime',
];
public function seance(): BelongsTo
{
return $this->belongsTo(Seance::class);
}
public function eleve(): BelongsTo
{
return $this->belongsTo(Eleve::class);
}
public function lab(): BelongsTo
{
return $this->belongsTo(Lab::class);
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Seance extends Model
{
use HasFactory;
public const PLANIFIEE = 'planifiee';
public const SALLE_ATTENTE = 'salle_attente';
public const EN_COURS = 'en_cours';
public const TERMINEE = 'terminee';
protected $fillable = ['classe_id', 'lab_id', 'statut', 'demarree_a', 'terminee_a'];
protected $casts = [
'demarree_a' => 'datetime',
'terminee_a' => 'datetime',
];
public function classe(): BelongsTo
{
return $this->belongsTo(Classe::class);
}
public function lab(): BelongsTo
{
return $this->belongsTo(Lab::class);
}
public function labSessions(): HasMany
{
return $this->hasMany(LabSession::class);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Models\Concerns\GereSessionUnique;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['name', 'email', 'password', 'etablissement_id'])]
#[Hidden(['password', 'remember_token', 'jeton_session_actif'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use GereSessionUnique, HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
public function etablissement(): BelongsTo
{
return $this->belongsTo(Etablissement::class);
}
public function classes(): HasMany
{
return $this->hasMany(Classe::class);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Services;
use App\Models\Eleve;
use Illuminate\Support\Str;
/**
* Génère les identifiants (prénom.initiale, avec un numéro pour les
* homonymes) et les codes secrets des élèves (étape 8 du déploiement).
*/
class GenerateurCompteEleve
{
// Sans 0/O ni 1/I/L, pour qu'un élève ne confonde pas à la lecture.
private const ALPHABET_CODE = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
public function genererIdentifiant(string $prenom, ?string $nom): string
{
$base = Str::slug($prenom, '');
if ($nom !== null && $nom !== '') {
$base .= '.'.Str::slug(mb_substr($nom, 0, 1));
}
$identifiant = $base;
$suffixe = 2;
while (Eleve::where('identifiant', $identifiant)->exists()) {
$identifiant = $base.$suffixe;
$suffixe++;
}
return $identifiant;
}
public function genererCodeSecret(): string
{
$code = '';
for ($i = 0; $i < 6; $i++) {
$code .= self::ALPHABET_CODE[random_int(0, strlen(self::ALPHABET_CODE) - 1)];
}
return $code;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Support;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
/**
* QR code en SVG (pas de dépendance Imagick/GD), pour les fiches de
* connexion imprimables de l'étape 8.
*/
class QrCode
{
public static function svg(string $contenu, int $taille = 180): string
{
$rendu = new ImageRenderer(new RendererStyle($taille), new SvgImageBackEnd);
return (new Writer($rendu))->writeString($contenu);
}
}

18
portail/artisan Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

23
portail/bootstrap/app.php Normal file
View file

@ -0,0 +1,23 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'session.unique' => \App\Http\Middleware\VerifieSessionUnique::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
})->create();

2
portail/bootstrap/cache/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -0,0 +1,7 @@
<?php
use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
];

92
portail/composer.json Normal file
View file

@ -0,0 +1,92 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.3",
"bacon/bacon-qr-code": "^3.1",
"laravel/framework": "^13.17",
"laravel/tinker": "^3.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.5",
"laravel/pao": "^1.0.6",
"laravel/pint": "^1.27",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.0",
"pestphp/pest-plugin-laravel": "^4.0",
"phpunit/phpunit": "^12.5.12"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install --ignore-scripts",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"@php artisan dev"
],
"test": [
"@php artisan config:clear --ansi @no_additional_args",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
},
"platform": {
"php": "8.3.0"
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

9820
portail/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

126
portail/config/app.php Normal file
View file

@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache", "array"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];

130
portail/config/auth.php Normal file
View file

@ -0,0 +1,130 @@
<?php
use App\Models\Eleve;
use App\Models\User;
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// Les élèves ne sont pas des "users" : identifiant + code secret,
// jamais d'e-mail (voir CLAUDE.md, minimisation des données mineurs).
'eleve' => [
'driver' => 'session',
'provider' => 'eleves',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
'eleves' => [
'driver' => 'eloquent',
'model' => Eleve::class,
],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

136
portail/config/cache.php Normal file
View file

@ -0,0 +1,136 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "storage", "octane",
| "session", "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/locks'),
],
'storage' => [
'driver' => 'storage',
'disk' => env('CACHE_STORAGE_DISK'),
'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
/*
|--------------------------------------------------------------------------
| Serializable Classes
|--------------------------------------------------------------------------
|
| This value determines the classes that can be unserialized from cache
| storage. By default, no PHP classes will be unserialized from your
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
*/
'serializable_classes' => false,
];

184
portail/config/database.php Normal file
View file

@ -0,0 +1,184 @@
<?php
use Illuminate\Support\Str;
use Pdo\Mysql;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];

View file

@ -0,0 +1,80 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

140
portail/config/logging.php Normal file
View file

@ -0,0 +1,140 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "monthly", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'max_files' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'monthly' => [
'driver' => 'monthly',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'max_files' => 3,
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

118
portail/config/mail.php Normal file
View file

@ -0,0 +1,118 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'log'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
],
];

129
portail/config/queue.php Normal file
View file

@ -0,0 +1,129 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue supports a variety of backends via a single, unified
| API, giving you convenient access to each backend using identical
| syntax for each. The default queue connection is defined below.
|
*/
'default' => env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Laravel. You're also free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];

View file

@ -0,0 +1,38 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Resend, Postmark, AWS, and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];

233
portail/config/session.php Normal file
View file

@ -0,0 +1,233 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option determines the default session driver that is utilized for
| incoming requests. Laravel supports a variety of storage options to
| persist session data. Database storage is a great default choice.
|
| Supported: "file", "cookie", "database", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain without subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
/*
|--------------------------------------------------------------------------
| Session Serialization
|--------------------------------------------------------------------------
|
| This value controls the serialization strategy for session data, which
| is JSON by default. Setting this to "php" allows the storage of PHP
| objects in the session but can make an application vulnerable to
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
| Supported: "json", "php"
|
*/
'serialization' => 'json',
];

1
portail/database/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.sqlite*

View file

@ -0,0 +1,20 @@
<?php
namespace Database\Factories;
use App\Models\Etablissement;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class ClasseFactory extends Factory
{
public function definition(): array
{
return [
'etablissement_id' => Etablissement::factory(),
'user_id' => User::factory(),
'nom' => 'BTS SIO 1 · Groupe '.fake()->randomLetter(),
'formation' => 'BTS SIO SISR',
];
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\Classe;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class EleveFactory extends Factory
{
public function definition(): array
{
$prenom = fake()->firstName();
return [
'classe_id' => Classe::factory(),
'prenom' => $prenom,
'nom' => fake()->lastName(),
'identifiant' => Str::slug($prenom, '').'.'.Str::random(4),
'code_secret' => Hash::make('123456'),
'jeton_qr' => Str::random(32),
];
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class EtablissementFactory extends Factory
{
public function definition(): array
{
return [
'nom' => 'Lycée '.fake()->lastName(),
'ville' => fake()->city(),
'uai' => fake()->bothify('0###??#'),
];
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class LabFactory extends Factory
{
public function definition(): array
{
$titre = fake()->unique()->sentence(3);
return [
'identifiant' => Str::slug($titre),
'titre' => $titre,
'niveau' => 'decouverte',
'duree_min' => 45,
];
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Database\Factories;
use App\Models\Etablissement;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
'etablissement_id' => Etablissement::factory(),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}

View file

@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('sessions');
}
};

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};

View file

@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedSmallInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->string('connection');
$table->string('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
$table->index(['connection', 'queue', 'failed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};

View file

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('etablissements', function (Blueprint $table) {
$table->id();
$table->string('nom');
$table->string('ville')->nullable();
$table->string('uai')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('etablissements');
}
};

View file

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('classes', function (Blueprint $table) {
$table->id();
$table->foreignId('etablissement_id')->constrained();
$table->foreignId('user_id')->constrained()->comment('le prof qui a créé la classe');
$table->string('nom');
$table->string('formation')->nullable();
$table->string('code_classe', 8)->unique()->comment('code que les élèves peuvent utiliser pour rejoindre (V2)');
$table->timestamp('archivee_a')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('classes');
}
};

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('eleves', function (Blueprint $table) {
$table->id();
$table->foreignId('classe_id')->constrained()->cascadeOnDelete();
$table->string('prenom');
$table->string('nom')->nullable()->comment('facultatif, visible uniquement du prof');
$table->string('identifiant')->unique()->comment('format prenom.initiale, suffixe numérique si homonyme');
$table->string('code_secret')->comment('haché, 6 caractères en clair à la génération');
$table->string('jeton_qr')->nullable()->unique()->comment('QR à usage unique de la fiche de connexion, vidé après le premier usage');
$table->string('jeton_session_actif')->nullable();
$table->timestamp('derniere_connexion_a')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('eleves');
}
};

View file

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('labs', function (Blueprint $table) {
$table->id();
$table->string('identifiant')->unique()->comment('ex: 08-deux-lan-routeur, nom du dossier paillasse-contenus');
$table->string('titre');
$table->string('niveau')->nullable();
$table->unsignedSmallInteger('duree_min')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('labs');
}
};

View file

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('seances', function (Blueprint $table) {
$table->id();
$table->foreignId('classe_id')->constrained()->cascadeOnDelete();
$table->foreignId('lab_id')->constrained();
$table->string('statut')->default('planifiee')->comment('planifiee, salle_attente, en_cours, terminee');
$table->timestamp('demarree_a')->nullable();
$table->timestamp('terminee_a')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('seances');
}
};

View file

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('lab_sessions', function (Blueprint $table) {
$table->id();
$table->foreignId('seance_id')->nullable()->constrained()->cascadeOnDelete();
$table->foreignId('eleve_id')->constrained()->cascadeOnDelete();
$table->foreignId('lab_id')->constrained();
$table->string('stack_id')->nullable()->comment('id de la stack Heat / des VM, rempli à l’étape 10');
$table->string('etat')->default('en_attente')->comment('en_attente, admis, actif, termine, expire');
$table->timestamp('expire_a')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('lab_sessions');
}
};

View file

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->foreignId('etablissement_id')->nullable()->after('id')->constrained();
$table->string('jeton_session_actif')->nullable()->after('remember_token');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropConstrainedForeignId('etablissement_id');
$table->dropColumn('jeton_session_actif');
});
}
};

View file

@ -0,0 +1,25 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
}
}

2147
portail/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

18
portail/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"$schema": "https://www.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"laravel-vite-plugin": "^3.1",
"tailwindcss": "^4.0.0",
"vite": "^8.0.0"
},
"optionalDependencies": {
"@laravel/multiplex": "^0.4.1"
}
}

36
portail/phpunit.xml Normal file
View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_URL" value=""/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
</php>
</phpunit>

25
portail/public/.htaccess Normal file
View file

@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

View file

@ -0,0 +1,151 @@
/* Syllabus — thème global. Surcharge les anciennes classes néo-brutalistes pour appliquer la DA "SaaS doux" de la bibliothèque v2. */
@import url('https://fonts.googleapis.com/css2?family=Mulish:wght@400;500;600;700;800&display=swap');
:root { --sy-bg:#f5f6f8; --sy-surface:#fff; --sy-line:#b8c1cf; --sy-ink:#111827; --sy-muted:#6b7280; --sy-blue:#1d5cf5; --sy-blue-dark:#174bd0; --sy-navy:#1e3a8a; }
html, body { background: var(--sy-bg) !important; color: var(--sy-ink) !important; font-family: Mulish, system-ui, sans-serif !important; }
body * { font-family: inherit; }
code, pre, kbd, .font-mono, [class*="font-mono"] { font-family: ui-monospace, "IBM Plex Mono", Menlo, monospace !important; }
a { color: inherit; } a:hover { color: var(--sy-blue); }
/* Bordures : fines et grises, coins arrondis */
[class~="border-4"]:not([class~="border-transparent"]), [class~="border-2"]:not([class~="border-transparent"]), [class~="border"] { border-width: 1px !important; border-color: var(--sy-line) !important; }
[class~="border-b-4"], [class~="border-b-2"] { border-bottom-width: 1px !important; border-bottom-color: var(--sy-line) !important; }
[class~="border-t-4"], [class~="border-t-2"] { border-top-width: 1px !important; border-top-color: var(--sy-line) !important; }
[class~="border-r-2"], [class~="border-l-2"] { border-color: var(--sy-line) !important; }
[class~="divide-black"] > * + * { border-color: var(--sy-line) !important; }
section[class*="border"], article[class*="border"], aside[class*="border"], div[class~="border-4"], div[class~="border-2"], li[class~="border-2"], figure[class*="border"], details[class*="border"], table { border-radius: 12px; }
a[class~="border-2"], button[class~="border-2"], summary[class~="border-2"], span[class~="border-2"], input, select, textarea { border-radius: 6px !important; }
[class*="border-dashed"] { border-style: dashed !important; }
/* Ombres dures → ombres douces ; plus de rotations ni de décors flottants */
[class*="shadow-["] { box-shadow: 0 1px 3px rgba(17,24,39,.06) !important; }
[class*="hover:shadow-["]:hover { box-shadow: 0 6px 18px rgba(17,24,39,.08) !important; }
[class*="rotate-"], [class*="-rotate-"] { transform: none !important; }
[aria-hidden="true"][class*="absolute"][class*="pointer-events-none"] { display: none !important; }
/* Palette : jaune/vert/rose/lavande fluo → pastels ; rouge/noir d'action → bleu */
[class~="bg-[#FFD23F]"] { background-color: #dbe6ff !important; color: var(--sy-navy) !important; }
[class~="bg-[#7CF2C0]"] { background-color: #dcfce7 !important; color: #166534 !important; }
[class~="bg-[#FF6FB5]"] { background-color: #fce7f3 !important; color: #9d174d !important; }
[class~="bg-[#C4B5FD]"] { background-color: #ede4ff !important; color: #5b21b6 !important; }
[class~="bg-[#FFFBEA]"], [class~="bg-[#F6F5F1]"], [class*="hover:bg-[#FFFBEA]"]:hover { background-color: #f1f2f5 !important; }
[class~="bg-[#ec3013]"] { background-color: var(--sy-blue) !important; color: #fff !important; }
[class~="bg-black"]:not(pre):not([class*="h-2"]):not([class*="h-3"]):not([class*="h-full"]) { background-color: var(--sy-blue) !important; color: #fff !important; }
[class*="hover:bg-black"]:hover { background-color: var(--sy-blue-dark) !important; color: #fff !important; }
[class~="bg-[#13233A]"] { background-color: #0f172a !important; border-radius: 12px; }
[class*="text-[#b3001b]"], [class*="text-[#ec3013]"] { color: #b91c1c !important; }
[class~="bg-[#F6F5F1]"] { background-color: #f8f9fb !important; }
body > header, header[class*="border-b"] { background: #fff !important; box-shadow: 0 1px 0 var(--sy-line) !important; }
/* Titres un peu moins massifs */
h1[class*="font-black"] { letter-spacing: -0.015em !important; }
[class*="font-black"] { font-weight: 800 !important; }
[class*="uppercase"][class*="tracking-wider"] { color: var(--sy-muted); }
/* Focus */
:focus-visible { outline: 2px solid var(--sy-blue) !important; outline-offset: 2px; }
/* États : page courante, menu ouvert, survol */
[aria-current="page"][class*="aria-[current=page]:bg-[#FFD23F]"], details[open] > [class*="group-open:bg-[#FFD23F]"] { background-color: #dbe6ff !important; color: var(--sy-blue) !important; border-color: transparent !important; border-radius: 6px; }
[aria-current="page"][class*="aria-[current=page]:border-black"] { border-color: transparent !important; }
[class*="hover:bg-[#FFD23F]"]:hover { background-color: #dbe6ff !important; }
[class*="hover:border-black"]:hover { border-color: var(--sy-line) !important; }
/* Ton posé : titres plus mesurés, moins de couleur, pas d'emoji ni d'effets */
h1:not([style*="font-size"]) { font-size: clamp(28px, 3.2vw, 40px) !important; line-height: 1.15 !important; font-weight: 800 !important; }
h2:not([style*="font-size"]) { font-size: clamp(22px, 2.2vw, 28px) !important; line-height: 1.2 !important; }
h3:not([style*="font-size"]) { font-size: 18px !important; line-height: 1.3 !important; }
[class*="text-7xl"], [class*="text-8xl"], [class*="text-6xl"] { font-size: clamp(30px, 3.4vw, 42px) !important; line-height: 1.15 !important; }
[class*="text-5xl"] { font-size: clamp(26px, 2.6vw, 32px) !important; }
[class*="leading-[0.9"], [class*="leading-[0.8"] { line-height: 1.15 !important; }
[class*="tracking-tighter"] { letter-spacing: -0.01em !important; }
[class*="hover:-translate-y"]:hover, [class*="hover:translate"]:hover { transform: none !important; }
[class*="animate-"] { animation: none !important; }
section[class~="bg-[#ec3013]"], section[class~="bg-black"], div[class~="bg-black"][class*="p-8"], div[class~="bg-black"][class*="p-6"] { background: #fff !important; color: var(--sy-ink) !important; border: 1px solid var(--sy-line) !important; }
section[class~="bg-[#ec3013]"] *, div[class~="bg-black"][class*="p-8"] *, div[class~="bg-black"][class*="p-6"] * { color: inherit; }
[class*="bg-[#FFD23F]"][class*="px-3"][class*="uppercase"] { background: transparent !important; color: var(--sy-blue) !important; padding-left: 0 !important; box-shadow: none !important; border: 0 !important; }
[class*="E6FF3F"],[class*="E8FF3C"]{background-color:#eef2ff!important;color:#1e3a8a!important}
/* Contraste : fond de page plus sombre, contours visibles partout, traits fins */
html, body { background: #eef1f5 !important; }
main section.bg-white, main section[class*="bg-white"] { background: #eef1f5 !important; }
[class*="border-4"], [class*="border-2"], [class*="border-black"], [class*="border-y-4"], [class*="border-t-4"], [class*="border-b-4"] { border-color: #b8c1cf !important; }
[class*="border-4"] { border-width: 1px !important; }
[class*="border-2"] { border-width: 1px !important; }
[class*="border-y-4"] { border-top-width: 1px !important; border-bottom-width: 1px !important; }
[class*="border-t-4"] { border-top-width: 1px !important; }
[class*="border-b-4"] { border-bottom-width: 1px !important; }
[class*="border-r-4"], [class*="border-l-4"] { border-width: 0 1px 0 0 !important; }
[class*="divide-black"] > * + * { border-color: #b8c1cf !important; }
main article, main figure, main li[class*="border"], main div[class*="border"][class*="bg-"] { box-shadow: 0 1px 2px rgba(17,24,39,.06), 0 2px 8px rgba(17,24,39,.05) !important; }
[class*="bg-white"][class*="border"], figure[style*="border"] { background: #fff !important; }
/* Badges collés aux cartes : dans le flux, alignés en haut */
main article > span[class*="absolute"], main li > span[class*="absolute"], main div[class*="relative"] > span[class*="absolute"][class*="border"] { position: static !important; display: inline-block !important; margin: 0 0 12px 0 !important; transform: none !important; }
header a, header summary, nav a, header [class*="border-transparent"], nav [class*="border-transparent"] { border-color: transparent !important; }
header a[aria-current="page"], header details[open] > summary { border-color: #bfd0f5 !important; }
header ul[class*="absolute"] { border: 1px solid #b8c1cf !important; }
main [class*="border-4"], main [class*="border-2"]:not([class*="border-transparent"]) { border-color: #b8c1cf !important; }
main li[class*="min-h-[340px]"] { min-height: 0 !important; }
main li[class*="min-h-[340px]"] > *:first-child + * { margin-top: 12px !important; }
h1 span[class*="bg-"] { background: transparent !important; border: 0 !important; padding: 0 !important; color: #1d4ed8 !important; }
[class*="border-transparent"] { border-color: transparent !important; }
[class*="border-transparent"][aria-current="page"], [class*="border-transparent"]:hover { border-color: transparent !important; background: #e8eefc !important; }
aside a[class*="border-black"][class*="bg-black"] { border-color: transparent !important; }
/* === SOBRIÉTÉ (inspiration Google Workspace) : l'espace et la typographie structurent, un seul appel à l'action fort === */
:root { --sy-ink:#0b1220; --sy-text:#3c4043; --sy-hair:#e3e5e8; --sy-soft:#f8f9fa; --sy-blue:#0b57d0; --sy-blue-dark:#0842a0; }
html, body { background:#fff !important; }
main p { color: var(--sy-text); }
main h1, main h2, main h3 { color: var(--sy-ink) !important; text-wrap: balance; }
main h1:not([style*="line-height"]) { font-weight:800 !important; line-height:1.06 !important; }
main p, main li { text-wrap: pretty; }
/* Fonds : blanc, et un gris très léger pour alterner les sections */
main section[class*="bg-"], main div[class~="bg-[#F6F5F1]"], main div[class~="bg-[#FFFBEA]"] { background-color:#fff !important; }
main section[class~="bg-[#F6F5F1]"], main section[class~="bg-[#FFFBEA]"] { background-color: var(--sy-soft) !important; }
main section { border-color: transparent !important; box-shadow:none !important; }
/* Pages vitrine : beaucoup plus d'air */
main > section > div[class*="py-"] { padding-top: clamp(64px, 9vw, 112px) !important; padding-bottom: clamp(64px, 9vw, 112px) !important; }
main > section > div[class*="max-w-"] > p[class*="text-xl"], main > section > div[class*="max-w-"] p[class*="text-lg"] { line-height:1.65 !important; }
/* Cartes et encadrés : trait très fin, grands rayons, aucune ombre */
[class~="border-4"]:not([class~="border-transparent"]), [class~="border-2"]:not([class~="border-transparent"]), [class~="border"] { border-color: var(--sy-hair) !important; }
main article[class*="border"], main li[class~="border-2"], main div[class~="border-2"], main div[class~="border-4"], main figure[class*="border"], main details[class*="border"] { border-radius:16px !important; }
[class*="shadow-["], [class*="shadow-md"], [class*="shadow-lg"] { box-shadow:none !important; }
[class*="rotate-"], [class*="-rotate-"], [class*="md:rotate"], [class*="md:-rotate"], [class*="translate-y-8"] { transform:none !important; }
main [aria-hidden="true"][class*="absolute"] { display:none !important; }
/* Badges et pastilles : neutres, texte foncé */
main span[class~="border-2"][class*="text-xs"], main span[class~="border-2"][class*="text-sm"][class*="px-2"], main span[class*="rounded-full"][class*="px-"] { background:#f1f3f4 !important; color:#3c4043 !important; border-color:transparent !important; border-radius:999px !important; }
[class~="bg-[#FFD23F]"], [class~="bg-[#7CF2C0]"], [class~="bg-[#FF6FB5]"], [class~="bg-[#C4B5FD]"] { background-color:#f1f3f4 !important; color:#202124 !important; }
main div[class*="bg-[#FFD23F]"][class*="p-"], main div[class*="bg-[#7CF2C0]"][class*="p-"], main div[class*="bg-[#C4B5FD]"][class*="p-"], main div[class*="bg-[#FF6FB5]"][class*="p-"] { background: var(--sy-soft) !important; }
/* Boutons : pilules ; l'action principale en bleu plein, le reste en contour discret */
a[class*="inline-flex"][class*="h-11"], a[class*="inline-flex"][class*="h-12"], a[class*="inline-flex"][class*="h-10"], button[class*="h-11"], button[class*="h-12"], button[class*="h-10"] { border-radius:999px !important; padding-left:24px !important; padding-right:24px !important; font-weight:700 !important; }
a[class~="bg-[#ec3013]"], button[class~="bg-[#ec3013]"], a[class~="bg-black"], button[class~="bg-black"] { background-color: var(--sy-blue) !important; border-color: var(--sy-blue) !important; color:#fff !important; }
a[class~="bg-[#ec3013]"]:hover, button[class~="bg-[#ec3013]"]:hover, a[class~="bg-black"]:hover, button[class~="bg-black"]:hover { background-color: var(--sy-blue-dark) !important; }
main a[class~="bg-white"][class*="inline-flex"], main button[class~="bg-white"][class*="h-"] { background:#fff !important; color: var(--sy-blue) !important; border:1px solid #dadce0 !important; }
main a[class~="bg-white"][class*="inline-flex"]:hover, main button[class~="bg-white"][class*="h-"]:hover { background:#f6f9fe !important; }
/* En-tête : conservé, épuré */
body > header { background:#fff !important; box-shadow:none !important; border-bottom:1px solid var(--sy-hair) !important; }
body > header nav a, body > header nav summary { font-weight:600 !important; color:#3c4043 !important; text-decoration:none !important; }
body > header nav a:hover, body > header nav summary:hover { color: var(--sy-ink) !important; background:transparent !important; }
body > header nav ul a[aria-current="page"] { color: var(--sy-blue) !important; background:transparent !important; }
body > header a[href*="inscription"] { background: var(--sy-blue) !important; color:#fff !important; border:0 !important; border-radius:999px !important; padding:0 22px !important; }
body > header a[href*="inscription"] > span[aria-hidden="true"] { display:none !important; }
body > header a[href*="inscription"]:hover { background: var(--sy-blue-dark) !important; color:#fff !important; }
body > header a[href*="connexion"] { border:0 !important; background:transparent !important; }
[class*="text-[#FFD23F]"], [class*="text-[#7CF2C0]"], [class*="text-[#FF6FB5]"], [class*="text-[#C4B5FD]"], [class*="text-[#E6FF3F]"], [class*="text-[#E8FF3C]"] { color: var(--sy-blue) !important; }
main li[class*="min-h-[340px]"] { min-height:0 !important; aspect-ratio:auto !important; }
main li[class*="min-h-[340px]"] > div[class*="justify-between"] { justify-content:flex-start !important; gap:16px !important; padding-bottom:0 !important; }
main li[class*="min-h-[340px]"] > div > span[class*="self-end"] { align-self:flex-start !important; font-size:40px !important; color: var(--sy-blue) !important; }
body > header a[href*="connexion"] { border:0 !important; background:transparent !important; box-shadow:none !important; padding-left:8px !important; padding-right:8px !important; }
main h1:not([style*="letter-spacing"]), main h2:not([style*="letter-spacing"]) { letter-spacing:-0.02em !important; }
header a[style*="background:#0b57d0"], header a[style*="background: #0b57d0"], nav a[style*="background:#0b57d0"] { color: #fff !important; }
header a[style*="background:#0b57d0"]:hover, nav a[style*="background:#0b57d0"]:hover { color: #fff !important; background: #0842a0 !important; }

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

20
portail/public/index.php Normal file
View file

@ -0,0 +1,20 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());

View file

@ -0,0 +1,95 @@
// Cours complets · lot 1 : représentation des données, terminal, droits Unix
window.CONTENUS_PRO = (window.CONTENUS_PRO || []).concat([
{ id: 'binaire', match: /binaire|hexad|représentation des (données|entiers)/i,
titre: 'Représentation des entiers : binaire et hexadécimal', duree: '4 h (CM 1 h 30 · TD 1 h 30 · TP 1 h)', niveau: 'NSI Première · BTS SIO / CIEL 1re année · BUT R&T / Info S1',
prerequis: ['Puissances de 10 et de 2', 'Division euclidienne'],
competences: ['NSI 1re · Représentation des données : types et valeurs de base', 'BTS SIO · Bloc 1 · Exploiter les éléments d’un réseau (adressage)', 'BUT R&T · Compétence « Administrer » · niveau 1'],
intro: 'Tout ce qu’un ordinateur stocke, du texte aux images en passant par les adresses IP, finit en suites de 0 et de 1. Savoir passer d’une base à l’autre est un geste de base de l’informaticien : il sert à calculer un masque de sous-réseau, lire une adresse MAC, choisir une couleur web ou comprendre un débordement d’entier.',
cm: [
{ t: 'Les systèmes de numération', d: '15 min', html: 'Un système de numération positionnel en base <b>b</b> utilise b chiffres (de 0 à b − 1). La valeur d’un chiffre dépend de sa position : le chiffre de rang k vaut chiffre × b<sup>k</sup>, les rangs étant comptés à partir de 0, depuis la droite.<br><br>En base 10 : 2 047 = 2×10³ + 0×10² + 4×10¹ + 7×10⁰. En base 2, seuls 0 et 1 existent. En base 16, on utilise 0-9 puis A (10), B (11), C (12), D (13), E (14), F (15).', table: [['Base', 'Nom', 'Chiffres', 'Notation'], ['2', 'binaire', '0 1', '0b1011 ou 1011₂'], ['10', 'décimal', '0 à 9', '11'], ['16', 'hexadécimal', '0 à 9, A à F', '0xB ou B₁₆']] },
{ t: 'Bits, octets et ordres de grandeur', d: '10 min', html: 'Un <b>bit</b> (binary digit) prend la valeur 0 ou 1. Avec n bits on code 2<sup>n</sup> valeurs, de 0 à 2<sup>n</sup> − 1. Un <b>octet</b> vaut 8 bits, donc 256 valeurs (0 à 255). Les préfixes normalisés sont décimaux (1 ko = 1 000 octets) ; les préfixes binaires s’écrivent Kio, Mio, Gio (1 Kio = 1 024 octets).', attention: 'Un disque de « 1 To » affiche environ 931 Gio dans le système : ce n’est pas une arnaque, c’est la différence entre 10¹² et 2⁴⁰.' },
{ t: 'Du binaire au décimal', d: '15 min', html: 'On additionne les poids des bits à 1. Les poids d’un octet sont 128, 64, 32, 16, 8, 4, 2, 1.', exemple: '1011 0010₂ = 128 + 32 + 16 + 2 = 178\n0100 0001₂ = 64 + 1 = 65 (code ASCII de « A »)' },
{ t: 'Du décimal au binaire', d: '20 min', html: '<b>Méthode 1, soustractions :</b> on retire la plus grande puissance de 2 possible, on écrit 1 à sa place, 0 sinon, et on recommence avec le reste.<br><b>Méthode 2, divisions successives :</b> on divise par 2 jusqu’à obtenir 0 ; les restes lus de bas en haut donnent le nombre binaire.', exemple: '45 ÷ 2 = 22 reste 1\n22 ÷ 2 = 11 reste 0\n11 ÷ 2 = 5 reste 1\n 5 ÷ 2 = 2 reste 1\n 2 ÷ 2 = 1 reste 0\n 1 ÷ 2 = 0 reste 1 → lecture de bas en haut : 101101₂', attention: 'On lit les restes de bas en haut. L’erreur la plus fréquente en devoir est de les lire dans l’ordre du calcul.' },
{ t: 'L’hexadécimal', d: '15 min', html: 'Comme 16 = 2⁴, un chiffre hexadécimal correspond exactement à un groupe de 4 bits (un <i>quartet</i>). Pour convertir, on découpe le binaire en groupes de 4 à partir de la droite.', table: [['Binaire', '0000', '0101', '1001', '1010', '1100', '1111'], ['Hexa', '0', '5', '9', 'A', 'C', 'F']], exemple: '1011 0010₂ → B 2 → 0xB2\n#FF6FB5 → rouge FF = 255, vert 6F = 111, bleu B5 = 181' },
{ t: 'Où les retrouve-t-on ?', d: '15 min', html: '<b>Adresses IPv4 :</b> 4 octets écrits en décimal pointé (192.168.10.65). <b>Masques :</b> /26 = 26 bits à 1 = 255.255.255.192. <b>Adresses MAC :</b> 6 octets en hexadécimal (3C:52:82:1A:0F:9B). <b>IPv6 :</b> 8 groupes de 16 bits en hexadécimal. <b>Couleurs web :</b> #RRVVBB. <b>Droits Unix :</b> notation octale, base 8 (chmod 755).' }],
synthese: ['n bits codent 2ⁿ valeurs, de 0 à 2ⁿ − 1', 'Poids d’un octet : 128 64 32 16 8 4 2 1', 'Décimal → binaire : divisions par 2, restes lus de bas en haut', '1 chiffre hexadécimal = 4 bits : on regroupe par 4 depuis la droite'],
qcm: [
['Combien de valeurs différentes peut-on coder sur 10 bits ?', ['10', '100', '1 000', '1 024'], 3, '2¹⁰ = 1 024.'],
['Que vaut 1100 1000₂ en décimal ?', ['200', '192', '208', '136'], 0, '128 + 64 + 8 = 200.'],
['Comment s’écrit 172 en binaire ?', ['1010 1100', '1011 0100', '1100 1010', '1010 1010'], 0, '128 + 32 + 8 + 4 = 172.'],
['Que vaut 0x3F en décimal ?', ['48', '63', '53', '36'], 1, '3 × 16 + 15 = 63.'],
['Quel est le plus grand nombre codable sur 16 bits non signés ?', ['32 767', '65 535', '65 536', '16 383'], 1, '2¹⁶ − 1 = 65 535.'],
['255.255.255.224 correspond à quel préfixe CIDR ?', ['/24', '/26', '/27', '/28'], 2, '224 = 1110 0000 : 24 + 3 = 27 bits à 1.'],
['Combien d’octets compte une adresse MAC ?', ['4', '6', '8', '16'], 1, '48 bits = 6 octets.'],
['Un disque vendu 500 Go contient environ :', ['500 Gio', '465 Gio', '512 Gio', '536 Gio'], 1, '500 × 10⁹ / 2³⁰ ≈ 465.']],
td: [
{ n: 'Échauffement', c: 'Convertir en binaire sur 8 bits : 7, 64, 99, 128, 250.', r: '0000 0111 · 0100 0000 · 0110 0011 · 1000 0000 · 1111 1010' },
{ n: 'Échauffement', c: 'Convertir en décimal : 1010 1010₂, 0x7F, 0xC0, 1111 0000₂.', r: '170 · 127 · 192 · 240' },
{ n: 'Application', c: 'Écrire l’adresse 172.16.5.130 en binaire, octet par octet.', r: '10101100.00010000.00000101.10000010' },
{ n: 'Application', c: 'Écrire le masque /20 en décimal pointé, puis en hexadécimal.', r: '255.255.240.0 · 0xFFFFF000' },
{ n: 'Approfondissement', c: 'Un capteur code une température sur 12 bits. Combien de valeurs distinctes peut-il transmettre ? Si la plage mesurée va de −40 °C à +85 °C, quelle est la résolution ?', r: '2¹² = 4 096 valeurs. Résolution = 125 / 4 095 ≈ 0,031 °C.' },
{ n: 'Approfondissement', c: 'Écrire en Python une fonction en_binaire(n) sans utiliser bin(), par divisions successives.', r: 'def en_binaire(n):\n s = ""\n while n > 0:\n s = str(n % 2) + s\n n //= 2\n return s or "0"' }],
tp: { type: 'Générateur', url: 'apercu-generateur.html', desc: 'Conversions aléatoires à l’infini, corrigées au fil de l’eau.', etapes: ['Série de 10 conversions décimal → binaire', 'Série de 10 conversions binaire → hexadécimal', 'Défi chronométré : 20 conversions en 5 minutes'] },
plusLoin: [['Complément à deux et entiers signés', ''], ['Norme IEEE 754 des flottants', ''], ['Défis « Bit Manipulation » sur HackerRank', 'https://www.hackerrank.com/domains/algorithms?filters%5Bsubdomains%5D%5B%5D=bit-manipulation']] },
{ id: 'terminal', match: /terminal|commandes de base|premiers pas|arborescence|FHS|shell/i,
titre: 'Le terminal Linux : se repérer et manipuler des fichiers', duree: '4 h (CM 1 h · TD 1 h · TP 2 h)', niveau: 'SNT · NSI · BTS SIO / CIEL · BUT R&T · Titre pro TSSR',
prerequis: ['Notion de fichier et de dossier'],
competences: ['BTS SIO · Bloc 1 · Gérer le patrimoine informatique', 'BTS CIEL · C04 · Installer un système', 'TSSR · Exploiter des serveurs Linux'],
intro: 'Les serveurs du monde entier se pilotent en ligne de commande. Le terminal est plus rapide que la souris, scriptable et accessible à distance par SSH. Ce cours pose les gestes que vous utiliserez dans tous les labs suivants.',
cm: [
{ t: 'Shell, terminal et invite', d: '10 min', html: 'Le <b>terminal</b> est la fenêtre ; le <b>shell</b> (bash, zsh) est le programme qui lit vos commandes et les exécute. L’invite <code>eleve@debian:~$</code> indique l’utilisateur, la machine, le dossier courant (~ = dossier personnel) et le type de compte ($ utilisateur, # administrateur).' },
{ t: 'L’arborescence Linux (FHS)', d: '15 min', html: 'Tout part de la racine <code>/</code>. Il n’y a pas de lettres de lecteur.', table: [['Dossier', 'Contenu'], ['/home', 'dossiers personnels des utilisateurs'], ['/etc', 'fichiers de configuration'], ['/var/log', 'journaux du système'], ['/bin, /usr/bin', 'commandes'], ['/tmp', 'fichiers temporaires, vidés au redémarrage'], ['/root', 'dossier personnel de l’administrateur']] },
{ t: 'Chemins absolus et relatifs', d: '10 min', html: 'Un chemin <b>absolu</b> commence par / et part de la racine : <code>/home/eleve/notes.txt</code>. Un chemin <b>relatif</b> part du dossier courant : <code>projets/web</code>. <code>.</code> désigne le dossier courant, <code>..</code> le dossier parent.', exemple: 'eleve@debian:~$ pwd\n/home/eleve\neleve@debian:~$ cd projets/web\neleve@debian:~/projets/web$ cd ../..\neleve@debian:~$' },
{ t: 'Lister et se déplacer', d: '10 min', html: '<code>pwd</code> affiche le dossier courant. <code>ls</code> liste ; <code>ls -l</code> détaille (droits, taille, date) ; <code>ls -a</code> montre les fichiers cachés (qui commencent par un point) ; <code>ls -lh</code> affiche les tailles lisibles. <code>cd</code> change de dossier.' },
{ t: 'Créer, copier, déplacer, supprimer', d: '15 min', html: '<code>mkdir -p a/b/c</code> crée une arborescence, <code>touch f</code> un fichier vide, <code>cp src dest</code> copie (<code>-r</code> pour un dossier), <code>mv</code> déplace ou renomme, <code>rm</code> supprime (<code>-r</code> pour un dossier).', attention: 'Il n’y a pas de corbeille. rm -rf supprime sans confirmation. Relisez toujours la commande avant Entrée, surtout avec sudo.' },
{ t: 'Aide et productivité', d: '10 min', html: '<code>man commande</code> ouvre le manuel (/ pour chercher, q pour quitter). <b>Tab</b> complète les noms, <b>↑</b> rappelle l’historique, <b>Ctrl+R</b> recherche dans l’historique, <b>Ctrl+C</b> interrompt une commande, <code>history</code> liste les commandes passées.' }],
synthese: ['/ est la racine, ~ le dossier personnel', 'Chemin absolu = commence par /', 'pwd, ls, cd pour se repérer ; mkdir, cp, mv, rm pour agir', 'Pas de corbeille : relire avant rm'],
qcm: [
['Où se trouvent les fichiers de configuration du système ?', ['/home', '/etc', '/var', '/bin'], 1, '/etc regroupe la configuration.'],
['Lequel est un chemin absolu ?', ['projets/web', '../notes', '/var/log/syslog', '~notes'], 2, 'Il commence par /.'],
['Que fait mkdir -p a/b/c ?', ['Erreur si a n’existe pas', 'Crée toute l’arborescence', 'Crée un fichier', 'Supprime a/b/c'], 1, '-p crée les dossiers parents manquants.'],
['Que signifie # à la fin de l’invite ?', ['Commentaire', 'Compte administrateur', 'Erreur', 'Dossier racine'], 1, '# indique root.'],
['Quelle commande copie le dossier site et son contenu ?', ['cp site copie', 'cp -r site copie', 'mv site copie', 'mkdir site copie'], 1, '-r = récursif.'],
['Quel raccourci recherche dans l’historique ?', ['Ctrl+C', 'Ctrl+R', 'Ctrl+Z', 'Tab'], 1, 'Ctrl+R lance la recherche inversée.'],
['Où sont les journaux du système ?', ['/var/log', '/etc/log', '/tmp', '/home/log'], 0, '/var/log contient syslog, auth.log…'],
['Depuis /home/eleve/projets, que désigne ../.. ?', ['/home', '/home/eleve', '/', '/projets'], 0, 'On remonte deux fois.']],
td: [
{ n: 'Échauffement', c: 'Depuis /home/eleve, donner le chemin absolu puis relatif du fichier /home/eleve/projets/web/index.html.', r: 'Absolu : /home/eleve/projets/web/index.html · relatif : projets/web/index.html' },
{ n: 'Application', c: 'Créer en une commande : tp/{reseau,systeme,web}.', r: 'mkdir -p tp/reseau tp/systeme tp/web (ou mkdir -p tp/{reseau,systeme,web})' },
{ n: 'Application', c: 'Copier /etc/hosts dans tp/reseau sous le nom hosts.bak, puis vérifier.', r: 'cp /etc/hosts tp/reseau/hosts.bak && ls -l tp/reseau' },
{ n: 'Approfondissement', c: 'Trouver tous les fichiers .conf de /etc de plus de 10 Ko.', r: 'find /etc -name "*.conf" -size +10k 2>/dev/null' }],
tp: { type: 'Lab VM', url: 'apercu-lab-essai.html', desc: 'Un vrai terminal Debian, étapes vérifiées automatiquement dans la VM.', etapes: ['Se repérer : pwd, ls -la', 'Créer l’arborescence du projet', 'Copier et renommer des fichiers', 'Retrouver un fichier avec find', 'Nettoyer sans rien casser'] },
plusLoin: [['Le jeu OverTheWire Bandit (niveaux 0 à 10)', 'https://overthewire.org/wargames/bandit/'], ['Linux Journey', 'https://linuxjourney.com/']] },
{ id: 'droits', match: /droits|chmod|chown|permissions|utilisateurs et groupes/i,
titre: 'Droits, utilisateurs et groupes sous Linux', duree: '5 h (CM 1 h 30 · TD 1 h 30 · TP 2 h)', niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR',
prerequis: ['Le terminal Linux', 'Binaire : poids des bits'],
competences: ['BTS SIO · Bloc 3 · Sécuriser les équipements et les usages', 'BTS CIEL · C08 · Sécuriser un système', 'BUT R&T · Compétence « Administrer »'],
intro: 'Sur un serveur partagé, chaque fichier appartient à quelqu’un et chacun n’a que les droits dont il a besoin : c’est le principe du moindre privilège. Une erreur de droits, c’est soit un service qui ne démarre pas, soit une fuite de données.',
cm: [
{ t: 'Utilisateurs et groupes', d: '15 min', html: 'Chaque utilisateur a un identifiant (UID) et un groupe principal (GID), déclarés dans <code>/etc/passwd</code> et <code>/etc/group</code>. Les mots de passe hachés sont dans <code>/etc/shadow</code>, lisible par root seulement. <code>id</code> affiche vos identifiants.', exemple: 'root@srv:~# adduser alice\nroot@srv:~# addgroup profs\nroot@srv:~# usermod -aG profs alice\nroot@srv:~# id alice\nuid=1001(alice) gid=1001(alice) groupes=1001(alice),1002(profs)' },
{ t: 'Lire les droits', d: '15 min', html: '<code>ls -l</code> affiche 10 caractères : le type puis trois triplets pour le <b>propriétaire (u)</b>, le <b>groupe (g)</b> et les <b>autres (o)</b>.', table: [['Droit', 'Sur un fichier', 'Sur un dossier'], ['r (4)', 'lire le contenu', 'lister les noms'], ['w (2)', 'modifier', 'créer, renommer, supprimer dedans'], ['x (1)', 'exécuter', 'traverser (cd)']], exemple: '-rwxr-x--- 1 alice profs 812 sept. 29 10:14 sauvegarde.sh\n│└┬┘└┬┘└┬┘\n│ u g o' },
{ t: 'chmod : octal et symbolique', d: '20 min', html: 'En <b>octal</b>, on additionne r = 4, w = 2, x = 1 pour chaque triplet : rwx = 7, r-x = 5, r-- = 4. En <b>symbolique</b>, on ajoute (+), retire (−) ou fixe (=) un droit pour u, g, o ou a.', exemple: 'chmod 750 sauvegarde.sh # rwxr-x---\nchmod u+x,g-w rapport.txt\nchmod -R o= /srv/partage # retire tout aux autres, récursivement' },
{ t: 'chown et umask', d: '15 min', html: '<code>chown alice:profs fichier</code> change propriétaire et groupe (réservé à root). <code>umask</code> fixe les droits retirés à la création : avec umask 022, un fichier est créé en 644 et un dossier en 755 ; avec 027, en 640 et 750.' },
{ t: 'Droits spéciaux', d: '15 min', html: '<b>SUID (4000)</b> : le programme s’exécute avec les droits de son propriétaire (ex. <code>passwd</code>). <b>SGID (2000)</b> sur un dossier : les fichiers créés héritent du groupe du dossier, idéal pour un partage. <b>Sticky bit (1000)</b> sur un dossier : seul le propriétaire d’un fichier peut le supprimer (ex. <code>/tmp</code>).', attention: 'Un script SUID root mal écrit est une porte d’entrée classique pour une élévation de privilèges. En audit, on les recherche avec find / -perm -4000.' },
{ t: 'Au-delà : les ACL', d: '10 min', html: 'Les ACL permettent de donner des droits à un utilisateur précis sans changer le groupe : <code>setfacl -m u:bob:rw rapport.txt</code>, et <code>getfacl</code> pour les lire.' }],
synthese: ['u g o × r w x, avec r = 4, w = 2, x = 1', 'x sur un dossier = pouvoir y entrer', 'SGID sur un dossier partagé, sticky bit sur /tmp', 'Moindre privilège : ne donner que le nécessaire'],
qcm: [
['rwxr-x--- en octal ?', ['750', '755', '740', '770'], 0, '7, 5, 0.'],
['Quel fichier contient les hachages des mots de passe ?', ['/etc/passwd', '/etc/shadow', '/etc/group', '/etc/login'], 1, 'shadow, lisible par root uniquement.'],
['Que fait chmod g+w rapport.txt ?', ['Donne l’écriture aux autres', 'Donne l’écriture au groupe', 'Retire l’écriture', 'Change le groupe'], 1, 'g = groupe, + = ajouter.'],
['Avec umask 027, un nouveau dossier a les droits :', ['755', '750', '770', '700'], 1, '777 − 027 = 750.'],
['Quel droit permet à un utilisateur de faire cd dans un dossier ?', ['r', 'w', 'x', 's'], 2, 'x sur un dossier = traverser.'],
['Le sticky bit sur /tmp sert à :', ['Accélérer', 'Empêcher de supprimer les fichiers des autres', 'Chiffrer', 'Hériter du groupe'], 1, 'Chacun ne supprime que ses fichiers.'],
['Comment ajouter alice au groupe profs sans retirer ses autres groupes ?', ['usermod -G profs alice', 'usermod -aG profs alice', 'chown profs alice', 'addgroup alice'], 1, '-a = append.'],
['Quelle commande trouve les fichiers SUID ?', ['find / -perm -4000', 'ls -suid', 'chmod -find', 'grep suid /'], 0, '-perm -4000 cherche le bit SUID.']],
td: [
{ n: 'Échauffement', c: 'Traduire en octal : rw-r--r--, rwx------, rwxrwxr-x, r--------.', r: '644 · 700 · 775 · 400' },
{ n: 'Échauffement', c: 'Traduire en symbolique : 640, 755, 600.', r: 'rw-r----- · rwxr-xr-x · rw-------' },
{ n: 'Application', c: 'Créer un dossier /srv/projet partagé par le groupe devs : les membres lisent et écrivent, les fichiers créés restent dans le groupe devs, les autres n’ont aucun accès.', r: 'mkdir /srv/projet\nchown root:devs /srv/projet\nchmod 2770 /srv/projet' },
{ n: 'Application', c: 'Le script sauvegarde.sh refuse de se lancer (« Permission non accordée »). Diagnostiquer et corriger.', r: 'ls -l sauvegarde.sh → pas de x. chmod u+x sauvegarde.sh' },
{ n: 'Approfondissement', c: 'Expliquer pourquoi un fichier en 777 dans un dossier en 700 appartenant à alice reste inaccessible à bob.', r: 'bob ne peut pas traverser le dossier (pas de x pour les autres) : il n’atteint jamais le fichier.' }],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Créer des comptes, un dossier partagé en SGID et tester les accès avec su.', etapes: ['Créer alice, bob et le groupe devs', 'Préparer /srv/projet en 2770', 'Tester l’écriture en tant qu’alice puis bob', 'Poser une ACL pour un stagiaire', 'Audit : lister les fichiers SUID'] },
plusLoin: [['ANSSI · Recommandations de sécurité relatives à un système GNU/Linux', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-un-systeme-gnulinux']] }
]);

View file

@ -0,0 +1,90 @@
// Cours complets · lot 2 : tubes, réseaux (OSI), SQL
window.CONTENUS_PRO = (window.CONTENUS_PRO || []).concat([
{ id: 'tubes', match: /redirection|tubes|grep|filtres|sed|awk/i,
titre: 'Redirections, tubes et filtres de texte', duree: '4 h (CM 1 h · TD 1 h 30 · TP 1 h 30)', niveau: 'BTS SIO · BTS CIEL · BUT R&T · TSSR',
prerequis: ['Le terminal Linux'],
competences: ['BTS SIO · Bloc 1 · Exploiter des journaux', 'BUT R&T · Compétence « Administrer » · automatiser'],
intro: 'Un administrateur passe beaucoup de temps à lire des journaux de plusieurs milliers de lignes. Plutôt que de les lire à l’œil, on enchaîne de petites commandes spécialisées. C’est la philosophie Unix : chaque outil fait une chose, et bien.',
cm: [
{ t: 'Les trois flux standard', d: '10 min', html: 'Chaque processus dispose de trois flux : <b>stdin (0)</b> l’entrée, <b>stdout (1)</b> la sortie normale, <b>stderr (2)</b> les erreurs. Les séparer permet de garder les résultats propres et d’isoler les erreurs.' },
{ t: 'Rediriger', d: '15 min', html: 'On envoie un flux vers un fichier ou on lit un fichier comme entrée.', table: [['Opérateur', 'Effet'], ['>', 'stdout vers un fichier (écrase)'], ['>>', 'stdout ajouté à la fin'], ['2>', 'stderr vers un fichier'], ['&> ou > f 2>&1', 'les deux flux dans le même fichier'], ['<', 'le fichier devient stdin'], ['> /dev/null', 'jeter la sortie']], exemple: 'find / -name "*.conf" > resultats.txt 2> /dev/null' },
{ t: 'Le tube', d: '10 min', html: 'Le tube <code>|</code> branche la sortie d’une commande sur l’entrée de la suivante, sans fichier intermédiaire. On construit une chaîne de traitement de gauche à droite.', exemple: 'cat /var/log/auth.log | grep "Failed password" | wc -l' },
{ t: 'La boîte à outils', d: '20 min', html: '', table: [['Commande', 'Rôle', 'Exemple'], ['grep', 'garder les lignes qui correspondent', 'grep -i erreur syslog'], ['cut', 'extraire des colonnes', 'cut -d: -f1,7 /etc/passwd'], ['sort', 'trier', 'sort -n, sort -rn'], ['uniq -c', 'compter les lignes identiques consécutives', 'sort | uniq -c'], ['wc -l', 'compter les lignes', ''], ['head / tail', 'début / fin', 'tail -f pour suivre en direct'], ['awk', 'traiter par colonnes', "awk '{print $1}'"], ['sed', 'remplacer', "sed 's/http/https/g'"]], attention: 'uniq ne regroupe que les lignes consécutives : il faut presque toujours trier avant avec sort.' },
{ t: 'Un cas réel : repérer une attaque SSH', d: '15 min', html: 'Objectif : trouver les adresses IP qui ont le plus échoué à se connecter.', exemple: "grep 'Failed password' /var/log/auth.log \\\n | awk '{print $(NF-3)}' \\\n | sort | uniq -c | sort -rn | head -5\n\n 412 203.0.113.42\n 87 198.51.100.7\n 12 192.0.2.19" }],
synthese: ['> écrase, >> ajoute, 2> capture les erreurs', '| enchaîne les traitements', 'sort | uniq -c | sort -rn : le classement le plus utile de l’administrateur', 'tail -f pour suivre un journal en direct'],
qcm: [
['Où va l’erreur de ls /inexistant > sortie.txt ?', ['Dans sortie.txt', 'À l’écran', 'Nulle part', 'Dans /dev/null'], 1, '> ne redirige que stdout ; stderr s’affiche.'],
['Comment jeter à la fois la sortie et les erreurs ?', ['> /dev/null', '2> /dev/null', '&> /dev/null', '| null'], 2, '&> redirige les deux flux.'],
['Que fait cut -d: -f1 /etc/passwd ?', ['Supprime la ligne 1', 'Affiche la 1re colonne séparée par :', 'Compte les lignes', 'Trie'], 1, '-d = séparateur, -f = champ.'],
['Pourquoi trier avant uniq -c ?', ['Pour aller plus vite', 'uniq ne regroupe que les lignes consécutives', 'Ce n’est pas utile', 'Pour supprimer les doublons'], 1, 'Sans tri, les doublons dispersés ne sont pas regroupés.'],
['Quelle commande suit un journal en temps réel ?', ['head -f', 'tail -f', 'cat -f', 'less -r'], 1, 'tail -f affiche les nouvelles lignes au fil de l’eau.'],
['Que fait sed "s/a/b/g" f.txt ?', ['Supprime a', 'Remplace tous les a par b à l’affichage', 'Modifie le fichier sur disque', 'Compte les a'], 1, 'Sans -i, sed ne modifie pas le fichier.']],
td: [
{ n: 'Échauffement', c: 'Enregistrer la liste de /etc dans liste.txt, puis y ajouter celle de /var.', r: 'ls /etc > liste.txt && ls /var >> liste.txt' },
{ n: 'Application', c: 'Afficher les comptes qui utilisent /bin/bash, avec seulement leur nom.', r: 'grep "/bin/bash$" /etc/passwd | cut -d: -f1' },
{ n: 'Application', c: 'Dans acces.log (format Apache), classer les 10 pages les plus demandées.', r: "awk '{print $7}' acces.log | sort | uniq -c | sort -rn | head -10" },
{ n: 'Approfondissement', c: 'Compter les erreurs 404 par heure dans acces.log.', r: "awk '$9 == 404 {print substr($4, 14, 2)}' acces.log | sort | uniq -c" }],
tp: { type: 'IDE Bash', url: 'apercu-ide.html?ex=bash-tp', desc: 'Scripts Bash sur de vrais journaux, vérifiés par des tests.', etapes: ['Compter les connexions échouées', 'Classer les IP suspectes', 'Écrire un rapport dans un fichier', 'Bonus : alerte si plus de 100 échecs'] },
plusLoin: [['Le jeu OverTheWire Bandit (niveaux 10 à 20)', 'https://overthewire.org/wargames/bandit/'], ['Défis « Linux Shell » sur HackerRank', 'https://www.hackerrank.com/domains/shell']] },
{ id: 'osi', match: /OSI|TCP\/IP|modèle en couches|encapsulation|Internet : IP/i,
titre: 'Modèles OSI et TCP/IP, encapsulation', duree: '4 h (CM 1 h 30 · TD 1 h · TP 1 h 30)', niveau: 'SNT · BTS SIO · BTS CIEL · BUT R&T S1',
prerequis: ['Binaire et hexadécimal'],
competences: ['BTS SIO · Bloc 1 · Exploiter les éléments d’un réseau', 'BTS CIEL · C03 · Analyser un réseau', 'BUT R&T · Compétence « Connecter »'],
intro: 'Quand un réseau tombe en panne, l’administrateur raisonne couche par couche : le câble est-il branché ? le switch voit-il la carte ? l’adresse IP est-elle bonne ? le service écoute-t-il ? Le modèle en couches est d’abord une méthode de diagnostic.',
cm: [
{ t: 'Pourquoi un modèle en couches', d: '10 min', html: 'Communiquer suppose de régler beaucoup de problèmes : transmettre des bits, les adresser, les acheminer, les remettre en ordre, les présenter à l’application. Chaque couche règle un problème et offre un service à la couche du dessus, sans connaître le détail des autres.' },
{ t: 'Les 7 couches OSI', d: '20 min', html: '', table: [['N°', 'Couche', 'Rôle', 'Unité', 'Exemples'], ['7', 'Application', 'service rendu à l’utilisateur', 'données', 'HTTP, DNS, SSH, SMTP'], ['6', 'Présentation', 'format, chiffrement', 'données', 'TLS, UTF-8'], ['5', 'Session', 'dialogue', 'données', 'RPC'], ['4', 'Transport', 'bout en bout, ports', 'segment', 'TCP, UDP'], ['3', 'Réseau', 'adressage logique, routage', 'paquet', 'IP, ICMP'], ['2', 'Liaison', 'accès au support, MAC', 'trame', 'Ethernet, Wi-Fi'], ['1', 'Physique', 'signal', 'bit', 'RJ45, fibre, radio']] },
{ t: 'Le modèle TCP/IP', d: '10 min', html: 'Le modèle réellement implémenté compte 4 couches : <b>Accès réseau</b> (OSI 1-2), <b>Internet</b> (3), <b>Transport</b> (4), <b>Application</b> (5-7).' },
{ t: 'L’encapsulation', d: '20 min', html: 'À l’émission, chaque couche ajoute son en-tête aux données reçues de la couche supérieure. À la réception, chaque couche lit et retire le sien.', exemple: '[ HTTP : GET /index.html ]\n[ TCP : port src 51234 → dst 443 ][ HTTP … ]\n[ IP : 192.168.10.65 → 93.184.216.34 ][ TCP … ][ HTTP … ]\n[ Eth : MAC src → MAC passerelle ][ IP … ][ TCP … ][ HTTP … ][ FCS ]' },
{ t: 'Les équipements par couche', d: '10 min', html: '<b>Hub</b> : couche 1, répète le signal. <b>Switch</b> : couche 2, commute selon les adresses MAC. <b>Routeur</b> : couche 3, achemine selon les adresses IP. <b>Pare-feu</b> : couches 3-4, filtre IP et ports (voire 7 pour un pare-feu applicatif).' },
{ t: 'Diagnostiquer couche par couche', d: '15 min', html: 'On part du bas : <code>ip link</code> (lien actif ?), <code>ip addr</code> (adresse ?), <code>ping</code> la passerelle (couche 3), <code>ss -tlnp</code> (le service écoute-t-il ?), <code>curl</code> (l’application répond-elle ?).', attention: 'Un ping qui échoue ne prouve pas que la machine est éteinte : ICMP est souvent filtré par les pare-feux.' }],
synthese: ['OSI 7 couches, TCP/IP 4 couches', 'bit → trame → paquet → segment → données', 'MAC couche 2, IP couche 3, port couche 4', 'On diagnostique toujours de bas en haut'],
qcm: [
['À quelle couche travaille un switch classique ?', ['1', '2', '3', '4'], 1, 'Il commute selon les adresses MAC.'],
['Quelle est l’unité de la couche Transport ?', ['Trame', 'Paquet', 'Segment', 'Bit'], 2, 'TCP et UDP manipulent des segments (datagrammes pour UDP).'],
['Le port 443 est une information de couche :', ['2', '3', '4', '7'], 2, 'Les ports sont gérés par la couche Transport.'],
['Quel protocole n’est PAS de couche application ?', ['DNS', 'SSH', 'ICMP', 'SMTP'], 2, 'ICMP accompagne IP, couche 3.'],
['Dans une trame Ethernet, l’adresse MAC de destination est celle :', ['du serveur web distant', 'de la passerelle ou du voisin direct', 'du DNS', 'du switch'], 1, 'La MAC change à chaque saut, l’IP de destination reste la même.'],
['Combien de couches compte le modèle TCP/IP ?', ['3', '4', '5', '7'], 1, 'Accès réseau, Internet, Transport, Application.'],
['Un poste a une IP mais n’accède pas à Internet. Première vérification ?', ['Réinstaller', 'ping la passerelle', 'Changer le câble', 'Vider le cache DNS'], 1, 'On teste la couche 3 vers la passerelle avant d’aller plus haut.']],
td: [
{ n: 'Échauffement', c: 'Classer par couche OSI : HTTP, IP, switch, TCP, câble RJ45, routeur, TLS, Wi-Fi.', r: '7 · 3 · 2 · 4 · 1 · 3 · 6 · 1-2' },
{ n: 'Application', c: 'Dessiner l’encapsulation d’une requête DNS du poste 192.168.1.20 vers 1.1.1.1.', r: 'DNS (requête) → UDP (port src aléatoire → 53) → IP (192.168.1.20 → 1.1.1.1) → Ethernet (MAC poste → MAC box)' },
{ n: 'Application', c: 'Un utilisateur ne peut pas ouvrir intranet.lycee.fr. Proposer une démarche de diagnostic en 5 commandes, de bas en haut.', r: 'ip link · ip addr · ping passerelle · nslookup intranet.lycee.fr · curl -I https://intranet.lycee.fr' },
{ n: 'Approfondissement', c: 'Expliquer pourquoi l’adresse MAC de destination change à chaque routeur traversé alors que l’adresse IP de destination reste la même.', r: 'La couche 2 ne sert qu’à atteindre le voisin direct ; chaque routeur ré-encapsule dans une nouvelle trame. La couche 3 porte l’adresse finale.' }],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Capturer du trafic avec tcpdump et reconnaître chaque en-tête.', etapes: ['Lancer tcpdump sur eth0', 'Faire un curl vers un site', 'Identifier Ethernet, IP, TCP, HTTP', 'Comparer avec une requête DNS en UDP'] },
plusLoin: [['Cisco Networking Academy · cours gratuits', 'https://www.netacad.com/'], ['RFC 1122 (Requirements for Internet Hosts)', 'https://www.rfc-editor.org/rfc/rfc1122']] },
{ id: 'sql', match: /SQL|SELECT|requête|bases de données relationnelles|jointure/i,
titre: 'SQL : interroger une base de données relationnelle', duree: '6 h (CM 2 h · TD 2 h · TP 2 h)', niveau: 'NSI Terminale · BTS SIO SLAM · BUT Informatique · MIAGE',
prerequis: ['Notion de tableau de données'],
competences: ['NSI Tle · Bases de données : langage SQL', 'BTS SIO SLAM · Bloc 2 · Concevoir et exploiter une base de données', 'BUT Info · Compétence « Gérer des données »'],
intro: 'Presque toutes les applications stockent leurs données dans une base relationnelle : un ENT, un site marchand, Syllabus elle-même. SQL est le langage standard pour les interroger. Il se lit presque comme une phrase.',
cm: [
{ t: 'Le modèle relationnel', d: '20 min', html: 'Une base est un ensemble de <b>tables</b> (relations). Chaque ligne est un enregistrement, chaque colonne un attribut typé. La <b>clé primaire</b> identifie une ligne de façon unique ; une <b>clé étrangère</b> référence la clé primaire d’une autre table.', table: [['eleves', 'id (PK)', 'nom', 'prenom', 'classe_id (FK)'], ['', '1', 'Martin', 'Léa', '3'], ['', '2', 'Kaci', 'Inès', '3']] },
{ t: 'Sélectionner : SELECT … FROM', d: '15 min', html: 'On choisit les colonnes, puis la table. <code>*</code> sélectionne toutes les colonnes, <code>DISTINCT</code> supprime les doublons, <code>AS</code> renomme.', exemple: 'SELECT nom, prenom FROM eleves;\nSELECT DISTINCT ville FROM eleves;\nSELECT nom AS "Nom de famille" FROM eleves;' },
{ t: 'Filtrer et trier', d: '20 min', html: '<code>WHERE</code> filtre avec =, <>, <, >, BETWEEN, IN, LIKE (% = n’importe quelle suite), IS NULL, combinés par AND, OR, NOT. <code>ORDER BY</code> trie (ASC, DESC), <code>LIMIT</code> restreint.', exemple: "SELECT nom, moyenne FROM eleves\nWHERE classe_id = 3 AND moyenne >= 10\nORDER BY moyenne DESC\nLIMIT 5;" },
{ t: 'Agréger : COUNT, AVG, GROUP BY', d: '20 min', html: 'Les fonctions d’agrégat calculent sur un ensemble de lignes : COUNT, SUM, AVG, MIN, MAX. <code>GROUP BY</code> forme des groupes, <code>HAVING</code> filtre les groupes (WHERE filtre les lignes avant).', exemple: 'SELECT classe_id, COUNT(*) AS effectif, AVG(moyenne) AS moy\nFROM eleves\nGROUP BY classe_id\nHAVING COUNT(*) > 20;', attention: 'Toute colonne du SELECT qui n’est pas agrégée doit figurer dans le GROUP BY.' },
{ t: 'Joindre des tables', d: '25 min', html: 'La jointure relie deux tables par leurs clés. <b>INNER JOIN</b> garde les lignes qui ont une correspondance des deux côtés ; <b>LEFT JOIN</b> garde toutes les lignes de gauche, avec NULL s’il n’y a pas de correspondance.', exemple: 'SELECT e.nom, c.libelle\nFROM eleves e\nJOIN classes c ON e.classe_id = c.id;\n\n-- Classes sans aucun élève :\nSELECT c.libelle FROM classes c\nLEFT JOIN eleves e ON e.classe_id = c.id\nWHERE e.id IS NULL;' },
{ t: 'Modifier les données', d: '15 min', html: '<code>INSERT INTO</code> ajoute, <code>UPDATE … SET … WHERE</code> modifie, <code>DELETE FROM … WHERE</code> supprime.', attention: 'Un UPDATE ou un DELETE sans WHERE touche toutes les lignes. On teste toujours d’abord le WHERE avec un SELECT.' },
{ t: 'Sécurité : l’injection SQL', d: '10 min', html: 'Construire une requête en collant du texte saisi par l’utilisateur permet de détourner la requête (<code>\' OR 1=1 --</code>). On utilise toujours des <b>requêtes préparées</b> avec paramètres.' }],
synthese: ['SELECT colonnes FROM table WHERE condition ORDER BY tri', 'WHERE filtre les lignes, HAVING filtre les groupes', 'JOIN … ON relie clé étrangère et clé primaire', 'Jamais de DELETE ni d’UPDATE sans WHERE', 'Requêtes préparées contre l’injection'],
qcm: [
['Quelle clause filtre les groupes après un GROUP BY ?', ['WHERE', 'HAVING', 'ORDER BY', 'LIMIT'], 1, 'HAVING s’applique aux groupes.'],
['Que renvoie SELECT COUNT(*) FROM eleves WHERE moyenne IS NULL ?', ['Les élèves sans moyenne', 'Le nombre d’élèves sans moyenne', 'Une erreur', '0 toujours'], 1, 'COUNT compte les lignes retenues.'],
['Quelle jointure garde les classes sans élève ?', ['INNER JOIN', 'LEFT JOIN depuis classes', 'CROSS JOIN', 'Aucune'], 1, 'LEFT JOIN conserve toutes les lignes de la table de gauche.'],
['LIKE "Ma%" correspond à :', ['Ma exactement', 'tout texte commençant par Ma', 'tout texte finissant par Ma', 'tout texte contenant %'], 1, '% remplace n’importe quelle suite de caractères.'],
['Que se passe-t-il avec DELETE FROM eleves; ?', ['Rien', 'Toutes les lignes sont supprimées', 'La table est supprimée', 'Erreur de syntaxe'], 1, 'Sans WHERE, toutes les lignes partent. La table reste.'],
['Quel est le rôle d’une clé étrangère ?', ['Chiffrer', 'Référencer une ligne d’une autre table', 'Accélérer les tris', 'Rendre une colonne unique'], 1, 'Elle garantit l’intégrité référentielle.'],
['Comment se protéger de l’injection SQL ?', ['Masquer les erreurs', 'Requêtes préparées', 'Mettre des guillemets', 'Utiliser HTTPS'], 1, 'Les paramètres ne sont jamais interprétés comme du SQL.'],
['Classer les élèves du meilleur au moins bon :', ['ORDER BY moyenne', 'ORDER BY moyenne DESC', 'SORT BY moyenne', 'GROUP BY moyenne'], 1, 'DESC = décroissant.']],
td: [
{ n: 'Échauffement', c: 'Afficher nom et prénom des élèves de la classe 3, triés par nom.', r: 'SELECT nom, prenom FROM eleves WHERE classe_id = 3 ORDER BY nom;' },
{ n: 'Application', c: 'Afficher l’effectif et la moyenne de chaque classe, avec son libellé.', r: 'SELECT c.libelle, COUNT(e.id), AVG(e.moyenne)\nFROM classes c LEFT JOIN eleves e ON e.classe_id = c.id\nGROUP BY c.libelle;' },
{ n: 'Application', c: 'Lister les élèves qui n’ont rendu aucun devoir (tables eleves, rendus).', r: 'SELECT e.nom FROM eleves e\nLEFT JOIN rendus r ON r.eleve_id = e.id\nWHERE r.id IS NULL;' },
{ n: 'Approfondissement', c: 'Afficher les classes dont la moyenne est supérieure à la moyenne générale.', r: 'SELECT classe_id, AVG(moyenne) FROM eleves GROUP BY classe_id\nHAVING AVG(moyenne) > (SELECT AVG(moyenne) FROM eleves);' },
{ n: 'Approfondissement', c: 'Réécrire en requête préparée (PHP PDO) : "SELECT * FROM users WHERE login = \'" . $login . "\'"', r: '$st = $pdo->prepare("SELECT * FROM users WHERE login = ?");\n$st->execute([$login]);' }],
tp: { type: 'IDE SQL', url: 'apercu-ide.html?ex=sql-select', desc: 'Une base SQLite dans le navigateur, requêtes vérifiées automatiquement.', etapes: ['Explorer les tables', '8 requêtes SELECT progressives', '3 jointures', '2 requêtes d’agrégat', 'Défi : le palmarès par classe'] },
plusLoin: [['SQLBolt · exercices interactifs', 'https://sqlbolt.com/'], ['Défis SQL sur HackerRank', 'https://www.hackerrank.com/domains/sql'], ['DataCamp · Introduction to SQL', 'https://www.datacamp.com/courses/introduction-to-sql']] }
]);

View file

@ -0,0 +1,92 @@
// Cours complets · lot 3 : Python, Web, cryptographie
window.CONTENUS_PRO = (window.CONTENUS_PRO || []).concat([
{ id: 'python', match: /Python|programmation|boucle|variable|algorithm/i,
titre: 'Python : variables, conditions, boucles et fonctions', duree: '6 h (CM 2 h · TD 2 h · TP 2 h)', niveau: 'SNT · NSI Première · BTS SIO SLAM · BUT S1',
prerequis: ['Aucun'],
competences: ['NSI 1re · Langages et programmation', 'BTS SIO SLAM · Bloc 2 · Développer une solution applicative', 'BUT Info · Compétence « Réaliser »'],
intro: 'Python est le langage d’enseignement du lycée à l’université, mais aussi celui des administrateurs, des data engineers et de l’IA. Sa syntaxe courte permet de se concentrer sur l’essentiel : décomposer un problème en instructions.',
cm: [
{ t: 'Variables et types', d: '20 min', html: 'Une variable associe un nom à une valeur. Le type est déduit de la valeur.', table: [['Type', 'Exemple', 'Usage'], ['int', '17', 'entiers'], ['float', '3.5', 'nombres à virgule'], ['str', '"Léa"', 'texte'], ['bool', 'True', 'vrai / faux'], ['list', '[12, 15, 9]', 'suite modifiable'], ['dict', '{"nom": "Léa"}', 'associations clé → valeur']], exemple: 'age = 17\nnom = "Léa"\nprint(f"{nom} a {age} ans") # Léa a 17 ans\nprint(type(age)) # <class \'int\'>' },
{ t: 'Opérateurs et entrées', d: '15 min', html: '+ − * / (division réelle), // (division entière), % (reste), ** (puissance). <code>input()</code> lit une saisie, toujours sous forme de texte : on la convertit avec <code>int()</code> ou <code>float()</code>.', attention: 'input() renvoie une chaîne : "3" + "4" donne "34", pas 7.' },
{ t: 'Conditions', d: '20 min', html: 'Le bloc exécuté dépend d’une condition. Python délimite les blocs par l’<b>indentation</b> (4 espaces).', exemple: 'note = float(input("Note : "))\nif note >= 16:\n print("Très bien")\nelif note >= 10:\n print("Admis")\nelse:\n print("À retravailler")' },
{ t: 'Boucle for', d: '20 min', html: '<code>for</code> parcourt une séquence. <code>range(n)</code> produit 0, 1, …, n − 1 ; <code>range(a, b, pas)</code> va de a à b − 1.', exemple: 'notes = [12, 15, 9, 18]\ntotal = 0\nfor n in notes:\n total += n\nprint(total / len(notes)) # 13.5' },
{ t: 'Boucle while', d: '15 min', html: '<code>while</code> répète tant que la condition est vraie. On l’utilise quand on ne connaît pas le nombre de tours à l’avance.', exemple: 'essais = 0\nmdp = ""\nwhile mdp != "paillasse" and essais < 3:\n mdp = input("Mot de passe : ")\n essais += 1', attention: 'Si la condition ne devient jamais fausse, la boucle est infinie. Ctrl+C l’interrompt.' },
{ t: 'Fonctions', d: '20 min', html: 'Une fonction regroupe des instructions réutilisables. Elle reçoit des <b>paramètres</b> et renvoie un résultat avec <code>return</code>. On la documente avec une docstring et on la teste avec <code>assert</code>.', exemple: 'def moyenne(valeurs):\n """Renvoie la moyenne d\'une liste non vide."""\n return sum(valeurs) / len(valeurs)\n\nassert moyenne([10, 20]) == 15' },
{ t: 'Listes et dictionnaires', d: '10 min', html: 'Une liste s’indexe à partir de 0 : <code>notes[0]</code>, <code>notes[-1]</code> (dernier), <code>notes.append(14)</code>. Un dictionnaire associe des clés à des valeurs : <code>eleve["nom"]</code>.' }],
synthese: ['L’indentation délimite les blocs', 'input() renvoie du texte : convertir avec int() ou float()', 'for quand on connaît la séquence, while sinon', 'Une fonction = paramètres + return + tests'],
qcm: [
['Que vaut 7 // 2 ?', ['3.5', '3', '4', '1'], 1, '// est la division entière.'],
['Que vaut 7 % 2 ?', ['1', '3', '3.5', '0'], 0, '% donne le reste.'],
['Combien de tours fait for i in range(2, 10, 3) ?', ['2', '3', '4', '8'], 1, 'i prend 2, 5, 8.'],
['Que renvoie input() ?', ['Un int', 'Un str', 'Un float', 'Un bool'], 1, 'Toujours une chaîne.'],
['Que vaut notes[-1] pour notes = [12, 15, 9] ?', ['12', '15', '9', 'Erreur'], 2, '-1 désigne le dernier élément.'],
['Que fait return dans une fonction ?', ['Affiche une valeur', 'Renvoie une valeur et sort de la fonction', 'Recommence la fonction', 'Rien'], 1, 'return termine la fonction avec un résultat.'],
['Quelle boucle convient pour « redemander tant que la saisie est invalide » ?', ['for', 'while', 'range', 'if'], 1, 'On ne connaît pas le nombre de tours.'],
['Que produit "3" + "4" ?', ['7', '"34"', 'Erreur', '"7"'], 1, 'Concaténation de chaînes.']],
td: [
{ n: 'Échauffement', c: 'Écrire un programme qui demande l’année de naissance et affiche l’âge en 2026.', r: 'annee = int(input("Année : "))\nprint(2026 - annee)' },
{ n: 'Application', c: 'Écrire est_pair(n) qui renvoie True si n est pair, puis afficher les nombres pairs de 1 à 30.', r: 'def est_pair(n):\n return n % 2 == 0\n\nfor i in range(1, 31):\n if est_pair(i):\n print(i)' },
{ n: 'Application', c: 'Écrire mention(note) qui renvoie « TB », « B », « AB », « Passable » ou « Insuffisant ».', r: 'def mention(n):\n if n >= 16: return "TB"\n if n >= 14: return "B"\n if n >= 12: return "AB"\n if n >= 10: return "Passable"\n return "Insuffisant"' },
{ n: 'Approfondissement', c: 'Écrire maximum(liste) sans utiliser max(), avec ses tests.', r: 'def maximum(l):\n m = l[0]\n for x in l:\n if x > m:\n m = x\n return m\n\nassert maximum([3, 9, 2]) == 9' },
{ n: 'Approfondissement', c: 'Compter le nombre d’occurrences de chaque mot d’une phrase avec un dictionnaire.', r: 'compte = {}\nfor mot in phrase.split():\n compte[mot] = compte.get(mot, 0) + 1' }],
tp: { type: 'IDE Python', url: 'apercu-ide.html?ex=py-pair', desc: 'Exercices corrigés par des tests automatiques, directement dans le navigateur.', etapes: ['Parité et multiples', 'Somme et moyenne d’une liste', 'Compter les voyelles', 'Jeu du nombre mystère', 'Défi : FizzBuzz'] },
plusLoin: [['France-IOI · parcours Python', 'https://www.france-ioi.org/'], ['HackerRank · Python', 'https://www.hackerrank.com/domains/python'], ['DataCamp · Introduction to Python', 'https://www.datacamp.com/courses/intro-to-python-for-data-science']] },
{ id: 'web', match: /HTML|CSS|Le Web|HTTP|serveur web|Apache/i,
titre: 'Le Web : HTTP, HTML, CSS et serveur web', duree: '5 h (CM 1 h 30 · TD 1 h 30 · TP 2 h)', niveau: 'SNT · NSI Première · BTS SIO · BTS CIEL',
prerequis: ['Modèles OSI et TCP/IP (conseillé)'],
competences: ['SNT · Le Web', 'NSI 1re · Interactions entre l’homme et la machine sur le Web', 'BTS SIO · Bloc 1 · Mettre à disposition un service'],
intro: 'Chaque fois que vous ouvrez une page, votre navigateur échange des messages HTTP avec un serveur, puis affiche du HTML mis en forme par du CSS. Comprendre cet échange, c’est pouvoir créer un site, mais aussi le dépanner et le sécuriser.',
cm: [
{ t: 'URL et architecture client-serveur', d: '15 min', html: 'Le <b>client</b> (navigateur) envoie une requête, le <b>serveur</b> (Apache, Nginx) répond. L’URL précise tout : <code>https://www.lycee.fr:443/cours/index.html?id=3</code> = protocole, nom d’hôte, port, chemin, paramètres.' },
{ t: 'Le protocole HTTP', d: '20 min', html: 'HTTP est un protocole texte. La requête contient une méthode, un chemin et des en-têtes ; la réponse contient un code d’état, des en-têtes et un corps.', exemple: 'GET /index.html HTTP/1.1\nHost: www.lycee.fr\n\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\n\n<!doctype html>…', table: [['Code', 'Sens'], ['200', 'OK'], ['301 / 302', 'redirection'], ['403', 'accès interdit'], ['404', 'ressource introuvable'], ['500', 'erreur du serveur']] },
{ t: 'HTML : la structure', d: '20 min', html: 'HTML décrit le sens du contenu avec des balises : titres <code>&lt;h1&gt;</code> à <code>&lt;h6&gt;</code>, paragraphes, listes, liens, images, formulaires. Une page bien structurée est lisible par les moteurs de recherche et les lecteurs d’écran.', exemple: '<!doctype html>\n<html lang="fr">\n<head><meta charset="utf-8"><title>Mon TP</title></head>\n<body>\n <h1>Mon premier site</h1>\n <p>Hébergé sur <a href="https://debian.org">Debian</a>.</p>\n</body>\n</html>' },
{ t: 'CSS : la présentation', d: '20 min', html: 'Une règle CSS associe un sélecteur à des déclarations. Sélecteurs : balise (<code>p</code>), classe (<code>.carte</code>), identifiant (<code>#menu</code>). La mise en page moderne repose sur <code>display: flex</code> et <code>display: grid</code>.', exemple: '.carte {\n border: 2px solid #000;\n padding: 16px;\n}\n.grille { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }' },
{ t: 'Formulaires : GET et POST', d: '10 min', html: 'Un formulaire envoie des données au serveur. <b>GET</b> les place dans l’URL (recherche, filtres). <b>POST</b> les place dans le corps de la requête (connexion, envoi de fichier).', attention: 'Un mot de passe ne doit jamais passer en GET : il finirait dans l’historique et les journaux du serveur.' },
{ t: 'Héberger : le serveur web', d: '15 min', html: 'Sous Debian, <code>apt install apache2</code> installe le serveur ; les pages sont dans <code>/var/www/html</code>, la configuration dans <code>/etc/apache2</code>, les journaux dans <code>/var/log/apache2</code>. <b>HTTPS</b> ajoute TLS et un certificat (Let’s Encrypt).' }],
synthese: ['Client → requête HTTP → serveur → réponse avec code', 'HTML = structure, CSS = présentation', 'GET dans l’URL, POST dans le corps', 'Apache : /var/www/html et /var/log/apache2'],
qcm: [
['Que signifie le code 403 ?', ['Introuvable', 'Accès interdit', 'Erreur serveur', 'Redirection'], 1, '403 Forbidden.'],
['Quelle partie de l’URL est ?id=3 ?', ['Le chemin', 'Les paramètres', 'Le port', 'Le protocole'], 1, 'La chaîne de requête.'],
['Quelle balise pour le titre principal ?', ['<title>', '<h1>', '<head>', '<header>'], 1, '<title> est le titre de l’onglet, <h1> celui de la page.'],
['Quel sélecteur CSS cible class="carte" ?', ['#carte', '.carte', 'carte', '*carte'], 1, 'Le point cible une classe.'],
['Pour un formulaire de connexion, on utilise :', ['GET', 'POST', 'PUT', 'HEAD'], 1, 'Les identifiants ne doivent pas apparaître dans l’URL.'],
['Où Apache sert-il les pages par défaut sous Debian ?', ['/etc/apache2', '/var/www/html', '/home/www', '/srv/http'], 1, 'DocumentRoot par défaut.'],
['Quel port utilise HTTPS ?', ['80', '443', '8080', '22'], 1, 'HTTP 80, HTTPS 443.']],
td: [
{ n: 'Échauffement', c: 'Décomposer https://ent.lycee.fr:8443/notes?eleve=12 : protocole, hôte, port, chemin, paramètres.', r: 'https · ent.lycee.fr · 8443 · /notes · eleve=12' },
{ n: 'Application', c: 'Écrire une page HTML avec un titre, une liste de 3 TP et un lien vers le lycée.', r: '<h1>Mes TP</h1>\n<ul><li>Terminal</li><li>Droits</li><li>Apache</li></ul>\n<a href="https://…">Mon lycée</a>' },
{ n: 'Application', c: 'Écrire le CSS pour afficher 3 cartes côte à côte, avec une bordure noire.', r: '.grille { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }\n.carte { border: 2px solid #000; padding: 16px; }' },
{ n: 'Approfondissement', c: 'Avec curl, afficher uniquement les en-têtes de réponse de https://example.org et relever le code et le type de contenu.', r: 'curl -I https://example.org → HTTP/2 200, content-type: text/html' }],
tp: { type: 'Lab web', url: 'apercu-lab-web.html', desc: 'Terminal et navigateur reliés : installer Apache, publier sa page, la voir en direct.', etapes: ['apt install apache2', 'Vérifier le service', 'Remplacer index.html', 'Actualiser le navigateur relié', 'Lire le journal d’accès'] },
plusLoin: [['MDN Web Docs (Mozilla)', 'https://developer.mozilla.org/fr/'], ['freeCodeCamp · Responsive Web Design', 'https://www.freecodecamp.org/']] },
{ id: 'crypto', match: /mot de passe|hachage|chiffrement|crypto|cybers|sécurité des accès|SSH par clé/i,
titre: 'Cryptographie appliquée : hachage, chiffrement, clés', duree: '5 h (CM 2 h · TD 1 h 30 · TP 1 h 30)', niveau: 'BTS SIO · BTS CIEL · BUT R&T Cyber · Master cybersécurité',
prerequis: ['Binaire et hexadécimal', 'Le terminal Linux'],
competences: ['BTS SIO · Bloc 3 · Cybersécurité des services informatiques', 'BTS CIEL · C08 · Sécuriser', 'BUT R&T parcours Cyber · Compétence « Protéger »'],
intro: 'Mots de passe stockés, sites en HTTPS, connexions SSH, signatures de mises à jour : la cryptographie protège presque tout ce que nous faisons en ligne. L’objectif n’est pas de devenir cryptographe, mais de savoir choisir et configurer les bons outils.',
cm: [
{ t: 'Les trois objectifs', d: '10 min', html: '<b>Confidentialité</b> : seul le destinataire lit le message (chiffrement). <b>Intégrité</b> : le message n’a pas été modifié (hachage, MAC). <b>Authenticité</b> : on sait qui l’a envoyé (signature, certificat).' },
{ t: 'Les fonctions de hachage', d: '20 min', html: 'Une fonction de hachage produit une empreinte de taille fixe. Elle est à sens unique, et un seul bit changé en entrée change complètement l’empreinte. Usages : vérifier un fichier téléchargé, stocker un mot de passe, signer.', exemple: '$ echo -n "bonjour" | sha256sum\n2cb4b1431b84ec15d35ed83bb927e27e8967d75f4bcd9cc4b25c8d879ae23e18\n$ echo -n "Bonjour" | sha256sum\n9172e8eec99f144f72eca9a568759580edadb2cfd154857f07e657569493bc44', attention: 'MD5 et SHA-1 sont cassés (collisions) : ne plus les utiliser pour la sécurité.' },
{ t: 'Stocker un mot de passe', d: '20 min', html: 'On ne stocke jamais un mot de passe en clair ni chiffré, mais un <b>hachage lent et salé</b>. Le <b>sel</b>, aléatoire et unique par compte, empêche les tables précalculées ; la lenteur (bcrypt, scrypt, Argon2) freine la force brute.', table: [['Méthode', 'Verdict'], ['En clair', 'interdit'], ['MD5 / SHA-1', 'cassé'], ['SHA-256 sans sel', 'insuffisant'], ['bcrypt, Argon2id', 'recommandé']] },
{ t: 'Chiffrement symétrique', d: '15 min', html: 'Une même clé chiffre et déchiffre. <b>AES</b> (128 ou 256 bits) est le standard : très rapide, il chiffre les disques, les fichiers, le trafic une fois la session établie. Le problème est d’échanger la clé de façon sûre.' },
{ t: 'Chiffrement asymétrique', d: '20 min', html: 'Chaque personne possède une paire de clés : la <b>publique</b>, diffusée, et la <b>privée</b>, gardée secrète. On chiffre avec la clé publique du destinataire ; lui seul déchiffre avec sa clé privée. À l’inverse, on <b>signe</b> avec sa clé privée et chacun vérifie avec la clé publique. Algorithmes : RSA, courbes elliptiques (Ed25519).' },
{ t: 'En pratique : HTTPS et SSH', d: '15 min', html: '<b>HTTPS (TLS)</b> : le serveur présente un certificat signé par une autorité ; un échange asymétrique établit une clé de session, puis AES chiffre le trafic. <b>SSH</b> : l’authentification par clé remplace le mot de passe.', exemple: '$ ssh-keygen -t ed25519 -C "eleve@syllabus"\n$ ssh-copy-id eleve@192.168.10.10\n$ ssh eleve@192.168.10.10 # plus de mot de passe demandé' }],
synthese: ['Hachage = sens unique, pour l’intégrité', 'Mots de passe : bcrypt ou Argon2, avec sel', 'Symétrique (AES) rapide, asymétrique (RSA, Ed25519) pour échanger et signer', 'Chiffrer avec la clé publique, signer avec la clé privée'],
qcm: [
['Quel objectif assure une fonction de hachage ?', ['Confidentialité', 'Intégrité', 'Disponibilité', 'Anonymat'], 1, 'Elle détecte toute modification.'],
['Pourquoi ajouter un sel ?', ['Accélérer', 'Rendre chaque hachage unique et empêcher les tables précalculées', 'Chiffrer le mot de passe', 'Compresser'], 1, 'Deux mots de passe identiques n’ont plus la même empreinte.'],
['Quel algorithme est recommandé pour stocker un mot de passe ?', ['MD5', 'SHA-1', 'Argon2id', 'AES'], 2, 'Lent, salé, conçu pour ça.'],
['Pour envoyer un message confidentiel à Alice, on chiffre avec :', ['sa clé privée', 'sa clé publique', 'notre clé publique', 'un hachage'], 1, 'Seule sa clé privée le déchiffrera.'],
['Pour signer un document, on utilise :', ['sa propre clé privée', 'la clé publique du destinataire', 'AES', 'SHA-1'], 0, 'Tout le monde vérifie avec notre clé publique.'],
['Dans HTTPS, les données de la page sont chiffrées avec :', ['RSA uniquement', 'une clé de session symétrique', 'le certificat', 'un hachage'], 1, 'L’asymétrique sert à établir la clé de session.'],
['Quelle commande génère une paire de clés SSH moderne ?', ['ssh-keygen -t ed25519', 'ssh-copy-id', 'openssl md5', 'gpg --hash'], 0, 'Ed25519 est recommandé.']],
td: [
{ n: 'Échauffement', c: 'Pour chaque besoin, dire s’il faut hacher, chiffrer ou signer : stocker les mots de passe, envoyer un bulletin confidentiel, prouver qu’une mise à jour vient bien de l’éditeur.', r: 'hacher (lent, salé) · chiffrer · signer' },
{ n: 'Application', c: 'Vérifier l’intégrité d’une image ISO téléchargée à partir de son fichier SHA256SUMS.', r: 'sha256sum -c SHA256SUMS --ignore-missing' },
{ n: 'Application', c: 'Une base de 10 000 comptes a fuité, hachée en MD5 sans sel. Expliquer les risques et la correction.', r: 'Les mots de passe courants sont retrouvés instantanément (tables arc-en-ciel). Forcer la réinitialisation et passer à Argon2id avec sel.' },
{ n: 'Approfondissement', c: 'Configurer un serveur SSH pour refuser les connexions par mot de passe et celles de root.', r: 'Dans /etc/ssh/sshd_config :\nPasswordAuthentication no\nPermitRootLogin no\npuis systemctl restart ssh' }],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Générer une paire de clés, se connecter sans mot de passe, durcir sshd.', etapes: ['Comparer des hachages SHA-256', 'Générer une clé Ed25519', 'Déposer la clé sur le serveur', 'Désactiver l’authentification par mot de passe', 'Vérifier dans /var/log/auth.log'] },
plusLoin: [['ANSSI · Guide de sélection d’algorithmes cryptographiques', 'https://cyber.gouv.fr/publications/mecanismes-cryptographiques'], ['CNIL · Recommandations sur les mots de passe', 'https://www.cnil.fr/fr/mots-de-passe-une-nouvelle-recommandation-pour-maitriser-sa-securite'], ['Root-Me · challenges cryptanalyse', 'https://www.root-me.org/']] }
]);

View file

@ -0,0 +1,33 @@
// Élargit la correspondance de cours complets existants aux chapitres qu’ils couvrent réellement
(function () {
var ext = {
terminal: /Fichiers : cp, mv|Lire et éditer|cat, less|Mode examen « Admin Linux »/i,
archi: /Performance : vmstat|Noyau et modules|lsmod|sysctl/i,
diagnostic0: /\bARP\b|ICMP, ping, traceroute|TCP et UDP : ports|Analyse de trames|Dépannage : réseau cassé|Mode examen « Réseau »/i,
sql: /Modèle relationnel|Agrégats : GROUP BY|DDL : CREATE|DML : INSERT|Vues, procédures|Transactions et ACID|Index, EXPLAIN/i,
tubes: /Expressions régulières/i,
binaire: /Codage des entiers/i,
crypto: /PKI, autorité de certification|X\.509|\bGPG\b/i,
pandas: /Nettoyage et qualité des données|Visualisation et tableaux de bord|Mode examen « Data »/i,
ml: /Sur-apprentissage|Biais, éthique|Sécurité des modèles/i,
neurones: /Traitement du langage et de la voix/i,
iot: /Node-RED|Modbus|CoAP/i,
python: /Mode examen « épreuve pratique NSI »|Types construits|Épreuve pratique chronométrée|Projets?$|Grand oral et projets|SAÉ|Projet technique|Projet de fin d’études|Projet data|Réaliser un développement d’application|Développement web et applications/i,
web2: /Photographie numérique|Histoire de l’informatique|Applications web|Développement : web, API/i,
iot2: /Chaîne d’information et chaîne d’énergie|Localisation et cartographie|GPS, NMEA/i,
ipv42: /Préparer une installation|plan d’adressage|Réseaux et infrastructure du SI/i,
dns2: /Présence en ligne/i,
sauvegarde: /Maintenir et dépanner : diagnostic, sauvegarde/i,
diagnostic2: /Épreuves pratiques|Épreuves blanches|Maintenir et sécuriser l’infrastructure|Administrer et sécuriser les infrastructures/i,
systemd: /Systèmes : Linux|Exploiter les serveurs Linux|LPIC-1|LFCS/i,
routagedyn: /réseau fondamental|CCNA/i,
vulns: /Sécurité offensive|pentest|exploitation/i,
ml2: /Éthique, biais et AI Act/i,
bigdata: /Ingénierie des données et big data/i,
modelisation: /Systèmes d’information et urbanisation|Génie logiciel et architecture/i
};
(window.CONTENUS_PRO || []).forEach(function (c) {
if (!c.intro) return;
Object.keys(ext).forEach(function (k) { if (k.replace(/\d+$/, '') === c.id) c.match = new RegExp(c.match.source + '|' + ext[k].source, 'i'); });
});
})();

View file

@ -0,0 +1,171 @@
// Cours complets · Data & IA lot 4 : pandas, apprentissage automatique, réseaux de neurones
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'pandas', match: /pandas|analyse de données|dataframe|traitement de données|données tabulaires|CSV/i,
titre: 'Analyse de données avec pandas : charger, nettoyer, agréger', duree: '',
niveau: 'NSI Terminale · BTS SIO SLAM · BUT Informatique / SD · Master data',
prerequis: ['Python : listes, dictionnaires, fonctions', 'SQL : SELECT, GROUP BY'],
competences: ['NSI · Traitement de données en tables', 'BTS SIO SLAM · Exploiter des données', 'BUT SD · Traiter des données'],
intro: 'Les données réelles arrivent sales : exports Pronote en point-virgule avec des virgules décimales, cellules vides, doublons, dates au format texte, noms mal orthographiés. Avant toute statistique ou tout modèle d’IA, un data engineer passe l’essentiel de son temps à les charger, les nettoyer et les restructurer. pandas est la bibliothèque Python de référence pour ce travail : elle manipule des tableaux de millions de lignes avec une syntaxe proche de SQL.',
cm: [
{ t: 'Series et DataFrame', d: '', html: 'Une <b>Series</b> est une colonne typée avec un <b>index</b> ; un <b>DataFrame</b> est un tableau de Series partageant le même index. Chaque colonne a un <b>dtype</b> (int64, float64, object pour le texte, datetime64, category). Les opérations sont <b>vectorisées</b> : <code>df["note"] * 2</code> s’applique à toute la colonne en une instruction, bien plus vite qu’une boucle Python.' },
{ t: 'Charger des données réelles', d: '', html: '<code>pd.read_csv</code> accepte de nombreux paramètres indispensables avec les exports français : <code>sep=";"</code>, <code>decimal=","</code>, <code>encoding="utf-8-sig"</code> ou <code>"latin-1"</code>, <code>parse_dates</code>, <code>dtype</code>. On inspecte toujours avant de calculer : <code>df.head()</code>, <code>df.info()</code> (types et valeurs manquantes), <code>df.describe()</code> (statistiques).', exemple: 'import pandas as pd\ndf = pd.read_csv("notes.csv", sep=";", decimal=",", encoding="utf-8-sig", parse_dates=["date"], dayfirst=True)\ndf.info()' },
{ t: 'Sélectionner et filtrer', d: '', html: '<code>df["col"]</code> sélectionne une colonne, <code>df[["a","b"]]</code> plusieurs. <code>df.loc[lignes, colonnes]</code> sélectionne par étiquette, <code>df.iloc</code> par position. Le filtrage utilise des masques booléens combinés par <code>&amp;</code>, <code>|</code> et <code>~</code>, chaque condition entre parenthèses.', exemple: 'df.loc[(df["classe"] == "SIO1") & (df["note"] < 8), ["nom", "matiere", "note"]]', attention: 'Modifier le résultat d’un filtre (df[df.note < 8]["note"] = 8) ne modifie pas df et produit un avertissement SettingWithCopy. On écrit df.loc[df.note < 8, "note"] = 8.' },
{ t: 'Nettoyer', d: '', html: 'Les valeurs manquantes sont <code>NaN</code>. <code>isna().sum()</code> les compte, <code>dropna</code> retire les lignes, <code>fillna</code> les remplace. Le choix est métier : une note absente n’est pas un zéro. <code>drop_duplicates</code> retire les doublons, <code>str.strip().str.upper()</code> normalise le texte, <code>astype</code> et <code>pd.to_numeric(errors="coerce")</code> convertissent les types en transformant les valeurs invalides en NaN.' },
{ t: 'Agréger et joindre', d: '', html: '<code>groupby</code> découpe, applique, combine, exactement comme GROUP BY en SQL : <code>df.groupby("classe")["note"].agg(["mean","median","count"])</code>. <code>pivot_table</code> produit un tableau croisé. <code>merge</code> joint deux DataFrame comme un JOIN, avec <code>how="left"</code> pour garder toutes les lignes de gauche et <code>validate="m:1"</code> pour vérifier qu’on ne multiplie pas les lignes par erreur.' },
{ t: 'Performances et export', d: '', html: 'Sur de gros volumes : lire seulement les colonnes utiles (<code>usecols</code>), typer le texte répétitif en <code>category</code>, éviter <code>apply</code> ligne à ligne au profit des opérations vectorisées, et stocker les résultats intermédiaires en <b>Parquet</b>, format colonne compressé et typé, bien plus rapide que le CSV. L’export final se fait avec <code>to_csv</code>, <code>to_excel</code> ou directement dans une base avec <code>to_sql</code>.' }
],
synthese: ['DataFrame = colonnes typées partageant un index ; opérations vectorisées', 'read_csv avec sep, decimal, encoding, parse_dates', 'Inspecter : head, info, describe, isna().sum()', 'loc pour sélectionner et modifier ; masques entre parenthèses', 'Valeurs manquantes : décision métier', 'groupby, pivot_table, merge avec validate', 'Parquet et category pour les gros volumes'],
qcm: [
['Quel paramètre pour lire « 12,5 » comme un nombre ?', ['sep=","', 'decimal=","', 'dtype=float', 'encoding'], 1, 'Virgule décimale française.'],
['Que montre df.info() ?', ['Les 5 premières lignes', 'Types et nombre de valeurs non nulles par colonne', 'La moyenne', 'Le graphique'], 1, 'Premier réflexe.'],
['Comment combiner deux conditions ?', ['and', '(c1) & (c2)', 'c1 && c2', 'c1 + c2'], 1, 'Opérateurs bit à bit sur des masques.'],
['Une note absente doit-elle devenir 0 ?', ['Toujours', 'Non, c’est une décision métier (absence ≠ zéro)', 'Oui avec fillna', 'Peu importe'], 1, 'Elle fausserait la moyenne.'],
['Équivalent pandas de GROUP BY ?', ['merge', 'groupby', 'pivot', 'concat'], 1, 'Découper, appliquer, combiner.'],
['Pourquoi préférer Parquet au CSV pour les fichiers intermédiaires ?', ['Plus lisible', 'Typé, compressé, lecture par colonne rapide', 'Obligatoire', 'Compatible Excel'], 1, 'Format colonne.']
],
td: [
{ n: 'Application', c: 'Charger l’export notes.csv (point-virgule, virgule décimale), afficher le nombre de notes manquantes par matière, puis la moyenne par classe et par matière en tableau croisé.', r: 'df = pd.read_csv("notes.csv", sep=";", decimal=",")\ndf.groupby("matiere")["note"].apply(lambda s: s.isna().sum())\ndf.pivot_table(index="classe", columns="matiere", values="note", aggfunc="mean").round(1)' },
{ n: 'Application', c: 'Joindre notes.csv et eleves.csv sur l’identifiant, en vérifiant qu’aucun élève n’est dupliqué, et lister les élèves sans aucune note.', r: 'm = eleves.merge(notes, on="id", how="left", validate="1:m")\nm[m["note"].isna()].drop_duplicates("id")[["nom","prenom"]]' },
{ n: 'Approfondissement', c: 'Repérer les élèves dont la moyenne a baissé de plus de 3 points entre le 1er et le 2e trimestre.', r: 'p = df.pivot_table(index="id", columns="trimestre", values="note", aggfunc="mean")\np[(p[1] - p[2]) > 3]' }
],
tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'Jeu de données anonymisé d’un établissement, tests automatiques.', etapes: ['Charger et inspecter', 'Nettoyer types et doublons', 'Agrégats par classe', 'Jointure avec les effectifs', 'Exporter un bilan en CSV'] },
plusLoin: [['Documentation pandas', 'https://pandas.pydata.org/docs/'], ['DataCamp · Data Manipulation with pandas', 'https://www.datacamp.com/courses/data-manipulation-with-pandas'], ['data.gouv.fr · jeux de données ouverts', 'https://www.data.gouv.fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Le tableau de bord du conseil de classe affiche une moyenne générale de 4,2 pour la SIO1, alors que les enseignants constatent des moyennes autour de 12. Trouver et corriger l’erreur du script d’analyse.', etapes: [
['Inspecter les types', 'df.info()\n note 1840 non-null object\n→ La colonne note est du texte : read_csv n’a pas reconnu la virgule décimale.'],
['Voir ce qui a été calculé', 'df["note"].head(3)\n0 12,5\n1 14\n2 abs\n→ Des valeurs « abs » et des virgules. Le script faisait pd.to_numeric(errors="coerce") : « 12,5 » et « abs » devenaient NaN puis étaient remplacés par 0 avec fillna(0).'],
['Corriger le chargement', 'df = pd.read_csv("notes.csv", sep=";", decimal=",", na_values=["abs", "ABS", "disp"])\ndf["note"].dtype\nfloat64\n→ Les absences deviennent des valeurs manquantes, pas des zéros.'],
['Recalculer', 'df.groupby("classe")["note"].mean().round(2)\nSIO1 11.87\n→ Moyenne cohérente avec le terrain.'],
['Prévenir', '→ Ajout de contrôles automatiques dans le script : assert df["note"].between(0, 20).all(), et journalisation du nombre d’absences traitées. Aucun fillna(0) sur des notes.']
] },
erreurs: ['Oublier decimal="," sur un export français', 'Remplacer les absences par zéro', 'Modifier un filtre au lieu d’utiliser loc', 'Boucler ligne par ligne avec iterrows', 'Joindre sans vérifier les doublons', 'Calculer sans avoir regardé info()'],
memo: [['Instruction', 'Usage'], ['pd.read_csv(f, sep=";", decimal=",")', 'lire un export français'], ['df.head() · df.info() · df.describe()', 'inspecter'], ['df.isna().sum()', 'valeurs manquantes par colonne'], ['df.loc[masque, cols]', 'filtrer et sélectionner'], ['df.groupby(k)[c].agg([...])', 'agréger'], ['df.pivot_table(index, columns, values)', 'tableau croisé'], ['a.merge(b, on, how, validate)', 'joindre'], ['df.drop_duplicates()', 'retirer les doublons'], ['pd.to_numeric(s, errors="coerce")', 'convertir en nombre'], ['df.to_parquet() · df.to_csv()', 'exporter']],
examen: { titre: 'Sujet type · 20 points', q: ['Écrire le chargement d’un export fourni. (3 pts)', 'Nettoyer les valeurs manquantes en justifiant. (4 pts)', 'Produire trois agrégats demandés. (6 pts)', 'Joindre deux tables et détecter les incohérences. (4 pts)', 'Expliquer vectorisation et Parquet. (3 pts)'], r: ['read_csv paramétré', 'na_values, pas de 0', 'groupby / pivot_table', 'merge validate, how', 'voir cours'] },
prof: { deroule: [['Ouverture', 'Un vrai export raté projeté'], ['Cours', 'DataFrame, chargement, inspection'], ['Pratique', 'Nettoyage dans l’IDE'], ['Pratique', 'Agrégats et jointures'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : cellules à trous. Élèves avancés : Parquet, polars, DataCamp.', eval: 'Tests IDE, sujet type.' } },
exemples: [
['Inspecter un export', 'Premier chargement.', 'df.info()', 'RangeIndex: 1840 entries\n # Column Non-Null Count Dtype\n 0 nom 1840 non-null object\n 1 classe 1840 non-null object\n 2 note 1712 non-null float64\n 3 date 1840 non-null datetime64[ns]', '128 notes manquantes (absences), types corrects : on peut calculer.'],
['Compter par groupe', 'Effectifs par classe.', 'df.groupby("classe")["nom"].nunique()', 'classe\nSIO1 31\nSIO2 27', 'nunique compte les élèves distincts, pas le nombre de lignes de notes.'],
['Détecter des doublons', 'Un élève apparaît deux fois.', 'eleves[eleves.duplicated("ine", keep=False)]', ' ine nom prenom\n12 070123456AB MARTIN Léa\n88 070123456AB Martin Lea', 'Même identifiant INE, casse et accents différents : on normalise avant de dédoublonner.'],
['Jointure qui multiplie les lignes', 'Le nombre de lignes explose après merge.', 'notes.merge(eleves, on="nom", validate="m:1")', 'MergeError: Merge keys are not unique in right dataset; not a many-to-one merge', 'Deux élèves portent le même nom : on joint sur l’identifiant, jamais sur le nom.'],
['Tableau croisé', 'Bilan pour le conseil.', 'df.pivot_table(index="classe", columns="matiere", values="note", aggfunc="mean").round(1)', 'matiere Maths Réseaux Dev\nclasse\nSIO1 11.2 12.8 10.4\nSIO2 12.0 13.1 12.7', 'Le Dev de SIO1 est le point faible, piste pour le conseil.'],
['Mesurer un gain de performance', 'Script lent sur 2 millions de lignes.', '%timeit df["total"] = df.apply(lambda r: r.note * r.coef, axis=1)\n%timeit df["total"] = df["note"] * df["coef"]', '8.41 s ± 120 ms\n6.2 ms ± 0.3 ms', 'La version vectorisée est plus de mille fois plus rapide.']
],
termes: ['pandas', 'DataFrame', 'Series', 'index', 'dtype', 'read_csv', 'loc', 'iloc', 'NaN', 'isna', 'fillna', 'dropna', 'groupby', 'agg', 'pivot_table', 'merge', 'validate', 'drop_duplicates', 'Parquet', 'category', 'vectorisé']
});
reg({
cours: { id: 'ml', match: /apprentissage automatique|machine learning|scikit|régression|classification|intelligence artificielle|\bIA\b/i,
titre: 'Apprentissage automatique : du jeu de données au modèle évalué', duree: '',
niveau: 'NSI Terminale (k plus proches voisins) · BUT SD / Informatique · École d’ingénieurs · Master IA',
prerequis: ['Analyse de données avec pandas', 'Statistiques descriptives'],
competences: ['NSI · Algorithme des k plus proches voisins', 'BUT SD · Modéliser', 'Master · Concevoir un système d’apprentissage'],
intro: 'Un modèle d’apprentissage automatique apprend une règle à partir d’exemples plutôt qu’on ne la lui programme. Il détecte un courriel frauduleux, prédit le risque de décrochage d’un élève ou reconnaît un caractère manuscrit. Mais un modèle mal évalué donne des chiffres flatteurs et des décisions fausses. Ce module suit la démarche complète avec scikit-learn : préparer les données, séparer apprentissage et test, choisir un modèle, mesurer correctement, et comprendre les limites éthiques et réglementaires.',
cm: [
{ t: 'Apprendre à partir d’exemples', d: '', html: 'En <b>apprentissage supervisé</b>, chaque exemple a des <b>variables explicatives</b> (X) et une <b>cible</b> (y) connue : on apprend à prédire y à partir de X. C’est une <b>classification</b> si y est une catégorie (décroche / ne décroche pas), une <b>régression</b> si y est un nombre (note finale). En <b>apprentissage non supervisé</b>, il n’y a pas de cible : on cherche une structure, par exemple des groupes d’élèves aux profils proches (clustering).' },
{ t: 'Séparer apprentissage et test', d: '', html: 'Un modèle évalué sur les données qui ont servi à l’entraîner paraît toujours excellent : il les a apprises par cœur. On sépare donc un <b>jeu d’apprentissage</b> et un <b>jeu de test</b> jamais vu (<code>train_test_split</code>, souvent 80/20, stratifié pour garder la proportion des classes). Pour régler les hyperparamètres sans toucher au test, on utilise la <b>validation croisée</b> : l’apprentissage est découpé en k parts, chacune servant tour à tour de validation.<br><br>Le <b>surapprentissage</b> (overfitting) se voit quand le score d’apprentissage est très bon et celui de test nettement moins bon ; le <b>sous-apprentissage</b> quand les deux sont mauvais.', attention: 'La fuite de données (data leakage) fausse tout : normaliser toutes les données avant la séparation, ou inclure une variable connue seulement après coup (la note finale pour prédire le décrochage), donne un modèle parfait en test et inutile en réalité.' },
{ t: 'Préparer les variables', d: '', html: 'Les modèles attendent des nombres. On <b>encode</b> les catégories (OneHotEncoder), on <b>normalise</b> les échelles (StandardScaler) pour les modèles sensibles aux distances comme les k plus proches voisins, on traite les valeurs manquantes (SimpleImputer). Un <b>Pipeline</b> enchaîne ces étapes et le modèle : il apprend les paramètres de préparation sur l’apprentissage uniquement et les applique au test, ce qui évite la fuite de données.', exemple: 'from sklearn.pipeline import make_pipeline\nfrom sklearn.compose import make_column_transformer\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\nfrom sklearn.ensemble import RandomForestClassifier\n\nprep = make_column_transformer(\n (StandardScaler(), ["absences", "moyenne_t1", "retards"]),\n (OneHotEncoder(handle_unknown="ignore"), ["filiere"]))\nmodele = make_pipeline(prep, RandomForestClassifier(n_estimators=300, random_state=0))\nmodele.fit(X_train, y_train)' },
{ t: 'Quelques modèles de référence', d: '', html: 'Les <b>k plus proches voisins</b> (au programme de NSI) classent un exemple selon la classe majoritaire de ses k voisins les plus proches. La <b>régression linéaire</b> et la <b>régression logistique</b> sont simples, rapides et interprétables. Les <b>arbres de décision</b> produisent des règles lisibles ; les <b>forêts aléatoires</b> et le <b>gradient boosting</b> en combinent des centaines et sont souvent les plus performants sur des données tabulaires. On commence toujours par une <b>référence simple</b> (prédire la classe majoritaire) pour savoir si le modèle apporte vraiment quelque chose.' },
{ t: 'Mesurer correctement', d: '', html: 'L’<b>exactitude</b> (accuracy) trompe quand les classes sont déséquilibrées : si 5 % des élèves décrochent, prédire « personne ne décroche » donne 95 %. La <b>matrice de confusion</b> distingue vrais et faux positifs et négatifs. La <b>précision</b> mesure la part de vrais positifs parmi les alertes, le <b>rappel</b> la part des vrais cas détectés, le <b>F1</b> leur moyenne harmonique. Le choix dépend du coût des erreurs : manquer un élève en difficulté coûte plus cher qu’un entretien inutile, on privilégie donc le rappel. En régression, on utilise l’erreur absolue moyenne (MAE) ou la racine de l’erreur quadratique (RMSE).' },
{ t: 'Éthique et cadre légal', d: '', html: 'Un modèle reproduit les biais de ses données : entraîné sur des décisions passées discriminantes, il les automatise. Pour des décisions qui touchent des personnes, et a fortiori des mineurs, on vérifie les performances par sous-groupe, on garde un humain dans la décision et on explique les facteurs du modèle. Le <b>RGPD</b> encadre les décisions individuelles automatisées (article 22) et impose la minimisation des données ; le <b>règlement européen sur l’IA</b> (AI Act) classe l’éducation parmi les usages à haut risque, soumis à des obligations de documentation, de qualité des données et de supervision humaine.' }
],
synthese: ['Supervisé (classification, régression) ou non supervisé', 'Toujours évaluer sur un jeu de test jamais vu ; validation croisée pour régler', 'Pipeline pour préparer sans fuite de données', 'Partir d’une référence simple', 'Accuracy trompeuse si déséquilibre : matrice de confusion, précision, rappel, F1', 'Biais, supervision humaine, RGPD article 22, AI Act'],
qcm: [
['Prédire la note finale d’un élève est une :', ['classification', 'régression', 'clustering', 'réduction de dimension'], 1, 'La cible est un nombre.'],
['Score d’apprentissage 0,99, score de test 0,71 : c’est', ['du sous-apprentissage', 'du surapprentissage', 'normal', 'une fuite de données'], 1, 'Le modèle a appris par cœur.'],
['5 % de décrocheurs : un modèle qui prédit « aucun » a une accuracy de :', ['5 %', '50 %', '95 %', '0 %'], 2, 'D’où les autres métriques.'],
['Quelle métrique privilégier pour ne manquer aucun élève en difficulté ?', ['Précision', 'Rappel', 'Accuracy', 'MAE'], 1, 'Part des vrais cas détectés.'],
['Pourquoi utiliser un Pipeline ?', ['Esthétique', 'Pour appliquer la préparation apprise sur l’apprentissage seulement, sans fuite', 'Plus rapide', 'Obligatoire'], 1, 'Évite la fuite de données.'],
['Selon l’AI Act, l’usage de l’IA dans l’éducation est classé :', ['interdit', 'à haut risque', 'sans risque', 'non concerné'], 1, 'Obligations renforcées.']
],
td: [
{ n: 'Échauffement', c: 'Implémenter les k plus proches voisins en Python pur pour classer un point à partir d’une liste de points étiquetés.', r: 'def knn(pts, x, k):\n d = sorted(pts, key=lambda p: (p[0]-x[0])**2 + (p[1]-x[1])**2)[:k]\n etiq = [p[2] for p in d]\n return max(set(etiq), key=etiq.count)' },
{ n: 'Application', c: 'Entraîner une régression logistique et une forêt aléatoire sur le jeu fourni, comparer leur F1 en validation croisée à 5 plis, puis évaluer la meilleure sur le test.', r: 'from sklearn.model_selection import cross_val_score\nfor m in [logit, foret]:\n print(cross_val_score(m, X_train, y_train, cv=5, scoring="f1").mean())\nmeilleur.fit(X_train, y_train); f1_score(y_test, meilleur.predict(X_test))' },
{ n: 'Approfondissement', c: 'Vérifier si le modèle de détection du décrochage a un rappel différent pour les filles et les garçons, et proposer une conduite à tenir.', r: 'for g in ["F","M"]: m = X_test["sexe"] == g; recall_score(y_test[m], pred[m])\nSi écart important : examiner les données, rééquilibrer, ajuster le seuil par sous-groupe avec prudence, documenter, et garder la décision humaine.' }
],
tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'scikit-learn dans le navigateur (Pyodide), jeu de données synthétique.', etapes: ['Séparer apprentissage et test', 'Construire un Pipeline', 'Comparer trois modèles en validation croisée', 'Lire la matrice de confusion', 'Analyser les performances par sous-groupe'] },
plusLoin: [['scikit-learn · guide utilisateur', 'https://scikit-learn.org/stable/user_guide.html'], ['CNIL · IA : dossiers et recommandations', 'https://www.cnil.fr/fr/intelligence-artificielle'], ['DataCamp · Supervised Learning with scikit-learn', 'https://www.datacamp.com/courses/supervised-learning-with-scikit-learn'], ['Kaggle Learn · Intro to Machine Learning', 'https://www.kaggle.com/learn/intro-to-machine-learning']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Un prototype de détection du décrochage affiche 98 % d’exactitude et 0,97 de F1 en test. Mis en service sur la nouvelle année, il ne détecte presque aucun élève. Trouver pourquoi.', etapes: [
['Regarder les variables', 'modele[-1].feature_importances_ triées\nnb_absences_annee 0.71\nmoyenne_t1 0.08\n→ Le nombre d’absences sur l’année entière domine. En septembre, cette variable n’est pas encore connue.'],
['Identifier la fuite', '→ La variable résume toute l’année, y compris la période où l’élève a déjà décroché : le modèle « prédit » à partir d’un effet du décrochage. C’est une fuite de données temporelle.'],
['Reconstruire le jeu', 'Variables limitées à ce qui est connu au 15 octobre : absences et retards de septembre, moyenne des premières évaluations, filière.\nSéparation temporelle : apprentissage sur 2022-2024, test sur 2025.\n→ On évalue comme on utilisera le modèle.'],
['Réévaluer', 'F1 test : 0.58, rappel 0.71, précision 0.49\n→ Scores plus modestes mais honnêtes : le modèle repère 71 % des futurs décrocheurs, avec une alerte sur deux justifiée.'],
['Décider de l’usage', '→ Usage limité à une aide au repérage pour l’équipe de vie scolaire, jamais une décision automatique ; analyse d’impact (AIPD) et information des familles. Documentation du modèle et de ses limites.']
] },
erreurs: ['Évaluer sur les données d’apprentissage', 'Normaliser avant de séparer', 'Utiliser une variable connue après coup', 'Se fier à l’accuracy sur des classes déséquilibrées', 'Pas de référence simple', 'Décision automatique sur des élèves sans supervision humaine'],
memo: [['Instruction', 'Usage'], ['train_test_split(X, y, test_size=0.2, stratify=y)', 'séparer'], ['make_pipeline(prep, modele)', 'préparer sans fuite'], ['cross_val_score(m, X, y, cv=5, scoring="f1")', 'validation croisée'], ['GridSearchCV', 'régler les hyperparamètres'], ['DummyClassifier', 'référence simple'], ['confusion_matrix · classification_report', 'mesurer'], ['precision_score · recall_score · f1_score', 'métriques'], ['mean_absolute_error', 'régression'], ['feature_importances_ · permutation_importance', 'comprendre le modèle']],
examen: { titre: 'Sujet type · 20 points', q: ['Distinguer classification, régression, clustering. (3 pts)', 'Construire et évaluer un pipeline sur un jeu fourni. (8 pts)', 'Interpréter une matrice de confusion. (4 pts)', 'Identifier une fuite de données dans un cas donné. (3 pts)', 'Citer deux obligations RGPD ou AI Act. (2 pts)'], r: ['voir cours', 'split, pipeline, cv, test', 'précision, rappel', 'variable postérieure', 'art. 22, haut risque'] },
prof: { deroule: [['Ouverture', 'Un modèle à 95 % qui ne sert à rien'], ['Cours', 'Supervisé, séparation, métriques'], ['Pratique', 'kNN à la main'], ['Pratique', 'Pipeline scikit-learn'], ['Débat', 'IA et décisions sur les élèves'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : notebook guidé. Élèves avancés : gradient boosting, interprétabilité (SHAP), Kaggle.', eval: 'Notebook rendu, sujet type.' } },
exemples: [
['Séparer proprement', 'Préparer l’évaluation.', 'X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)\nprint(y_tr.mean(), y_te.mean())', '0.0512 0.0508', 'stratify garde la même proportion de cas positifs dans les deux jeux.'],
['Référence simple', 'Savoir si le modèle apporte quelque chose.', 'DummyClassifier(strategy="most_frequent").fit(X_tr, y_tr).score(X_te, y_te)', '0.949', 'Toute accuracy inférieure à 95 % est pire que ne rien faire : il faut regarder rappel et F1.'],
['Matrice de confusion', 'Évaluer le modèle.', 'confusion_matrix(y_te, pred)', '[[1782 31]\n [ 28 69]]', '69 décrocheurs repérés sur 97 (rappel 0,71), 31 fausses alertes sur 100 alertes (précision 0,69).'],
['Rapport complet', 'Présenter les résultats.', 'print(classification_report(y_te, pred, target_names=["reste", "décroche"]))', ' precision recall f1-score support\n reste 0.98 0.98 0.98 1813\n décroche 0.69 0.71 0.70 97', 'La classe minoritaire est celle qui compte ; ses scores sont bien plus faibles que ceux de la majorité.'],
['Détecter le surapprentissage', 'Arbre de décision très profond.', 'arbre.score(X_tr, y_tr), arbre.score(X_te, y_te)', '(1.0, 0.89)', 'Score parfait en apprentissage, bien moindre en test : on limite max_depth ou on passe à une forêt aléatoire.'],
['Importance par permutation', 'Expliquer le modèle à l’équipe.', 'r = permutation_importance(modele, X_te, y_te, scoring="f1", n_repeats=10)\nsorted(zip(r.importances_mean, X_te.columns), reverse=True)[:3]', '[(0.21, "absences_sept"), (0.09, "moyenne_eval1"), (0.04, "retards_sept")]', 'Les absences de septembre sont le premier signal, cohérent avec l’expérience des CPE.']
],
termes: ['apprentissage supervisé', 'classification', 'régression', 'clustering', 'jeu d’apprentissage', 'jeu de test', 'validation croisée', 'surapprentissage', 'fuite de données', 'Pipeline', 'StandardScaler', 'OneHotEncoder', 'k plus proches voisins', 'forêt aléatoire', 'matrice de confusion', 'précision', 'rappel', 'F1', 'accuracy', 'scikit-learn', 'AI Act', 'RGPD']
});
reg({
cours: { id: 'neurones', match: /réseaux? de neurones|deep learning|apprentissage profond|PyTorch|TensorFlow|OCR|reconnaissance vocale/i,
titre: 'Réseaux de neurones : principe, entraînement et applications (OCR, voix)', duree: '',
niveau: 'BUT Informatique / SD · École d’ingénieurs · Master IA',
prerequis: ['Apprentissage automatique', 'Dérivées et matrices'],
competences: ['Master · Concevoir et entraîner un modèle d’apprentissage profond', 'École d’ingénieurs · Intégrer un modèle en production'],
intro: 'Reconnaître un caractère manuscrit, transcrire une voix, décrire une image : ces tâches, longtemps hors de portée des programmes classiques, sont aujourd’hui résolues par des réseaux de neurones. Ce module explique ce qu’est réellement un réseau, comment il apprend par descente de gradient et rétropropagation, quelles architectures conviennent aux images et au son, et comment on met un tel modèle en production.',
cm: [
{ t: 'Le neurone et la couche', d: '', html: 'Un neurone artificiel calcule une somme pondérée de ses entrées, ajoute un biais et applique une <b>fonction d’activation</b> non linéaire : ReLU (max(0, x)) dans les couches cachées, sigmoïde ou softmax en sortie pour obtenir des probabilités. Une <b>couche dense</b> est un produit matriciel suivi d’une activation. Empiler des couches permet de représenter des fonctions très complexes ; sans non-linéarité, plusieurs couches se réduiraient à une seule transformation linéaire.' },
{ t: 'Apprendre : perte, gradient, rétropropagation', d: '', html: 'L’entraînement minimise une <b>fonction de perte</b> (entropie croisée pour la classification, erreur quadratique pour la régression). La <b>descente de gradient</b> ajuste chaque poids dans la direction qui fait baisser la perte, d’un pas fixé par le <b>taux d’apprentissage</b>. La <b>rétropropagation</b> calcule efficacement ces gradients couche par couche grâce à la règle de dérivation des fonctions composées. On traite les données par <b>lots</b> (mini-batches) et on parcourt plusieurs fois le jeu (<b>époques</b>). Un taux trop grand fait diverger, trop petit rend l’apprentissage interminable.' },
{ t: 'Images : réseaux convolutifs', d: '', html: 'Une image de 28×28 pixels a 784 entrées ; une photo en a des millions. Les <b>réseaux convolutifs</b> (CNN) appliquent de petits filtres qui glissent sur l’image et détectent des motifs locaux (bords, courbes), puis des motifs de plus en plus abstraits d’une couche à l’autre. Les poids d’un filtre sont partagés sur toute l’image, ce qui réduit énormément le nombre de paramètres et rend le réseau insensible à la position du motif. C’est le cœur des systèmes d’<b>OCR</b> : détection des zones de texte, puis reconnaissance des caractères.' },
{ t: 'Séquences : son, texte et transformeurs', d: '', html: 'Le son est une séquence : on le convertit d’abord en <b>spectrogramme</b> (énergie par fréquence au cours du temps, souvent en échelle de Mel), que l’on traite comme une image ou comme une séquence. Les réseaux récurrents ont longtemps servi à la reconnaissance vocale ; ils sont aujourd’hui largement remplacés par les <b>transformeurs</b>, fondés sur le mécanisme d’<b>attention</b> qui relie chaque élément de la séquence à tous les autres. Whisper (transcription) et les grands modèles de langage reposent sur cette architecture.' },
{ t: 'Entraîner en pratique', d: '', html: 'On entraîne rarement de zéro : l’<b>apprentissage par transfert</b> part d’un modèle pré-entraîné sur un grand corpus et l’ajuste (fine-tuning) sur ses propres données. Contre le surapprentissage : <b>augmentation de données</b> (rotations, bruit, variations de vitesse pour la voix), <b>dropout</b>, <b>arrêt précoce</b> quand la perte de validation remonte. Les calculs se font sur GPU ; un suivi des courbes de perte d’apprentissage et de validation est indispensable.' },
{ t: 'Mettre en production', d: '', html: 'Un modèle en production doit être rapide et léger : export en format standard (ONNX), <b>quantification</b> (poids en 8 bits au lieu de 32), exécution sur CPU si possible. On surveille la <b>dérive</b> : les données réelles s’éloignent de celles de l’entraînement (nouvelle police de caractères, micro différent), et les performances baissent sans erreur visible. Côté réglementaire, la voix est une donnée biométrique potentielle au sens du RGPD : traitement local de préférence, finalité limitée, conservation minimale.' }
],
synthese: ['Neurone = somme pondérée + biais + activation non linéaire', 'Perte, descente de gradient, rétropropagation, lots et époques', 'CNN pour les images et l’OCR ; spectrogrammes et transformeurs pour la voix', 'Transfert, augmentation, dropout, arrêt précoce', 'Production : ONNX, quantification, surveillance de la dérive, RGPD'],
qcm: [
['Pourquoi une activation non linéaire ?', ['Pour accélérer', 'Sinon les couches se réduisent à une seule transformation linéaire', 'Pour normaliser', 'Obligatoire en Python'], 1, 'La non-linéarité donne la puissance.'],
['Que calcule la rétropropagation ?', ['Les prédictions', 'Les gradients de la perte par rapport aux poids', 'Le taux d’apprentissage', 'Les lots'], 1, 'Règle de dérivation en chaîne.'],
['Avantage principal d’une convolution sur une image ?', ['Couleurs', 'Partage des poids et détection de motifs locaux', 'Plus de paramètres', 'Aucun'], 1, 'Moins de paramètres, invariance à la position.'],
['Comment représente-t-on souvent le son pour un réseau ?', ['En texte', 'En spectrogramme', 'En binaire brut', 'En CSV'], 1, 'Temps × fréquence.'],
['La perte de validation remonte alors que celle d’apprentissage baisse :', ['tout va bien', 'surapprentissage, arrêt précoce', 'taux trop faible', 'données manquantes'], 1, 'On arrête ou on régularise.'],
['Qu’est-ce que la quantification ?', ['Compter les données', 'Réduire la précision des poids pour alléger le modèle', 'Ajouter des couches', 'Chiffrer le modèle'], 1, 'Souvent 8 bits.']
],
td: [
{ n: 'Application', c: 'Calculer à la main la sortie d’un neurone : entrées (1, 2), poids (0,5 ; −1), biais 0,2, activation ReLU.', r: '0,5 × 1 + (−1) × 2 + 0,2 = −1,3 → ReLU = 0' },
{ n: 'Application', c: 'Écrire en PyTorch un réseau à deux couches pour MNIST (784 → 128 → 10) et sa boucle d’entraînement.', r: 'model = nn.Sequential(nn.Flatten(), nn.Linear(784,128), nn.ReLU(), nn.Linear(128,10))\nopt = torch.optim.Adam(model.parameters(), 1e-3); loss = nn.CrossEntropyLoss()\nfor x, y in loader: opt.zero_grad(); l = loss(model(x), y); l.backward(); opt.step()' },
{ n: 'Approfondissement', c: 'Un OCR entraîné sur des copies scannées échoue sur des photos prises au téléphone. Proposer trois actions.', r: 'Augmentation (perspective, flou, éclairage) · fine-tuning sur un échantillon de photos annotées · prétraitement (redressement, binarisation) et surveillance de la dérive.' }
],
tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'Réseau dense sur des chiffres manuscrits, dans le navigateur.', etapes: ['Visualiser le jeu de données', 'Construire le réseau', 'Tracer les courbes de perte', 'Mesurer l’exactitude par chiffre', 'Tester l’augmentation de données'] },
plusLoin: [['3Blue1Brown · Neural networks (vidéos)', 'https://www.3blue1brown.com/topics/neural-networks'], ['Tutoriels PyTorch', 'https://pytorch.org/tutorials/'], ['Fidle (CNRS) · formation deep learning en français', 'https://fidle.cnrs.fr/'], ['CNIL · la voix, une donnée à protéger', 'https://www.cnil.fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Le module de transcription vocale des consignes, déployé en salle de TP, fonctionnait bien en tests mais produit des transcriptions incohérentes depuis l’installation des nouveaux casques.', etapes: [
['Comparer les entrées', 'soxi test_ancien.wav nouveau.wav\nSample Rate : 16000 | Sample Rate : 48000\n→ Les nouveaux casques enregistrent en 48 kHz ; le modèle attend du 16 kHz.'],
['Vérifier le prétraitement', '→ Le code passait l’audio brut au modèle sans rééchantillonnage : le spectrogramme calculé était « compressé » en fréquence, comme une voix accélérée trois fois.'],
['Corriger', 'audio = torchaudio.functional.resample(audio, orig_freq=sr, new_freq=16000)\n→ Rééchantillonnage systématique à l’entrée, avec contrôle du format.'],
['Valider', 'WER sur 50 consignes de test : 38 % → 7 %\n→ Le taux d’erreur de mots revient au niveau des tests.'],
['Prévenir', '→ Assertion sur la fréquence et le nombre de canaux, surveillance du WER sur un échantillon hebdomadaire, et traitement local sans conservation des enregistrements.']
] },
erreurs: ['Pas de jeu de validation', 'Taux d’apprentissage choisi au hasard', 'Ignorer les courbes de perte', 'Prétraitement différent entre entraînement et production', 'Entraîner de zéro sans nécessité', 'Envoyer des voix d’élèves à un service tiers sans analyse RGPD'],
memo: [['Élément', 'À retenir'], ['nn.Linear · nn.Conv2d', 'couche dense · convolution'], ['nn.ReLU · softmax', 'activations'], ['nn.CrossEntropyLoss', 'perte de classification'], ['torch.optim.Adam(lr=1e-3)', 'optimiseur courant'], ['loss.backward() · opt.step()', 'rétropropagation et mise à jour'], ['model.eval() · torch.no_grad()', 'mode inférence'], ['Dropout · EarlyStopping', 'contre le surapprentissage'], ['torch.onnx.export', 'export pour la production'], ['WER · CER', 'erreurs de mots et de caractères (voix, OCR)']],
examen: { titre: 'Sujet type · 20 points', q: ['Calculer la sortie d’un petit réseau. (4 pts)', 'Expliquer descente de gradient et rétropropagation. (5 pts)', 'Justifier le choix d’un CNN pour l’OCR. (4 pts)', 'Analyser des courbes de perte fournies. (4 pts)', 'Citer deux précautions de mise en production. (3 pts)'], r: ['produit matriciel + activation', 'voir cours', 'motifs locaux, partage des poids', 'surapprentissage ou non', 'dérive, quantification, RGPD'] },
prof: { deroule: [['Ouverture', 'Démonstration OCR sur une copie'], ['Cours', 'Neurone, perte, gradient'], ['Pratique', 'Réseau dense MNIST'], ['Cours', 'CNN et spectrogrammes'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : calculs à la main guidés. Élèves avancés : fine-tuning, Fidle.', eval: 'Notebook, sujet type.' } },
exemples: [
['Vérifier les dimensions', 'Erreur à la première passe.', 'x, y = next(iter(loader)); print(x.shape, model(x).shape)', 'torch.Size([64, 1, 28, 28]) torch.Size([64, 10])', 'Un lot de 64 images en entrée, 10 scores en sortie : les dimensions sont cohérentes.'],
['Suivre l’apprentissage', 'Entraînement en cours.', 'for e in range(5): … print(e, perte_train, perte_val, acc_val)', '0 0.412 0.228 0.934\n1 0.191 0.160 0.953\n2 0.139 0.131 0.961\n3 0.108 0.129 0.962\n4 0.085 0.134 0.961', 'La perte de validation remonte à l’époque 4 : on garde le modèle de l’époque 3 (arrêt précoce).'],
['Taux d’apprentissage trop grand', 'La perte explose.', 'opt = torch.optim.SGD(model.parameters(), lr=5.0)', 'époque 0 perte 2.30 → époque 1 perte nan', 'La perte devient NaN : les poids divergent. On revient à lr=1e-2 pour SGD ou 1e-3 pour Adam.'],
['Mesurer l’OCR', 'Évaluer sur un jeu annoté.', 'jiwer.cer(references, predictions)', '0.043', '4,3 % de caractères erronés : acceptable pour du texte imprimé, à surveiller pour l’écriture manuscrite.'],
['Alléger le modèle', 'Déploiement sur un mini-PC de salle.', 'q = torch.quantization.quantize_dynamic(model, {nn.Linear}, dtype=torch.qint8)\nprint(taille(model), taille(q))', '1.62 Mo 0.43 Mo', 'Le modèle quantifié est près de quatre fois plus léger, pour une perte d’exactitude généralement faible, à mesurer.'],
['Transcrire localement', 'Consigne vocale en salle.', 'whisper consigne.wav --model small --language fr', '[00:00.000 --> 00:04.200] Ouvrez le terminal et tapez ip addr.', 'Le modèle tourne sur la machine locale : aucune voix d’élève ne sort de l’établissement.']
],
termes: ['réseau de neurones', 'neurone', 'couche', 'ReLU', 'softmax', 'fonction de perte', 'descente de gradient', 'rétropropagation', 'taux d’apprentissage', 'époque', 'lot', 'CNN', 'convolution', 'spectrogramme', 'transformeur', 'attention', 'apprentissage par transfert', 'dropout', 'arrêt précoce', 'ONNX', 'quantification', 'dérive', 'OCR', 'WER', 'PyTorch']
});
})();

View file

@ -0,0 +1,182 @@
// Cours complets · DevOps lot 1 : Docker, Docker Compose, Ansible
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'docker', match: /Docker(?! Compose)|Dockerfile|conteneur/i,
titre: 'Conteneurs Docker : images, Dockerfile, réseaux et volumes', duree: '',
niveau: 'BTS SIO SISR / SLAM · BTS CIEL IR · BUT R&T parcours DevCloud · AIS · Master',
prerequis: ['Le terminal Linux', 'Processus et services systemd', 'Réseau : ports, NAT'],
competences: ['BTS SIO SISR · Bloc 2 · Virtualisation et conteneurs', 'BTS SIO SLAM · Bloc 2 · Déployer une solution applicative', 'BUT R&T DevCloud · Compétence « Automatiser le déploiement »', 'AIS · Administrer une infrastructure virtualisée'],
intro: '« Chez moi ça marche. » Pendant des années, le passage d’une application du poste du développeur au serveur de production a été la principale source d’incidents : version de PHP différente, bibliothèque manquante, configuration oubliée. Le conteneur règle ce problème en livrant l’application avec tout son environnement d’exécution, dans une image immuable qui s’exécute à l’identique partout. Docker a popularisé cette approche en 2013 ; elle est aujourd’hui le socle du déploiement moderne, des petites applications de lycée jusqu’aux plateformes Kubernetes des hébergeurs. Ce module explique ce qu’est réellement un conteneur pour le noyau Linux, comment construire des images propres et sûres, et comment les exploiter en production.',
cm: [
{ t: 'Ce qu’est un conteneur pour le noyau', d: '', html: 'Un conteneur n’est pas une machine virtuelle. Il n’y a ni matériel émulé ni second noyau : c’est un <b>processus Linux ordinaire</b>, que le noyau isole grâce à deux mécanismes. Les <b>namespaces</b> donnent au processus sa propre vue du système : arborescence de processus (pid), interfaces réseau (net), points de montage (mnt), nom d’hôte (uts), utilisateurs (user), communications inter-processus (ipc). Les <b>cgroups</b> (control groups) limitent et mesurent les ressources qu’il consomme : processeur, mémoire, entrées-sorties.<br><br>Conséquences directes : un conteneur démarre en une fraction de seconde, consomme très peu de mémoire en plus de son application, et partage le noyau de l’hôte. C’est aussi sa limite de sécurité : une faille du noyau touche tous les conteneurs. Une VM isole plus fortement, un conteneur est plus léger ; en production, on combine souvent les deux (conteneurs dans des VM).', table: [['', 'Machine virtuelle', 'Conteneur'], ['Isolation', 'hyperviseur, noyau séparé', 'namespaces et cgroups, noyau partagé'], ['Démarrage', 'dizaines de secondes', 'fraction de seconde'], ['Taille', 'plusieurs Go', 'quelques Mo à centaines de Mo'], ['OS invité', 'n’importe lequel', 'même noyau que l’hôte (Linux)'], ['Usage type', 'serveur complet, isolation forte', 'application et ses dépendances']] },
{ t: 'Images, couches et registres', d: '', html: 'Une <b>image</b> est un modèle en lecture seule, composé de <b>couches</b> superposées : chaque instruction d’un Dockerfile qui modifie le système de fichiers ajoute une couche. Le pilote de stockage (overlay2) les empile et ajoute au-dessus une couche inscriptible propre à chaque conteneur. Plusieurs conteneurs issus de la même image partagent ses couches sur le disque.<br><br>Une image est identifiée par un nom, un <b>tag</b> et un <b>digest</b> (empreinte SHA-256 du contenu). Le tag est une étiquette mobile : <code>nginx:latest</code> ne désigne pas la même image aujourd’hui et dans six mois. En production, on fixe une version précise (<code>nginx:1.27.2-alpine</code>) voire le digest, pour que chaque déploiement soit reproductible.<br><br>Les images sont stockées dans des <b>registres</b> : Docker Hub, GitHub Container Registry, ou un registre privé (Harbor, Forgejo, GitLab). <code>docker pull</code> télécharge, <code>docker push</code> publie.', attention: 'Ne jamais utiliser le tag latest en production : un redémarrage peut récupérer silencieusement une nouvelle version majeure et casser l’application.' },
{ t: 'Écrire un Dockerfile de production', d: '', html: 'Le <b>Dockerfile</b> décrit la construction d’une image. Chaque instruction produit une couche, et Docker réutilise en cache les couches dont les instructions et les fichiers n’ont pas changé. L’ordre est donc décisif : on place en premier ce qui change rarement (système, dépendances) et en dernier ce qui change souvent (code).<br><br>Quatre bonnes pratiques font la différence entre un Dockerfile de TP et un Dockerfile de production. Partir d’une <b>image de base minimale et versionnée</b> (debian:bookworm-slim, alpine, distroless). Utiliser un <b>build multi-étapes</b> : on compile dans une première image complète, et on ne copie que le résultat dans une image finale légère, sans compilateurs. <b>Ne pas exécuter en root</b> : créer un utilisateur et le déclarer avec USER. Ajouter un <b>.dockerignore</b> pour ne pas envoyer .git, les secrets ou node_modules dans le contexte de construction.', exemple: '# syntax=docker/dockerfile:1\nFROM python:3.12-slim AS build\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --prefix=/install -r requirements.txt\n\nFROM python:3.12-slim\nRUN useradd --create-home --uid 10001 app\nCOPY --from=build /install /usr/local\nWORKDIR /app\nCOPY --chown=app:app . .\nUSER app\nEXPOSE 8000\nHEALTHCHECK CMD python -c "import urllib.request;urllib.request.urlopen(\'http://localhost:8000/health\')"\nCMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]' },
{ t: 'Cycle de vie d’un conteneur', d: '', html: '<code>docker run</code> crée et démarre un conteneur à partir d’une image. Les options essentielles : <code>-d</code> en arrière-plan, <code>--name</code> pour le nommer, <code>-p 8080:80</code> pour publier un port, <code>-e</code> pour une variable d’environnement, <code>-v</code> pour un volume, <code>--restart unless-stopped</code> pour le redémarrer automatiquement, <code>--memory</code> et <code>--cpus</code> pour le limiter.<br><br>Le processus lancé par CMD ou ENTRYPOINT est le <b>PID 1</b> du conteneur : quand il s’arrête, le conteneur s’arrête. C’est pourquoi on ne lance pas un service en arrière-plan dans un conteneur, et pourquoi le programme doit traiter correctement le signal SIGTERM envoyé par <code>docker stop</code> (au bout de 10 secondes, Docker envoie SIGKILL).<br><br>Un conteneur est <b>jetable</b> : on ne le modifie pas à la main, on le recrée à partir d’une nouvelle image. Tout ce qui est écrit dans sa couche inscriptible disparaît avec <code>docker rm</code>.' },
{ t: 'Persister les données : volumes et bind mounts', d: '', html: 'Puisque le conteneur est jetable, les données qui doivent survivre (base de données, fichiers envoyés, configuration) vont dans un <b>volume</b>. Un <b>volume nommé</b> est géré par Docker dans /var/lib/docker/volumes : c’est la solution recommandée pour les données d’application. Un <b>bind mount</b> monte un dossier précis de l’hôte : pratique pour la configuration ou le développement, mais il dépend de l’arborescence de l’hôte et de ses droits.<br><br>La sauvegarde ne se fait pas en copiant un volume de base de données à chaud : on utilise l’outil de la base (pg_dump, mariadb-dump) depuis le conteneur, ou on arrête le service le temps de la copie.', exemple: 'docker volume create pgdata\ndocker run -d --name db -e POSTGRES_PASSWORD_FILE=/run/secrets/pg -v pgdata:/var/lib/postgresql/data postgres:16\ndocker exec db pg_dump -U postgres app > app-$(date +%F).sql' },
{ t: 'Réseaux Docker', d: '', html: 'Par défaut, chaque conteneur est relié au réseau <b>bridge</b> docker0 et reçoit une adresse privée en 172.17.0.0/16. Pour être joignable de l’extérieur, un port doit être publié avec <code>-p</code> : Docker crée alors une règle de <b>DNAT</b> dans netfilter vers le conteneur.<br><br>Sur un <b>réseau bridge utilisateur</b> (<code>docker network create</code>), les conteneurs se trouvent par leur <b>nom</b> grâce au DNS intégré de Docker (127.0.0.11) : l’application joint la base de données par le nom db, sans connaître son adresse. C’est la pratique recommandée. Le mode <b>host</b> supprime l’isolation réseau : le conteneur utilise directement les interfaces de l’hôte.', attention: 'Les ports publiés par Docker passent par ses propres règles netfilter, avant celles d’un pare-feu comme UFW : un -p 5432:5432 expose la base de données sur Internet même si UFW semble la bloquer. On publie uniquement sur la boucle locale (-p 127.0.0.1:5432:5432) ou pas du tout, en utilisant un réseau interne.' },
{ t: 'Sécurité des conteneurs', d: '', html: 'Appartenir au groupe <b>docker</b> équivaut à être root sur l’hôte : on peut lancer un conteneur qui monte / et le modifier. On réserve ce groupe aux administrateurs, ou on utilise Docker en mode <b>rootless</b> ou Podman.<br><br>Les règles de base : images officielles ou construites en interne, <b>analyse de vulnérabilités</b> des images (Trivy, Docker Scout) dans la chaîne d’intégration, exécution en utilisateur non privilégié, jamais de <code>--privileged</code> sans raison impérative, secrets passés par fichiers ou gestionnaire de secrets plutôt qu’en variables d’environnement visibles dans <code>docker inspect</code>, et limites de ressources pour qu’un conteneur ne puisse pas saturer l’hôte.' }
],
synthese: ['Conteneur = processus isolé par namespaces et limité par cgroups, noyau partagé', 'Image en couches, immuable, identifiée par tag et digest ; jamais latest en production', 'Dockerfile : base minimale versionnée, multi-étapes, USER non root, .dockerignore', 'PID 1 et SIGTERM : un processus au premier plan par conteneur', 'Données dans des volumes nommés, sauvegarde avec les outils de la base', 'Réseau bridge utilisateur et DNS par nom ; attention aux ports publiés qui contournent UFW', 'Groupe docker = root ; scanner les images, limiter les ressources'],
qcm: [
['Quels mécanismes du noyau isolent un conteneur ?', ['Hyperviseur et BIOS', 'Namespaces et cgroups', 'chroot et sudo', 'SELinux uniquement'], 1, 'Namespaces pour la vue, cgroups pour les ressources.'],
['Pourquoi éviter le tag latest en production ?', ['Il est payant', 'Il peut désigner une nouvelle version à chaque pull', 'Il est lent', 'Il n’existe pas'], 1, 'Le déploiement devient non reproductible.'],
['Quel est l’intérêt d’un build multi-étapes ?', ['Accélérer le réseau', 'Obtenir une image finale sans outils de compilation', 'Lancer plusieurs conteneurs', 'Chiffrer l’image'], 1, 'Image plus petite et surface d’attaque réduite.'],
['Que se passe-t-il quand le processus PID 1 d’un conteneur se termine ?', ['Rien', 'Le conteneur s’arrête', 'Docker le relance toujours', 'L’hôte redémarre'], 1, 'Le conteneur vit tant que son processus principal vit.'],
['Où stocker les données d’une base PostgreSQL conteneurisée ?', ['Dans la couche du conteneur', 'Dans un volume nommé', 'Dans l’image', 'Dans /tmp'], 1, 'Elles survivent à la suppression du conteneur.'],
['Comment une application joint-elle la base de données sur un réseau bridge utilisateur ?', ['Par l’adresse 172.17.0.1', 'Par le nom du conteneur', 'Par localhost', 'Impossible'], 1, 'DNS intégré de Docker.'],
['Pourquoi -p 5432:5432 peut-il exposer la base malgré UFW ?', ['UFW est désactivé par Docker', 'Docker insère ses règles DNAT avant celles d’UFW', 'Le port 5432 est public par nature', 'Ce n’est pas possible'], 1, 'Publier sur 127.0.0.1 ou ne pas publier.'],
['Être membre du groupe docker revient à :', ['n’avoir aucun droit', 'être root sur l’hôte', 'pouvoir seulement lire les journaux', 'être administrateur réseau'], 1, 'On peut monter / dans un conteneur.']
],
td: [
{ n: 'Échauffement', c: 'Classer VM ou conteneur : (a) serveur Windows pour un logiciel métier, (b) API Python déployée dix fois par jour, (c) isolation forte entre deux clients hébergés, (d) base de tests jetable pour chaque élève.', r: '(a) VM · (b) conteneur · (c) VM (ou conteneurs dans des VM séparées) · (d) conteneur' },
{ n: 'Application', c: 'Réécrire ce Dockerfile pour la production : « FROM python:latest / COPY . /app / RUN pip install -r /app/requirements.txt / CMD python /app/app.py ».', r: 'Version fixée (python:3.12-slim), COPY requirements.txt avant le code pour le cache, pip --no-cache-dir, utilisateur non root et USER, .dockerignore, CMD en forme exec ["python","app.py"], éventuellement multi-étapes et HEALTHCHECK.' },
{ n: 'Application', c: 'Lancer Nginx accessible uniquement depuis l’hôte sur le port 8080, redémarré automatiquement, limité à 256 Mo, avec un dossier de configuration en lecture seule.', r: 'docker run -d --name web --restart unless-stopped --memory 256m -p 127.0.0.1:8080:80 -v /srv/web/conf:/etc/nginx/conf.d:ro nginx:1.27-alpine' },
{ n: 'Approfondissement', c: 'Montrer avec des commandes qu’un conteneur n’est qu’un processus de l’hôte, et retrouver son PID et ses cgroups.', r: 'docker inspect -f "{{.State.Pid}}" web → 4312\nps -fp 4312 · cat /proc/4312/cgroup · ls -l /proc/4312/ns' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Une VM Debian avec Docker : construire, publier, exploiter.', etapes: ['Construire l’image d’une API Python multi-étapes', 'La lancer avec limites de ressources et redémarrage automatique', 'Ajouter PostgreSQL sur un réseau utilisateur avec volume', 'Vérifier les ports réellement exposés', 'Scanner l’image avec Trivy'] },
plusLoin: [['Docker Docs · Dockerfile best practices', 'https://docs.docker.com/build/building/best-practices/'], ['ANSSI · Recommandations de sécurité relatives au déploiement de conteneurs Docker', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-au-deploiement-de-conteneurs-docker'], ['Trivy', 'https://trivy.dev/']] },
plus: {
cas: { titre: '', texte: '' },
resolu: { enonce: 'L’application de réservation de salles, conteneurisée, redémarre en boucle depuis la mise à jour de ce matin. Les utilisateurs obtiennent une erreur 502 du reverse proxy. Diagnostiquer et rétablir.', etapes: [
['Constater l’état', 'docker ps -a --filter name=resa\nNAMES IMAGE STATUS\nresa resa-app:2.4.0 Restarting (1) 12 seconds ago\n→ Le conteneur démarre puis s’arrête avec le code 1, et la politique de redémarrage le relance sans fin. Le reverse proxy ne trouve donc jamais de backend : d’où les 502.'],
['Lire les journaux', 'docker logs --tail 20 resa\nKeyError: \'DATABASE_URL\'\n→ L’application s’arrête au démarrage faute de la variable DATABASE_URL. La nouvelle version attend un nom de variable différent de l’ancienne (DB_URL).'],
['Comparer avec la version précédente', 'docker inspect -f "{{range .Config.Env}}{{println .}}{{end}}" resa | grep -i db\nDB_URL=postgresql://resa@db/resa\n→ Le conteneur est lancé avec l’ancien nom de variable. Le changement était annoncé dans les notes de version 2.4.0, mais le fichier de déploiement n’a pas été adapté.'],
['Rétablir le service', 'docker rm -f resa\ndocker run -d --name resa --network resa-net --restart unless-stopped -e DATABASE_URL=postgresql://resa@db/resa resa-app:2.4.0\ndocker ps --filter name=resa\nresa resa-app:2.4.0 Up 20 seconds (healthy)\n→ On ne corrige pas le conteneur existant : on le recrée avec la bonne configuration. Le HEALTHCHECK confirme que l’application répond.'],
['Vérifier et prévenir', 'curl -sI https://resa.lycee.local | head -1\nHTTP/2 200\n→ Service rétabli. Actions préventives : décrire le déploiement dans un fichier Compose versionné plutôt qu’une commande docker run tapée à la main, relire les notes de version, et tester la nouvelle image sur un environnement de préproduction avant la production.']
] },
erreurs: ['Utiliser latest en production', 'Lancer le processus en root dans l’image', 'Stocker des données dans la couche du conteneur', 'Modifier un conteneur à la main au lieu de reconstruire l’image', 'Publier une base de données sur 0.0.0.0', 'Mettre des secrets dans le Dockerfile ou l’image', 'Oublier .dockerignore et embarquer .git', 'Donner le groupe docker à des utilisateurs non administrateurs'],
memo: [['Commande', 'Usage en production'], ['docker ps -a', 'conteneurs et leur état, y compris arrêtés'], ['docker logs -f --tail 100 <c>', 'journaux de l’application'], ['docker inspect <c>', 'configuration complète : env, montages, réseau'], ['docker exec -it <c> sh', 'shell dans un conteneur en cours'], ['docker stats', 'CPU et mémoire consommés en direct'], ['docker build -t app:1.2.0 .', 'construire une image versionnée'], ['docker image ls · docker image prune', 'lister et nettoyer les images'], ['docker volume ls', 'volumes de données'], ['docker network ls · inspect', 'réseaux et conteneurs rattachés'], ['docker system df', 'espace disque utilisé par Docker'], ['docker run --rm -it image sh', 'explorer une image sans la garder']],
examen: { titre: 'Sujet type · 20 points', q: ['Comparer VM et conteneur en citant les mécanismes du noyau. (4 pts)', 'Critiquer et corriger un Dockerfile fourni. (6 pts)', 'Écrire la commande de lancement d’une application avec volume, réseau, limites et redémarrage. (4 pts)', 'Diagnostiquer un conteneur en redémarrage permanent à partir de sorties fournies. (4 pts)', 'Citer quatre mesures de sécurité des conteneurs. (2 pts)'], r: ['namespaces, cgroups, noyau partagé', 'version, cache, USER, multi-étapes, .dockerignore', 'docker run -d --name … --restart … --memory … -v … --network …', 'logs, inspect, code de sortie', 'non root, scan, pas de --privileged, secrets hors image'] },
prof: { deroule: [['Ouverture', 'Démonstration : la même application qui échoue sur deux machines, puis qui marche partout en conteneur'], ['Cours', 'Namespaces, cgroups, images et couches'], ['Démonstration', 'Dockerfile naïf puis Dockerfile de production, comparaison de taille'], ['Pratique', 'Lab : API + base de données, volumes et réseau'], ['Incident', 'Conteneur en boucle de redémarrage'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : Dockerfile à compléter, fiche mémo. Élèves avancés : mode rootless, scan Trivy dans une CI, image distroless.', eval: 'Étapes du lab validées, analyse du Dockerfile, sujet type.' }
},
exemples: [
['Pourquoi ce conteneur s’arrête-t-il ?', 'Un conteneur passe en Exited juste après son lancement.', 'docker ps -a --filter name=api --format "{{.Names}} {{.Status}}"\ndocker logs api | tail -3', 'api Exited (137) 2 minutes ago\nKilled', 'Le code 137 = 128 + 9 : le processus a reçu SIGKILL. Sans docker stop, c’est presque toujours le noyau qui l’a tué pour dépassement de mémoire (OOM). On le confirme avec docker inspect -f "{{.State.OOMKilled}}" api, puis on augmente --memory ou on corrige la fuite.'],
['Mesurer la consommation', 'Le serveur rame depuis le déploiement.', 'docker stats --no-stream --format "table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}"', 'NAME CPU % MEM USAGE / LIMIT\nresa 0.4% 182MiB / 512MiB\nglpi 187.3% 1.1GiB / 15.6GiB\ndb 3.1% 420MiB / 1GiB', 'Le conteneur glpi consomme presque deux cœurs et n’a pas de limite mémoire (la limite affichée est la RAM de l’hôte). On regarde ses journaux et on lui fixe --cpus et --memory pour protéger les autres services.'],
['Vérifier les ports réellement exposés', 'Audit avant mise en production.', 'docker ps --format "{{.Names}} {{.Ports}}"', 'web 0.0.0.0:443->443/tcp\ndb 0.0.0.0:5432->5432/tcp\nredis 127.0.0.1:6379->6379/tcp', 'La base PostgreSQL est publiée sur toutes les interfaces : joignable depuis Internet malgré le pare-feu de l’hôte. On la retire du -p et on la laisse sur le réseau interne ; Redis, publié sur 127.0.0.1, est correct.'],
['Comprendre le cache de construction', 'Chaque build prend trois minutes.', 'docker build -t app:dev .', ' => CACHED [2/5] WORKDIR /app\n => [3/5] COPY . .\n => [4/5] RUN pip install -r requirements.txt 172.4s', 'COPY . . est placé avant l’installation des dépendances : toute modification du code invalide le cache et relance pip. En copiant requirements.txt seul, puis en installant, puis en copiant le code, le build passe à quelques secondes.'],
['Retrouver un conteneur par son nom', 'L’application ne joint pas la base de données.', 'docker exec api getent hosts db ; docker network inspect -f "{{range .Containers}}{{.Name}} {{end}}" appnet', '(aucune sortie)\napi', 'Le nom db ne se résout pas, car le conteneur de base de données n’est pas sur le réseau appnet. On le rattache : docker network connect appnet db.'],
['Libérer de l’espace disque', 'Le disque de l’hôte est plein à 95 %.', 'docker system df', 'TYPE TOTAL ACTIVE SIZE RECLAIMABLE\nImages 48 6 21.3GB 18.9GB (88%)\nContainers 9 6 1.2GB 0.4GB\nLocal Volumes 12 5 8.7GB 3.1GB\nBuild Cache — — 6.4GB 6.4GB', 'Les anciennes images et le cache de construction occupent 25 Go récupérables. docker image prune -a et docker builder prune les suppriment. On évite docker volume prune sans vérifier : il supprimerait des données.']
],
termes: ['Docker', 'conteneur', 'image', 'Dockerfile', 'namespaces', 'cgroups', 'couche', 'tag', 'digest', 'registre', 'volume', 'bind mount', 'bridge', 'docker run', 'docker ps', 'docker logs', 'docker exec', 'docker inspect', 'docker stats', 'PID 1', 'SIGTERM', 'SIGKILL', 'multi-étapes', 'USER', 'HEALTHCHECK', 'latest', 'Trivy', 'rootless', 'OOM']
});
reg({
cours: { id: 'compose', match: /Docker Compose/i,
titre: 'Docker Compose : décrire et exploiter une application multi-conteneurs', duree: '',
niveau: 'BTS SIO SISR / SLAM · BUT R&T DevCloud · AIS',
prerequis: ['Conteneurs Docker'],
competences: ['BTS SIO · Bloc 2 · Déployer une solution applicative', 'BUT R&T DevCloud · Automatiser le déploiement', 'AIS · Industrialiser le déploiement'],
intro: 'Une application réelle ne tient jamais dans un seul conteneur : un site Laravel a besoin d’un serveur web, de PHP-FPM, d’une base de données, souvent d’un cache Redis et d’un planificateur de tâches. Lancer et relier tout cela à coups de docker run est fragile et impossible à reproduire. Docker Compose décrit l’ensemble dans un fichier YAML versionné : services, réseaux, volumes, variables, ordre de démarrage. Ce fichier devient la documentation exécutable du déploiement.',
cm: [
{ t: 'Le fichier compose.yaml', d: '', html: 'Le fichier <code>compose.yaml</code> (anciennement docker-compose.yml) déclare des <b>services</b>, chacun correspondant à un conteneur issu d’une image ou d’un Dockerfile, ainsi que des <b>volumes</b> et des <b>réseaux</b>. Compose crée automatiquement un réseau par projet : chaque service y est joignable par son nom.<br><br>La commande actuelle est <code>docker compose</code> (plugin intégré, version 2), qui remplace l’ancien binaire docker-compose écrit en Python.', exemple: 'services:\n web:\n image: nginx:1.27-alpine\n ports: ["443:443"]\n volumes: ["./nginx:/etc/nginx/conf.d:ro", "certs:/etc/ssl/app:ro"]\n depends_on: [app]\n app:\n build: .\n env_file: .env\n depends_on:\n db: { condition: service_healthy }\n db:\n image: postgres:16\n environment:\n POSTGRES_PASSWORD_FILE: /run/secrets/db_password\n secrets: [db_password]\n volumes: ["pgdata:/var/lib/postgresql/data"]\n healthcheck:\n test: ["CMD", "pg_isready", "-U", "postgres"]\n interval: 5s\n retries: 10\nvolumes:\n pgdata:\n certs:\nsecrets:\n db_password:\n file: ./secrets/db_password.txt' },
{ t: 'Ordre de démarrage et healthchecks', d: '', html: '<code>depends_on</code> seul garantit uniquement que le conteneur de la base est <b>démarré</b> avant l’application, pas qu’il est <b>prêt</b> à accepter des connexions. PostgreSQL met plusieurs secondes à s’initialiser au premier lancement, et l’application qui s’y connecte trop tôt échoue.<br><br>La bonne pratique combine un <b>healthcheck</b> sur la base et <code>condition: service_healthy</code> dans le depends_on de l’application. L’application doit aussi savoir réessayer sa connexion : en production, un redémarrage de la base peut survenir à tout moment.' },
{ t: 'Variables, fichiers .env et secrets', d: '', html: 'Compose lit automatiquement le fichier <code>.env</code> du dossier du projet pour substituer les variables <code>${VAR}</code> du fichier YAML, et <code>env_file</code> injecte des variables dans un conteneur. Ces fichiers ne doivent <b>jamais être versionnés</b> s’ils contiennent des mots de passe : on versionne un <code>.env.example</code> sans valeurs.<br><br>Les <b>secrets</b> de Compose montent un fichier en lecture seule dans <code>/run/secrets/</code> : le mot de passe n’apparaît ni dans les variables d’environnement, ni dans <code>docker inspect</code>. Les images officielles comme postgres ou mariadb acceptent les variables en <code>_FILE</code> pour lire leur valeur dans ce fichier.' },
{ t: 'Exploiter un projet Compose', d: '', html: '<code>docker compose up -d</code> crée ou met à jour tout le projet ; il ne recrée que les services dont la configuration ou l’image a changé. <code>docker compose pull</code> récupère les nouvelles images, <code>docker compose up -d --build</code> reconstruit celles qui ont un Dockerfile. <code>docker compose ps</code>, <code>logs -f service</code>, <code>exec service sh</code> et <code>down</code> complètent l’exploitation courante.<br><br><code>docker compose down</code> supprime conteneurs et réseau mais <b>conserve les volumes</b> ; <code>down -v</code> supprime aussi les volumes, donc les données. Cette option ne doit jamais être tapée par réflexe en production.', attention: 'docker compose down -v sur le serveur de production efface la base de données. Sauvegarder avant toute opération de maintenance, et réserver -v aux environnements de test.' },
{ t: 'Plusieurs environnements', d: '', html: 'Un même projet tourne en développement, en préproduction et en production avec des différences : ports, volumes de code montés, niveau de journalisation, nombre de répliques. Compose fusionne plusieurs fichiers : <code>compose.yaml</code> contient la base commune, <code>compose.override.yaml</code> (chargé automatiquement) les réglages de développement, et <code>compose.prod.yaml</code> ceux de production, appliqués avec <code>docker compose -f compose.yaml -f compose.prod.yaml up -d</code>. Les <b>profils</b> permettent d’activer des services optionnels (outils d’administration, tests).' },
{ t: 'Limites de Compose', d: '', html: 'Compose pilote les conteneurs d’<b>une seule machine</b>. Il ne répartit pas la charge entre plusieurs serveurs, ne déplace pas un service quand un nœud tombe, et ne gère pas les mises à jour sans interruption. Pour un établissement ou une PME, c’est souvent largement suffisant et bien plus simple à maintenir. Au-delà, on passe à un orchestrateur comme <b>Kubernetes</b>, dont les notions (services, volumes, secrets, sondes de santé) prolongent directement celles de Compose.' }
],
synthese: ['compose.yaml versionné = documentation exécutable du déploiement', 'Réseau par projet, services joignables par leur nom', 'depends_on + healthcheck + condition service_healthy', '.env jamais versionné ; secrets montés dans /run/secrets', 'up -d ne recrée que ce qui a changé ; down -v efface les données', 'Plusieurs fichiers pour plusieurs environnements', 'Une seule machine : au-delà, Kubernetes'],
qcm: [
['Que garantit depends_on sans condition ?', ['Que la base accepte les connexions', 'Que le conteneur de la base est démarré avant', 'Que la base est sauvegardée', 'Rien'], 1, 'Démarré ne veut pas dire prêt.'],
['Que fait docker compose down -v ?', ['Redémarre', 'Supprime conteneurs, réseau et volumes', 'Met à jour les images', 'Affiche la version'], 1, 'Les données sont effacées.'],
['Comment le service app joint-il le service db ?', ['Par l’IP de l’hôte', 'Par le nom db', 'Par localhost', 'Par un port publié'], 1, 'DNS du réseau du projet.'],
['Pourquoi préférer les secrets aux variables d’environnement pour un mot de passe ?', ['Plus rapide', 'Le mot de passe n’apparaît pas dans docker inspect', 'Obligatoire', 'Chiffrement automatique sur le disque'], 1, 'Fichier monté en lecture seule.'],
['Quel fichier est chargé automatiquement en plus de compose.yaml ?', ['compose.prod.yaml', 'compose.override.yaml', 'docker.yaml', '.env.example'], 1, 'Réglages de développement.'],
['Après modification de l’image d’un seul service, docker compose up -d :', ['recrée tous les services', 'recrée seulement ce service', 'ne fait rien', 'supprime les volumes'], 1, 'Mise à jour ciblée.']
],
td: [
{ n: 'Application', c: 'Écrire le compose.yaml d’un GLPI : service web (image diouxx/glpi ou équivalent), service MariaDB avec volume et healthcheck, mot de passe en secret, seul le web publié sur 443.', r: 'services: web (ports 443, depends_on db service_healthy) ; db (mariadb:11, MARIADB_ROOT_PASSWORD_FILE, secrets, volume dbdata, healthcheck healthcheck.sh --connect) ; volumes: dbdata ; secrets: db_root.' },
{ n: 'Application', c: 'Écrire la procédure de mise à jour d’un projet Compose en production sans perte de données.', r: '1. Sauvegarde (docker compose exec db pg_dump …) · 2. docker compose pull · 3. docker compose up -d · 4. docker compose ps et logs · 5. test fonctionnel · 6. en cas d’échec : revenir au tag précédent dans compose.yaml puis up -d.' },
{ n: 'Approfondissement', c: 'Séparer la configuration de développement (code monté, port 8000 publié, débogage) de celle de production (image construite, pas de port applicatif publié).', r: 'compose.yaml : base commune · compose.override.yaml : volumes ./:/app, ports 8000, APP_DEBUG=1 · compose.prod.yaml : image registre:tag, restart unless-stopped, pas de ports sur app · lancement prod : -f compose.yaml -f compose.prod.yaml.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Déployer une application web, sa base et son cache avec Compose.', etapes: ['Écrire compose.yaml avec trois services', 'Ajouter healthcheck et secrets', 'Démarrer, vérifier ps et logs', 'Mettre à jour une image sans perte de données', 'Sauvegarder et restaurer la base'] },
plusLoin: [['Docker Docs · Compose file reference', 'https://docs.docker.com/reference/compose-file/'], ['Docker Docs · Secrets in Compose', 'https://docs.docker.com/compose/how-tos/use-secrets/']] },
plus: {
cas: { titre: '', texte: '' },
resolu: { enonce: 'Après un redémarrage du serveur, l’application Laravel du CDI affiche « SQLSTATE[HY000] [2002] Connection refused » pendant plusieurs minutes, puis parfois reste en erreur. Diagnostiquer et fiabiliser.', etapes: [
['Observer l’ordre de démarrage', 'docker compose ps\nNAME SERVICE STATUS\ncdi-app app Up 3 minutes\ncdi-db db Up 3 minutes (health: starting)\n→ L’application tourne alors que la base n’est pas encore prête.'],
['Lire les journaux', 'docker compose logs app | head -3\nSQLSTATE[HY000] [2002] Connection refused\ndocker compose logs db | tail -2\nInnoDB: Starting crash recovery...\nready for connections.\n→ Après un arrêt brutal, MariaDB effectue une récupération qui prend du temps. L’application a tenté de se connecter trop tôt et a mis la base en échec dans son cache de configuration.'],
['Lire le fichier Compose', 'grep -A3 depends_on compose.yaml\n depends_on:\n - db\n→ depends_on sans condition : Compose attend seulement que le conteneur db soit lancé.'],
['Corriger', 'db:\n healthcheck:\n test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]\n interval: 5s\n retries: 30\napp:\n depends_on:\n db: { condition: service_healthy }\n restart: unless-stopped\ndocker compose up -d\n→ L’application ne démarre plus qu’une fois la base réellement prête, et redémarre automatiquement en cas d’échec.'],
['Valider', 'sudo reboot (sur la préproduction)\ndocker compose ps\ncdi-db Up 2 minutes (healthy)\ncdi-app Up 1 minute\n→ L’ordre est respecté. Compte rendu : cause (depends_on sans healthcheck), correction, test de redémarrage ajouté à la procédure de validation.']
] },
erreurs: ['depends_on sans healthcheck', 'Versionner le fichier .env avec les mots de passe', 'Taper docker compose down -v en production', 'Publier les ports de la base de données', 'Ne pas fixer les versions des images', 'Mettre à jour sans sauvegarde préalable'],
memo: [['Commande', 'Usage'], ['docker compose up -d', 'créer ou mettre à jour le projet'], ['docker compose ps', 'état et santé des services'], ['docker compose logs -f <s>', 'journaux d’un service'], ['docker compose exec <s> sh', 'shell dans un service'], ['docker compose pull', 'récupérer les nouvelles images'], ['docker compose up -d --build', 'reconstruire et redéployer'], ['docker compose config', 'afficher la configuration fusionnée et validée'], ['docker compose down', 'arrêter en conservant les volumes'], ['docker compose -f a.yaml -f b.yaml up -d', 'combiner des fichiers d’environnement'], ['docker compose --profile outils up -d', 'activer des services optionnels']],
examen: { titre: 'Sujet type · 20 points', q: ['Écrire le compose.yaml d’une application trois tiers. (8 pts)', 'Expliquer depends_on et les healthchecks. (4 pts)', 'Rédiger la procédure de mise à jour sans perte de données. (5 pts)', 'Justifier le passage à Kubernetes pour un cas donné. (3 pts)'], r: ['services, volumes, secrets, réseau', 'démarré ≠ prêt, condition service_healthy', 'sauvegarde, pull, up -d, contrôle, retour arrière', 'plusieurs nœuds, haute disponibilité'] },
prof: { deroule: [['Ouverture', 'Lancer à la main trois conteneurs reliés, puis la même chose en un fichier'], ['Cours', 'Services, réseaux, volumes, secrets'], ['Pratique', 'Lab application trois tiers'], ['Incident', 'Ordre de démarrage au redémarrage'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : compose.yaml à compléter. Élèves avancés : plusieurs environnements, profils, déploiement par CI.', eval: 'Lab validé, sujet type.' }
},
exemples: [
['Valider le fichier avant déploiement', 'Une modification a été faite dans compose.yaml.', 'docker compose config --quiet && echo OK', 'services.app.depends_on.db.condition must be one of the following: "service_started", "service_healthy", "service_completed_successfully"', 'Faute de frappe dans la condition. docker compose config valide et fusionne les fichiers sans rien lancer : à exécuter avant chaque up.'],
['Voir la santé des services', 'L’application répond lentement au démarrage.', 'docker compose ps --format "table {{.Service}}\\t{{.Status}}"', 'SERVICE STATUS\ndb Up 40 seconds (healthy)\nredis Up 40 seconds\napp Up 12 seconds (health: starting)', 'La base est prête, l’application exécute encore son healthcheck. Tant qu’elle n’est pas healthy, le reverse proxy peut renvoyer des erreurs : c’est normal pendant quelques secondes.'],
['Mettre à jour une seule image', 'Correctif de sécurité publié pour Nginx.', 'docker compose pull web && docker compose up -d web', '[+] Pulling web … Pulled\n[+] Running 1/1 ✔ Container cdi-web Recreated', 'Seul le service web est recréé ; la base et l’application continuent de tourner.'],
['Sauvegarder la base d’un projet', 'Avant une mise à jour.', 'docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz ; ls -lh backup-*', '-rw-r--r-- 1 admin admin 14M sept. 30 07:02 backup-2026-09-30.sql.gz', '-T désactive le pseudo-terminal pour que la sortie puisse être redirigée proprement dans un fichier.'],
['Retrouver le nom réel des ressources', 'Un collègue cherche le volume de la base.', 'docker volume ls --filter label=com.docker.compose.project=cdi', 'DRIVER VOLUME NAME\nlocal cdi_pgdata', 'Compose préfixe les volumes et réseaux par le nom du projet (le dossier). Deux projets peuvent donc avoir chacun un volume pgdata sans conflit.'],
['Afficher les journaux de plusieurs services', 'Erreur 500 intermittente.', 'docker compose logs --since 10m app web | grep -E " 500 |ERROR"', 'cdi-web | "POST /reservations HTTP/1.1" 500\ncdi-app | ERROR: duplicate key value violates unique constraint "resa_salle_creneau"', 'En croisant les journaux, on relie l’erreur 500 vue par Nginx à une violation de contrainte dans l’application : deux réservations simultanées du même créneau. C’est un bug applicatif, pas une panne d’infrastructure.']
],
termes: ['Docker Compose', 'compose.yaml', 'service', 'depends_on', 'healthcheck', 'service_healthy', 'secrets', '.env', 'env_file', 'docker compose up', 'docker compose down', 'docker compose ps', 'docker compose logs', 'docker compose config', 'profils', 'volume', 'Kubernetes']
});
reg({
cours: { id: 'ansible', match: /Ansible|IaC|infrastructure as code/i,
titre: 'Ansible : automatiser la configuration des serveurs', duree: '',
niveau: 'BTS SIO SISR · BUT R&T DevCloud · AIS · Master',
prerequis: ['Le terminal Linux', 'SSH par clé', 'Paquets et services'],
competences: ['BTS SIO SISR · Bloc 2 · Automatiser les tâches d’administration', 'BUT R&T DevCloud · Automatiser le déploiement d’une infrastructure', 'AIS · Industrialiser l’administration'],
intro: 'Configurer un serveur à la main prend une heure ; en configurer quarante de façon identique est impossible sans erreur, et personne ne sait plus, six mois après, ce qui a été fait sur lequel. Ansible décrit l’état attendu des machines dans des fichiers YAML versionnés, et l’applique par SSH, sans agent à installer. Relancé autant de fois qu’on veut, il ne change que ce qui n’est pas conforme. C’est l’outil d’automatisation le plus répandu dans les services informatiques, des établissements scolaires aux grands hébergeurs.',
cm: [
{ t: 'Principe : décrire un état, pas des commandes', d: '', html: 'Un script Bash enchaîne des actions : « installe Nginx, copie ce fichier, redémarre ». Lancé deux fois, il peut échouer ou dupliquer une ligne. Ansible décrit un <b>état désiré</b> : « Nginx doit être installé, ce fichier doit avoir ce contenu, le service doit être actif ». Chaque <b>module</b> vérifie l’état actuel et n’agit que si nécessaire : c’est l’<b>idempotence</b>. Un playbook relancé sur une machine déjà conforme affiche <code>changed=0</code>.<br><br>Ansible est <b>sans agent</b> : le poste de contrôle se connecte en SSH, envoie un petit programme Python, l’exécute et le supprime. Il suffit d’un accès SSH et de Python sur les machines gérées.' },
{ t: 'L’inventaire', d: '', html: 'L’<b>inventaire</b> liste les machines gérées et les range en <b>groupes</b>. Il peut être statique (fichier INI ou YAML) ou dynamique (généré depuis un hyperviseur ou un fournisseur cloud). Les <b>variables</b> se placent dans <code>group_vars/</code> et <code>host_vars/</code>, ce qui sépare les données (quelle machine, quelle valeur) de la logique (les tâches).', exemple: '# inventaire/hosts.yaml\nall:\n children:\n web:\n hosts:\n web1.lycee.local:\n web2.lycee.local:\n db:\n hosts:\n db1.lycee.local:\n ansible_host: 10.62.40.31\n vars:\n ansible_user: admin\n\n$ ansible -i inventaire all -m ping\nweb1.lycee.local | SUCCESS => {"changed": false, "ping": "pong"}' },
{ t: 'Playbooks, tâches et modules', d: '', html: 'Un <b>playbook</b> est une liste de <b>plays</b> : chacun associe un groupe de machines à une liste de <b>tâches</b>. Chaque tâche appelle un <b>module</b> avec des paramètres : <code>ansible.builtin.apt</code> pour les paquets, <code>template</code> pour générer un fichier de configuration, <code>service</code> ou <code>systemd</code> pour les services, <code>user</code>, <code>lineinfile</code>, <code>copy</code>, <code>file</code>. On utilise les noms complets (FQCN) pour éviter toute ambiguïté.<br><br><code>become: true</code> élève les privilèges avec sudo. Les <b>handlers</b> sont des tâches déclenchées par <code>notify</code> seulement si une tâche a réellement changé quelque chose : on ne redémarre Nginx que si sa configuration a été modifiée.', exemple: '- name: Serveurs web\n hosts: web\n become: true\n tasks:\n - name: Installer nginx\n ansible.builtin.apt:\n name: nginx\n state: present\n update_cache: true\n - name: Déployer la configuration du site\n ansible.builtin.template:\n src: site.conf.j2\n dest: /etc/nginx/conf.d/site.conf\n mode: "0644"\n notify: Recharger nginx\n - name: Nginx actif au démarrage\n ansible.builtin.service:\n name: nginx\n state: started\n enabled: true\n handlers:\n - name: Recharger nginx\n ansible.builtin.service:\n name: nginx\n state: reloaded' },
{ t: 'Templates Jinja2 et variables', d: '', html: 'Le module <code>template</code> génère un fichier à partir d’un modèle <b>Jinja2</b> : <code>{{ variable }}</code> insère une valeur, <code>{% for %}</code> et <code>{% if %}</code> ajoutent de la logique. La même configuration sert ainsi à toutes les machines, avec leurs valeurs propres. Les <b>facts</b> collectés automatiquement (adresse IP, distribution, mémoire) sont aussi disponibles comme variables.<br><br>Les variables ont un ordre de <b>priorité</b> : valeurs par défaut d’un rôle, group_vars, host_vars, variables du playbook, puis <code>-e</code> en ligne de commande, qui l’emporte sur tout. Mal maîtrisé, cet ordre explique des valeurs « inattendues ».' },
{ t: 'Rôles et collections', d: '', html: 'Au-delà de quelques dizaines de tâches, on range le code en <b>rôles</b> : une arborescence normalisée (tasks, handlers, templates, defaults, vars, files) qui regroupe tout ce qu’il faut pour une fonction, par exemple « serveur web » ou « durcissement SSH ». Un rôle se réutilise dans plusieurs playbooks et se partage. Les <b>collections</b> distribuent rôles et modules (community.general, ansible.posix) et s’installent avec <code>ansible-galaxy</code>.' },
{ t: 'Tester avant d’appliquer et protéger les secrets', d: '', html: '<code>--check</code> simule l’exécution sans rien modifier, <code>--diff</code> montre les différences de fichiers, <code>--limit web1</code> restreint à une machine, <code>--tags</code> à certaines tâches. En production, on valide toujours sur une machine avant tout le parc.<br><br>Les mots de passe et clés ne doivent pas être en clair dans le dépôt Git. <b>Ansible Vault</b> chiffre un fichier de variables avec AES-256 ; le mot de passe du coffre est fourni à l’exécution. <code>ansible-lint</code> détecte les mauvaises pratiques (tâches non nommées, modules shell là où un module dédié existe, droits de fichiers absents).', attention: 'Les modules command et shell ne sont pas idempotents par nature : ils s’exécutent à chaque passage. Préférer un module dédié, ou ajouter creates, removes ou changed_when pour décrire l’état.' }
],
synthese: ['Ansible décrit un état désiré ; les modules sont idempotents', 'Sans agent : SSH et Python sur les machines gérées', 'Inventaire en groupes, variables dans group_vars et host_vars', 'Playbook → plays → tâches → modules ; become pour sudo', 'Handlers déclenchés uniquement en cas de changement', 'Templates Jinja2, rôles réutilisables', '--check --diff et --limit avant d’appliquer ; Vault pour les secrets'],
qcm: [
['Que signifie qu’une tâche Ansible est idempotente ?', ['Elle est rapide', 'La relancer sur une machine conforme ne change rien', 'Elle s’exécute en parallèle', 'Elle est chiffrée'], 1, 'changed=0 au second passage.'],
['De quoi Ansible a-t-il besoin sur les machines gérées ?', ['Un agent Ansible', 'SSH et Python', 'Docker', 'Un serveur web'], 1, 'Architecture sans agent.'],
['Quand un handler s’exécute-t-il ?', ['À chaque tâche', 'Quand une tâche qui le notifie a produit un changement', 'Au début du playbook', 'Jamais automatiquement'], 1, 'Par exemple recharger Nginx seulement si sa configuration a changé.'],
['Quelle option simule sans modifier ?', ['--dry', '--check', '--test', '--simulate'], 1, 'Souvent combinée avec --diff.'],
['Où placer une variable commune à tous les serveurs web ?', ['host_vars/web1.yaml', 'group_vars/web.yaml', 'dans le template', 'dans /etc/ansible/ansible.cfg'], 1, 'Variables de groupe.'],
['Comment protéger un mot de passe dans le dépôt ?', ['Le mettre en commentaire', 'Ansible Vault', 'Le coder en base64', 'Le mettre dans le template'], 1, 'Chiffrement AES-256.'],
['Pourquoi éviter le module shell pour installer un paquet ?', ['Il est interdit', 'Il n’est pas idempotent et masque l’état réel', 'Il est plus lent', 'Il ne supporte pas sudo'], 1, 'Utiliser apt ou dnf.']
],
td: [
{ n: 'Application', c: 'Écrire un playbook qui crée l’utilisateur technicien, lui dépose une clé SSH publique, désactive l’authentification SSH par mot de passe et recharge sshd seulement si nécessaire.', r: 'user (name, groups: sudo, shell) · ansible.posix.authorized_key · lineinfile sur /etc/ssh/sshd_config avec regexp "^#?PasswordAuthentication" line "PasswordAuthentication no" validate "sshd -t -f %s" notify Recharger ssh · handler service ssh reloaded.' },
{ n: 'Application', c: 'Transformer ce passage en tâche idempotente : « shell: echo "10.62.40.20 ent" >> /etc/hosts ».', r: 'ansible.builtin.lineinfile: path=/etc/hosts line="10.62.40.20 ent" (la ligne n’est ajoutée qu’une fois).' },
{ n: 'Approfondissement', c: 'Organiser en rôle le déploiement d’un serveur web : arborescence, variables par défaut, template et handler.', r: 'roles/web/{tasks/main.yml, handlers/main.yml, templates/site.conf.j2, defaults/main.yml (web_port: 443, web_root: /var/www/site)} · playbook : roles: [web].' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un poste de contrôle et trois VM Debian à configurer.', etapes: ['Écrire l’inventaire et tester ansible -m ping', 'Playbook : paquets et utilisateurs', 'Déployer Nginx avec un template et un handler', 'Vérifier l’idempotence (changed=0)', 'Chiffrer un secret avec Vault', 'Passer ansible-lint'] },
plusLoin: [['Documentation Ansible', 'https://docs.ansible.com/'], ['Ansible Lint', 'https://ansible.readthedocs.io/projects/lint/'], ['Xavki · tutoriels Ansible en français', 'https://www.youtube.com/@xavki']] },
plus: {
cas: { titre: '', texte: '' },
resolu: { enonce: 'Le playbook de mise à jour des 12 serveurs web s’est interrompu cette nuit : 4 serveurs ont la nouvelle configuration Nginx, 8 l’ancienne, et web3 ne répond plus. Diagnostiquer, rétablir et fiabiliser.', etapes: [
['Lire le récapitulatif', 'PLAY RECAP\nweb1 ok=6 changed=2 failed=0\nweb3 ok=4 changed=1 failed=1\nweb4…web12 (non traités)\n→ Ansible s’est arrêté après l’échec sur web3 : par défaut, un play traite les machines par lots et s’arrête si toutes celles du lot échouent. L’état du parc est hétérogène.'],
['Identifier l’erreur', 'TASK [Recharger nginx] fatal: [web3]: FAILED! => {"msg": "Unable to reload service nginx: Job for nginx.service failed"}\nssh web3 sudo nginx -t\nnginx: [emerg] unknown directive "ssl_protocol" in /etc/nginx/conf.d/site.conf:7\n→ Le template contient une faute (ssl_protocol au lieu de ssl_protocols). La configuration invalide a été déposée, le rechargement a échoué et Nginx s’est arrêté sur web3.'],
['Rétablir web3', 'Correction du template site.conf.j2 (ssl_protocols TLSv1.2 TLSv1.3;)\nansible-playbook site.yml --limit web3 --diff\nweb3 ok=6 changed=2 failed=0\ncurl -sI https://web3.lycee.local | head -1\nHTTP/2 200\n→ On corrige la source (le template), jamais le fichier à la main sur le serveur : sinon le prochain passage réintroduirait l’erreur.'],
['Fiabiliser le playbook', '- ansible.builtin.template:\n src: site.conf.j2\n dest: /etc/nginx/conf.d/site.conf\n validate: nginx -t -c /etc/nginx/nginx.conf\n notify: Recharger nginx\n- hosts: web\n serial: 2\n max_fail_percentage: 0\n→ validate teste le fichier avant de le mettre en place ; serial: 2 déploie deux serveurs à la fois et arrête tout au premier échec. Une erreur ne touche plus qu’un lot.'],
['Appliquer au parc et contrôler', 'ansible-playbook site.yml --check --diff --limit web4\nansible-playbook site.yml\nweb1…web12 failed=0\nansible-playbook site.yml (second passage)\nweb1…web12 changed=0\n→ Le parc est homogène et le second passage confirme l’idempotence. Compte rendu : cause, impact, correction, et règle d’équipe : --check sur une machine témoin avant tout déploiement, validation des templates dans la CI avec ansible-lint.']
] },
erreurs: ['Utiliser shell ou command là où un module dédié existe', 'Modifier un fichier à la main sur un serveur géré par Ansible', 'Laisser des mots de passe en clair dans le dépôt', 'Déployer sur tout le parc sans --check ni serial', 'Redémarrer un service à chaque passage au lieu d’un handler', 'Déposer une configuration sans validate'],
memo: [['Commande', 'Usage'], ['ansible all -m ping', 'tester la connexion à tout l’inventaire'], ['ansible web -m ansible.builtin.setup', 'afficher les facts d’un groupe'], ['ansible-playbook site.yml --check --diff', 'simuler et voir les différences'], ['ansible-playbook site.yml --limit web1', 'restreindre à une machine'], ['ansible-playbook site.yml --tags nginx', 'n’exécuter que certaines tâches'], ['ansible-inventory --graph', 'visualiser l’inventaire'], ['ansible-vault create|edit|encrypt', 'gérer les secrets chiffrés'], ['ansible-playbook … --ask-vault-pass', 'fournir le mot de passe du coffre'], ['ansible-galaxy collection install', 'installer une collection'], ['ansible-lint', 'contrôler les bonnes pratiques']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer l’idempotence et l’architecture sans agent. (4 pts)', 'Écrire un inventaire et un playbook de déploiement avec template et handler. (8 pts)', 'Rendre idempotentes trois tâches shell fournies. (4 pts)', 'Proposer une stratégie de déploiement sûr sur 50 serveurs. (4 pts)'], r: ['État désiré, SSH + Python', 'hosts.yaml, apt, template, service, handler', 'lineinfile, apt, creates', '--check, serial, validate, max_fail_percentage'] },
prof: { deroule: [['Ouverture', 'Configurer trois VM à la main en chronométrant, puis avec Ansible'], ['Cours', 'Inventaire, playbooks, modules, idempotence'], ['Pratique', 'Lab trois VM'], ['Incident', 'Template invalide sur un parc'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : playbook à compléter. Élèves avancés : rôles, Vault, CI avec ansible-lint, Molecule.', eval: 'Lab validé (idempotence vérifiée), sujet type.' }
},
exemples: [
['Tester l’accès à tout le parc', 'Avant un déploiement.', 'ansible -i inventaire all -m ping -o', 'web1.lycee.local | SUCCESS => {"changed": false,"ping": "pong"}\nweb2.lycee.local | UNREACHABLE! => {"msg": "Failed to connect to the host via ssh: Connection timed out"}', 'web2 est injoignable en SSH : on le traite avant de lancer le playbook, sinon le parc sera partiellement configuré.'],
['Simuler un changement', 'Nouvelle version du template Nginx.', 'ansible-playbook site.yml --check --diff --limit web1', '--- before: /etc/nginx/conf.d/site.conf\n+++ after: site.conf.j2\n- client_max_body_size 8m;\n+ client_max_body_size 64m;\nweb1 : ok=5 changed=1 failed=0', 'Seule la ligne attendue change. On peut appliquer sereinement.'],
['Vérifier l’idempotence', 'Contrôle qualité d’un playbook.', 'ansible-playbook site.yml | tail -3', 'PLAY RECAP\nweb1 : ok=7 changed=1 unreachable=0 failed=0\nweb2 : ok=7 changed=1 unreachable=0 failed=0', 'Au second passage, une tâche est encore « changed » : c’est une tâche shell sans changed_when. Il faut la remplacer par un module dédié ou décrire sa condition de changement.'],
['Lire les facts d’une machine', 'Adapter un template à la mémoire disponible.', 'ansible db1 -m ansible.builtin.setup -a "filter=ansible_memtotal_mb"', 'db1 | SUCCESS => {"ansible_facts": {"ansible_memtotal_mb": 7957}}', 'La variable ansible_memtotal_mb est utilisable dans un template, par exemple pour dimensionner shared_buffers de PostgreSQL au quart de la mémoire.'],
['Chiffrer un secret', 'Mot de passe de la base dans group_vars.', 'ansible-vault encrypt group_vars/db/vault.yml ; head -1 group_vars/db/vault.yml', 'Encryption successful\n$ANSIBLE_VAULT;1.1;AES256', 'Le fichier peut être versionné : sans le mot de passe du coffre, il est illisible.'],
['Détecter les mauvaises pratiques', 'Revue de code avant fusion.', 'ansible-lint site.yml', 'command-instead-of-module: apt-get used in place of apt module\nsite.yml:14 Task/Handler: shell apt-get install -y nginx\nname[missing]: All tasks should be named\nsite.yml:22', 'ansible-lint signale une installation via shell et une tâche sans nom. On l’intègre dans la CI pour bloquer ces erreurs avant qu’elles atteignent la production.']
],
termes: ['Ansible', 'playbook', 'inventaire', 'module', 'tâche', 'handler', 'notify', 'idempotence', 'become', 'group_vars', 'host_vars', 'Jinja2', 'template', 'facts', 'rôle', 'collection', 'ansible-galaxy', 'Ansible Vault', 'ansible-lint', '--check', '--diff', '--limit', 'serial', 'validate', 'changed=0']
});
})();

View file

@ -0,0 +1,122 @@
// Cours complets · DevOps lot 2 : Git, CI/CD
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'git', match: /\bGit\b|versionn|gestion de versions/i,
titre: 'Git : versionner, collaborer et revenir en arrière', duree: '',
niveau: 'NSI Terminale · BTS SIO SLAM / SISR · BUT Informatique / R&T · Master',
prerequis: ['Le terminal Linux'],
competences: ['BTS SIO · Bloc 1 · Travailler en mode projet', 'BTS SIO SLAM · Bloc 2 · Gérer les versions d’une solution applicative', 'BUT Info · Compétence « Collaborer au sein d’une équipe informatique »'],
intro: 'Un fichier « rapport_final_v3_corrigé_VRAIMENT_final.docx » résume le problème que Git résout. Dès que plusieurs personnes modifient le même code, ou qu’une seule personne veut pouvoir revenir à la version d’hier, il faut un historique fiable. Git enregistre chaque état du projet, permet de travailler en parallèle sur des branches et de fusionner le travail de chacun. C’est l’outil commun à tous les développeurs et, de plus en plus, aux administrateurs qui versionnent leurs configurations, leurs playbooks et leurs fichiers Compose.',
cm: [
{ t: 'Le modèle de Git : instantanés et empreintes', d: '', html: 'Git ne stocke pas des différences mais des <b>instantanés</b> complets du projet. Chaque fichier, chaque dossier et chaque <b>commit</b> est un objet identifié par l’empreinte de son contenu (SHA-1, en transition vers SHA-256). Deux fichiers identiques ne sont stockés qu’une fois, et toute modification de l’historique change les empreintes : l’intégrité est vérifiable.<br><br>Un commit contient un instantané, un auteur, une date, un message et un lien vers son ou ses <b>parents</b>. L’historique est donc un graphe orienté de commits. Une <b>branche</b> n’est qu’un pointeur mobile vers un commit, et <b>HEAD</b> désigne la branche ou le commit sur lequel on travaille.' },
{ t: 'Les trois zones', d: '', html: 'Un fichier passe par trois zones. Le <b>répertoire de travail</b> contient les fichiers tels qu’on les édite. L’<b>index</b> (staging area) contient ce qui sera enregistré au prochain commit : on y ajoute les modifications avec <code>git add</code>. Le <b>dépôt</b> (.git) contient l’historique, et <code>git commit</code> y enregistre l’index.<br><br>Cette étape intermédiaire permet de faire des commits cohérents : on n’ajoute que les modifications qui vont ensemble, même si on a travaillé sur plusieurs sujets à la fois. <code>git status</code> montre à tout moment l’état des trois zones.', exemple: 'git init\ngit add index.html style.css\ngit commit -m "Page d’accueil du TP"\ngit status\ngit log --oneline --graph' },
{ t: 'Branches et fusions', d: '', html: 'On crée une branche pour chaque fonctionnalité ou correction (<code>git switch -c correctif-login</code>). La branche principale (main) reste stable. Quand le travail est terminé, on le <b>fusionne</b>. Si main n’a pas bougé, Git avance simplement le pointeur (<b>fast-forward</b>). Sinon, il crée un <b>commit de fusion</b> qui a deux parents.<br><br>Si les deux branches ont modifié les mêmes lignes, Git ne peut pas choisir : c’est un <b>conflit</b>. Il marque les zones concernées dans le fichier (<code>&lt;&lt;&lt;&lt;&lt;&lt;&lt;</code>, <code>=======</code>, <code>&gt;&gt;&gt;&gt;&gt;&gt;&gt;</code>). On édite pour garder la bonne version, on ajoute le fichier et on termine le commit.<br><br>Le <b>rebase</b> est l’alternative à la fusion : il rejoue les commits d’une branche au-dessus d’une autre, pour un historique linéaire. Il réécrit les commits, donc on ne rebase jamais une branche déjà partagée.' },
{ t: 'Travailler avec un dépôt distant', d: '', html: 'Un <b>dépôt distant</b> (GitHub, GitLab, Forgejo auto-hébergé) sert de point de rencontre. <code>git clone</code> copie un dépôt, <code>git fetch</code> récupère les nouveautés sans les appliquer, <code>git pull</code> récupère et fusionne, <code>git push</code> publie ses commits. Les branches distantes apparaissent sous le nom <code>origin/main</code>.<br><br>En équipe, on ne pousse pas directement sur main : on pousse une branche et on ouvre une <b>demande de fusion</b> (pull request ou merge request), relue par un collègue et validée par les tests automatiques avant d’être intégrée.', attention: 'Un push refusé (« rejected, non-fast-forward ») signifie que le dépôt distant a des commits que vous n’avez pas. On fait git pull (ou fetch puis rebase), jamais git push --force sur une branche partagée : on effacerait le travail des autres.' },
{ t: 'Revenir en arrière', d: '', html: 'Git offre plusieurs façons d’annuler, selon ce qu’on veut défaire. <code>git restore fichier</code> annule les modifications non ajoutées. <code>git restore --staged fichier</code> retire un fichier de l’index. <code>git commit --amend</code> corrige le dernier commit s’il n’a pas été poussé. <code>git revert id</code> crée un nouveau commit qui annule un commit précédent : c’est la seule méthode sûre sur un historique partagé. <code>git reset</code> déplace la branche en arrière et réécrit l’historique local.<br><br>Même après une erreur, rien n’est perdu tout de suite : <code>git reflog</code> liste les positions successives de HEAD et permet de retrouver un commit « supprimé ».' },
{ t: 'Ce qui ne va pas dans Git', d: '', html: 'Le fichier <code>.gitignore</code> exclut ce qui ne doit pas être versionné : dépendances (node_modules, vendor), fichiers générés, journaux, et surtout <b>secrets</b> (.env, clés). Un mot de passe poussé une fois reste dans l’historique même après suppression du fichier : il faut le considérer comme compromis et le changer.<br><br>Un bon <b>message de commit</b> décrit pourquoi, pas seulement quoi : « Corrige le calcul de la moyenne quand un élève est absent » plutôt que « fix ». Beaucoup d’équipes adoptent la convention Conventional Commits (feat:, fix:, docs:).' }
],
synthese: ['Git stocke des instantanés identifiés par empreinte ; une branche est un pointeur', 'Répertoire de travail → index (add) → dépôt (commit)', 'Une branche par fonctionnalité, fusion ou rebase ; conflit = même ligne modifiée des deux côtés', 'fetch, pull, push ; demande de fusion relue avant main', 'revert sur l’historique partagé, reset seulement en local, reflog pour retrouver', '.gitignore et aucun secret dans l’historique'],
qcm: [
['Que contient l’index ?', ['L’historique', 'Ce qui sera enregistré au prochain commit', 'Les branches distantes', 'Les fichiers ignorés'], 1, 'On y ajoute avec git add.'],
['Qu’est-ce qu’une branche pour Git ?', ['Une copie du projet', 'Un pointeur mobile vers un commit', 'Un dossier', 'Un dépôt distant'], 1, 'Créer une branche est instantané.'],
['Quand survient un conflit de fusion ?', ['Toujours', 'Quand les deux branches ont modifié les mêmes lignes', 'Quand on oublie git add', 'Quand le dépôt est trop gros'], 1, 'Git ne peut pas choisir seul.'],
['Comment annuler proprement un commit déjà poussé sur main ?', ['git reset --hard', 'git revert', 'git push --force', 'supprimer .git'], 1, 'revert ajoute un commit d’annulation.'],
['Différence entre fetch et pull ?', ['Aucune', 'fetch récupère sans fusionner, pull récupère et fusionne', 'fetch publie', 'pull supprime'], 1, 'fetch est sans risque.'],
['Un mot de passe a été poussé puis supprimé au commit suivant. Il est :', ['effacé', 'toujours dans l’historique, à changer', 'chiffré', 'invisible'], 1, 'L’historique garde tout.'],
['Quelle commande retrouve un commit perdu après un reset ?', ['git log', 'git reflog', 'git status', 'git show'], 1, 'Historique des positions de HEAD.']
],
td: [
{ n: 'Échauffement', c: 'Initialiser un dépôt, créer README.md, le versionner, le modifier, afficher la différence, puis faire un second commit.', r: 'git init · git add README.md · git commit -m "…" · git diff · git commit -am "…"' },
{ n: 'Application', c: 'Créer une branche nouvelle-page, y faire deux commits, revenir sur main, faire un commit qui touche la même ligne, fusionner et résoudre le conflit.', r: 'git switch -c nouvelle-page … git switch main … git merge nouvelle-page → conflit → éditer, git add, git commit' },
{ n: 'Application', c: 'Écrire le .gitignore d’un projet Laravel.', r: '/vendor\n/node_modules\n.env\n/storage/*.key\n/public/storage\n*.log' },
{ n: 'Approfondissement', c: 'Vous avez fait git reset --hard HEAD~2 par erreur. Retrouver les deux commits.', r: 'git reflog → repérer l’id avant le reset · git reset --hard <id> (ou git branch secours <id>)' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un dépôt partagé sur un Forgejo de lab, travail en binôme.', etapes: ['Cloner le dépôt', 'Créer une branche et ouvrir une demande de fusion', 'Provoquer et résoudre un conflit avec le binôme', 'Annuler un commit avec revert', 'Retrouver un commit avec reflog'] },
plusLoin: [['Pro Git (livre officiel, en français)', 'https://git-scm.com/book/fr/v2'], ['Learn Git Branching (exercices interactifs)', 'https://learngitbranching.js.org/?locale=fr_FR'], ['Conventional Commits', 'https://www.conventionalcommits.org/fr/']] },
plus: {
cas: { titre: '', texte: '' },
resolu: { enonce: 'Vendredi 17 h : un développeur a poussé sur main un commit qui casse la page de connexion de l’application de vie scolaire, puis un collègue a déjà poussé deux commits par-dessus. Rétablir sans perdre le travail du collègue.', etapes: [
['Identifier le commit fautif', 'git log --oneline -5\n9c1e4f2 Traductions du menu\n51ab0d7 Ajout export CSV\ne73f210 Refonte formulaire connexion\n4d02a9b Correctif date\n→ La panne est apparue après e73f210. Les deux commits suivants ne touchent pas à la connexion.'],
['Confirmer avec git bisect si le doute subsiste', 'git bisect start ; git bisect bad ; git bisect good 4d02a9b\n… git bisect good / bad à chaque étape\ne73f210 is the first bad commit\ngit bisect reset\n→ bisect teste par dichotomie et désigne le premier commit fautif.'],
['Annuler ce seul commit', 'git revert e73f210\n[main 0f5a8c3] Revert "Refonte formulaire connexion"\n→ Un nouveau commit annule les modifications de e73f210 ; les commits du collègue sont conservés. Surtout pas de reset suivi d’un push --force sur main.'],
['Publier et vérifier', 'git push origin main\n→ La chaîne d’intégration redéploie ; la connexion fonctionne à nouveau.'],
['Reprendre le travail proprement', 'git switch -c refonte-connexion\ngit revert 0f5a8c3 (réapplique la refonte sur la branche)\n→ La refonte est corrigée sur une branche, testée, puis fusionnée par demande de fusion. Action préventive : protéger main (fusion uniquement par demande relue et tests verts).']
] },
erreurs: ['Pousser directement sur main', 'Utiliser push --force sur une branche partagée', 'Versionner .env ou des clés', 'Faire un seul énorme commit pour tout', 'Messages de commit du type « fix » ou « modif »', 'Rebaser une branche déjà partagée'],
memo: [['Commande', 'Usage'], ['git status', 'état des trois zones'], ['git add -p', 'ajouter une partie des modifications'], ['git commit -m "…"', 'enregistrer'], ['git log --oneline --graph --all', 'voir le graphe des commits'], ['git diff · git diff --staged', 'différences non ajoutées / ajoutées'], ['git switch -c <branche>', 'créer une branche et s’y placer'], ['git merge <branche>', 'fusionner'], ['git pull --rebase', 'récupérer en gardant un historique linéaire'], ['git push -u origin <branche>', 'publier une branche'], ['git revert <id>', 'annuler un commit partagé'], ['git reflog', 'retrouver une position perdue'], ['git bisect', 'trouver le commit qui a introduit un bug']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer les trois zones de Git avec un schéma. (4 pts)', 'Décrire les commandes d’un travail en branche jusqu’à la demande de fusion. (5 pts)', 'Résoudre un conflit fourni. (4 pts)', 'Choisir entre revert et reset dans trois situations. (4 pts)', 'Rédiger le .gitignore d’un projet donné. (3 pts)'], r: ['working tree, index, dépôt', 'switch -c, add, commit, push -u, PR', 'selon le fichier', 'partagé → revert, local → reset', 'dépendances, secrets, fichiers générés'] },
prof: { deroule: [['Ouverture', 'Le fichier « final_v3_VRAIMENT_final » projeté'], ['Cours', 'Commits, index, branches'], ['Pratique', 'Exercices Learn Git Branching'], ['Pratique', 'Binômes sur un dépôt partagé, conflit provoqué'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : fiche mémo, un seul flux sans branches. Élèves avancés : rebase interactif, bisect, hooks.', eval: 'Historique du dépôt de binôme évalué, sujet type.' }
},
exemples: [
['Lire l’état du dépôt', 'Avant un commit.', 'git status -s', ' M app/Http/Controllers/NoteController.php\nM routes/web.php\n?? notes.csv', 'M en deuxième colonne : modifié, pas encore ajouté. M en première : ajouté à l’index. ?? : fichier non suivi ; ici un export de notes à ne surtout pas versionner, à ajouter au .gitignore.'],
['Voir le graphe', 'Comprendre où en sont les branches.', 'git log --oneline --graph --all -6', '* 9c1e4f2 (HEAD -> main, origin/main) Traductions du menu\n| * 3aa71be (export-pdf) Export PDF des bulletins\n|/\n* 51ab0d7 Ajout export CSV', 'La branche export-pdf est partie de 51ab0d7 ; main a avancé depuis. La fusion créera un commit de fusion, ou un rebase rendra l’historique linéaire.'],
['Push refusé', 'Publication de son travail.', 'git push', '! [rejected] main -> main (fetch first)\nhint: Updates were rejected because the remote contains work that you do not have locally.', 'Un collègue a poussé avant vous. git pull --rebase rejoue vos commits au-dessus des siens, puis git push passe.'],
['Résoudre un conflit', 'Fusion de deux branches.', 'git merge nouvelle-page ; git diff --name-only --diff-filter=U', 'CONFLICT (content): Merge conflict in index.html\nAutomatic merge failed; fix conflicts and then commit the result.\nindex.html', 'On ouvre index.html, on choisit le contenu entre les marqueurs, on supprime les marqueurs, puis git add index.html et git commit.'],
['Retrouver un travail « perdu »', 'Après un reset malheureux.', 'git reflog -4', '4d02a9b HEAD@{0}: reset: moving to HEAD~2\n9c1e4f2 HEAD@{1}: commit: Traductions du menu\n51ab0d7 HEAD@{2}: commit: Ajout export CSV', 'Le commit 9c1e4f2 existe toujours. git reset --hard 9c1e4f2 le récupère.'],
['Trouver qui a modifié une ligne', 'Une règle de calcul semble fausse.', 'git blame -L 42,44 app/Services/Moyenne.php', '51ab0d7 (Inès K. 2026-09-24) $coef = $note->coef ?? 1;\ne73f210 (Tom L. 2026-09-27) $total += $note->valeur;\ne73f210 (Tom L. 2026-09-27) return $total / count($notes);', 'blame associe chaque ligne à son dernier commit. La division par count($notes) ignore les coefficients : on regarde le commit e73f210 avec git show pour comprendre le changement.']
],
termes: ['Git', 'commit', 'index', 'branche', 'HEAD', 'main', 'merge', 'rebase', 'conflit', 'fast-forward', 'git add', 'git commit', 'git status', 'git log', 'git diff', 'git switch', 'git pull', 'git push', 'git fetch', 'git revert', 'git reset', 'git reflog', 'git bisect', 'git blame', '.gitignore', 'pull request', 'origin']
});
reg({
cours: { id: 'cicd', match: /CI\/CD|intégration continue|déploiement continu|pipeline/i,
titre: 'CI/CD : intégration et déploiement continus', duree: '',
niveau: 'BTS SIO SLAM / SISR · BUT Informatique / R&T DevCloud · AIS · Master',
prerequis: ['Git', 'Conteneurs Docker'],
competences: ['BTS SIO SLAM · Bloc 2 · Déployer et tester une solution applicative', 'BUT R&T DevCloud · Automatiser le déploiement', 'AIS · Industrialiser les mises en production'],
intro: 'Livrer à la main, c’est se connecter au serveur, tirer le code, lancer les migrations, vider les caches et espérer ne rien oublier. Une chaîne d’intégration et de déploiement continus automatise ces étapes : à chaque push, les tests s’exécutent, l’image est construite et, si tout est vert, l’application est déployée. Les erreurs sont détectées en minutes plutôt qu’en production, et chaque livraison devient un non-événement.',
cm: [
{ t: 'Intégration, livraison, déploiement', d: '', html: 'L’<b>intégration continue</b> (CI) fusionne le travail de chacun fréquemment et vérifie automatiquement chaque modification : compilation, analyse statique, tests. La <b>livraison continue</b> (continuous delivery) produit à chaque fois un artefact prêt à déployer, la mise en production restant une décision humaine. Le <b>déploiement continu</b> (continuous deployment) va jusqu’à la production sans intervention.<br><br>Le principe commun : la branche principale est toujours dans un état déployable, et toute régression est signalée immédiatement à celui qui l’a introduite.' },
{ t: 'Anatomie d’un pipeline', d: '', html: 'Un <b>pipeline</b> est une suite d’<b>étapes</b> (stages) composées de <b>jobs</b>, exécutés par des <b>runners</b> dans des environnements propres, souvent des conteneurs. On y trouve typiquement : vérification du style (lint), tests unitaires, construction de l’image, analyse de sécurité, publication dans un registre, déploiement en préproduction, puis en production.<br><br>Chaque job reçoit le code du commit, produit éventuellement des <b>artefacts</b> (paquet, rapport de tests, image) et renvoie un statut. Un seul échec arrête le pipeline.', exemple: '# .github/workflows/ci.yml (GitHub Actions)\nname: ci\non: [push, pull_request]\njobs:\n tests:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v5\n with: { python-version: "3.12" }\n - run: pip install -r requirements.txt\n - run: ruff check .\n - run: pytest -q\n image:\n needs: tests\n if: github.ref == \'refs/heads/main\'\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: docker build -t registre.lycee.local/resa:${{ github.sha }} .\n - run: trivy image --exit-code 1 --severity HIGH,CRITICAL registre.lycee.local/resa:${{ github.sha }}\n - run: docker push registre.lycee.local/resa:${{ github.sha }}' },
{ t: 'Les tests, cœur de la CI', d: '', html: 'Une CI ne vaut que par ses tests. Les <b>tests unitaires</b> vérifient une fonction isolée, en millisecondes. Les <b>tests d’intégration</b> vérifient l’application avec sa base de données, souvent démarrée comme service du pipeline. Les <b>tests de bout en bout</b> pilotent un navigateur (Playwright, Cypress). On en écrit beaucoup des premiers, peu des derniers : c’est la pyramide des tests.<br><br>Un test instable (qui échoue une fois sur dix sans raison) est pire que pas de test : l’équipe apprend à ignorer le rouge. Il se corrige en priorité.' },
{ t: 'Déployer sans interruption', d: '', html: 'Le déploiement tire l’image construite et testée, identifiée par l’empreinte du commit, jamais par latest. Plusieurs stratégies limitent le risque. Le <b>déploiement progressif</b> (rolling) remplace les instances une à une. Le <b>blue-green</b> prépare la nouvelle version à côté de l’ancienne puis bascule le trafic. Le <b>canary</b> envoie d’abord une petite part des utilisateurs sur la nouvelle version.<br><br>Dans tous les cas, on prévoit le <b>retour arrière</b> : redéployer l’image précédente doit prendre une commande. Les migrations de base de données doivent rester compatibles avec la version précédente le temps de la bascule.' },
{ t: 'Secrets et sécurité de la chaîne', d: '', html: 'Le pipeline manipule des secrets : accès au registre, clés SSH de déploiement, jetons d’API. Ils se stockent dans le gestionnaire de secrets de la plateforme (secrets GitHub, variables masquées GitLab), jamais dans le dépôt, et avec des droits minimaux (une clé de déploiement ne peut que déployer).<br><br>La chaîne elle-même est une cible : une action tierce compromise ou une dépendance malveillante s’exécute avec les secrets du pipeline. On épingle les actions sur une version précise, on analyse les dépendances (Dependabot, Renovate) et on protège les branches : pas de déploiement en production depuis une branche non relue.' }
],
synthese: ['CI : vérifier chaque modification ; CD : livrer ou déployer automatiquement', 'Pipeline = étapes → jobs → runners, arrêt au premier échec', 'Pyramide des tests ; un test instable se corrige en priorité', 'Image identifiée par le commit ; stratégie de déploiement et retour arrière prévus', 'Secrets dans la plateforme, actions épinglées, branches protégées'],
qcm: [
['Différence entre livraison continue et déploiement continu ?', ['Aucune', 'La livraison laisse la mise en production à une décision humaine', 'Le déploiement ne teste pas', 'La livraison est manuelle'], 1, 'Continuous delivery vs deployment.'],
['Que se passe-t-il quand un job échoue ?', ['Le pipeline continue', 'Le pipeline s’arrête et signale l’échec', 'Le job est ignoré', 'Le code est supprimé'], 1, 'On ne déploie pas du rouge.'],
['Pourquoi taguer l’image avec l’empreinte du commit ?', ['Esthétique', 'Pour savoir exactement quel code tourne et pouvoir revenir en arrière', 'Obligatoire', 'Plus léger'], 1, 'Traçabilité et reproductibilité.'],
['Quelle stratégie envoie d’abord 5 % des utilisateurs sur la nouvelle version ?', ['Rolling', 'Blue-green', 'Canary', 'Big bang'], 2, 'Détection précoce sur un petit échantillon.'],
['Où stocker la clé de déploiement ?', ['Dans le dépôt', 'Dans les secrets de la plateforme CI', 'Dans le Dockerfile', 'Dans le README'], 1, 'Jamais dans le code.'],
['Que faire d’un test qui échoue au hasard ?', ['L’ignorer', 'Le corriger en priorité', 'Le supprimer toujours', 'Relancer jusqu’au vert'], 1, 'Sinon on ne croit plus au rouge.']
],
td: [
{ n: 'Application', c: 'Écrire un pipeline GitLab CI (.gitlab-ci.yml) en trois étapes : lint, test, build, avec une base PostgreSQL en service pour les tests.', r: 'stages: [lint, test, build]\nlint: { stage: lint, image: python:3.12, script: [pip install ruff, ruff check .] }\ntest: { stage: test, image: python:3.12, services: [postgres:16], variables: { POSTGRES_PASSWORD: test, DATABASE_URL: postgresql://postgres:test@postgres/postgres }, script: [pip install -r requirements.txt, pytest] }\nbuild: { stage: build, image: docker:27, services: [docker:27-dind], script: [docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA ., docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA], rules: [{ if: $CI_COMMIT_BRANCH == "main" }] }' },
{ n: 'Application', c: 'Proposer une procédure de retour arrière pour une application déployée avec Compose et des images taguées par commit.', r: 'Garder le tag précédent dans l’historique des déploiements · modifier le tag dans compose.yaml (ou variable IMAGE_TAG) · docker compose pull && up -d · vérifier · migrations compatibles N-1.' },
{ n: 'Approfondissement', c: 'Lister les risques de sécurité d’un pipeline et une mesure pour chacun.', r: 'Secret fuité → secrets masqués, droits minimaux · action tierce compromise → épingler par empreinte · dépendance malveillante → analyse et verrouillage des versions · déploiement non relu → branches protégées.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un Forgejo avec runner, une API et son déploiement.', etapes: ['Écrire le workflow lint + tests', 'Casser un test et observer le blocage', 'Construire et scanner l’image', 'Déployer sur la VM de préproduction', 'Revenir à la version précédente'] },
plusLoin: [['GitHub Actions · documentation', 'https://docs.github.com/fr/actions'], ['GitLab CI/CD · documentation', 'https://docs.gitlab.com/ee/ci/'], ['Forgejo Actions', 'https://forgejo.org/docs/latest/user/actions/']] },
plus: {
cas: { titre: '', texte: '' },
resolu: { enonce: 'Le pipeline de l’application de réservation est rouge depuis ce matin sur l’étape tests, alors que les tests passent sur le poste du développeur. Plus aucune correction ne peut être déployée.', etapes: [
['Lire le job en échec', 'tests / pytest\nE sqlalchemy.exc.OperationalError: could not connect to server: Connection refused\n Is the server running on host "localhost" and accepting TCP/IP connections on port 5432?\n→ Ce n’est pas un test fonctionnel qui échoue : l’application ne trouve pas sa base.'],
['Comparer les environnements', 'Sur le poste : PostgreSQL installé localement, DATABASE_URL absente → valeur par défaut localhost.\nDans le pipeline : la base est un service nommé postgres.\n→ Un commit d’hier a supprimé la variable DATABASE_URL du workflow en « nettoyant » le fichier.'],
['Corriger le workflow', 'services:\n postgres:\n image: postgres:16\n env: { POSTGRES_PASSWORD: test }\n options: --health-cmd pg_isready --health-interval 5s --health-retries 10\nenv:\n DATABASE_URL: postgresql://postgres:test@localhost:5432/postgres\n→ Le service est déclaré avec un healthcheck et la variable est rétablie.'],
['Vérifier', 'git push → pipeline : lint ✓ tests ✓ image ✓\n→ Le pipeline est vert ; la correction en attente est déployée.'],
['Prévenir', '→ Le fichier de workflow passe sous relue obligatoire (CODEOWNERS), et un test de fumée vérifie au démarrage que DATABASE_URL est définie, avec un message explicite plutôt qu’une erreur de connexion.']
] },
erreurs: ['Pipeline sans tests réels', 'Tolérer un pipeline rouge « connu »', 'Déployer l’image latest', 'Secrets en clair dans le workflow', 'Actions tierces non épinglées', 'Pas de procédure de retour arrière'],
memo: [['Élément', 'À retenir'], ['on: push / pull_request', 'déclencheurs d’un workflow GitHub'], ['needs:', 'ordre entre jobs'], ['services:', 'base de données pour les tests'], ['secrets.NOM', 'accès à un secret masqué'], ['rules / if', 'n’exécuter que sur main'], ['artifacts', 'conserver rapports et paquets'], ['cache', 'accélérer les dépendances'], ['${{ github.sha }} / $CI_COMMIT_SHA', 'taguer l’image par commit'], ['trivy image --exit-code 1', 'bloquer sur une vulnérabilité critique'], ['environments + approbation', 'validation humaine avant production']],
examen: { titre: 'Sujet type · 20 points', q: ['Définir CI, livraison et déploiement continus. (3 pts)', 'Écrire un pipeline complet pour une application fournie. (8 pts)', 'Comparer trois stratégies de déploiement. (4 pts)', 'Diagnostiquer un pipeline en échec à partir d’un journal. (3 pts)', 'Citer deux risques de sécurité de la chaîne. (2 pts)'], r: ['voir cours', 'lint, tests, build, push, deploy', 'rolling, blue-green, canary', 'selon le journal', 'secrets, actions tierces'] },
prof: { deroule: [['Ouverture', 'Déploiement manuel chronométré, puis le même en un push'], ['Cours', 'Pipeline, tests, artefacts'], ['Pratique', 'Workflow lint + tests'], ['Pratique', 'Build et déploiement'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : workflow à compléter. Élèves avancés : déploiement canary, analyse de dépendances.', eval: 'Pipeline vert et retour arrière démontré, sujet type.' }
},
exemples: [
['Lire l’état des pipelines', 'Après un push.', 'gh run list --limit 3', 'STATUS TITLE WORKFLOW BRANCH\n✓ Export CSV ci main\nX Refonte connexion ci refonte\n✓ Traductions ci main', 'La branche refonte est rouge : elle ne pourra pas être fusionnée tant que la protection de branche exige des tests verts.'],
['Voir le journal d’un job', 'Pipeline rouge.', 'gh run view --log-failed | tail -4', 'tests FAILED tests/test_moyenne.py::test_absent - ZeroDivisionError\ntests 1 failed, 47 passed in 3.21s', 'Un seul test échoue, avec une erreur précise : la moyenne d’un élève sans note divise par zéro. La CI a attrapé le bug avant la production.'],
['Relancer localement comme la CI', 'Reproduire un échec.', 'docker run --rm -v "$PWD":/app -w /app python:3.12 sh -c "pip install -q -r requirements.txt && pytest -q"', '1 failed, 47 passed', 'Exécuter les tests dans la même image que la CI élimine le « chez moi ça marche ».'],
['Bloquer une image vulnérable', 'Étape de sécurité.', 'trivy image --exit-code 1 --severity CRITICAL registre.lycee.local/resa:9c1e4f2', 'Total: 1 (CRITICAL: 1)\nopenssl 3.0.2 CVE-2024-XXXX fixed in 3.0.14', 'Le code de sortie 1 fait échouer le job : l’image n’est pas publiée. On met à jour l’image de base et on reconstruit.'],
['Déployer une version précise', 'Déploiement depuis le pipeline.', 'ssh deploy@preprod "cd /srv/resa && IMAGE_TAG=9c1e4f2 docker compose up -d && docker compose ps"', 'resa-app registre.lycee.local/resa:9c1e4f2 Up 5 seconds (healthy)', 'Le tag est l’empreinte du commit : on sait exactement quel code tourne.'],
['Revenir en arrière', 'La nouvelle version provoque des erreurs.', 'ssh deploy@prod "cd /srv/resa && IMAGE_TAG=51ab0d7 docker compose up -d"', 'Container resa-app Recreated', 'Retour à la version précédente en une commande, parce que chaque image est conservée avec son tag de commit.']
],
termes: ['CI/CD', 'intégration continue', 'déploiement continu', 'livraison continue', 'pipeline', 'job', 'runner', 'artefact', 'workflow', 'GitHub Actions', 'GitLab CI', 'Forgejo', 'tests unitaires', 'lint', 'canary', 'blue-green', 'rolling', 'retour arrière', 'secrets', 'Trivy']
});
})();

View file

@ -0,0 +1,183 @@
// Exemples concrets commentés : [titre, contexte, code, sortie, explication]
window.CONTENUS_EXEMPLES = {
binaire: [
['Lire une adresse MAC', 'Sur l’étiquette d’une carte réseau.', '3C:52:82:1A:0F:9B', '6 octets · 3C = 0011 1100 = 60', 'Chaque paire hexadécimale est un octet. Les 3 premiers identifient le fabricant (OUI), les 3 derniers la carte.'],
['Décoder une couleur web', 'Dans une feuille CSS.', '#FF6FB5', 'rouge 255 · vert 111 · bleu 181', 'Trois octets en hexadécimal. FF est le maximum : la couleur est très rouge, un peu bleue, c’est un rose.'],
['Calculer un masque de tête', 'Un collègue demande « c’est quoi un /22 ? ».', '/22 = 11111111.11111111.11111100.00000000', '255.255.252.0 · 1 022 hôtes', '22 bits à 1 : deux octets pleins, puis 6 bits à 1 (128+64+32+16+8+4 = 252). Il reste 10 bits d’hôte : 2¹⁰ − 2 = 1 022.'],
['Convertir en Python', 'Vérifier un calcul fait à la main.', 'bin(178), hex(178), int("B2", 16), int("10110010", 2)', "('0b10110010', '0xb2', 178, 178)", 'bin et hex convertissent vers le texte ; int avec une base convertit dans l’autre sens.'],
['Débordement sur 8 bits', 'Un compteur de capteur repasse à zéro.', 'compteur = (255 + 1) % 256', '0', 'Sur un octet non signé, 255 + 1 ne tient plus : le 9e bit est perdu et l’on repart de 0.'],
['Taille d’une image', 'Estimer le poids d’une photo non compressée.', '1920 × 1080 pixels × 3 octets', '6 220 800 octets ≈ 5,9 Mio', 'Chaque pixel a 3 couleurs sur 1 octet. C’est pour cela qu’on compresse en JPEG ou PNG.']],
terminal: [
['Préparer un TP', 'Créer d’un coup l’arborescence d’un projet.', 'mkdir -p ~/tp/{reseau,systeme,web} && ls ~/tp', 'reseau systeme web', 'Les accolades génèrent trois chemins. -p évite l’erreur si ~/tp existe déjà.'],
['Retrouver un fichier perdu', 'Un élève ne sait plus où il a enregistré rapport.txt.', 'find ~ -name "rapport*.txt" 2>/dev/null', '/home/eleve/Documents/old/rapport-v2.txt', 'find part du dossier personnel ; 2>/dev/null masque les dossiers inaccessibles.'],
['Voir les derniers événements', 'Le service web ne démarre pas.', 'sudo journalctl -u apache2 -n 20 --no-pager', 'AH00072: make_sock: could not bind to address [::]:80', 'Le port 80 est déjà pris par un autre programme : on le cherche avec sudo ss -tlnp | grep :80.'],
['Sauvegarder un dossier', 'Avant une modification risquée.', 'tar czf site-$(date +%F).tar.gz /var/www/site', 'site-2026-09-30.tar.gz', '$(date +%F) insère la date du jour : chaque sauvegarde a un nom unique.'],
['Repérer ce qui ralentit la machine', 'Le serveur répond lentement.', 'ps aux --sort=-%cpu | head -5', 'www-data 8812 97.3 … php-fpm', 'Un processus PHP consomme presque tout le processeur : on regarde le script en cause avant de le tuer.'],
['Rejouer une commande', 'Vous avez tapé une longue commande hier.', 'Ctrl+R puis « rsync »', '(reverse-i-search)`rsync\': rsync -av site/ srv:/var/www/', 'La recherche dans l’historique évite de tout retaper.']],
droits: [
['Lire une ligne de ls -l', 'Comprendre pourquoi un fichier est inaccessible.', 'ls -l notes.csv', '-rw-r----- 1 alice profs 4096 sept. 30 notes.csv', 'alice lit et écrit, le groupe profs lit, les autres rien. Un élève ne peut pas l’ouvrir.'],
['Rendre un script exécutable', '« Permission non accordée » au lancement.', 'chmod u+x sauvegarde.sh && ./sauvegarde.sh', 'Sauvegarde terminée.', 'Seul le propriétaire reçoit le droit d’exécution, pas tout le monde.'],
['Créer un espace partagé', 'Les profs de SIO veulent un dossier commun.', 'sudo mkdir /srv/sio && sudo chown root:profsio /srv/sio && sudo chmod 2770 /srv/sio', 'drwxrws--- root profsio /srv/sio', 'Le s du groupe (SGID) fait que chaque fichier créé appartient à profsio.'],
['Vérifier ses groupes', 'Après avoir été ajouté à un groupe.', 'id', 'uid=1001(bob) … groupes=1001(bob),1002(devs)', 'Si devs n’apparaît pas, il faut se déconnecter puis se reconnecter.'],
['Trouver les fichiers trop ouverts', 'Audit rapide d’un serveur.', 'sudo find /var/www -perm -o+w -type f', '/var/www/site/config.php', 'Un fichier de configuration modifiable par tous est une faille : chmod o-w config.php.'],
['Donner un accès ponctuel', 'Un stagiaire doit lire un seul rapport.', 'setfacl -m u:stagiaire:r rapport.pdf && getfacl rapport.pdf', 'user:stagiaire:r--', 'L’ACL donne l’accès à une seule personne sans toucher au groupe.']],
tubes: [
['Compter les comptes humains', 'Combien d’utilisateurs ont un shell ?', 'grep -c "/bin/bash$" /etc/passwd', '14', 'grep -c compte au lieu d’afficher ; $ ancre la recherche en fin de ligne.'],
['Top des pages visitées', 'Préparer un bilan pour la direction.', "awk '{print $7}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -3", ' 812 /\n 430 /planning\n 211 /cours', 'Le 7e champ d’une ligne Apache est le chemin demandé.'],
['Suivre les connexions en direct', 'Pendant un examen en ligne.', 'tail -f /var/log/auth.log | grep --line-buffered sshd', 'Accepted publickey for prof from 10.0.0.12', 'tail -f affiche au fil de l’eau ; --line-buffered évite que grep retienne les lignes.'],
['Nettoyer les vieux journaux', 'Libérer de l’espace chaque semaine.', 'find /var/log/app -name "*.log" -mtime +30 | xargs rm -v', "supprimé '/var/log/app/2026-07-01.log'", 'xargs transforme la liste en arguments pour rm.'],
['Remplacer dans un fichier', 'Passer toutes les URL en HTTPS.', "sed -i 's#http://#https://#g' index.html", '(aucune sortie)', '-i modifie le fichier sur place ; # remplace / comme séparateur pour éviter d’échapper les barres.'],
['Garder une trace d’un diagnostic', 'Envoyer le résultat au support.', './diagnostic.sh 2>&1 | tee rapport.txt', '(affichage à l’écran + fichier rapport.txt)', '2>&1 fusionne les erreurs dans la sortie, tee écrit tout dans le fichier.']],
osi: [
['Tester la couche 3', 'Le poste accède-t-il à la passerelle ?', 'ping -c 3 192.168.10.1', '3 packets transmitted, 3 received, 0% packet loss', 'La couche réseau fonctionne jusqu’à la passerelle.'],
['Suivre le chemin d’un paquet', 'Un site distant est lent.', 'traceroute www.lycee.fr', ' 1 192.168.10.1 1 ms\n 2 10.0.0.1 4 ms\n 3 * * *\n 4 93.184.216.34 18 ms', 'Chaque ligne est un routeur. Les étoiles indiquent un routeur qui ne répond pas à ICMP, pas forcément une panne.'],
['Voir les services qui écoutent', 'Le site ne répond pas.', 'sudo ss -tlnp', 'LISTEN 0 511 *:80 users:(("apache2",pid=812))', 'Apache écoute bien sur le port 80 en TCP : le problème est ailleurs (pare-feu, DNS).'],
['Interroger le DNS', 'Vérifier une nouvelle entrée.', 'dig +short intranet.lycee.fr', '192.168.10.20', 'Le nom se résout : la couche application peut être testée avec curl.'],
['Lire la table ARP', 'Suspicion de conflit d’adresse.', 'ip neigh', '192.168.10.1 dev eth0 lladdr 3c:52:82:1a:0f:9b REACHABLE', 'Si deux IP différentes affichent la même MAC, on soupçonne une usurpation ARP.'],
['Capturer une poignée de main', 'Observer TCP en vrai.', 'sudo tcpdump -i eth0 -n "tcp port 443 and tcp[tcpflags] & tcp-syn != 0"', 'Flags [S] … Flags [S.] …', '[S] = SYN envoyé, [S.] = SYN-ACK reçu.']],
sql: [
['Liste de classe', 'Imprimer la liste de BTS SIO 1.', "SELECT nom, prenom FROM eleves e JOIN classes c ON c.id = e.classe_id WHERE c.libelle = 'BTS SIO 1' ORDER BY nom;", 'Diallo Adam · Kaci Inès · Martin Léa …', 'La jointure permet de filtrer par libellé plutôt que par numéro.'],
['Moyenne par classe', 'Préparer le conseil de classe.', 'SELECT c.libelle, ROUND(AVG(e.moyenne), 1) AS moy FROM eleves e JOIN classes c ON c.id = e.classe_id GROUP BY c.libelle;', 'BTS SIO 1 | 11.8\nBTS SIO 2 | 12.6', 'AVG calcule la moyenne de chaque groupe, ROUND arrondit.'],
['Élèves sans rendu', 'Relancer ceux qui n’ont pas rendu le DM.', 'SELECT e.nom FROM eleves e LEFT JOIN rendus r ON r.eleve_id = e.id AND r.devoir_id = 3 WHERE r.eleve_id IS NULL;', 'Petit · Leroy', 'LEFT JOIN garde tout le monde ; IS NULL isole ceux qui n’ont pas de correspondance.'],
['Top 3', 'Afficher les meilleures notes du DM.', 'SELECT e.nom, r.note FROM rendus r JOIN eleves e ON e.id = r.eleve_id WHERE r.devoir_id = 3 ORDER BY r.note DESC LIMIT 3;', 'Kaci 19 · Nguyen 16 · Martin 11', 'ORDER BY … DESC puis LIMIT pour un classement.'],
['Corriger une erreur de saisie', 'Une note a été saisie à 1,5 au lieu de 15.', 'UPDATE rendus SET note = 15 WHERE eleve_id = 7 AND devoir_id = 3;', '1 ligne modifiée', 'Le WHERE est testé d’abord avec un SELECT pour vérifier qu’il ne vise qu’une ligne.'],
['Bloquer une injection', 'Formulaire de connexion en PHP.', '$st = $pdo->prepare("SELECT id FROM users WHERE login = ?");\n$st->execute([$_POST["login"]]);', '(la saisie n’est jamais interprétée comme du SQL)', 'Même si l’utilisateur tape \' OR 1=1 --, la requête cherche ce texte littéral.']],
python: [
['Moyenne d’une classe', 'Calcul rapide à partir d’une liste.', 'notes = [12, 15, 9, 18, 7]\nprint(round(sum(notes) / len(notes), 2))', '12.2', 'sum additionne, len compte ; round arrondit à 2 décimales.'],
['Saisie robuste', 'Le programme ne doit pas planter sur « douze ».', 'try:\n n = int(input("Note : "))\nexcept ValueError:\n print("Entrez un nombre")', 'Note : douze\nEntrez un nombre', 'L’exception est capturée ; le programme continue.'],
['Compter les mots', 'Analyser un texte.', 'texte = "le chat et le chien"\nc = {}\nfor m in texte.split():\n c[m] = c.get(m, 0) + 1\nprint(c)', "{'le': 2, 'chat': 1, 'et': 1, 'chien': 1}", 'get(m, 0) renvoie 0 si le mot n’est pas encore dans le dictionnaire.'],
['Filtrer en une ligne', 'Lister les élèves admis.', 'eleves = {"Léa": 12, "Tom": 8, "Inès": 17}\nprint([n for n, v in eleves.items() if v >= 10])', "['Léa', 'Inès']", 'Liste en compréhension : on lit « les noms n pour chaque paire si la note ≥ 10 ».'],
['Lire un CSV exporté', 'Fichier de notes du logiciel de vie scolaire.', 'import csv\nwith open("notes.csv", encoding="utf-8") as f:\n for l in csv.DictReader(f, delimiter=";"):\n if float(l["note"]) < 8: print(l["nom"])', 'Petit\nLeroy', 'DictReader donne chaque ligne comme un dictionnaire, indexé par les en-têtes.'],
['Jeu du nombre mystère', 'Premier programme complet.', 'import random\ns = random.randint(1, 100)\nwhile (p := int(input("? "))) != s:\n print("plus" if p < s else "moins")\nprint("Trouvé !")', '? 50\nplus\n? 75\nmoins\n? 62\nTrouvé !', 'while répète tant que la proposition est fausse ; l’opérateur := affecte et teste en même temps.']],
web: [
['Voir une requête HTTP', 'Comprendre ce qu’envoie le serveur.', 'curl -I https://example.org', 'HTTP/2 200\ncontent-type: text/html; charset=UTF-8', '-I n’affiche que les en-têtes : code, type de contenu, taille.'],
['Page minimale valide', 'Point de départ de tout site.', '<!doctype html>\n<html lang="fr">\n<head><meta charset="utf-8"><title>TP</title></head>\n<body><h1>Bonjour</h1></body>\n</html>', '(une page avec un titre)', 'lang aide les lecteurs d’écran, charset évite les accents cassés.'],
['Grille de 3 cartes', 'Mise en page moderne.', '.grille { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }\n@media (max-width: 700px) { .grille { grid-template-columns: 1fr; } }', '3 colonnes sur ordinateur, 1 sur téléphone', 'Deux règles suffisent pour une mise en page adaptative.'],
['Réagir à un clic', 'Bouton « Lever la main ».', 'document.querySelector("#main").addEventListener("click", () => {\n document.body.style.background = "#FFD23F";\n});', '(la page devient jaune au clic)', 'addEventListener branche une fonction sur un événement.'],
['Diagnostiquer une 404', 'Une image ne s’affiche pas.', 'sudo tail -1 /var/log/apache2/access.log', '"GET /img/Logo.png HTTP/1.1" 404', 'Linux distingue les majuscules : le fichier s’appelle logo.png.'],
['Empêcher une XSS', 'Afficher un commentaire de visiteur en PHP.', 'echo htmlspecialchars($commentaire, ENT_QUOTES, "UTF-8");', '&lt;script&gt; s’affiche comme du texte', 'Les caractères spéciaux sont échappés : le navigateur n’exécute rien.']],
crypto: [
['Vérifier un téléchargement', 'Image ISO de Debian.', 'sha256sum -c SHA256SUMS --ignore-missing', 'debian-13.iso: Réussi', 'L’empreinte du fichier correspond à celle publiée : le fichier est intact.'],
['Constater l’effet avalanche', 'Changer une seule lettre.', 'echo -n bonjour | sha256sum\necho -n Bonjour | sha256sum', '2cb4b143… \n9172e8ee…', 'Une majuscule suffit à changer complètement l’empreinte.'],
['Hacher un mot de passe en Python', 'Application web.', 'import bcrypt\nh = bcrypt.hashpw(b"Soleil2026!", bcrypt.gensalt())\nbcrypt.checkpw(b"Soleil2026!", h)', 'True', 'gensalt produit un sel aléatoire, stocké dans le hachage lui-même.'],
['Générer une clé SSH', 'Première connexion à un serveur.', 'ssh-keygen -t ed25519 -C "eleve@syllabus"', 'Your public key has been saved in ~/.ssh/id_ed25519.pub', 'Deux fichiers : la clé privée (à garder) et la clé publique (.pub, à déposer).'],
['Lire un certificat', 'Vérifier qui a signé le certificat d’un site.', 'echo | openssl s_client -connect example.org:443 2>/dev/null | openssl x509 -noout -issuer -dates', "issuer=… DigiCert …\nnotAfter=Jan 15 2027", 'On voit l’autorité et la date d’expiration.'],
['Chiffrer un fichier', 'Envoyer les bulletins à la direction.', 'gpg -c --cipher-algo AES256 bulletins.pdf', 'bulletins.pdf.gpg', 'Le mot de passe se transmet par un autre canal (téléphone), jamais dans le même mail.']]
};
// === DIAGNOSTIC RÉSEAU === Cours complet : ip, ping, traceroute, ss, ARP, tcpdump / Wireshark
(function () {
window.CONTENUS_PRO = window.CONTENUS_PRO || [];
window.CONTENUS_PRO.unshift({
id: 'diagnostic',
match: /diagnostic réseau|outils de diagnostic|dépannage réseau/i,
titre: 'Diagnostic réseau en ligne de commande : ip, ping, traceroute, ss, ARP, tcpdump',
duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · Titre pro TSSR / AIS',
prerequis: ['Modèles OSI et TCP/IP, encapsulation', 'Adressage IPv4 et masques', 'Le terminal Linux'],
competences: ['BTS SIO · Bloc 1 · Répondre aux incidents et aux demandes d’assistance', 'BTS SIO SISR · Bloc 2 · Exploiter, dépanner et superviser une solution d’infrastructure réseau', 'BTS CIEL · C09 · Maintenir un réseau', 'BUT R&T · Compétence « Administrer les réseaux et l’Internet »', 'TSSR · Exploiter et maintenir les serveurs et le réseau'],
intro: 'Un serveur de production n’a presque jamais d’interface graphique. Il est rangé dans une baie, dans un datacenter à Paris ou à Roubaix, et l’on s’y connecte en SSH depuis un poste qui peut être à l’autre bout du pays. Quand un utilisateur appelle pour dire « le site ne marche plus », il n’y a ni fenêtre de paramètres réseau ni icône Wi-Fi à regarder : il y a un terminal, et une poignée d’outils en ligne de commande. Ce cours présente ces outils un par un, explique précisément ce que chacun mesure et ce qu’il ne mesure pas, puis les assemble en une méthode de diagnostic que vous pourrez appliquer à n’importe quel incident.',
cm: [
{ t: 'La méthode avant les outils', d: '', html: 'Le réflexe du débutant est de tout essayer en même temps : redémarrer le service, changer le câble, vider le cache du navigateur, relancer la machine. Parfois cela marche, mais on ne sait pas pourquoi, et la panne reviendra. Le professionnel suit une <b>démarche par couches</b>, du bas vers le haut, et ne passe à la couche suivante que lorsque la précédente est validée.<br><br>Concrètement, on se pose cinq questions dans l’ordre. Le lien est-il actif (couche 1-2) ? La machine a-t-elle une adresse IP correcte et une route vers la destination (couche 3 locale) ? Les paquets atteignent-ils la passerelle, puis la destination (couche 3 de bout en bout) ? Le service écoute-t-il sur le bon port et le pare-feu le laisse-t-il passer (couche 4) ? L’application répond-elle correctement (couche 7) ? Chaque outil de ce cours répond à une de ces questions.<br><br>Enfin, on <b>note tout</b>. Une commande, sa sortie, l’heure. Un diagnostic écrit peut être relu par un collègue, joint à un ticket, et sert de preuve quand le problème vient d’un prestataire.', table: [['Question', 'Couche', 'Outil principal'], ['Le lien est-il monté ?', '1-2', 'ip link'], ['Ai-je une adresse et une route ?', '3 locale', 'ip addr, ip route'], ['Mon voisin direct répond-il ?', '2-3', 'ip neigh, ping passerelle'], ['La destination est-elle joignable ?', '3', 'ping, traceroute, mtr'], ['Le service écoute-t-il ?', '4', 'ss'], ['Que se passe-t-il vraiment sur le fil ?', 'toutes', 'tcpdump, Wireshark'], ['Le nom se résout-il ?', '7', 'dig'], ['L’application répond-elle ?', '7', 'curl']] },
{ t: 'ip : l’état réseau de la machine', d: '', html: 'La commande <code>ip</code>, du paquet iproute2, remplace depuis longtemps les anciens <code>ifconfig</code>, <code>route</code> et <code>arp</code>, qui ne sont plus installés par défaut sur Debian. Elle se décline en sous-commandes : <code>ip link</code> pour les interfaces, <code>ip addr</code> pour les adresses, <code>ip route</code> pour la table de routage, <code>ip neigh</code> pour la table de voisinage (ARP).<br><br><b>ip link</b> indique si l’interface est administrativement activée (UP) et si la couche physique détecte une porteuse (LOWER_UP). Une interface « UP » sans « LOWER_UP » a été activée mais ne voit pas de câble ou de switch en face : le problème est physique. <code>ip -s link</code> ajoute les compteurs de paquets, d’erreurs et de pertes, très utiles pour repérer un câble abîmé ou un duplex mal négocié.<br><br><b>ip addr</b> affiche les adresses. L’option <code>-br</code> (brief) donne une ligne par interface, idéale pour un coup d’œil. On y vérifie l’adresse, mais surtout le <b>masque</b> : un poste en 192.168.10.65/24 alors que le réseau est un /26 croira que 192.168.10.130 est sur son réseau local et ne passera jamais par la passerelle.<br><br><b>ip route</b> montre la table de routage. La ligne <code>default via 192.168.10.1</code> est la passerelle par défaut : sans elle, la machine ne sait joindre que son propre réseau. La commande <code>ip route get 8.8.8.8</code> est précieuse : elle indique quelle route, quelle interface et quelle adresse source seraient utilisées pour joindre une destination donnée, sans envoyer le moindre paquet.', exemple: '$ ip -br link\nlo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>\nens18 UP bc:24:11:5e:2a:10 <BROADCAST,MULTICAST,UP,LOWER_UP>\n\n$ ip -br addr\nlo UNKNOWN 127.0.0.1/8 ::1/128\nens18 UP 192.168.10.20/24 fe80::be24:11ff:fe5e:2a10/64\n\n$ ip route\ndefault via 192.168.10.1 dev ens18 proto static\n192.168.10.0/24 dev ens18 proto kernel scope link src 192.168.10.20\n\n$ ip route get 1.1.1.1\n1.1.1.1 via 192.168.10.1 dev ens18 src 192.168.10.20 uid 1000', attention: 'Les modifications faites avec ip addr add ou ip route add sont perdues au redémarrage. Sur Debian, la configuration permanente est dans /etc/network/interfaces ou gérée par systemd-networkd / NetworkManager. On teste avec ip, on pérennise dans le fichier.' },
{ t: 'ping : la preuve de joignabilité', d: '', html: '<code>ping</code> envoie des messages <b>ICMP Echo Request</b> et attend des <b>Echo Reply</b>. Il mesure trois choses : si la destination répond, en combien de temps (le RTT, round-trip time, aller et retour), et quelle proportion des messages se perd en route.<br><br>Chaque ligne de réponse contient un numéro de séquence (<code>icmp_seq</code>), un <b>TTL</b> et un temps. Des numéros de séquence qui sautent indiquent des pertes. Le TTL de la réponse donne un indice sur la distance et le système distant : Linux part en général de 64, Windows de 128, beaucoup d’équipements réseau de 255 ; un TTL reçu de 57 signifie qu’un paquet parti à 64 a traversé 7 routeurs.<br><br>Le résumé final est la partie la plus importante. <code>packet loss</code> doit être à 0 % sur un réseau sain ; quelques pour cent de pertes suffisent à rendre une visioconférence pénible et un transfert de fichiers très lent, car TCP réduit son débit à chaque perte. La ligne <code>rtt min/avg/max/mdev</code> donne le temps minimum, moyen, maximum et l’écart moyen (<b>gigue</b>). Une gigue élevée révèle un lien saturé ou un Wi-Fi perturbé.<br><br>Options utiles : <code>-c 5</code> limite le nombre de paquets (sans elle, ping tourne jusqu’à Ctrl+C), <code>-i 0.2</code> accélère l’envoi, <code>-s 1472 -M do</code> envoie un gros paquet interdit de fragmentation pour tester la MTU d’un chemin, <code>-I ens18</code> force l’interface de sortie.', exemple: '$ ping -c 4 192.168.10.1\nPING 192.168.10.1 (192.168.10.1) 56(84) bytes of data.\n64 bytes from 192.168.10.1: icmp_seq=1 ttl=64 time=0.412 ms\n64 bytes from 192.168.10.1: icmp_seq=2 ttl=64 time=0.387 ms\n64 bytes from 192.168.10.1: icmp_seq=3 ttl=64 time=0.401 ms\n64 bytes from 192.168.10.1: icmp_seq=4 ttl=64 time=0.395 ms\n\n--- 192.168.10.1 ping statistics ---\n4 packets transmitted, 4 received, 0% packet loss, time 3055ms\nrtt min/avg/max/mdev = 0.387/0.398/0.412/0.009 ms', attention: 'Un ping qui échoue ne prouve pas qu’une machine est éteinte. De nombreux pare-feux, dont celui de Windows par défaut et ceux des hébergeurs, bloquent ICMP. À l’inverse, un ping qui répond ne prouve pas qu’un service fonctionne : la machine peut répondre au ping avec un serveur web planté.' },
{ t: 'traceroute et mtr : voir le chemin', d: '', html: 'Quand la destination ne répond pas ou répond lentement, il faut savoir <b>où</b> se situe le problème. <code>traceroute</code> exploite astucieusement le champ TTL de l’en-tête IP. Chaque routeur décrémente ce TTL de 1 ; lorsqu’il atteint 0, le routeur détruit le paquet et renvoie un message <b>ICMP Time Exceeded</b> à l’expéditeur. traceroute envoie donc un premier paquet avec un TTL de 1 (il meurt au premier routeur, qui se dénonce), puis un TTL de 2 (il meurt au deuxième), et ainsi de suite jusqu’à la destination.<br><br>Chaque ligne correspond à un saut, avec trois mesures de temps. Des <b>étoiles</b> (* * *) signifient qu’aucune réponse n’est revenue pour ce saut. Si les sauts suivants répondent, ce n’est pas une panne : le routeur est simplement configuré pour ne pas répondre ou pour limiter ses réponses ICMP. Si tout s’arrête à un saut et que plus rien ne répond ensuite, le problème se situe à cet endroit ou juste après.<br><br>Par défaut, traceroute sous Linux envoie des datagrammes UDP vers des ports élevés. Les pare-feux les bloquent souvent ; <code>traceroute -I</code> utilise ICMP comme Windows (tracert), et <code>traceroute -T -p 443</code> utilise des paquets TCP SYN vers le port 443, qui passent presque partout puisqu’ils ressemblent à une connexion web.<br><br><code>mtr</code> combine ping et traceroute : il interroge chaque saut en continu et affiche pour chacun le taux de perte et les temps. C’est l’outil de référence pour documenter un problème de qualité auprès d’un opérateur. Attention à l’interprétation : une perte affichée sur un saut intermédiaire mais pas sur les suivants vient d’un routeur qui limite ses réponses, pas d’une vraie perte. Seule compte la perte qui persiste jusqu’à la destination.', exemple: '$ traceroute -n 1.1.1.1\ntraceroute to 1.1.1.1, 30 hops max, 60 byte packets\n 1 192.168.10.1 0.512 ms 0.470 ms 0.455 ms\n 2 10.255.0.1 2.118 ms 2.097 ms 2.230 ms\n 3 * * *\n 4 62.34.2.57 9.874 ms 9.812 ms 9.930 ms\n 5 1.1.1.1 10.402 ms 10.377 ms 10.361 ms', attention: 'Un temps qui augmente brusquement à un saut puis reste élevé indique un lien lent ou long (câble sous-marin, par exemple). Un temps élevé sur un seul saut, suivi de temps normaux, n’indique rien : le routeur traite simplement ses réponses ICMP avec une faible priorité.' },
{ t: 'ss : qui écoute, qui est connecté', d: '', html: '<code>ss</code> (socket statistics) remplace <code>netstat</code>. Il liste les <b>sockets</b> ouvertes sur la machine, c’est-à-dire les points de communication entre un programme et le réseau. C’est l’outil qui répond à la question « le service écoute-t-il ? », la plus fréquente après « la machine est-elle joignable ? ».<br><br>La combinaison à connaître par cœur est <code>ss -tlnp</code> : <b>t</b> pour TCP, <b>l</b> pour les sockets en écoute (LISTEN), <b>n</b> pour afficher les numéros de port au lieu des noms, <b>p</b> pour afficher le processus propriétaire (nécessite sudo). Remplacez t par u pour UDP, ou mettez les deux.<br><br>La colonne <b>Local Address:Port</b> est décisive. <code>0.0.0.0:80</code> ou <code>*:80</code> signifie que le service écoute sur toutes les interfaces. <code>127.0.0.1:3306</code> signifie qu’il n’écoute que sur la boucle locale : il est invisible depuis le réseau, ce qui est voulu pour une base de données, mais une erreur classique pour un service censé être public.<br><br>Sans l’option l, ss montre les connexions établies et leurs <b>états TCP</b>. ESTAB : connexion établie. SYN-SENT : on a envoyé un SYN et on attend la réponse, souvent le signe d’un pare-feu qui jette les paquets. TIME-WAIT : connexion fermée récemment, normal en grand nombre sur un serveur web. CLOSE-WAIT en grand nombre : l’application ne referme pas ses connexions, c’est un bug applicatif. Des filtres permettent d’être précis : <code>ss -tn state established &#39;( dport = :443 )&#39;</code>.', exemple: '$ sudo ss -tlnp\nState Recv-Q Send-Q Local Address:Port Peer Address:Port Process\nLISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=611,fd=3))\nLISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("apache2",pid=842,fd=4))\nLISTEN 0 80 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=790,fd=21))\n\n$ ss -tn state established\nRecv-Q Send-Q Local Address:Port Peer Address:Port\n0 0 192.168.10.20:22 192.168.10.54:51522' },
{ t: 'ARP et ip neigh : le voisinage', d: '', html: 'Pour envoyer une trame Ethernet, il faut l’adresse MAC du destinataire direct, c’est-à-dire la machine elle-même si elle est sur le même réseau, sinon la passerelle. Le protocole <b>ARP</b> fait la correspondance : la machine diffuse à tout le réseau local « qui a 192.168.10.1 ? », la passerelle répond avec sa MAC, et la réponse est gardée en cache.<br><br><code>ip neigh</code> affiche ce cache avec l’état de chaque entrée. <b>REACHABLE</b> : le voisin a été confirmé récemment. <b>STALE</b> : l’entrée est ancienne mais encore utilisable, elle sera vérifiée au prochain envoi. <b>INCOMPLETE</b> : une requête ARP est partie, aucune réponse n’est encore arrivée. <b>FAILED</b> : le voisin n’a jamais répondu.<br><br>Une passerelle en FAILED explique à elle seule une panne complète : la machine a une adresse, une route, mais ne parvient même pas à parler à son routeur. Les causes sont souvent au niveau 2 : mauvais VLAN sur le port du switch, câble dans la mauvaise prise, passerelle éteinte.<br><br><code>arping</code> envoie des requêtes ARP directement et permet de détecter un <b>conflit d’adresse</b> : si deux MAC différentes répondent pour la même IP, deux machines ont été configurées avec la même adresse. Le même phénomène peut signaler une attaque par <b>usurpation ARP</b> (ARP spoofing), où un attaquant se fait passer pour la passerelle afin d’intercepter le trafic.', exemple: '$ ip neigh\n192.168.10.1 dev ens18 lladdr 3c:52:82:1a:0f:9b REACHABLE\n192.168.10.54 dev ens18 lladdr 00:15:5d:0a:11:03 STALE\n192.168.10.99 dev ens18 FAILED\n\n$ sudo arping -c 3 -I ens18 192.168.10.30\nUnicast reply from 192.168.10.30 [00:15:5D:0A:11:07] 0.645ms\nUnicast reply from 192.168.10.30 [08:00:27:3B:C4:12] 0.702ms\nUnicast reply from 192.168.10.30 [00:15:5D:0A:11:07] 0.610ms', attention: 'Deux adresses MAC différentes pour une même IP, comme ci-dessus : conflit d’adresse à traiter immédiatement. Les symptômes typiques sont des coupures aléatoires de quelques secondes pour les deux machines.' },
{ t: 'tcpdump et Wireshark : regarder le fil', d: '', html: 'Tous les outils précédents déduisent ce qui se passe. <b>tcpdump</b> montre ce qui passe réellement sur l’interface, paquet par paquet. C’est l’outil de dernier recours, et souvent celui qui tranche les débats entre équipes (« le pare-feu bloque » contre « l’application ne répond pas »).<br><br>tcpdump s’exécute avec sudo et accepte un <b>filtre BPF</b> qui limite la capture à ce qui nous intéresse : <code>host 192.168.10.54</code>, <code>port 443</code>, <code>tcp</code>, <code>icmp</code>, combinables avec and, or, not. Sans filtre, sur un serveur actif, l’écran défile trop vite pour être lu, et votre propre session SSH apparaît dans la capture ; on l’exclut avec <code>not port 22</code>. Les options essentielles : <code>-i ens18</code> choisit l’interface, <code>-n</code> évite la résolution des noms (plus rapide, plus lisible), <code>-c 20</code> s’arrête après 20 paquets, <code>-w capture.pcap</code> enregistre dans un fichier.<br><br>Chaque ligne TCP affiche des <b>drapeaux</b> entre crochets. [S] : SYN, demande d’ouverture. [S.] : SYN-ACK, le serveur accepte. [.] : simple ACK. [P.] : données poussées. [F.] : fin de connexion. [R] ou [R.] : RST, refus ou coupure brutale. Savoir lire ces drapeaux permet de distinguer en une seconde un port <b>fermé</b> (le serveur répond RST) d’un port <b>filtré</b> (aucune réponse, les SYN se répètent).<br><br><b>Wireshark</b> est l’interface graphique de référence pour analyser une capture. Sur un serveur sans écran, on capture avec <code>tcpdump -w</code>, on rapatrie le fichier .pcap avec scp, puis on l’ouvre dans Wireshark sur son poste. Wireshark décode chaque protocole couche par couche, reconstitue les échanges avec « Suivre le flux TCP » et permet des filtres d’affichage comme <code>tcp.flags.syn == 1 && tcp.flags.ack == 0</code> ou <code>http.response.code == 500</code>.', exemple: '$ sudo tcpdump -ni ens18 -c 6 "tcp port 80 and host 192.168.10.54"\n10:14:02.118 IP 192.168.10.54.51544 > 192.168.10.20.80: Flags [S], seq 3308715651, win 64240\n10:14:02.118 IP 192.168.10.20.80 > 192.168.10.54.51544: Flags [S.], seq 1920458830, ack 3308715652\n10:14:02.119 IP 192.168.10.54.51544 > 192.168.10.20.80: Flags [.], ack 1\n10:14:02.119 IP 192.168.10.54.51544 > 192.168.10.20.80: Flags [P.], length 78: HTTP: GET / HTTP/1.1\n10:14:02.121 IP 192.168.10.20.80 > 192.168.10.54.51544: Flags [P.], length 3460: HTTP: HTTP/1.1 200 OK\n10:14:02.122 IP 192.168.10.54.51544 > 192.168.10.20.80: Flags [.], ack 3461', attention: 'Capturer du trafic, c’est potentiellement lire des données personnelles et des mots de passe transmis en clair. Sur un réseau d’entreprise ou d’établissement, on ne capture qu’avec une autorisation et on supprime les fichiers après analyse. C’est une obligation liée au RGPD et à la charte informatique.' },
{ t: 'Les compléments indispensables : dig, curl, nc', d: '', html: 'Trois outils finissent le diagnostic au niveau applicatif. <b>dig</b> interroge le DNS et montre la réponse brute : <code>dig +short intranet.lycee.fr</code> donne l’adresse, <code>dig @1.1.1.1 intranet.lycee.fr</code> interroge un serveur précis pour comparer, et le champ <code>status: NXDOMAIN</code> indique que le nom n’existe pas. Beaucoup de « pannes réseau » sont en réalité des pannes DNS.<br><br><b>curl</b> joue le rôle du navigateur sans navigateur. <code>curl -I</code> affiche les en-têtes de réponse, <code>curl -v</code> détaille toute la négociation (résolution, connexion TCP, poignée de main TLS, requête, réponse), <code>curl --resolve site.fr:443:192.168.10.20 https://site.fr</code> teste un serveur précis en contournant le DNS.<br><br><b>nc</b> (netcat) teste l’ouverture d’un port TCP sans protocole applicatif : <code>nc -zv 192.168.10.20 3306</code> répond « succeeded » ou « Connection refused ». C’est le moyen le plus rapide de vérifier qu’un pare-feu laisse passer un flux entre deux serveurs.' }
],
synthese: ['On diagnostique de bas en haut et on note chaque résultat', 'ip link / ip addr / ip route : lien, adresse, masque, passerelle', 'ping mesure joignabilité, latence, pertes ; ICMP peut être filtré', 'traceroute et mtr localisent l’endroit du problème ; seule compte la perte qui persiste jusqu’à la destination', 'ss -tlnp : le service écoute-t-il, et sur quelle adresse ?', 'ip neigh : une passerelle en FAILED oriente vers la couche 2', 'tcpdump tranche : RST = port fermé, silence = port filtré', 'dig, curl, nc terminent le diagnostic applicatif'],
qcm: [
['ip link affiche ens18 en UP mais sans LOWER_UP. Que faut-il vérifier en priorité ?', ['Le DNS', 'Le câble et le port du switch', 'Le pare-feu', 'Le service web'], 1, 'L’interface est activée mais ne détecte pas de lien physique.'],
['Quelle commande indique la route et l’adresse source utilisées pour joindre 8.8.8.8, sans rien envoyer ?', ['ping 8.8.8.8', 'ip route get 8.8.8.8', 'traceroute 8.8.8.8', 'ss -r'], 1, 'ip route get interroge la table de routage.'],
['Un ping renvoie « 100 % packet loss » vers un serveur web qui affiche pourtant bien son site. Explication la plus probable ?', ['Le serveur est éteint', 'ICMP est filtré par un pare-feu', 'Le DNS est en panne', 'Le câble est coupé'], 1, 'Le site répond, donc la machine est joignable ; seul ICMP est bloqué.'],
['Sur quel mécanisme repose traceroute ?', ['Le numéro de séquence TCP', 'La décrémentation du TTL et les messages ICMP Time Exceeded', 'Les requêtes ARP', 'Le DNS inverse'], 1, 'Chaque routeur qui met le TTL à 0 se signale.'],
['Dans mtr, le saut 4 affiche 40 % de pertes, les sauts 5 à 9 et la destination 0 %. Conclusion ?', ['Le lien du saut 4 est défaillant', 'Le routeur 4 limite ses réponses ICMP, pas de vraie perte', 'La destination est saturée', 'Il faut changer d’opérateur'], 1, 'Une perte qui ne se propage pas n’est pas une vraie perte.'],
['ss -tlnp montre 127.0.0.1:8080 pour l’application. Les postes du réseau ne peuvent pas y accéder. Pourquoi ?', ['Le port 8080 est interdit', 'L’application n’écoute que sur la boucle locale', 'Il manque sudo', 'Le pare-feu est désactivé'], 1, 'Il faut la configurer pour écouter sur 0.0.0.0 ou l’IP du serveur.'],
['Que signifie l’état SYN-SENT persistant dans ss ?', ['Connexion établie', 'On attend une réponse au SYN, souvent un pare-feu qui jette les paquets', 'Connexion fermée proprement', 'Le serveur a refusé'], 1, 'Un refus donnerait un RST immédiat.'],
['ip neigh affiche la passerelle en FAILED. Où chercher ?', ['Au niveau DNS', 'Au niveau 2 : VLAN, câble, passerelle éteinte', 'Dans Apache', 'Dans le navigateur'], 1, 'La machine n’obtient même pas la MAC de son routeur.'],
['Dans tcpdump, un SYN reçoit immédiatement une réponse [R.]. Le port est :', ['filtré', 'fermé', 'ouvert', 'chiffré'], 1, 'La machine répond activement qu’il n’y a rien sur ce port.'],
['Pourquoi ajouter not port 22 à une capture tcpdump lancée en SSH ?', ['Pour aller plus vite', 'Pour ne pas capturer sa propre session', 'C’est obligatoire', 'Pour chiffrer la capture'], 1, 'Sinon chaque ligne affichée génère du trafic SSH capturé.'],
['Quel est le rôle de l’option -n de tcpdump et ss ?', ['Capturer moins', 'Afficher les adresses et ports en numérique, sans résolution de noms', 'Ignorer IPv6', 'Afficher en couleur'], 1, 'Plus rapide et plus lisible pour le diagnostic.'],
['Deux MAC différentes répondent à arping pour la même IP. C’est :', ['normal', 'un conflit d’adresse ou une usurpation ARP', 'un problème DNS', 'un effet du Wi-Fi'], 1, 'À traiter immédiatement.']
],
td: [
{ n: 'Échauffement', c: 'Pour chaque symptôme, indiquer la couche concernée et la première commande à lancer : (a) « la carte réseau n’a pas de voyant », (b) « je n’ai pas d’adresse IP », (c) « je ping la passerelle mais pas Internet », (d) « le site s’ouvre par son IP mais pas par son nom ».', r: '(a) couches 1-2 · ip link\n(b) couche 3 locale · ip addr puis journal DHCP\n(c) couche 3 au-delà de la passerelle · ip route puis traceroute\n(d) couche 7 · dig nom-du-site' },
{ n: 'Échauffement', c: 'Interpréter la ligne : « 64 bytes from 10.0.0.5: icmp_seq=7 ttl=121 time=38.2 ms ». Que peut-on dire du système distant et de la distance ?', r: 'TTL initial probablement 128 (Windows) : 128 − 121 = 7 routeurs traversés. RTT de 38 ms aller-retour.' },
{ n: 'Application', c: 'Un serveur répond au ping mais le site ne s’affiche pas. Écrire la suite de commandes, sur le serveur, qui permet de savoir si Apache tourne, sur quel port et sur quelle adresse il écoute, et s’il répond localement.', r: 'systemctl status apache2\nsudo ss -tlnp | grep -E ":80|:443"\ncurl -I http://127.0.0.1\n(puis, depuis un poste client : curl -I http://IP-du-serveur)' },
{ n: 'Application', c: 'Écrire la commande tcpdump qui capture, sur ens18, les 50 premiers paquets échangés avec 192.168.10.54 sur le port 443, sans résolution de noms, dans le fichier https.pcap.', r: 'sudo tcpdump -ni ens18 -c 50 -w https.pcap "host 192.168.10.54 and tcp port 443"' },
{ n: 'Application', c: 'À partir de la sortie mtr fournie par l’enseignant, déterminer si la perte vient du réseau local, de l’opérateur ou de la destination, et rédiger le message à envoyer au support de l’opérateur.', r: 'Repérer le premier saut où la perte apparaît ET persiste jusqu’à la destination. Si c’est un saut de l’opérateur, joindre la sortie mtr -rwc 100, l’heure, l’IP source et l’IP destination.' },
{ n: 'Approfondissement', c: 'Proposer une méthode, avec les commandes, pour distinguer un port fermé d’un port filtré par un pare-feu, depuis un client Linux.', r: 'nc -zv IP port : « refused » immédiat = fermé ; attente puis timeout = filtré.\nConfirmation : sudo tcpdump -ni ens18 host IP and port N → RST reçu (fermé) ou SYN répétés sans réponse (filtré).' },
{ n: 'Approfondissement', c: 'Un transfert de gros fichiers entre deux sites échoue alors que les petites requêtes passent. Formuler une hypothèse sur la MTU et la vérifier avec ping.', r: 'Hypothèse : un tunnel ou un lien réduit la MTU et les paquets ICMP « Fragmentation needed » sont filtrés.\nTest : ping -M do -s 1472 IP (1500 octets) puis réduire -s jusqu’à ce que ça passe, par exemple 1392 → MTU 1420.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Deux VM Debian reliées, pannes injectées par l’enseignant, diagnostic vérifié étape par étape.', etapes: ['Relever l’état réseau complet avec ip -br link, ip -br addr, ip route', 'Mesurer latence et pertes vers la passerelle et vers Internet', 'Localiser une panne avec traceroute -n puis mtr', 'Trouver pourquoi le service web est injoignable avec ss et curl', 'Capturer une connexion avec tcpdump et identifier SYN, SYN-ACK, ACK', 'Rédiger le compte rendu d’incident'] },
plusLoin: [['Documentation iproute2 (man ip, man ss)', 'https://man7.org/linux/man-pages/man8/ip.8.html'], ['Documentation officielle de tcpdump et des filtres pcap', 'https://www.tcpdump.org/manpages/pcap-filter.7.html'], ['Wireshark User’s Guide', 'https://www.wireshark.org/docs/wsug_html_chunked/'], ['RFC 792 · ICMP', 'https://www.rfc-editor.org/rfc/rfc792'], ['RFC 9293 · TCP', 'https://www.rfc-editor.org/rfc/rfc9293']]
});
window.CONTENUS_PLUS = window.CONTENUS_PLUS || {};
window.CONTENUS_PLUS.diagnostic = {
cas: { titre: 'Mise en situation · Le lundi où l’ENT n’a plus répondu', texte: 'Lundi, 8 h 05. Le service informatique du lycée reçoit une dizaine d’appels : l’intranet pédagogique ne s’affiche plus, ni en salle de classe ni en salle des professeurs. Le serveur est une VM Debian hébergée sur l’hyperviseur de l’établissement. Le technicien s’y connecte en SSH, suit sa méthode couche par couche, découvre que le service web a bien redémarré après la mise à jour du week-end mais qu’il n’écoute plus que sur la boucle locale à cause d’une directive Listen modifiée, corrige la configuration, vérifie depuis un poste client puis rédige un compte rendu d’incident pour l’équipe. Temps d’indisponibilité : 25 minutes, dont 5 de diagnostic effectif.' },
erreurs: ['Conclure qu’une machine est éteinte parce qu’elle ne répond pas au ping', 'Interpréter les étoiles d’un saut de traceroute comme une panne alors que les sauts suivants répondent', 'Lancer tcpdump sans filtre en SSH et capturer sa propre session', 'Oublier sudo avec ss -p : la colonne Process reste vide', 'Vérifier l’adresse IP sans vérifier le masque', 'Modifier la configuration avec ip addr add et oublier de la rendre permanente', 'Tout redémarrer avant d’avoir compris, et perdre les traces de la panne', 'Tester uniquement depuis le serveur lui-même (curl localhost) sans tester depuis un client'],
memo: [
['Commande', 'À quoi ça sert', 'Ce qu’on regarde'],
['ip -br link', 'état des interfaces', 'UP et LOWER_UP'],
['ip -br addr', 'adresses et masques', 'IP attendue, bon préfixe /24, /26…'],
['ip route', 'table de routage', 'ligne default via <passerelle>'],
['ip neigh', 'cache ARP', 'passerelle en REACHABLE, pas FAILED'],
['ping -c 4 <ip>', 'joignabilité, latence', '% packet loss, rtt avg et mdev'],
['traceroute -n <ip>', 'chemin saut par saut', 'dernier saut qui répond'],
['mtr -rwc 100 <ip>', 'pertes par saut dans la durée', 'perte qui persiste jusqu’à la destination'],
['sudo ss -tlnp', 'services en écoute', 'port attendu, adresse 0.0.0.0 ou 127.0.0.1'],
['sudo tcpdump -ni <if> <filtre>', 'trafic réel', 'drapeaux [S] [S.] [R.], absence de réponse'],
['dig +short <nom>', 'résolution DNS', 'adresse renvoyée, NXDOMAIN'],
['curl -Iv <url>', 'réponse applicative', 'connexion, TLS, code HTTP'],
['nc -zv <ip> <port>', 'test d’ouverture de port', 'succeeded, refused ou timeout']
],
resolu: { enonce: 'Le serveur web intranet (192.168.10.20) ne répond plus depuis les postes du lycée. Diagnostiquer et rétablir le service.', etapes: [
['1 · Qualifier le symptôme depuis un client', 'curl -I --max-time 5 http://192.168.10.20\ncurl: (7) Failed to connect to 192.168.10.20 port 80: Connection refused\n→ « Connection refused » est une information précieuse : la machine a répondu, et elle a refusé. Elle est donc allumée et joignable. Un timeout aurait orienté vers un problème réseau ou un pare-feu.'],
['2 · Confirmer la couche 3', 'ping -c 3 192.168.10.20\n3 packets transmitted, 3 received, 0% packet loss\n→ Réseau et routage fonctionnent. Inutile de regarder les câbles, les switchs ou le DHCP. On passe sur le serveur.'],
['3 · Vérifier l’état réseau du serveur', 'ssh admin@192.168.10.20\nip -br addr\nens18 UP 192.168.10.20/24\nip route | head -1\ndefault via 192.168.10.1 dev ens18\n→ Adresse, masque et passerelle corrects. Le problème est au-dessus de la couche 3.'],
['4 · Le service tourne-t-il et où écoute-t-il ?', 'systemctl is-active apache2\nactive\nsudo ss -tlnp | grep apache2\nLISTEN 0 511 127.0.0.1:80 0.0.0.0:* users:(("apache2",pid=1204,fd=4))\n→ Voilà la cause : Apache tourne, mais n’écoute que sur 127.0.0.1. Depuis le réseau, le port 80 est fermé, d’où le refus.'],
['5 · Trouver l’origine et corriger', 'grep -rn "^Listen" /etc/apache2/\n/etc/apache2/ports.conf:5:Listen 127.0.0.1:80\nsudo sed -i "s/^Listen 127.0.0.1:80/Listen 80/" /etc/apache2/ports.conf\nsudo apachectl configtest && sudo systemctl restart apache2\nsudo ss -tlnp | grep apache2\nLISTEN 0 511 *:80 *:*\n→ La directive avait été modifiée lors de la maintenance du week-end. On valide la syntaxe avant de redémarrer.'],
['6 · Vérifier depuis un client et documenter', 'curl -I http://192.168.10.20\nHTTP/1.1 200 OK\n→ Service rétabli. Compte rendu : heure de détection, symptôme, commandes et sorties clés, cause (directive Listen), correction, heure de rétablissement, action préventive (relecture des changements de configuration avant maintenance, supervision du port 80 depuis l’extérieur).']
] },
vf: [['Un ping sans réponse prouve que la machine est éteinte.', 0, 'ICMP est souvent filtré.'], ['Des étoiles au saut 3 de traceroute, puis des réponses aux sauts 4 et 5, ne sont pas une panne.', 1, 'Le routeur 3 ne répond simplement pas à ICMP.'], ['127.0.0.1:3306 dans ss signifie que MariaDB est accessible depuis le réseau.', 0, 'Seulement depuis la machine elle-même.'], ['Une réponse RST à un SYN signifie que le port est fermé.', 1, 'Un port filtré ne répond pas du tout.']],
examen: { titre: 'Sujet type · 20 points', q: ['Donner, dans l’ordre, les cinq vérifications d’une démarche de diagnostic par couches, avec une commande pour chacune. (5 pts)', 'Interpréter la sortie ping fournie : pertes, latence moyenne, gigue, et conclusion sur la qualité du lien. (3 pts)', 'Expliquer le fonctionnement de traceroute et interpréter une ligne « * * * ». (4 pts)', 'Un service écoute en 127.0.0.1:8080. Expliquer la conséquence et la correction. (3 pts)', 'Écrire la commande tcpdump qui capture les SYN vers le port 443 de 10.0.0.5, puis expliquer comment distinguer un port fermé d’un port filtré. (5 pts)'], r: ['ip link · ip addr / ip route · ping passerelle puis destination · ss -tlnp · curl / dig', 'Selon la sortie : % packet loss, avg, mdev', 'TTL croissant, ICMP Time Exceeded ; étoiles = pas de réponse du saut, pas forcément une panne', 'Injoignable depuis le réseau ; écouter sur 0.0.0.0:8080 ou l’IP du serveur', 'sudo tcpdump -ni ens18 "dst host 10.0.0.5 and tcp dst port 443 and tcp[tcpflags] & tcp-syn != 0" · RST = fermé, pas de réponse = filtré'] },
prof: { deroule: [['Ouverture', 'Scénario réel : « l’ENT ne répond plus », la classe propose des hypothèses, on les classe par couche'], ['Cours', 'ip, ping, traceroute, avec démonstration en direct sur une VM projetée'], ['Cours', 'ss, ARP, tcpdump ; lecture commentée d’une capture'], ['Pratique', 'Lab VM : pannes injectées (mauvais masque, passerelle absente, service sur 127.0.0.1, pare-feu)'], ['Clôture', 'Quiz flash, puis chaque binôme présente un diagnostic en 2 minutes']], diff: 'Élèves en difficulté : fiche mémo imprimée, un seul outil par panne, indices débloqués après 3 essais. Élèves avancés : analyse d’une capture dans Wireshark, diagnostic MTU, rédaction d’un compte rendu d’incident au format ITIL.', eval: 'Chaque panne du lab est validée automatiquement dans la VM. Le compte rendu d’incident est évalué sur la rigueur (commandes, sorties, cause, correction, prévention). Sujet type sur 20.' }
};
window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {};
window.CONTENUS_EXEMPLES.diagnostic = [
['Diagnostiquer un port bloqué avec ss', 'L’application métier est installée, mais aucun poste n’y accède sur le port 8080.', 'sudo ss -tlnp "sport = :8080"', 'State Recv-Q Send-Q Local Address:Port Peer Address:Port Process\nLISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("java",pid=2214,fd=38))', 'Le processus Java écoute bien, mais uniquement sur 127.0.0.1 : il est invisible depuis le réseau. Il faut changer l’adresse d’écoute dans sa configuration (server.address=0.0.0.0 pour une application Spring, par exemple), puis vérifier que le pare-feu autorise le port 8080. Si la ligne avait affiché 0.0.0.0:8080, on aurait orienté la recherche vers le pare-feu avec nc -zv depuis un client.'],
['Mesurer une perte de paquets', 'Les visioconférences de la salle B204 coupent régulièrement.', 'ping -c 100 -i 0.2 -q 192.168.20.1', '--- 192.168.20.1 ping statistics ---\n100 packets transmitted, 93 received, 7% packet loss, time 19821ms\nrtt min/avg/max/mdev = 0.9/14.6/212.4/31.8 ms', '7 % de pertes vers la passerelle locale, c’est énorme : sur un réseau filaire sain, on attend 0 %. La gigue (mdev) de 31,8 ms et un maximum à 212 ms confirment un lien instable. -q n’affiche que le résumé, -i 0.2 envoie 5 paquets par seconde. Suite du diagnostic : ip -s link pour chercher des erreurs sur l’interface, puis vérification du câble ou de la borne Wi-Fi.'],
['Lire une poignée de main TCP', 'Vérifier qu’une connexion au serveur de base de données s’établit.', 'sudo tcpdump -ni ens18 -c 3 "host 192.168.10.30 and tcp port 5432"', '09:41:12.004 IP 192.168.10.20.40818 > 192.168.10.30.5432: Flags [S], seq 17290431, win 64240, options [mss 1460]\n09:41:12.005 IP 192.168.10.30.5432 > 192.168.10.20.40818: Flags [S.], seq 91022876, ack 17290432, win 65160\n09:41:12.005 IP 192.168.10.20.40818 > 192.168.10.30.5432: Flags [.], ack 1, win 502', 'Les trois temps de l’ouverture TCP sont visibles : [S] le client demande, [S.] le serveur accepte en acquittant le numéro de séquence + 1, [.] le client confirme. La connexion est établie en 1 ms. Le port source 40818 est choisi au hasard par le client, le port 5432 est celui de PostgreSQL. Si le problème persiste, il est donc applicatif (authentification, base inexistante), pas réseau.'],
['Distinguer port fermé et port filtré', 'Un collègue affirme que « le pare-feu bloque le port 3306 ».', 'nc -zv -w 3 192.168.10.30 3306\nnc -zv -w 3 192.168.10.30 3307', 'nc: connect to 192.168.10.30 port 3306 (tcp) failed: Connection refused\nnc: connect to 192.168.10.30 port 3307 (tcp) timed out: Operation now in progress', 'Deux comportements différents. Sur 3306, « refused » immédiat : la machine a répondu par un RST, donc aucun pare-feu ne bloque, mais aucun service n’écoute ; il faut regarder MariaDB (arrêté ou en écoute sur 127.0.0.1). Sur 3307, « timed out » : aucun retour, les paquets sont jetés en route, c’est la signature d’un pare-feu. Deux commandes suffisent à trancher le débat.'],
['Localiser une panne avec traceroute', 'Le serveur de sauvegarde distant (site B) est injoignable depuis le site A.', 'traceroute -n -T -p 22 10.50.0.10', 'traceroute to 10.50.0.10, 30 hops max\n 1 192.168.10.1 0.48 ms 0.44 ms 0.41 ms\n 2 10.255.0.1 2.31 ms 2.28 ms 2.40 ms\n 3 10.254.8.2 6.12 ms 6.05 ms 6.20 ms\n 4 * * *\n 5 * * *\n 6 * * *', 'Les trois premiers sauts répondent : réseau local, routeur du site, puis entrée du lien inter-sites. Plus rien au-delà du saut 3, et la destination ne répond pas non plus : le problème se situe sur le lien entre le site A et le site B ou sur le routeur du site B. -T -p 22 envoie des SYN TCP vers le port SSH, qui passent les pare-feux mieux que l’UDP par défaut. On transmet cette sortie à l’équipe réseau ou à l’opérateur.'],
['Repérer un problème de voisinage ARP', 'Un serveur fraîchement déplacé dans une autre baie n’a plus aucun accès réseau.', 'ip neigh show dev ens18\nping -c 2 192.168.30.1', '192.168.30.1 dev ens18 FAILED\nFrom 192.168.30.15 icmp_seq=1 Destination Host Unreachable\nFrom 192.168.30.15 icmp_seq=2 Destination Host Unreachable', 'La passerelle est en FAILED : aucune réponse ARP. Le message « Destination Host Unreachable » émis par la machine elle-même (From 192.168.30.15) confirme qu’elle n’a jamais obtenu la MAC du routeur. Adresse et route peuvent être parfaites, rien ne sortira. La cause est presque toujours au niveau 2 : port du switch dans le mauvais VLAN après le déplacement, ou câble branché sur la mauvaise prise. Vérification côté switch : show mac address-table interface <port>.']
];
window.CONTENUS_LONG = window.CONTENUS_LONG || {};
var K = 'stroke="#000" stroke-width="3"', F = 'font-family="Archivo,sans-serif" font-weight="800"';
var bx = function (x, y, w, h, c, t) { return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" fill="' + c + '" ' + K + '/><text x="' + (x + w / 2) + '" y="' + (y + h / 2 + 5) + '" text-anchor="middle" ' + F + ' font-size="15">' + t + '</text>'; };
window.CONTENUS_LONG.diagnostic = {
figure: '<svg viewBox="0 0 720 300" class="h-auto w-full" role="img">' + [['7 · Application', 'curl · dig', '#FF6FB5'], ['4 · Transport', 'ss · nc', '#C4B5FD'], ['3 · Réseau', 'ping · traceroute · mtr · ip route', '#7CF2C0'], ['2 · Liaison', 'ip neigh · arping', '#FFD23F'], ['1 · Physique', 'ip link · ip -s link', '#F6F5F1']].map(function (r, i) { return bx(20, 10 + i * 56, 220, 48, r[2], r[0]) + bx(260, 10 + i * 56, 440, 48, '#fff', r[1]); }).join('') + '</svg>',
legende: 'Chaque outil répond à une couche. On remonte de bas en haut, tcpdump observe toutes les couches à la fois.',
photo: 'Photo : baie serveur avec switchs et câbles RJ45 étiquetés',
lecture: [
'Sur un poste de travail, quand Internet ne fonctionne plus, on regarde l’icône réseau, on ouvre les paramètres, on clique sur « Résoudre les problèmes ». Sur un serveur, rien de tout cela n’existe. Les machines qui font tourner les ENT, les sites des établissements, les bases de données de vie scolaire ou les services des hébergeurs français sont installées sans interface graphique, pour des raisons de sécurité, de stabilité et de performances : chaque logiciel installé en plus est une faille potentielle et consomme de la mémoire. L’administrateur s’y connecte en SSH, et tout se fait au clavier.',
'Ces outils en ligne de commande ont un autre avantage décisif : leur sortie est du texte. On peut la copier dans un ticket, l’envoyer à un collègue, la comparer avec celle de la veille, la filtrer avec grep ou l’intégrer dans un script de supervision qui alerte automatiquement quand une valeur dérape. Une capture d’écran d’une fenêtre de paramètres ne se prête à rien de tout cela. Dans une équipe d’exploitation, on attend d’un technicien qu’il fournisse les commandes et les sorties qui prouvent son diagnostic, pas une impression.',
'Ils ont aussi l’avantage d’être partout. ip, ping, ss et tcpdump sont présents ou installables en une commande sur toutes les distributions Linux, dans les conteneurs, sur les routeurs et les pare-feux basés sur Linux, et leurs équivalents existent sur Windows Server et sur les équipements réseau. Ce que vous apprenez ici se transpose directement sur un switch Cisco, un pare-feu pfSense ou une instance cloud chez Scaleway ou OVHcloud.',
'La compétence visée n’est pas de connaître toutes les options, elles sont dans le manuel. C’est de savoir quel outil lancer pour quelle question, et de savoir lire ce qu’il répond, y compris ce qu’il ne dit pas. Un ping sans réponse ne dit pas qu’une machine est éteinte. Des étoiles dans un traceroute ne disent pas qu’un lien est coupé. Un service « active » dans systemctl ne dit pas qu’il est joignable. Savoir faire ces nuances, c’est ce qui distingue un technicien qui cherche au hasard d’un technicien qui résout un incident en cinq minutes.'
]
};
window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {};
window.CONTENUS_SCHEMAS.diagnostic = {
termes: ['ip link', 'ip addr', 'ip route', 'ip route get', 'ip neigh', 'ip -s link', 'ip -br', 'ping', 'traceroute', 'mtr', 'ss -tlnp', 'ss', 'netstat', 'ifconfig', 'arping', 'tcpdump', 'Wireshark', 'dig', 'curl', 'nc -zv', 'ICMP', 'TTL', 'RTT', 'ARP', 'SYN', 'SYN-ACK', 'RST', 'ACK', 'LISTEN', 'ESTAB', 'SYN-SENT', 'TIME-WAIT', 'CLOSE-WAIT', 'REACHABLE', 'STALE', 'FAILED', 'INCOMPLETE', 'LOWER_UP', 'MTU', '127.0.0.1', '0.0.0.0', 'BPF', 'Time Exceeded', 'Echo Request', 'Echo Reply'],
figs: [
[3, 'traceroute : le TTL augmente d’un cran à chaque essai', '<svg viewBox="0 0 720 230" class="h-auto w-full" role="img">' + bx(10, 90, 110, 48, '#FFD23F', 'Poste') + [1, 2, 3].map(function (n, i) { return bx(160 + i * 150, 90, 110, 48, '#F6F5F1', 'Routeur ' + n) + '<text x="' + (215 + i * 150) + '" y="' + (40 + i * 0) + '" text-anchor="middle" ' + F + ' font-size="14">TTL=' + n + '</text><text x="' + (215 + i * 150) + '" y="180" text-anchor="middle" ' + F + ' font-size="13">Time Exceeded</text>'; }).join('') + bx(610, 90, 100, 48, '#7CF2C0', 'Cible') + '<text x="660" y="180" text-anchor="middle" ' + F + ' font-size="13">réponse</text></svg>'],
[4, 'Lire la colonne Local Address de ss', '<svg viewBox="0 0 720 150" class="h-auto w-full" role="img">' + bx(20, 20, 220, 50, '#7CF2C0', '0.0.0.0:80') + '<text x="20" y="100" ' + F + ' font-size="14">toutes les interfaces : joignable</text>' + bx(260, 20, 220, 50, '#FFD23F', '192.168.10.20:80') + '<text x="260" y="100" ' + F + ' font-size="14">une seule interface</text>' + bx(500, 20, 200, 50, '#FF6FB5', '127.0.0.1:80') + '<text x="500" y="100" ' + F + ' font-size="14">boucle locale : invisible</text></svg>'],
[6, 'Port fermé ou port filtré, vu par tcpdump', '<svg viewBox="0 0 720 170" class="h-auto w-full" role="img">' + bx(20, 20, 330, 44, '#F6F5F1', 'Fermé : [S] → [R.] immédiat') + bx(20, 80, 330, 44, '#FF6FB5', 'Filtré : [S] … [S] … [S] sans réponse') + bx(380, 20, 320, 44, '#7CF2C0', 'Ouvert : [S] → [S.] → [.]') + '<text x="380" y="110" ' + F + ' font-size="14">nc -zv : succeeded / refused / timed out</text></svg>']
]
};
})();

View file

@ -0,0 +1,172 @@
// Cours complets · Infra lot 3 : Kubernetes, supervision, annuaire LDAP / Active Directory
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'k8s', match: /Kubernetes|k8s|orchestr/i,
titre: 'Kubernetes : orchestrer des conteneurs en production', duree: '',
niveau: 'BUT R&T DevCloud · BUT Informatique · AIS · Master',
prerequis: ['Conteneurs Docker', 'Docker Compose', 'Réseau : DNS, NAT, équilibrage'],
competences: ['BUT R&T DevCloud · Déployer et exploiter une plateforme de conteneurs', 'AIS · Administrer une infrastructure cloud', 'Master · Architecture des systèmes distribués'],
intro: 'Compose pilote une machine. Quand l’application doit tenir la charge de milliers d’utilisateurs, survivre à la panne d’un serveur et se mettre à jour sans coupure, il faut un orchestrateur. Kubernetes répartit les conteneurs sur un groupe de machines, les relance quand ils tombent, les déplace quand un nœud disparaît et ajuste leur nombre à la demande. Toutes les offres cloud françaises (Scaleway Kapsule, OVHcloud Managed Kubernetes, Outscale) le proposent. Ce module présente son architecture, ses objets principaux et le diagnostic d’un déploiement en panne.',
cm: [
{ t: 'Architecture d’un cluster', d: '', html: 'Un cluster se compose d’un <b>plan de contrôle</b> et de <b>nœuds de travail</b>. Le plan de contrôle comprend l’<b>API server</b>, point d’entrée unique de toutes les commandes ; <b>etcd</b>, base clé-valeur qui stocke l’état du cluster ; le <b>scheduler</b>, qui choisit le nœud de chaque nouveau pod ; et le <b>controller manager</b>, qui fait converger l’état réel vers l’état désiré. Chaque nœud exécute <b>kubelet</b>, qui démarre les conteneurs via un moteur (containerd), et <b>kube-proxy</b> ou un équivalent qui programme le réseau des services.<br><br>Le principe central est la <b>boucle de réconciliation</b> : on déclare un état (« 3 répliques de cette image ») et des contrôleurs corrigent sans cesse tout écart. On ne dit jamais « démarre un conteneur », on décrit ce qui doit exister.' },
{ t: 'Pods, Deployments et ReplicaSets', d: '', html: 'Le <b>pod</b> est la plus petite unité : un ou plusieurs conteneurs qui partagent une adresse IP et des volumes. On ne crée presque jamais un pod directement : un <b>Deployment</b> décrit l’image, le nombre de répliques et la stratégie de mise à jour, et crée un <b>ReplicaSet</b> qui maintient ce nombre de pods. Une mise à jour d’image crée un nouveau ReplicaSet et fait basculer progressivement les pods (rolling update) ; <code>kubectl rollout undo</code> revient à la version précédente.', exemple: 'apiVersion: apps/v1\nkind: Deployment\nmetadata: { name: resa, namespace: vie-scolaire }\nspec:\n replicas: 3\n selector: { matchLabels: { app: resa } }\n template:\n metadata: { labels: { app: resa } }\n spec:\n containers:\n - name: app\n image: registre.lycee.local/resa:9c1e4f2\n ports: [{ containerPort: 8000 }]\n resources:\n requests: { cpu: 100m, memory: 128Mi }\n limits: { memory: 256Mi }\n readinessProbe: { httpGet: { path: /health, port: 8000 } }\n livenessProbe: { httpGet: { path: /health, port: 8000 }, initialDelaySeconds: 10 }' },
{ t: 'Services et Ingress', d: '', html: 'Les pods sont éphémères et changent d’adresse. Un <b>Service</b> fournit une adresse stable et un nom DNS (resa.vie-scolaire.svc.cluster.local) et répartit le trafic entre les pods dont les <b>labels</b> correspondent à son sélecteur. Le type <b>ClusterIP</b> est interne, <b>NodePort</b> ouvre un port sur chaque nœud, <b>LoadBalancer</b> demande un équilibreur au fournisseur cloud.<br><br>Pour le trafic HTTP, un <b>Ingress</b> route selon le nom d’hôte et le chemin vers les services, et termine TLS. Il nécessite un contrôleur d’Ingress (ingress-nginx, Traefik) ; cert-manager y ajoute des certificats Let’s Encrypt automatiques.', attention: 'Un Service qui ne sélectionne aucun pod (faute de frappe dans un label) ne renvoie aucune erreur à sa création : il n’a simplement aucun endpoint. kubectl get endpoints le révèle immédiatement.' },
{ t: 'Configuration, secrets et stockage', d: '', html: 'Les <b>ConfigMap</b> portent la configuration non sensible, les <b>Secret</b> les mots de passe et clés ; ils s’injectent en variables ou en fichiers. Les Secret ne sont qu’encodés en base64 par défaut : on active le chiffrement d’etcd et on limite leur lecture par RBAC.<br><br>Le stockage persistant passe par des <b>PersistentVolumeClaim</b> : le pod demande « 10 Gi en lecture-écriture », et une <b>StorageClass</b> provisionne le volume chez le fournisseur. Pour une base de données, on utilise un <b>StatefulSet</b>, qui donne à chaque réplique un nom et un volume stables.' },
{ t: 'Sondes, ressources et haute disponibilité', d: '', html: 'La <b>readinessProbe</b> indique si un pod peut recevoir du trafic : tant qu’elle échoue, le Service ne lui envoie rien. La <b>livenessProbe</b> indique s’il est vivant : en cas d’échec répété, kubelet redémarre le conteneur. Confondre les deux provoque des redémarrages en boucle au démarrage d’une application lente.<br><br>Les <b>requests</b> servent au scheduler pour placer les pods ; les <b>limits</b> plafonnent la consommation. Un conteneur qui dépasse sa limite mémoire est tué (OOMKilled). Le <b>HorizontalPodAutoscaler</b> ajuste le nombre de répliques selon la charge, et un <b>PodDisruptionBudget</b> garantit un nombre minimal de pods pendant une maintenance.' },
{ t: 'Accès et sécurité', d: '', html: 'L’accès à l’API passe par des certificats ou des jetons, et les droits par <b>RBAC</b> : Role et ClusterRole définissent des verbes sur des ressources, RoleBinding les attribue. Les <b>namespaces</b> séparent les équipes ou les environnements. Les <b>NetworkPolicy</b> filtrent le trafic entre pods : sans elles, tout pod peut joindre tout autre pod. Les Pod Security Standards interdisent les conteneurs privilégiés ou en root.' }
],
synthese: ['Plan de contrôle (API server, etcd, scheduler, contrôleurs) et nœuds (kubelet)', 'État désiré et boucle de réconciliation', 'Deployment → ReplicaSet → pods ; rollout et rollout undo', 'Service stable par labels, Ingress pour HTTP et TLS', 'ConfigMap, Secret, PVC et StatefulSet', 'readiness ≠ liveness ; requests et limits', 'RBAC, namespaces, NetworkPolicy'],
qcm: [
['Quel composant stocke l’état du cluster ?', ['kubelet', 'etcd', 'kube-proxy', 'Ingress'], 1, 'Base clé-valeur répliquée.'],
['Que se passe-t-il si l’on supprime un pod géré par un Deployment ?', ['Il disparaît définitivement', 'Le ReplicaSet en recrée un', 'Le Deployment est supprimé', 'Le cluster s’arrête'], 1, 'Réconciliation.'],
['Comment un Service trouve-t-il ses pods ?', ['Par leur IP', 'Par sélecteur de labels', 'Par leur nom', 'Par le nœud'], 1, 'Les endpoints suivent les labels.'],
['Une readinessProbe qui échoue provoque :', ['un redémarrage', 'le retrait du pod du Service', 'la suppression du nœud', 'rien'], 1, 'La liveness, elle, redémarre.'],
['Que signifie OOMKilled ?', ['Réseau coupé', 'Dépassement de la limite mémoire', 'Image absente', 'Droits insuffisants'], 1, 'Le noyau tue le conteneur.'],
['Quel objet pour une base de données répliquée ?', ['Deployment', 'StatefulSet', 'DaemonSet', 'Job'], 1, 'Identités et volumes stables.'],
['Sans NetworkPolicy, les pods :', ['sont isolés', 'peuvent tous se joindre', 'n’ont pas de réseau', 'ne sortent pas'], 1, 'Tout est ouvert par défaut.']
],
td: [
{ n: 'Application', c: 'Écrire le Service ClusterIP et l’Ingress qui publient le Deployment resa sur resa.lycee.fr en HTTPS.', r: 'Service : selector app: resa, port 80 → targetPort 8000.\nIngress : host resa.lycee.fr, path /, backend service resa port 80, tls secretName resa-tls, annotation cert-manager.io/cluster-issuer.' },
{ n: 'Application', c: 'Mettre à jour l’image vers le tag 51ab0d7, suivre le déploiement, puis revenir en arrière.', r: 'kubectl -n vie-scolaire set image deploy/resa app=registre.lycee.local/resa:51ab0d7\nkubectl -n vie-scolaire rollout status deploy/resa\nkubectl -n vie-scolaire rollout undo deploy/resa' },
{ n: 'Approfondissement', c: 'Écrire une NetworkPolicy qui n’autorise vers la base PostgreSQL que les pods labellisés app: resa.', r: 'podSelector: { matchLabels: { app: postgres } }, policyTypes: [Ingress], ingress: [{ from: [{ podSelector: { matchLabels: { app: resa } } }], ports: [{ port: 5432 }] }]' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un cluster k3s à trois nœuds.', etapes: ['Déployer l’application en 3 répliques', 'La publier par Service et Ingress', 'Tuer un pod et observer la réconciliation', 'Mettre à jour puis revenir en arrière', 'Isoler la base par NetworkPolicy'] },
plusLoin: [['Documentation Kubernetes (fr)', 'https://kubernetes.io/fr/docs/home/'], ['k3s', 'https://k3s.io/'], ['ANSSI · Recommandations de sécurité relatives au déploiement de conteneurs', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-au-deploiement-de-conteneurs-docker']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Après la mise à jour de l’application de réservation, le site renvoie « 503 Service Temporarily Unavailable ». Diagnostiquer et rétablir.', etapes: [
['Regarder les pods', 'kubectl -n vie-scolaire get pods -l app=resa\nNAME READY STATUS RESTARTS\nresa-7d9f6c5b8-2xkq4 0/1 CrashLoopBackOff 6\nresa-7d9f6c5b8-h7m2p 0/1 CrashLoopBackOff 6\n→ Aucun pod n’est prêt : l’Ingress n’a aucun backend, d’où les 503.'],
['Lire les événements et les journaux', 'kubectl -n vie-scolaire describe pod resa-7d9f6c5b8-2xkq4 | tail -3\nWarning Unhealthy Liveness probe failed: Get "http://10.42.1.17:8000/health": connection refused\nWarning BackOff Back-off restarting failed container\nkubectl -n vie-scolaire logs resa-7d9f6c5b8-2xkq4 --previous | tail -1\nRunning database migrations… (this may take 40s)\n→ La nouvelle version exécute des migrations au démarrage ; la livenessProbe démarre à 10 s et tue le conteneur avant la fin.'],
['Rétablir immédiatement', 'kubectl -n vie-scolaire rollout undo deploy/resa\nkubectl -n vie-scolaire rollout status deploy/resa\ndeployment "resa" successfully rolled out\n→ L’ancienne version est revenue ; le site répond.'],
['Corriger la cause', 'Migrations déplacées dans un Job exécuté avant le déploiement, et ajout d’une startupProbe :\nstartupProbe: { httpGet: { path: /health, port: 8000 }, failureThreshold: 30, periodSeconds: 5 }\n→ La liveness n’est évaluée qu’une fois la startupProbe réussie.'],
['Redéployer et documenter', 'kubectl apply -f resa.yaml ; kubectl -n vie-scolaire get pods -l app=resa\n3/3 Running\n→ Compte rendu : cause (liveness trop agressive face à des migrations), impact, rollback, correction, et test en préproduction avec une base de taille réelle.']
] },
erreurs: ['Confondre readiness et liveness', 'Pas de requests ni de limits', 'Utiliser latest comme image', 'Laisser les Secret non chiffrés et lisibles par tous', 'Aucune NetworkPolicy', 'Faute de label entre Service et pods', 'Base de données en Deployment sans volume persistant'],
memo: [['Commande', 'Usage'], ['kubectl get pods -A -o wide', 'tous les pods et leur nœud'], ['kubectl describe pod <p>', 'événements, sondes, raisons d’échec'], ['kubectl logs <p> --previous', 'journaux du conteneur précédent'], ['kubectl get events --sort-by=.lastTimestamp', 'derniers événements'], ['kubectl get endpoints <svc>', 'pods réellement derrière un Service'], ['kubectl rollout status|undo deploy/<d>', 'suivre ou annuler un déploiement'], ['kubectl exec -it <p> -- sh', 'shell dans un pod'], ['kubectl top pods', 'consommation CPU et mémoire'], ['kubectl apply -f fichier.yaml', 'appliquer un état désiré'], ['kubectl auth can-i <verbe> <ressource>', 'tester des droits RBAC']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire l’architecture d’un cluster. (4 pts)', 'Écrire Deployment, Service et Ingress pour une application donnée. (8 pts)', 'Expliquer readiness, liveness et startup probes. (3 pts)', 'Diagnostiquer un CrashLoopBackOff à partir de sorties fournies. (3 pts)', 'Citer deux mesures de sécurité. (2 pts)'], r: ['API server, etcd, scheduler, contrôleurs, kubelet', 'voir cours', 'trafic, redémarrage, démarrage lent', 'describe et logs --previous', 'RBAC, NetworkPolicy'] },
prof: { deroule: [['Ouverture', 'Arrêter un serveur Compose en direct, puis un nœud Kubernetes'], ['Cours', 'Architecture et réconciliation'], ['Pratique', 'Lab k3s'], ['Incident', 'CrashLoopBackOff'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : manifestes à compléter. Élèves avancés : Helm, HPA, GitOps avec Argo CD.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Voir où tournent les pods', 'Un nœud va être mis en maintenance.', 'kubectl get pods -n vie-scolaire -o wide', 'NAME READY STATUS NODE\nresa-6b8c7d-4kxz2 1/1 Running noeud-1\nresa-6b8c7d-9mpq7 1/1 Running noeud-1\nresa-6b8c7d-tt2r8 1/1 Running noeud-2', 'Deux répliques sont sur noeud-1 : drainer ce nœud en coupe deux sur trois. Un PodDisruptionBudget minAvailable: 2 et une règle d’anti-affinité répartiraient mieux la charge.'],
['Service sans endpoints', 'Le site renvoie 503 alors que les pods tournent.', 'kubectl -n vie-scolaire get endpoints resa', 'NAME ENDPOINTS AGE\nresa <none> 3d', 'Aucun pod ne correspond au sélecteur du Service. On compare kubectl get svc resa -o yaml (selector app: resa) avec les labels des pods (app: reservation) : faute de nommage.'],
['Conteneur tué pour mémoire', 'Redémarrages fréquents.', 'kubectl -n vie-scolaire describe pod resa-6b8c7d-4kxz2 | grep -A2 "Last State"', 'Last State: Terminated\n Reason: OOMKilled\n Exit Code: 137', 'La limite de 256 Mi est trop basse pour la charge actuelle. On mesure avec kubectl top pods puis on ajuste requests et limits.'],
['Suivre une mise à jour', 'Déploiement d’une nouvelle version.', 'kubectl -n vie-scolaire rollout status deploy/resa', 'Waiting for deployment "resa" rollout to finish: 1 of 3 updated replicas are available...\ndeployment "resa" successfully rolled out', 'Les pods sont remplacés un à un ; le service reste disponible pendant la mise à jour.'],
['Tester un droit RBAC', 'Un enseignant doit pouvoir lire les journaux de son namespace.', 'kubectl auth can-i get pods/log -n vie-scolaire --as prof-sio', 'yes', 'La commande vérifie les droits effectifs sans les tester réellement.'],
['Lire les derniers événements', 'Pod en Pending.', 'kubectl -n vie-scolaire get events --sort-by=.lastTimestamp | tail -2', 'Warning FailedScheduling pod/db-0 0/3 nodes are available: 3 Insufficient memory.', 'Aucun nœud n’a assez de mémoire libre pour la request demandée : réduire la request ou ajouter un nœud.']
],
termes: ['Kubernetes', 'cluster', 'pod', 'Deployment', 'ReplicaSet', 'StatefulSet', 'Service', 'ClusterIP', 'Ingress', 'ConfigMap', 'Secret', 'PersistentVolumeClaim', 'namespace', 'etcd', 'kubelet', 'API server', 'scheduler', 'readinessProbe', 'livenessProbe', 'startupProbe', 'requests', 'limits', 'OOMKilled', 'CrashLoopBackOff', 'RBAC', 'NetworkPolicy', 'kubectl', 'rollout', 'k3s']
});
reg({
cours: { id: 'supervision', match: /supervision|Zabbix|Prometheus|Grafana|monitoring|Nagios/i,
titre: 'Supervision : mesurer, alerter et anticiper avec Prometheus et Zabbix', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR / AIS',
prerequis: ['Diagnostic réseau en ligne de commande', 'Services systemd'],
competences: ['BTS SIO SISR · Bloc 2 · Superviser une infrastructure', 'BTS CIEL · C09 · Maintenir', 'TSSR · Superviser l’infrastructure'],
intro: 'Un service informatique qui apprend une panne par un appel d’utilisateur a déjà perdu. La supervision mesure en permanence l’état des serveurs, des réseaux et des applications, déclenche une alerte avant que les utilisateurs ne soient gênés et conserve l’historique qui permet de comprendre un incident ou d’anticiper une saturation. Ce module présente les notions (métriques, seuils, alertes), puis les deux outils les plus répandus dans les établissements et les entreprises : Zabbix et le couple Prometheus-Grafana.',
cm: [
{ t: 'Que superviser ?', d: '', html: 'On distingue la <b>disponibilité</b> (le service répond-il ?), la <b>performance</b> (en combien de temps ?), la <b>capacité</b> (reste-t-il de la place, de la mémoire, de la bande passante ?) et les <b>erreurs</b> (taux de réponses 5xx, échecs de sauvegarde). Pour une application, la méthode des « quatre signaux d’or » résume l’essentiel : latence, trafic, erreurs, saturation.<br><br>On supervise à plusieurs niveaux : matériel (disques, température), système (CPU, mémoire, charge, espace disque), réseau (interfaces, pertes), services (ports, processus), et surtout <b>expérience utilisateur</b> : une sonde qui se connecte réellement à l’ENT et vérifie que la page de connexion s’affiche en moins de deux secondes.' },
{ t: 'Collecte : agent, SNMP, export', d: '', html: 'Trois modes de collecte coexistent. Un <b>agent</b> installé sur la machine (Zabbix agent, node_exporter) remonte des métriques système détaillées. <b>SNMP</b> interroge les équipements réseau qui ne peuvent pas recevoir d’agent : switchs, bornes, onduleurs, imprimantes ; on lit des valeurs identifiées par des OID dans des MIB. Les applications modernes <b>exposent</b> elles-mêmes leurs métriques sur une page HTTP (/metrics).<br><br>SNMPv1 et v2c transmettent la communauté (le « mot de passe ») en clair ; on utilise <b>SNMPv3</b>, avec authentification et chiffrement, ou à défaut on limite v2c en lecture seule à l’adresse du serveur de supervision.' },
{ t: 'Prometheus et Grafana', d: '', html: '<b>Prometheus</b> vient <b>chercher</b> (pull) les métriques des cibles à intervalle régulier et les stocke en séries temporelles, chaque série étant identifiée par un nom et des <b>labels</b> (instance, job, mountpoint). Le langage <b>PromQL</b> interroge ces séries : taux, moyennes, prédictions. Les <b>règles d’alerte</b> sont évaluées par Prometheus et envoyées à <b>Alertmanager</b>, qui regroupe, déduplique et notifie par courriel, messagerie ou SMS. <b>Grafana</b> affiche les tableaux de bord.', exemple: '# Espace disque libre en %\n100 * node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}\n\n# Règle : disque plein dans moins de 24 h au rythme actuel\n- alert: DisquePleinSous24h\n expr: predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) < 0\n for: 30m\n labels: { severity: warning }\n annotations: { summary: "{{ $labels.instance }} : disque plein sous 24 h" }' },
{ t: 'Zabbix', d: '', html: '<b>Zabbix</b> est une solution intégrée très répandue dans les collectivités et les établissements : serveur, base de données, interface web et agents. On y définit des <b>hôtes</b>, auxquels on applique des <b>modèles</b> (templates) qui regroupent <b>éléments</b> (items, les valeurs collectées), <b>déclencheurs</b> (triggers, les conditions d’alerte) et graphiques. Les modèles fournis couvrent Linux, Windows, les principaux switchs et les services courants. La découverte automatique (LLD) crée les éléments pour chaque disque ou chaque interface détectée.' },
{ t: 'Bien alerter', d: '', html: 'Une alerte doit être <b>actionnable</b> : si personne ne doit rien faire en la recevant, ce n’est pas une alerte mais une information pour un tableau de bord. Trop d’alertes produisent la <b>fatigue d’alerte</b> : l’équipe ne les lit plus, et la vraie panne passe inaperçue.<br><br>Les bonnes pratiques : alerter sur les symptômes vus par l’utilisateur plutôt que sur chaque cause possible ; ajouter une durée (for: 5m) pour ignorer les pics d’une seconde ; utiliser des seuils prédictifs (plein dans 24 h) plutôt que fixes ; hiérarchiser (info, avertissement, critique) ; et regrouper les alertes dépendantes : si le routeur d’un site tombe, on veut une alerte, pas cinquante.' },
{ t: 'Journaux centralisés', d: '', html: 'Les métriques disent qu’il se passe quelque chose ; les <b>journaux</b> disent quoi. On les centralise pour les consulter en un seul endroit et les conserver même si un serveur est compromis : rsyslog ou journald vers un serveur central, ou des piles comme Loki avec Grafana, ou Elasticsearch. La centralisation est aussi une exigence de sécurité : l’ANSSI recommande de conserver et d’horodater les journaux d’authentification et d’administration.' }
],
synthese: ['Disponibilité, performance, capacité, erreurs ; latence, trafic, erreurs, saturation', 'Agent, SNMP (v3), métriques exposées', 'Prometheus : pull, séries à labels, PromQL, Alertmanager ; Grafana pour l’affichage', 'Zabbix : hôtes, modèles, éléments, déclencheurs', 'Alertes actionnables, durée, seuils prédictifs, dépendances', 'Journaux centralisés et conservés'],
qcm: [
['Quel protocole pour superviser un switch sans agent ?', ['SSH', 'SNMP', 'SMTP', 'DHCP'], 1, 'Lecture d’OID.'],
['Pourquoi préférer SNMPv3 ?', ['Plus rapide', 'Authentification et chiffrement', 'Plus de métriques', 'Obligatoire'], 1, 'v2c envoie la communauté en clair.'],
['Prometheus collecte les métriques en mode :', ['push', 'pull', 'SNMP trap', 'courriel'], 1, 'Il interroge les cibles.'],
['Que fait for: 5m dans une règle d’alerte ?', ['Répète l’alerte', 'N’alerte que si la condition dure 5 minutes', 'Supprime l’alerte', 'Envoie 5 messages'], 1, 'Ignore les pics.'],
['Qu’est-ce qu’une alerte actionnable ?', ['Une alerte rouge', 'Une alerte qui demande une action précise', 'Une alerte fréquente', 'Une alerte par SMS'], 1, 'Sinon c’est du bruit.'],
['Dans Zabbix, un déclencheur est :', ['une valeur collectée', 'une condition qui génère un problème', 'un graphique', 'un agent'], 1, 'Trigger.']
],
td: [
{ n: 'Application', c: 'Lister ce qu’il faut superviser pour l’ENT d’un lycée, du matériel à l’expérience utilisateur, avec un seuil pour chaque élément.', r: 'Disque < 15 % · mémoire > 90 % pendant 10 min · charge > nb de cœurs · port 443 fermé · certificat expirant < 14 j · page de connexion > 2 s · taux de 5xx > 2 % · sauvegarde de la nuit en échec.' },
{ n: 'Application', c: 'Écrire la requête PromQL du taux d’erreurs HTTP 5xx sur 5 minutes, et la règle d’alerte associée.', r: 'sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.02 avec for: 10m.' },
{ n: 'Approfondissement', c: 'Le routeur du site B tombe : 40 alertes arrivent en même temps. Proposer la configuration pour n’en recevoir qu’une.', r: 'Zabbix : dépendance des déclencheurs des hôtes du site B envers celui du routeur. Alertmanager : inhibit_rules qui masquent les alertes d’un site quand l’alerte « routeur injoignable » du même site est active.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Prometheus, node_exporter, Alertmanager et Grafana sur deux VM.', etapes: ['Installer node_exporter', 'Déclarer la cible dans Prometheus', 'Créer un tableau de bord Grafana', 'Écrire une alerte disque prédictive', 'Remplir le disque et recevoir l’alerte'] },
plusLoin: [['Documentation Prometheus', 'https://prometheus.io/docs/'], ['Documentation Zabbix', 'https://www.zabbix.com/documentation/current/fr/manual'], ['Google SRE Book · Monitoring Distributed Systems', 'https://sre.google/sre-book/monitoring-distributed-systems/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Lundi 7 h 50 : l’alerte « DisquePleinSous24h » est arrivée à 2 h sur le serveur de fichiers, sans que personne ne réagisse. À 7 h 45 le disque est plein et les enseignants ne peuvent plus enregistrer. Rétablir puis fiabiliser la chaîne d’alerte.', etapes: [
['Rétablir le service', 'df -h /srv\n/dev/sdb1 500G 500G 0 100% /srv\nsudo du -xh --max-depth=2 /srv | sort -h | tail -3\n180G /srv/partage/tmp/exports-pronote\n→ Un script d’export mal configuré écrit un fichier toutes les minutes depuis vendredi.'],
['Arrêter la cause et libérer de la place', 'sudo systemctl stop export-pronote.timer\nsudo find /srv/partage/tmp/exports-pronote -mtime +0 -delete\ndf -h /srv\n/dev/sdb1 500G 322G 178G 65% /srv\n→ Les enseignants peuvent de nouveau enregistrer.'],
['Comprendre pourquoi l’alerte n’a servi à rien', 'amtool alert query DisquePleinSous24h\nalertname=DisquePleinSous24h severity=warning instance=fichiers:9100\n→ L’alerte a bien été émise, mais en severity warning, routée vers une adresse de courriel lue seulement en journée.'],
['Corriger le routage', 'route:\n routes:\n - matchers: [severity="critical"]\n receiver: astreinte-sms\n# nouvelle règle : disque plein sous 6 h → critical\n→ L’alerte à 24 h reste un avertissement par courriel ; une seconde règle à 6 h devient critique et part vers l’astreinte.'],
['Superviser la cause', '→ Ajout d’une alerte sur le nombre de fichiers créés par heure dans /srv/partage/tmp, et correction du script d’export (rotation des anciens fichiers). Compte rendu : l’outil avait détecté le problème six heures à l’avance ; le défaut était dans l’organisation de la réponse.']
] },
erreurs: ['Alerter sur tout, sans hiérarchie', 'Seuils fixes sans durée', 'Aucune supervision de l’expérience utilisateur', 'SNMP v2c en écriture avec la communauté public', 'Alertes envoyées à une boîte que personne ne lit', 'Oublier de superviser les sauvegardes et les certificats'],
memo: [['Outil / commande', 'Usage'], ['curl -s localhost:9100/metrics | head', 'vérifier un node_exporter'], ['promtool check config prometheus.yml', 'valider la configuration'], ['promtool check rules regles.yml', 'valider les règles d’alerte'], ['amtool alert query', 'alertes actives dans Alertmanager'], ['snmpwalk -v3 -l authPriv … <hôte> IF-MIB::ifOperStatus', 'lire l’état des ports d’un switch'], ['zabbix_get -s <hôte> -k system.cpu.load', 'tester un élément Zabbix'], ['systemctl status zabbix-agent2', 'état de l’agent'], ['journalctl -u prometheus', 'journaux du serveur'], ['rate(), increase(), predict_linear()', 'fonctions PromQL essentielles']],
examen: { titre: 'Sujet type · 20 points', q: ['Proposer un plan de supervision pour un service donné. (6 pts)', 'Comparer agent, SNMP et métriques exposées. (4 pts)', 'Écrire deux règles d’alerte avec seuils et durées. (5 pts)', 'Analyser un incident de fatigue d’alerte. (5 pts)'], r: ['matériel → expérience utilisateur', 'voir cours', 'expr + for + severity', 'actionnable, hiérarchie, dépendances'] },
prof: { deroule: [['Ouverture', 'Quelle panne avez-vous apprise par un utilisateur ?'], ['Cours', 'Quoi superviser, comment collecter'], ['Pratique', 'Lab Prometheus et Grafana'], ['Incident', 'Alerte émise mais ignorée'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : tableau de bord importé, une seule alerte. Élèves avancés : SNMPv3, Loki, inhibitions.', eval: 'Alerte reçue pendant le lab, sujet type.' } },
exemples: [
['Vérifier une cible Prometheus', 'Un serveur n’apparaît plus dans Grafana.', 'curl -s http://prometheus:9090/api/v1/targets | jq -r \'.data.activeTargets[] | "\\(.labels.instance) \\(.health) \\(.lastError)"\'', 'fichiers:9100 up \nweb1:9100 down Get "http://web1:9100/metrics": dial tcp 10.62.40.21:9100: connect: connection refused', 'node_exporter ne tourne plus sur web1 (ou le pare-feu bloque 9100). systemctl status prometheus-node-exporter sur web1.'],
['Lire l’état des ports d’un switch', 'Un bâtiment signale des coupures.', 'snmpwalk -v3 -l authPriv -u supervision -a SHA -A "…" -x AES -X "…" sw-bat-b IF-MIB::ifOperStatus | grep -v up', 'IF-MIB::ifOperStatus.10017 = INTEGER: down(2)', 'Le port 17 est tombé. Avec ifAlias, on retrouve qu’il s’agit de la liaison montante vers le bâtiment B.'],
['Prédire la saturation', 'Planifier l’achat de disques.', 'predict_linear(node_filesystem_avail_bytes{instance="fichiers:9100",mountpoint="/srv"}[7d], 30*24*3600)', '-4.2e+10', 'Au rythme des sept derniers jours, il manquera environ 42 Go dans 30 jours : on commande avant l’incident.'],
['Tester un élément Zabbix', 'Un élément reste vide dans l’interface.', 'zabbix_get -s 10.62.40.10 -k vfs.fs.size[/srv,pfree]', 'ZBX_NOTSUPPORTED: Cannot obtain filesystem information: [2] No such file or directory', 'Le point de montage n’existe pas sur cet hôte : le modèle appliqué ne correspond pas à la machine. On utilise la découverte automatique des systèmes de fichiers.'],
['Voir les alertes actives', 'Point d’astreinte du matin.', 'amtool alert query --active', 'Alertname Starts At Summary\nCertificatExpire 2026-09-28 06:00 ent.lycee.fr expire dans 9 jours\nSauvegardeEchec 2026-09-30 03:12 sauvegarde nocturne de db1 en échec', 'Deux actions claires pour la journée : renouveler le certificat (vérifier certbot) et relancer la sauvegarde de db1.'],
['Valider les règles avant rechargement', 'Nouvelle règle d’alerte.', 'promtool check rules /etc/prometheus/regles.yml', 'Checking /etc/prometheus/regles.yml\n FAILED:\n group "systeme", rule 3: could not parse expression: unexpected "}"', 'Une accolade en trop dans la règle 3. Sans ce contrôle, Prometheus aurait refusé de recharger toute la configuration.']
],
termes: ['supervision', 'métrique', 'alerte', 'seuil', 'SNMP', 'SNMPv3', 'OID', 'MIB', 'agent', 'Prometheus', 'PromQL', 'node_exporter', 'Alertmanager', 'Grafana', 'Zabbix', 'déclencheur', 'modèle', 'predict_linear', 'rate', 'fatigue d’alerte', 'Loki', 'rsyslog']
});
reg({
cours: { id: 'annuaire', match: /LDAP|Active Directory|annuaire|AD DS|GPO|Samba/i,
titre: 'Annuaires : LDAP, Active Directory, GPO et Samba', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR',
prerequis: ['DNS : résolution et zones', 'Droits et utilisateurs'],
competences: ['BTS SIO SISR · Bloc 2 · Administrer les services d’annuaire', 'BTS SIO · Bloc 3 · Gérer les identités et les accès', 'TSSR · Exploiter un annuaire'],
intro: 'Dans un établissement de 1 500 comptes, créer chaque utilisateur sur chaque serveur est impossible. L’annuaire centralise les identités, les groupes et les droits : un élève se connecte avec le même identifiant sur n’importe quel poste, au Wi-Fi, à l’imprimante et aux applications. Active Directory domine dans les établissements et les entreprises ; il repose sur des standards ouverts (LDAP, Kerberos, DNS) que l’on retrouve dans Samba et OpenLDAP. Ce module en décrit le fonctionnement, l’administration et la sécurisation.',
cm: [
{ t: 'LDAP : un arbre d’objets', d: '', html: 'LDAP (Lightweight Directory Access Protocol) est le protocole d’interrogation des annuaires. Les données forment un <b>arbre</b> (DIT) : chaque entrée a un <b>DN</b> (Distinguished Name) unique, lu de la feuille vers la racine, par exemple <code>CN=Léa M.,OU=SIO1,OU=Eleves,DC=lycee,DC=local</code>. Les entrées appartiennent à des <b>classes d’objets</b> (user, group, organizationalUnit) qui définissent leurs <b>attributs</b> (sAMAccountName, mail, memberOf). LDAP écoute sur le port 389, LDAPS (chiffré) sur 636.' },
{ t: 'Active Directory : domaine, forêt, contrôleurs', d: '', html: 'Un <b>domaine</b> AD regroupe comptes, groupes et ordinateurs sous une même autorité, avec un nom DNS (lycee.local, ou mieux un sous-domaine d’un domaine possédé : ad.lycee-exemple.fr). Les <b>contrôleurs de domaine</b> (DC) hébergent l’annuaire et le répliquent entre eux ; on en installe au moins deux. Plusieurs domaines peuvent former une <b>forêt</b>.<br><br>AD dépend entièrement du <b>DNS</b> : les clients trouvent les DC grâce aux enregistrements SRV (<code>_ldap._tcp.dc._msdcs.lycee.local</code>). Un poste qui n’utilise pas le DNS du domaine ne peut pas le rejoindre. L’authentification repose sur <b>Kerberos</b>, qui impose des horloges synchronisées à cinq minutes près.' },
{ t: 'Organiser : OU, groupes et délégation', d: '', html: 'Les <b>unités d’organisation</b> (OU) rangent les objets pour appliquer des stratégies et déléguer l’administration : OU Eleves, Professeurs, Administration, Ordinateurs\\Salles. Les <b>groupes</b> portent les droits. La méthode AGDLP résume la bonne pratique : les comptes (A) vont dans des groupes globaux par fonction (G, « Profs-SIO »), placés dans des groupes locaux de domaine par ressource (DL, « Partage-SIO-Modification »), auxquels on donne les permissions (P). On ne donne jamais un droit directement à un utilisateur.' },
{ t: 'Stratégies de groupe (GPO)', d: '', html: 'Une <b>GPO</b> applique des paramètres aux ordinateurs et aux utilisateurs d’une OU : fond d’écran, lecteurs réseau, imprimantes, mises à jour, pare-feu, restrictions, déploiement de logiciels. L’ordre d’application est LSDOU : local, site, domaine, OU (de la plus haute à la plus basse) ; la dernière appliquée l’emporte, sauf blocage ou application forcée. <code>gpupdate /force</code> applique immédiatement, <code>gpresult /r</code> montre les GPO effectivement reçues.' },
{ t: 'Samba et intégration Linux', d: '', html: '<b>Samba</b> partage des fichiers et des imprimantes avec les postes Windows (protocole SMB) et peut même servir de <b>contrôleur de domaine</b> compatible AD, solution répandue dans les établissements attachés aux logiciels libres. Un serveur Linux rejoint un domaine AD avec <b>realmd</b> et <b>SSSD</b> (<code>realm join</code>) : les comptes du domaine s’y connectent en SSH et les groupes AD pilotent les droits sudo.', exemple: 'sudo realm discover lycee.local\nsudo realm join -U administrateur lycee.local\nid lea.martin@lycee.local\nuid=1854201103(lea.martin@lycee.local) … groupes=…(eleves-sio1@lycee.local)' },
{ t: 'Sécurité de l’annuaire', d: '', html: 'L’annuaire est la cible principale des attaquants : compromettre un compte d’administrateur du domaine, c’est posséder tout le réseau. Les recommandations de l’ANSSI s’organisent en niveaux (tiering) : les comptes d’administration du domaine ne se connectent qu’aux DC, jamais à un poste utilisateur. Autres mesures : comptes d’administration séparés des comptes du quotidien, mots de passe longs, <b>LAPS</b> pour des mots de passe administrateur local différents sur chaque poste, désactivation des protocoles anciens (NTLMv1, SMBv1), LDAPS ou signature LDAP, et audit régulier avec des outils comme PingCastle.' }
],
synthese: ['LDAP : arbre, DN, classes et attributs ; 389 et 636', 'AD : domaine, forêt, au moins deux DC ; dépend du DNS et de Kerberos', 'OU pour organiser et appliquer, groupes pour les droits (AGDLP)', 'GPO appliquées en LSDOU ; gpresult pour vérifier', 'Samba, realmd et SSSD pour Linux', 'Tiering, comptes séparés, LAPS, protocoles anciens désactivés'],
qcm: [
['Que désigne un DN ?', ['Un nom DNS', 'Le chemin unique d’une entrée dans l’arbre LDAP', 'Un groupe', 'Un port'], 1, 'Distinguished Name.'],
['Pourquoi un poste ne trouve-t-il pas le domaine ?', ['Pare-feu Windows', 'Il n’utilise pas le DNS du domaine', 'Mauvais fond d’écran', 'LAPS'], 1, 'Enregistrements SRV.'],
['Quel écart d’horloge maximal tolère Kerberos par défaut ?', ['1 s', '5 min', '1 h', 'Aucun'], 1, 'D’où NTP obligatoire.'],
['Où donner une permission sur un partage selon AGDLP ?', ['À l’utilisateur', 'À un groupe local de domaine', 'À l’OU', 'Au DC'], 1, 'Comptes → global → local → permission.'],
['Quelle commande montre les GPO reçues par un poste ?', ['gpupdate', 'gpresult /r', 'ipconfig', 'netstat'], 1, 'Résultat de la stratégie.'],
['Quel outil donne un mot de passe administrateur local différent à chaque poste ?', ['GPO', 'LAPS', 'SSSD', 'Kerberos'], 1, 'Local Administrator Password Solution.']
],
td: [
{ n: 'Application', c: 'Proposer l’arborescence d’OU et les groupes d’un lycée avec BTS SIO, sa salle de TP et le partage « Sujets » accessible en modification aux profs de SIO, en lecture aux élèves de SIO.', r: 'OU Utilisateurs\\Eleves\\SIO1, SIO2 · OU Utilisateurs\\Profs · OU Ordinateurs\\Salles\\B204 · groupes globaux G-Eleves-SIO, G-Profs-SIO · groupes locaux DL-Sujets-Lecture, DL-Sujets-Modif · permissions NTFS et partage sur ces DL.' },
{ n: 'Application', c: 'Écrire la requête ldapsearch qui liste les comptes de l’OU SIO1 avec leur adresse mail.', r: 'ldapsearch -H ldaps://dc1.lycee.local -D "cn=lecture,…" -W -b "OU=SIO1,OU=Eleves,DC=lycee,DC=local" "(objectClass=user)" sAMAccountName mail' },
{ n: 'Approfondissement', c: 'Expliquer pourquoi un administrateur du domaine ne doit pas ouvrir de session sur un poste d’élève.', r: 'Son jeton ou ses identifiants restent en mémoire du poste ; si celui-ci est compromis, l’attaquant les récupère et devient administrateur du domaine (modèle de tiering de l’ANSSI).' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un contrôleur Samba AD, un client Windows et un serveur Linux.', etapes: ['Provisionner le domaine Samba AD', 'Vérifier les enregistrements SRV', 'Créer OU, comptes et groupes', 'Joindre le serveur Linux avec realmd', 'Appliquer une GPO de lecteur réseau'] },
plusLoin: [['ANSSI · Recommandations de sécurité relatives à Active Directory', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-active-directory'], ['Samba Wiki · Setting up Samba as an AD DC', 'https://wiki.samba.org/index.php/Setting_up_Samba_as_an_Active_Directory_Domain_Controller'], ['PingCastle', 'https://www.pingcastle.com/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Ce matin, aucun élève de la salle B204 ne parvient à ouvrir de session (« La relation d’approbation entre cette station de travail et le domaine principal a échoué » sur certains postes, « L’heure du serveur diffère » sur d’autres). Les autres salles fonctionnent.', etapes: [
['Vérifier l’heure', 'Sur un poste de B204 (session locale) : w32tm /stripchart /computer:dc1.lycee.local /samples:1\n11:47:02, d:+00.0012s o:-00:07:41.3\n→ Le poste a 7 min 41 s de retard sur le DC : Kerberos refuse l’authentification au-delà de 5 minutes.'],
['Trouver pourquoi', 'w32tm /query /source\nLocal CMOS Clock\n→ Les postes de B204, réinstallés pendant les vacances à partir d’une image, ne prennent plus l’heure du domaine.'],
['Corriger l’heure', 'w32tm /config /syncfromflags:domhier /update\nnet stop w32time && net start w32time\nw32tm /resync\n→ Le poste se synchronise sur la hiérarchie du domaine.'],
['Réparer la relation d’approbation', 'Sur les postes au message « relation d’approbation » :\nTest-ComputerSecureChannel -Repair -Credential lycee\\technicien\nTrue\n→ Le mot de passe du compte ordinateur, restauré depuis une image ancienne, ne correspondait plus à celui de l’annuaire ; le canal sécurisé est réinitialisé.'],
['Généraliser et prévenir', '→ La correction est déployée par GPO sur l’OU Salles (source de temps du domaine) ; les images de poste sont préparées avec Sysprep et jointes au domaine après déploiement. Compte rendu rédigé.']
] },
erreurs: ['Postes qui n’utilisent pas le DNS du domaine', 'Horloges non synchronisées', 'Un seul contrôleur de domaine', 'Droits donnés directement aux utilisateurs', 'Administrateur du domaine utilisé sur les postes', 'Même mot de passe administrateur local partout', 'LDAP simple non chiffré'],
memo: [['Commande', 'Usage'], ['nslookup -type=SRV _ldap._tcp.dc._msdcs.lycee.local', 'trouver les contrôleurs de domaine'], ['ldapsearch -H ldaps://… -b … "(filtre)"', 'interroger l’annuaire'], ['gpupdate /force', 'appliquer les GPO'], ['gpresult /r', 'GPO effectivement appliquées'], ['w32tm /query /status', 'source et écart de temps'], ['Test-ComputerSecureChannel -Repair', 'réparer la relation avec le domaine'], ['samba-tool user create|list', 'gérer les comptes Samba AD'], ['samba-tool drs showrepl', 'état de la réplication'], ['realm join -U admin domaine', 'joindre un Linux au domaine'], ['id utilisateur@domaine', 'vérifier un compte depuis Linux']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire le rôle de DNS et Kerberos dans AD. (4 pts)', 'Proposer OU et groupes pour un cas donné selon AGDLP. (6 pts)', 'Expliquer l’ordre d’application des GPO et diagnostiquer une GPO non appliquée. (5 pts)', 'Citer cinq mesures de sécurité de l’annuaire. (5 pts)'], r: ['SRV, tickets, horloge', 'voir TD', 'LSDOU, gpresult', 'tiering, LAPS, comptes séparés, NTLMv1/SMBv1, audit'] },
prof: { deroule: [['Ouverture', 'Combien de mots de passe auriez-vous sans annuaire ?'], ['Cours', 'LDAP, AD, DNS, Kerberos'], ['Pratique', 'Lab Samba AD'], ['Incident', 'Heure et relation d’approbation'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : arborescence fournie. Élèves avancés : audit PingCastle, tiering.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Trouver les contrôleurs de domaine', 'Un poste ne rejoint pas le domaine.', 'nslookup -type=SRV _ldap._tcp.dc._msdcs.lycee.local', '_ldap._tcp.dc._msdcs.lycee.local SRV service location:\n priority = 0, weight = 100, port = 389, svr hostname = dc1.lycee.local\n priority = 0, weight = 100, port = 389, svr hostname = dc2.lycee.local', 'Les deux DC sont annoncés. Si la commande échoue, le poste n’interroge pas le DNS du domaine : on vérifie sa configuration DNS avant tout le reste.'],
['Chercher un compte', 'Retrouver les groupes d’un élève.', 'ldapsearch -LLL -H ldaps://dc1.lycee.local -D "CN=lecture,OU=Services,DC=lycee,DC=local" -W -b "DC=lycee,DC=local" "(sAMAccountName=lea.martin)" memberOf', 'dn: CN=Léa M.,OU=SIO1,OU=Eleves,DC=lycee,DC=local\nmemberOf: CN=G-Eleves-SIO,OU=Groupes,DC=lycee,DC=local', 'LDAPS chiffre l’échange ; le compte de lecture dédié n’a aucun autre droit.'],
['Vérifier les GPO d’un poste', 'Le lecteur réseau n’apparaît pas.', 'gpresult /r /scope:user', 'Objets Stratégie de groupe appliqués\n GPO-Eleves-Fond\nLes GPO suivants n’ont pas été appliqués car ils ont été filtrés\n GPO-Lecteurs-SIO\n Filtrage : Refusé (Sécurité)', 'La GPO des lecteurs est filtrée par sécurité : le groupe de l’élève n’a pas le droit « Appliquer la stratégie de groupe ».'],
['Contrôler la réplication', 'Un compte créé sur dc1 n’existe pas sur dc2.', 'sudo samba-tool drs showrepl | grep -A3 "INBOUND" | tail -3', 'DC=lycee,DC=local\n Default-First-Site-Name\\DC1 via RPC\n Last attempt @ … failed, result 1256 (WERR_NO_SUCH_SERVICE)', 'La réplication de dc1 vers dc2 échoue. On vérifie la résolution de dc1 depuis dc2 et l’ouverture des ports RPC.'],
['Joindre un Linux au domaine', 'Nouveau serveur Debian.', 'sudo realm join -U technicien lycee.local && realm list | grep -E "realm-name|configured"', 'realm-name: LYCEE.LOCAL\nconfigured: kerberos-member', 'Le serveur est membre ; les comptes du domaine peuvent s’y connecter selon les règles de realm permit.'],
['Repérer les mots de passe qui n’expirent pas', 'Audit des comptes.', 'Get-ADUser -Filter {PasswordNeverExpires -eq $true -and Enabled -eq $true} | Select Name', 'Name\n----\nsvc-sauvegarde\nancien.stagiaire', 'Un compte de stagiaire parti est encore actif avec un mot de passe éternel : on le désactive. Le compte de service doit devenir un compte de service géré (gMSA).']
],
termes: ['LDAP', 'LDAPS', 'DN', 'OU', 'Active Directory', 'domaine', 'forêt', 'contrôleur de domaine', 'Kerberos', 'SRV', 'GPO', 'LSDOU', 'AGDLP', 'gpupdate', 'gpresult', 'Samba', 'SSSD', 'realmd', 'LAPS', 'tiering', 'NTLM', 'SMB', 'ldapsearch', 'samba-tool']
});
})();

View file

@ -0,0 +1,64 @@
// Lecture longue, schémas et emplacements photo pour chaque cours complet
(function () {
var K = 'stroke="#000" stroke-width="3"';
var box = function (x, y, w, h, fill, txt, fs) { return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" fill="' + fill + '" ' + K + '/><text x="' + (x + w / 2) + '" y="' + (y + h / 2 + (fs || 16) / 3) + '" text-anchor="middle" font-family="Archivo,sans-serif" font-weight="800" font-size="' + (fs || 16) + '">' + txt + '</text>'; };
var arrow = function (x1, y1, x2, y2) { return '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" ' + K + ' marker-end="url(#fl)"/>'; };
var svg = function (w, h, body) { return '<svg viewBox="0 0 ' + w + ' ' + h + '" class="h-auto w-full" role="img"><defs><marker id="fl" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z"/></marker></defs>' + body + '</svg>'; };
var bits = [128, 64, 32, 16, 8, 4, 2, 1], v = [1, 0, 1, 1, 0, 0, 1, 0];
window.CONTENUS_LONG = Object.assign(window.CONTENUS_LONG || {}, {
binaire: { figure: svg(720, 200, bits.map(function (b, i) { return box(20 + i * 85, 20, 80, 50, '#F6F5F1', b, 16) + box(20 + i * 85, 80, 80, 60, v[i] ? '#FFD23F' : '#fff', v[i], 26); }).join('') + '<text x="20" y="180" font-family="IBM Plex Mono,monospace" font-weight="600" font-size="18">1011 0010₂ = 128 + 32 + 16 + 2 = 178</text>'), legende: 'Chaque case d’un octet a un poids. On additionne les poids des bits à 1.',
photo: 'Photo : barrette de mémoire vive ou vue au microscope d’une puce',
lecture: ['Imaginez une rangée de huit interrupteurs. Chacun peut être allumé ou éteint. Si l’on convient que le premier vaut 1, le deuxième 2, le troisième 4, et ainsi de suite en doublant à chaque fois, on peut représenter n’importe quel nombre de 0 à 255 en choisissant quels interrupteurs allumer. C’est exactement ce que fait un octet dans la mémoire d’un ordinateur.',
'Pourquoi doubler ? Parce que chaque interrupteur n’a que deux positions. En base 10, on a dix chiffres, donc chaque position vaut dix fois la précédente : unités, dizaines, centaines. En base 2, on n’a que deux chiffres, donc chaque position vaut deux fois la précédente. La logique est la même, seule la base change.',
'L’hexadécimal n’est qu’une écriture plus compacte du binaire. Lire 32 zéros et uns d’affilée est pénible et source d’erreurs ; en les regroupant par quatre, on obtient huit symboles faciles à recopier. C’est pour cela que les adresses MAC, les couleurs web et les adresses IPv6 s’écrivent en hexadécimal : l’humain lit vite, et la machine retrouve les bits sans calcul.',
'Dans la pratique, un administrateur réseau manipule ces conversions tous les jours sans même y penser : un masque /26, c’est 26 bits à 1, donc un dernier octet à 1100 0000, soit 192. Ceux qui maîtrisent ce réflexe découpent un réseau en sous-réseaux de tête ; les autres sortent la calculatrice. L’objectif de ce cours est de vous faire passer dans la première catégorie.'] },
terminal: { figure: svg(720, 260, box(310, 10, 100, 40, '#000', '<tspan fill="#fff">/</tspan>', 20) + [['etc', 20], ['home', 160], ['var', 300], ['usr', 440], ['tmp', 580]].map(function (d) { return arrow(360, 50, d[1] + 60, 95) + box(d[1], 100, 120, 40, '#fff', '/' + d[0], 16); }).join('') + arrow(220, 140, 220, 185) + box(160, 190, 120, 40, '#FFD23F', 'eleve', 16) + arrow(360, 140, 360, 185) + box(300, 190, 120, 40, '#7CF2C0', 'log', 16)), legende: 'L’arborescence Linux part d’une seule racine, /. Votre dossier personnel est /home/eleve.',
photo: 'Photo : une salle serveur ou un administrateur devant un terminal',
lecture: ['Quand on découvre le terminal, l’écran noir impressionne. Pourtant, il ne fait rien d’autre que ce que fait l’explorateur de fichiers : afficher des dossiers, en ouvrir, copier, déplacer. La différence, c’est qu’on écrit ce qu’on veut au lieu de cliquer, et que ce qu’on écrit peut être rejoué, partagé et automatisé.',
'Sur un serveur, il n’y a souvent pas d’interface graphique du tout. On s’y connecte à distance par SSH depuis un autre ordinateur, parfois depuis l’autre bout du monde, et le terminal est la seule porte d’entrée. Un technicien qui sait s’en servir intervient en quelques secondes là où un autre devrait se déplacer.',
'La première compétence est de toujours savoir où l’on est. L’invite vous le dit, pwd le confirme. La plupart des erreurs de débutant viennent d’une commande lancée dans le mauvais dossier : un fichier créé « nulle part », un rm qui supprime autre chose que prévu. Prenez l’habitude de regarder l’invite avant d’appuyer sur Entrée.',
'La deuxième compétence est de ne pas tout retenir. Personne ne connaît toutes les options de toutes les commandes. Les professionnels utilisent man, --help, la touche Tab et l’historique en permanence. Savoir chercher l’information est plus important que la connaître par cœur.'] },
droits: { figure: svg(720, 190, box(20, 20, 60, 60, '#F6F5F1', '-', 26) + [['r', 'w', 'x', '#FFD23F', 'propriétaire (u)'], ['r', '-', 'x', '#7CF2C0', 'groupe (g)'], ['-', '-', '-', '#FF6FB5', 'autres (o)']].map(function (g, i) { var x = 100 + i * 205; return box(x, 20, 60, 60, g[3], g[0], 26) + box(x + 62, 20, 60, 60, g[3], g[1], 26) + box(x + 124, 20, 60, 60, g[3], g[2], 26) + '<text x="' + (x + 92) + '" y="110" text-anchor="middle" font-family="Archivo,sans-serif" font-weight="800" font-size="16">' + g[4] + '</text><text x="' + (x + 92) + '" y="150" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-weight="600" font-size="26">' + [7, 5, 0][i] + '</text>'; }).join('')), legende: 'rwxr-x--- : trois triplets, un par catégorie. En octal, 750.',
photo: 'Photo : badge d’accès ou porte de salle serveur sécurisée',
lecture: ['Un serveur de lycée héberge à la fois les sujets d’examen des enseignants, les travaux des élèves et les fichiers de configuration du système. Sans règles, n’importe qui pourrait lire les sujets la veille de l’épreuve ou modifier la configuration. Les droits Unix sont ces règles, et elles s’appliquent à chaque fichier.',
'Le modèle est volontairement simple : trois catégories de personnes, trois actions possibles. Cette simplicité est une force, car elle se vérifie d’un coup d’œil avec ls -l. Elle a aussi des limites, que les ACL viennent combler quand il faut donner un accès à une seule personne sans créer de groupe.',
'Le piège classique est de « tout ouvrir » pour que ça marche : chmod 777. Le service redémarre, l’utilisateur est content, et le serveur vient de perdre toute protection. Un bon technicien cherche au contraire le droit minimum qui suffit. C’est le principe du moindre privilège, au cœur des recommandations de l’ANSSI.',
'Retenez enfin que sur un dossier, x ne veut pas dire « exécuter » mais « traverser ». Un dossier en r-- permet de voir les noms des fichiers sans pouvoir les ouvrir, ce qui surprend toujours la première fois.'] },
tubes: { figure: svg(720, 130, [['auth.log', '#F6F5F1'], ['grep', '#FFD23F'], ['awk', '#7CF2C0'], ['sort | uniq -c', '#C4B5FD'], ['head', '#FF6FB5']].map(function (s, i) { return box(10 + i * 142, 40, 120, 50, s[1], s[0], 14) + (i < 4 ? arrow(130 + i * 142, 65, 150 + i * 142, 65) : ''); }).join('')), legende: 'Chaque commande transforme le flux et passe le résultat à la suivante.',
photo: 'Photo : un écran de supervision avec des journaux qui défilent',
lecture: ['Un journal système, c’est un fichier texte où chaque ligne raconte un événement : une connexion, une erreur, un démarrage de service. Sur un serveur exposé à Internet, auth.log peut grossir de plusieurs milliers de lignes par jour. Personne ne les lit une par une.',
'La philosophie Unix propose une réponse élégante : de petits outils qui font chacun une seule chose, et un moyen de les brancher entre eux. grep sait filtrer, sort sait trier, uniq sait compter les doublons. Aucun ne sait analyser un journal seul ; ensemble, ils répondent en une ligne à des questions complexes.',
'Construisez toujours une chaîne pas à pas. Lancez la première commande, regardez le résultat, ajoutez un tube et la commande suivante, regardez encore. Une chaîne de six commandes écrite d’un coup ne marche presque jamais du premier essai ; construite progressivement, elle se met au point en deux minutes.',
'Quand une chaîne est utile, on la sauvegarde dans un script et on la planifie. C’est le premier pas vers l’automatisation, compétence très recherchée chez les techniciens et les administrateurs.'] },
osi: { figure: svg(720, 330, ['7 Application · HTTP, DNS', '6 Présentation · TLS', '5 Session', '4 Transport · TCP, UDP', '3 Réseau · IP', '2 Liaison · Ethernet', '1 Physique · câble, radio'].map(function (t, i) { return box(20, 10 + i * 44, 420, 40, ['#FF6FB5', '#FF6FB5', '#FF6FB5', '#C4B5FD', '#7CF2C0', '#FFD23F', '#F6F5F1'][i], t, 15); }).join('') + [['Application', 10, 130], ['Transport', 142, 40], ['Internet', 186, 40], ['Accès réseau', 230, 84]].map(function (c) { return box(470, c[1], 230, c[2], '#fff', c[0], 15); }).join('')), legende: 'À gauche les 7 couches OSI, à droite leurs équivalents dans le modèle TCP/IP.',
photo: 'Photo : baie de brassage avec switchs et câbles RJ45',
lecture: ['Quand vous ouvrez une page web, des dizaines d’équipements différents collaborent : votre carte Wi-Fi, la box, les routeurs de votre opérateur, ceux d’Internet, le serveur. Ils ont été fabriqués par des entreprises différentes, à des époques différentes. Ils se comprennent parce que chacun respecte des règles communes, rangées par couches.',
'L’analogie du courrier aide à fixer les idées. Vous écrivez une lettre (l’application), vous la mettez dans une enveloppe avec l’adresse (le réseau), la poste la place dans un sac à destination d’un centre de tri (la liaison), et un camion transporte le sac (le physique). À chaque étape, on ajoute un emballage sans ouvrir le précédent. À l’arrivée, on déballe dans l’ordre inverse.',
'Le modèle OSI à 7 couches est un modèle de référence, très utile pour raisonner et pour dialoguer entre techniciens (« c’est un problème de couche 2 »). Le modèle TCP/IP à 4 couches est celui qui est réellement implémenté. Vous devez connaître les deux et savoir passer de l’un à l’autre.',
'Le vrai intérêt du modèle en couches se révèle en panne. Plutôt que de tout tester au hasard, on part du bas : le lien, l’adresse, la passerelle, le nom, le service. La première couche qui échoue désigne le coupable. Cette méthode fait gagner un temps considérable.'] },
sql: { figure: svg(720, 200, box(20, 20, 280, 40, '#FFD23F', 'ELEVE', 16) + box(20, 60, 280, 34, '#fff', 'id · nom · prenom · #classe_id', 14) + box(420, 20, 280, 40, '#7CF2C0', 'CLASSE', 16) + box(420, 60, 280, 34, '#fff', 'id · libelle · annee', 14) + arrow(300, 77, 418, 77) + '<text x="360" y="130" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-weight="600" font-size="14">classe_id → id</text><text x="20" y="180" font-family="IBM Plex Mono,monospace" font-weight="600" font-size="15">SELECT e.nom, c.libelle FROM eleve e JOIN classe c ON e.classe_id = c.id;</text>'), legende: 'La clé étrangère classe_id relie chaque élève à sa classe. La jointure suit ce lien.',
photo: 'Photo : tableau de bord ou écran d’application de vie scolaire',
lecture: ['Un tableur suffit pour une classe. Pour un établissement entier, avec des milliers d’élèves, des centaines de devoirs et des notes qui arrivent en continu, il faut une base de données. Elle garantit que les données restent cohérentes, que plusieurs personnes peuvent écrire en même temps et qu’on retrouve n’importe quelle information en une fraction de seconde.',
'L’idée centrale du modèle relationnel est de ne jamais recopier une information. Le libellé d’une classe n’est écrit qu’une fois, dans la table des classes ; chaque élève ne contient que le numéro de sa classe. Si la classe change de nom, on ne modifie qu’une ligne. Les jointures reconstruisent l’information complète au moment de la requête.',
'SQL est un langage déclaratif : on décrit ce qu’on veut obtenir, pas comment le calculer. C’est la base de données qui choisit la meilleure façon d’y arriver. Cela rend les requêtes courtes et lisibles, presque comme des phrases, mais demande de bien comprendre l’ordre logique : FROM et JOIN d’abord, puis WHERE, GROUP BY, HAVING, SELECT et enfin ORDER BY.',
'Enfin, une base contient souvent des données personnelles. Le RGPD impose de les protéger : droits d’accès limités, requêtes préparées contre les injections, sauvegardes, et suppression des données qui ne sont plus nécessaires.'] },
python: { figure: svg(720, 250, box(260, 10, 200, 44, '#F6F5F1', 'note = saisie', 16) + arrow(360, 54, 360, 84) + '<polygon points="360,86 470,126 360,166 250,126" fill="#FFD23F" ' + K + '/><text x="360" y="131" text-anchor="middle" font-family="Archivo,sans-serif" font-weight="800" font-size="16">note ≥ 10 ?</text>' + arrow(470, 126, 560, 126) + box(560, 104, 140, 44, '#7CF2C0', 'Admis', 16) + arrow(250, 126, 160, 126) + box(20, 104, 140, 44, '#FF6FB5', 'À revoir', 16) + '<text x="505" y="115" font-family="Archivo" font-weight="800" font-size="14">oui</text><text x="190" y="115" font-family="Archivo" font-weight="800" font-size="14">non</text><text x="20" y="220" font-family="IBM Plex Mono,monospace" font-weight="600" font-size="15">if note >= 10: print("Admis") else: print("À revoir")</text>'), legende: 'Une condition est un embranchement : le programme suit une seule des deux branches.',
photo: 'Photo : élèves en séance de programmation',
lecture: ['Programmer, ce n’est pas taper du code : c’est découper un problème en étapes assez simples pour qu’une machine les exécute sans réfléchir. Le langage n’est qu’un moyen d’écrire ces étapes. Python est choisi dans l’enseignement parce que sa syntaxe gêne le moins possible cette réflexion.',
'Trois structures suffisent à écrire presque tous les programmes : la séquence (une instruction après l’autre), la condition (faire ceci ou cela) et la répétition (recommencer). Le reste, fonctions, listes, fichiers, sert à organiser ces trois briques pour que le programme reste lisible quand il grandit.',
'Un bon réflexe est d’écrire le programme en français avant de l’écrire en Python : « demander la note ; si elle est au moins 10, afficher admis ; sinon, afficher à revoir ». La traduction devient alors presque mécanique, et les erreurs de logique apparaissent avant la première ligne de code.',
'Enfin, testez tout le temps. Une fonction de trois lignes accompagnée de deux assert est plus précieuse qu’une fonction de trente lignes que personne n’ose modifier. C’est ce que font les développeurs professionnels, et c’est ce que vérifient les tests automatiques de l’IDE.'] },
web: { figure: svg(720, 200, box(20, 60, 180, 70, '#C4B5FD', 'Navigateur', 18) + box(520, 60, 180, 70, '#7CF2C0', 'Serveur Apache', 18) + arrow(200, 80, 518, 80) + arrow(520, 115, 202, 115) + '<text x="360" y="70" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-weight="600" font-size="14">GET /index.html</text><text x="360" y="140" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-weight="600" font-size="14">200 OK + HTML</text><text x="20" y="185" font-family="Archivo,sans-serif" font-weight="700" font-size="14">Le navigateur affiche le HTML, applique le CSS, exécute le JavaScript.</text>'), legende: 'Un échange HTTP : une requête du client, une réponse du serveur.',
photo: 'Photo : outils de développement du navigateur ouverts sur une page',
lecture: ['Le Web repose sur une idée simple inventée au CERN en 1989 : des documents reliés par des liens, que l’on demande à un serveur avec un protocole commun. Trente ans plus tard, la même idée fait fonctionner les réseaux sociaux, les ENT et les applications de banque.',
'Ouvrez les outils de développement de votre navigateur (F12), onglet Réseau, et rechargez une page : vous verrez des dizaines de requêtes, chacune avec sa méthode, son code et sa durée. C’est le meilleur moyen de comprendre HTTP, et c’est l’outil numéro un d’un développeur web pour dépanner.',
'Séparer la structure (HTML) de la présentation (CSS) n’est pas un caprice : le même contenu peut ainsi s’afficher sur un téléphone, un grand écran ou être lu à voix haute par un lecteur d’écran pour une personne malvoyante. Une page bien structurée est aussi mieux référencée par les moteurs de recherche.',
'Côté serveur, héberger un site, c’est installer un logiciel qui écoute sur le port 80 ou 443 et renvoie des fichiers. Apache et Nginx font tourner une grande partie des sites du monde. Savoir les installer, les configurer et lire leurs journaux fait partie du socle du technicien.'] },
crypto: { figure: svg(720, 210, box(20, 70, 150, 60, '#fff', 'Message', 18) + arrow(170, 100, 268, 100) + box(270, 60, 180, 80, '#FFD23F', 'Chiffrement', 18) + arrow(450, 100, 548, 100) + box(550, 70, 150, 60, '#13233A', '<tspan fill="#fff">x9#Qa…</tspan>', 18) + '<text x="360" y="40" text-anchor="middle" font-family="Archivo,sans-serif" font-weight="800" font-size="15">clé publique d’Alice</text><text x="360" y="185" text-anchor="middle" font-family="Archivo,sans-serif" font-weight="800" font-size="15">Seule la clé privée d’Alice peut déchiffrer</text>'), legende: 'Chiffrement asymétrique : on chiffre avec la clé publique du destinataire.',
photo: 'Photo : cadenas HTTPS dans la barre d’adresse ou clé de sécurité matérielle',
lecture: ['La cryptographie a longtemps été réservée aux militaires et aux diplomates. Aujourd’hui, elle protège chaque connexion HTTPS, chaque message sur une messagerie moderne, chaque paiement par carte. Vous l’utilisez des centaines de fois par jour sans le savoir.',
'Il faut d’abord bien distinguer les outils. Le hachage ne cache rien : il produit une empreinte qui permet de vérifier qu’un fichier ou un mot de passe n’a pas changé. Le chiffrement, lui, rend un message illisible sans la clé, et il est réversible. Confondre les deux est l’erreur la plus fréquente, en cours comme en entreprise.',
'Le chiffrement asymétrique a résolu un problème vieux de plusieurs siècles : comment se mettre d’accord sur une clé secrète sans jamais se rencontrer ? La réponse, publiée dans les années 1970, est d’utiliser deux clés liées mathématiquement. On peut diffuser l’une à tout le monde sans jamais compromettre l’autre.',
'En pratique, un technicien ne réinvente jamais un algorithme. Il choisit des outils éprouvés, suit les recommandations de l’ANSSI, active HTTPS partout, remplace les mots de passe SSH par des clés et vérifie que l’application stocke les mots de passe avec Argon2 ou bcrypt. La sécurité vient d’abord de la rigueur.'] }
});
})();

View file

@ -0,0 +1,227 @@
// Cours complets · lot 10 : algorithmique et structures, développement web back-end, JavaScript et TypeScript, langage C
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'algo', match: /Tris|dichotomique|complexité|Complexité et notation O|Récursivité|Piles, files|listes chaînées|Graphes : parcours|Dijkstra|Listes, tuples|Fonctions, portée|Tests : assert/i,
titre: 'Algorithmique : complexité, tris, récursivité, structures de données et graphes', duree: '',
niveau: 'NSI Première et Terminale · BTS SIO SLAM · BUT Informatique · classes préparatoires',
prerequis: ['Python : variables, boucles, fonctions'],
competences: ['NSI · Algorithmique et structures de données', 'BTS SIO SLAM · Concevoir une solution applicative', 'BUT Info · Compétence « Optimiser »'],
intro: 'Deux programmes corrects peuvent avoir des comportements radicalement différents : l’un trie un million de lignes en une seconde, l’autre en plusieurs heures. L’algorithmique donne les outils pour choisir la bonne méthode et la bonne structure de données, et pour prévoir comment un programme se comportera quand les données grandiront. Ce module couvre la complexité, les tris et la recherche, la récursivité, les structures linéaires et les graphes, avec leurs applications concrètes en réseau et en informatique de gestion.',
cm: [
{ t: 'Mesurer un algorithme : la complexité', d: '', html: 'La <b>complexité en temps</b> compte le nombre d’opérations élémentaires en fonction de la taille n des données, dans le pire cas. On ne garde que l’ordre de grandeur, noté <b>O</b> : une boucle simple est en <b>O(n)</b>, deux boucles imbriquées en <b>O(n²)</b>, une recherche qui divise l’espace par deux à chaque étape en <b>O(log n)</b>. Pour n = 1 000 000, O(n log n) représente environ 20 millions d’opérations, O(n²) mille milliards : la différence entre une seconde et plusieurs jours. La <b>complexité en espace</b> mesure la mémoire utilisée.', table: [['Complexité', 'Exemple', 'n = 10⁶'], ['O(1)', 'accès à un élément d’un tableau, d’un dictionnaire', '1'], ['O(log n)', 'recherche dichotomique', '≈ 20'], ['O(n)', 'parcours, recherche linéaire', '10⁶'], ['O(n log n)', 'tri fusion, tri de Python', '≈ 2 × 10⁷'], ['O(n²)', 'tri par sélection, double boucle', '10¹²'], ['O(2ⁿ)', 'tester tous les sous-ensembles', 'inenvisageable']] },
{ t: 'Recherche et tris', d: '', html: 'La <b>recherche dichotomique</b> dans un tableau <b>trié</b> compare la valeur cherchée au milieu, puis continue dans la moitié qui convient : O(log n). Les tris simples, par <b>sélection</b> et par <b>insertion</b>, sont en O(n²) ; le tri par insertion est très efficace sur des données presque triées. Le <b>tri fusion</b> découpe le tableau en deux, trie chaque moitié récursivement et fusionne : O(n log n) garanti. Le <b>tri rapide</b> est en moyenne en O(n log n). En pratique, on utilise le tri de la bibliothèque (<code>sorted</code>, Timsort), mais comprendre ces algorithmes permet de raisonner sur les coûts et de réussir les épreuves.', exemple: 'def dichotomie(t, x):\n g, d = 0, len(t) - 1\n while g <= d:\n m = (g + d) // 2\n if t[m] == x:\n return m\n if t[m] < x:\n g = m + 1\n else:\n d = m - 1\n return -1' },
{ t: 'Récursivité', d: '', html: 'Une fonction <b>récursive</b> s’appelle elle-même sur un problème plus petit, jusqu’à un <b>cas de base</b> qui s’arrête. Elle exprime naturellement les problèmes à structure récursive : parcours d’arborescence de fichiers, tri fusion, calculs sur des arbres. Chaque appel occupe une place sur la <b>pile d’appels</b> : sans cas de base atteint, on obtient un dépassement (RecursionError en Python, limite par défaut de 1 000 appels). Certaines récursions recalculent plusieurs fois les mêmes valeurs (Fibonacci naïf en O(2ⁿ)) : la <b>mémoïsation</b> (<code>functools.cache</code>) ou une version itérative corrige le problème.' },
{ t: 'Structures de données linéaires', d: '', html: 'Une <b>pile</b> (LIFO, dernier entré premier sorti) sert à l’historique d’annulation, à la pile d’appels, à la vérification des parenthèses. Une <b>file</b> (FIFO) sert aux files d’attente : impressions, requêtes, mains levées dans l’ordre d’arrivée. En Python, une liste fait une bonne pile (<code>append</code>, <code>pop</code>), mais une mauvaise file (<code>pop(0)</code> en O(n)) : on utilise <code>collections.deque</code>. Une <b>liste chaînée</b> relie des maillons par des références : insertion en tête en O(1), accès au k-ième élément en O(k). Le <b>dictionnaire</b> (table de hachage) donne un accès moyen en O(1) par clé : c’est souvent la structure qui transforme un O(n²) en O(n).' },
{ t: 'Graphes et parcours', d: '', html: 'Un <b>graphe</b> est un ensemble de sommets reliés par des arêtes, éventuellement orientées et pondérées : routeurs et liens, pages web, dépendances entre tâches. On le représente par une <b>matrice d’adjacence</b> ou, plus économiquement, par des <b>listes d’adjacence</b> (un dictionnaire de voisins). Le <b>parcours en largeur</b> (BFS, avec une file) visite par distance croissante et trouve le plus court chemin en nombre d’arêtes ; le <b>parcours en profondeur</b> (DFS, avec une pile ou la récursivité) sert à détecter des cycles ou des composantes connexes.' },
{ t: 'Plus courts chemins : Dijkstra', d: '', html: 'L’algorithme de <b>Dijkstra</b> calcule les plus courts chemins depuis un sommet dans un graphe à poids positifs : il sélectionne à chaque étape le sommet non traité le plus proche et met à jour les distances de ses voisins. Avec une file de priorité (<code>heapq</code>), il s’exécute en O((V + E) log V). C’est exactement ce que fait chaque routeur <b>OSPF</b> pour construire sa table de routage à partir de la carte du réseau, avec un coût par lien inversement proportionnel au débit.', exemple: 'import heapq\ndef dijkstra(g, s):\n dist = {s: 0}\n file = [(0, s)]\n while file:\n d, u = heapq.heappop(file)\n if d > dist[u]:\n continue\n for v, w in g[u].items():\n if d + w < dist.get(v, float("inf")):\n dist[v] = d + w\n heapq.heappush(file, (d + w, v))\n return dist' }
],
synthese: ['Complexité O : O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)', 'Dichotomie sur tableau trié en O(log n) ; tri fusion en O(n log n)', 'Récursivité : cas de base, pile d’appels, mémoïsation', 'Pile LIFO, file FIFO avec deque, dictionnaire en O(1) moyen', 'Graphes : listes d’adjacence ; BFS et DFS', 'Dijkstra et OSPF'],
qcm: [
['Complexité de la recherche dichotomique ?', ['O(1)', 'O(log n)', 'O(n)', 'O(n²)'], 1, 'On divise par deux à chaque étape.'],
['Condition pour utiliser la dichotomie ?', ['Tableau trié', 'Tableau court', 'Nombres entiers', 'Aucune'], 0, 'Sinon on ne sait pas quelle moitié choisir.'],
['Quelle structure pour une file d’attente en Python ?', ['list avec pop(0)', 'collections.deque', 'set', 'tuple'], 1, 'popleft en O(1).'],
['Que manque-t-il à une récursion qui provoque RecursionError ?', ['Un return', 'Un cas de base atteint', 'Une boucle', 'Un print'], 1, 'Elle ne s’arrête jamais.'],
['Quel parcours trouve le plus court chemin en nombre d’arêtes ?', ['DFS', 'BFS', 'Tri fusion', 'Dichotomie'], 1, 'Par distance croissante.'],
['Quel protocole de routage utilise Dijkstra ?', ['RIP', 'OSPF', 'DHCP', 'ARP'], 1, 'État de liens.'],
['Deux boucles imbriquées sur n éléments :', ['O(n)', 'O(n²)', 'O(log n)', 'O(2n)'], 1, 'n × n.']
],
td: [
{ n: 'Échauffement', c: 'Donner la complexité de : parcourir une liste ; vérifier si deux listes ont un élément commun avec deux boucles ; même chose en transformant la seconde en ensemble.', r: 'O(n) · O(n × m) · O(n + m)' },
{ n: 'Application', c: 'Écrire un tri par insertion, puis compter le nombre de comparaisons sur une liste déjà triée et sur une liste triée à l’envers de 1 000 éléments.', r: 'Déjà triée : environ n comparaisons (O(n)) · à l’envers : environ n²/2 = 500 000 (O(n²)).' },
{ n: 'Application', c: 'Vérifier le bon parenthésage d’une expression avec une pile.', r: 'Empiler chaque ouvrante ; pour chaque fermante, la pile ne doit pas être vide et son sommet doit correspondre ; à la fin, la pile doit être vide.' },
{ n: 'Approfondissement', c: 'Appliquer Dijkstra à la main sur un réseau de cinq routeurs fourni, puis en déduire la table de routage du routeur A.', r: 'Tableau des distances étape par étape ; la table associe à chaque destination le premier saut du plus court chemin.' }
],
tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'Exercices avec tests et mesure du temps d’exécution.', etapes: ['Dichotomie et tests', 'Tri fusion', 'Comparaison des temps selon n', 'File des mains levées avec deque', 'BFS sur un plan de salles', 'Dijkstra sur un réseau de routeurs'] },
plusLoin: [['France-IOI · parcours algorithmique', 'https://www.france-ioi.org/'], ['HackerRank · Data Structures et Algorithms', 'https://www.hackerrank.com/domains/algorithms'], ['VisuAlgo (visualisation)', 'https://visualgo.net/fr']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Le calcul des doublons dans l’export des 12 000 élèves de l’académie prend 40 minutes. Rendre le traitement quasi instantané.', etapes: [
['Lire le code', 'for a in eleves:\n for b in eleves:\n if a is not b and a["ine"] == b["ine"]: doublons.append(a)\n→ Deux boucles imbriquées : 144 millions de comparaisons, O(n²).'],
['Choisir la structure', '→ Un dictionnaire (ou un Counter) compte les occurrences de chaque identifiant en un seul parcours.'],
['Réécrire', 'from collections import Counter\nc = Counter(e["ine"] for e in eleves)\ndoublons = [e for e in eleves if c[e["ine"]] > 1]\n→ Deux parcours : O(n).'],
['Mesurer', 'Temps : 40 min → 0,02 s\n→ Même résultat vérifié par un test sur un petit jeu de données connu.'],
['Retenir', '→ Devant une double boucle de recherche, se demander quelle clé permettrait un accès direct par dictionnaire ou ensemble.']
] },
erreurs: ['Dichotomie sur une liste non triée', 'Oublier le cas de base', 'list.pop(0) pour une file', 'Double boucle là où un dictionnaire suffit', 'Recalculer sans mémoïsation', 'Dijkstra avec des poids négatifs'],
memo: [['Outil', 'Usage'], ['len, in sur set/dict', 'O(1) en moyenne'], ['sorted(t, key=…)', 'tri O(n log n)'], ['bisect', 'dichotomie de la bibliothèque'], ['collections.deque', 'file'], ['collections.Counter', 'compter'], ['heapq', 'file de priorité'], ['functools.cache', 'mémoïsation'], ['timeit', 'mesurer le temps'], ['assert f(x) == y', 'tests simples'], ['sys.setrecursionlimit', 'limite de récursion (avec prudence)']],
examen: { titre: 'Sujet type · 20 points', q: ['Déterminer la complexité de trois fonctions. (4 pts)', 'Écrire la recherche dichotomique et ses tests. (4 pts)', 'Écrire une fonction récursive et justifier sa terminaison. (4 pts)', 'Implémenter une file avec deque. (3 pts)', 'Appliquer Dijkstra à la main. (5 pts)'], r: ['O(n), O(n²), O(log n)', 'voir cours', 'cas de base, taille décroissante', 'append / popleft', 'tableau des distances'] },
prof: { deroule: [['Ouverture', 'Chronométrer une double boucle puis un dictionnaire'], ['Cours', 'Complexité'], ['Pratique', 'Tris et dichotomie'], ['Cours', 'Récursivité et structures'], ['Pratique', 'Graphes et Dijkstra'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : visualisations VisuAlgo. Élèves avancés : HackerRank Medium, A*.', eval: 'Tests IDE, sujet type.' } },
exemples: [
['Comparer deux complexités', 'Justifier un choix.', 'python3 -m timeit -s "l=list(range(10**5)); s=set(l)" "99999 in l"\npython3 -m timeit -s "l=list(range(10**5)); s=set(l)" "99999 in s"', '500 loops, best of 5: 612 usec per loop\n5000000 loops, best of 5: 42.1 nsec per loop', 'La recherche dans un ensemble est environ 15 000 fois plus rapide que dans une liste.'],
['Dichotomie de la bibliothèque', 'Retrouver un seuil de note.', 'import bisect; bisect.bisect_left([8, 10, 12, 14, 16], 13)', '3', 'Position d’insertion de 13 dans la liste triée, en O(log n).'],
['Limite de récursion', 'Fonction récursive sur une longue liste.', 'def somme(l): return 0 if not l else l[0] + somme(l[1:])\nsomme(list(range(5000)))', 'RecursionError: maximum recursion depth exceeded', 'Au-delà de 1 000 appels : on écrit une version itérative, plus adaptée ici.'],
['Mémoïsation', 'Fibonacci lent.', 'from functools import cache\n@cache\ndef fib(n): return n if n < 2 else fib(n-1) + fib(n-2)\nfib(200)', '280571172992510140037611932413038677189525', 'Instantané grâce au cache ; la version naïve ne terminerait pas.'],
['File des mains levées', 'Fonction Lever la main.', 'from collections import deque\nq = deque(["Nora", "Yanis"]); q.append("Léa"); q.popleft(), list(q)', "('Nora', ['Yanis', 'Léa'])", 'Premier arrivé, premier servi, en O(1).'],
['Plus courts chemins', 'Réseau de quatre routeurs.', 'g = {"A":{"B":10,"C":1},"B":{"D":1},"C":{"B":1,"D":20},"D":{}}\ndijkstra(g, "A")', "{'A': 0, 'C': 1, 'B': 2, 'D': 3}", 'Le chemin vers D passe par C puis B (coût 3) plutôt que par le lien direct C–D (21).']
],
termes: ['complexité', 'O(n)', 'O(log n)', 'O(n²)', 'O(n log n)', 'recherche dichotomique', 'tri par insertion', 'tri fusion', 'récursivité', 'cas de base', 'pile d’appels', 'mémoïsation', 'pile', 'file', 'deque', 'liste chaînée', 'dictionnaire', 'graphe', 'BFS', 'DFS', 'Dijkstra', 'heapq']
});
reg({
cours: { id: 'backend', match: /Laravel|MVC|API REST|OpenAPI|Authentification : sessions|OAuth|PHP : bases|Framework back|Déployer une application web|Sécurité web|CSP|CORS|Tests : unitaires, intégration/i,
titre: 'Développement web back-end : MVC avec Laravel, API REST, authentification et sécurité', duree: '',
niveau: 'BTS SIO SLAM · BUT Informatique · licence pro · Master',
prerequis: ['Le Web : HTTP et HTML', 'SQL', 'Git'],
competences: ['BTS SIO SLAM · Bloc 2 · Concevoir et développer une solution applicative', 'BTS SIO · Bloc 3 · Cybersécurité des services', 'BUT Info · Réaliser et déployer'],
intro: 'Une application web professionnelle se compose d’une interface, d’une logique métier, d’une base de données et d’une API, le tout sécurisé et testé. Les frameworks comme Laravel, sur lequel est construit Syllabus, apportent une organisation éprouvée et des protections intégrées. Ce module présente l’architecture MVC, le routage, l’ORM, la conception d’une API REST, l’authentification, les protections contre les failles courantes et le déploiement.',
cm: [
{ t: 'Architecture MVC', d: '', html: 'Le motif <b>Modèle-Vue-Contrôleur</b> sépare les responsabilités. Le <b>modèle</b> représente les données et les règles métier (un élève, une classe, un devoir). La <b>vue</b> présente les données (templates Blade). Le <b>contrôleur</b> reçoit la requête, appelle le modèle et choisit la vue ou la réponse. Le <b>routeur</b> associe chaque URL et méthode HTTP à une action. Cette séparation rend le code testable et permet à plusieurs développeurs de travailler en parallèle.', exemple: '// routes/web.php\nRoute::middleware("auth")->group(function () {\n Route::get("/classes/{classe}", [ClasseController::class, "show"])->name("classes.show");\n});\n\n// app/Http/Controllers/ClasseController.php\npublic function show(Classe $classe) {\n $this->authorize("view", $classe);\n return view("classes.show", [\n "classe" => $classe->load("eleves"),\n ]);\n}' },
{ t: 'Modèles, ORM et migrations', d: '', html: 'L’<b>ORM</b> Eloquent associe chaque table à une classe : <code>Eleve::where("classe_id", 3)->orderBy("nom")->get()</code> produit une requête SQL <b>préparée</b>. Les <b>relations</b> (hasMany, belongsTo, belongsToMany) décrivent les clés étrangères. Les <b>migrations</b> versionnent le schéma de la base avec le code : chaque modification est un fichier, appliqué dans le même ordre partout. Piège de performance classique : le problème <b>N+1</b>, une requête par élément d’une liste, évité par le chargement anticipé (<code>with("eleves")</code>).' },
{ t: 'Concevoir une API REST', d: '', html: 'Une API <b>REST</b> expose des <b>ressources</b> identifiées par des URL et manipulées par les méthodes HTTP : <code>GET /api/devoirs</code> liste, <code>POST /api/devoirs</code> crée, <code>GET /api/devoirs/12</code> lit, <code>PUT</code> ou <code>PATCH</code> modifie, <code>DELETE</code> supprime. Les réponses sont en <b>JSON</b> avec des codes cohérents : 200, 201 Created, 204 No Content, 400 et 422 pour une requête invalide, 401 non authentifié, 403 interdit, 404 introuvable. On <b>versionne</b> l’API (/api/v1), on pagine les listes, et on la documente avec <b>OpenAPI</b>, qui génère une documentation interactive et des clients.' },
{ t: 'Authentification et autorisation', d: '', html: 'L’<b>authentification</b> établit qui est l’utilisateur ; l’<b>autorisation</b> décide de ce qu’il peut faire. Pour une application web, une <b>session</b> côté serveur, identifiée par un cookie <code>HttpOnly</code>, <code>Secure</code> et <code>SameSite</code>, reste la solution la plus sûre. Pour une API consommée par d’autres applications, on utilise des <b>jetons</b> (tokens d’API, Laravel Sanctum) ou <b>OAuth 2</b> / <b>OpenID Connect</b> pour déléguer l’authentification à un fournisseur d’identité (ENT, EduConnect). Les mots de passe sont hachés avec bcrypt ou Argon2. L’autorisation se centralise dans des <b>politiques</b> (policies) : un professeur ne voit que ses classes.' },
{ t: 'Sécurité : les failles courantes', d: '', html: 'L’<b>OWASP Top 10</b> recense les failles les plus fréquentes. <b>Contrôle d’accès défaillant</b> : changer un identifiant dans l’URL donne accès aux données d’un autre (IDOR) ; on vérifie l’autorisation à chaque action. <b>Injection</b> : requêtes préparées systématiques. <b>XSS</b> : Blade échappe automatiquement <code>{{ }}</code> ; <code>{!! !!}</code> ne s’utilise que sur du contenu maîtrisé. <b>CSRF</b> : un jeton dans chaque formulaire (<code>@csrf</code>). <b>Assignation de masse</b> : déclarer les champs modifiables (<code>$fillable</code>). À quoi s’ajoutent les <b>en-têtes</b> : Content-Security-Policy contre l’injection de scripts, CORS limité aux origines autorisées, HSTS.', attention: 'Le mode débogage (APP_DEBUG=true) en production affiche les traces d’erreur, les variables d’environnement et parfois les mots de passe de la base. Il doit être désactivé, et le fichier .env ne doit jamais être accessible par le web ni versionné.' },
{ t: 'Tester et déployer', d: '', html: 'Les <b>tests unitaires</b> vérifient une classe isolée, les <b>tests de fonctionnalité</b> envoient de vraies requêtes à l’application (<code>$this->actingAs($prof)->get("/classes/3")->assertOk()</code>), les tests de <b>bout en bout</b> pilotent un navigateur. Le déploiement suit toujours les mêmes étapes, automatisées par la CI : récupération du code, <code>composer install --no-dev</code>, migrations, mise en cache de la configuration et des routes, redémarrage des workers de file d’attente, derrière Nginx et PHP-FPM, avec un retour arrière possible.' }
],
synthese: ['MVC : modèle, vue, contrôleur, routeur', 'ORM et migrations ; éviter le N+1 avec with()', 'REST : ressources, méthodes, codes HTTP, JSON, versionnement, OpenAPI', 'Session sécurisée ou jetons ; OAuth 2 / OIDC ; policies pour l’autorisation', 'OWASP : contrôle d’accès, injection, XSS, CSRF, assignation de masse ; en-têtes', 'APP_DEBUG=false, .env protégé ; tests et déploiement automatisés'],
qcm: [
['Dans MVC, qui reçoit la requête et choisit la réponse ?', ['Le modèle', 'Le contrôleur', 'La vue', 'La base'], 1, 'Il orchestre.'],
['Quelle méthode pour créer une ressource en REST ?', ['GET', 'POST', 'DELETE', 'HEAD'], 1, 'Réponse 201.'],
['Code HTTP pour « authentifié mais pas autorisé » ?', ['401', '403', '404', '500'], 1, '401 = non authentifié.'],
['Qu’est-ce que le problème N+1 ?', ['Une faille', 'Une requête SQL par élément d’une liste', 'Un code HTTP', 'Une erreur de migration'], 1, 'Chargement anticipé.'],
['Protection contre le CSRF ?', ['Un jeton dans chaque formulaire', 'HTTPS seul', 'Un captcha', 'Le hachage'], 0, '@csrf.'],
['Pourquoi désactiver APP_DEBUG en production ?', ['Performance', 'Il expose des informations sensibles', 'Obligation légale', 'Pour le cache'], 1, 'Traces, variables, secrets.']
],
td: [
{ n: 'Application', c: 'Concevoir les routes REST de la ressource « devoirs » d’une classe, avec les codes de réponse attendus.', r: 'GET /api/v1/classes/{c}/devoirs 200 · POST 201 · GET /devoirs/{d} 200/404 · PATCH 200/422 · DELETE 204 · 401 sans jeton, 403 si pas professeur de la classe.' },
{ n: 'Application', c: 'Corriger la faille : « public function show($id) { return Eleve::find($id); } ».', r: 'Liaison de modèle + autorisation : show(Eleve $eleve) { $this->authorize("view", $eleve); return $eleve; } avec une policy vérifiant que l’élève appartient à une classe du professeur.' },
{ n: 'Approfondissement', c: 'Écrire un test de fonctionnalité qui vérifie qu’un professeur ne peut pas voir la fiche d’un élève d’une autre classe.', r: '$prof = User::factory()->prof()->create(); $eleve = Eleve::factory()->create(); $this->actingAs($prof)->get(route("eleves.show", $eleve))->assertForbidden();' }
],
tp: { type: 'IDE', url: 'apercu-ide.html', desc: 'Mini-application Laravel de gestion de devoirs.', etapes: ['Migration et modèle', 'Contrôleur et vues', 'API REST', 'Policy d’autorisation', 'Tests de fonctionnalité', 'Déploiement par CI'] },
plusLoin: [['Documentation Laravel', 'https://laravel.com/docs'], ['OWASP Top 10', 'https://owasp.org/Top10/fr/'], ['OpenAPI Specification', 'https://spec.openapis.org/oas/latest.html'], ['ANSSI · Recommandations pour la mise en œuvre d’un site web', 'https://cyber.gouv.fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Un élève signale qu’en modifiant le numéro dans l’adresse de sa page de résultats, il voit les notes d’un camarade.', etapes: [
['Reproduire', 'Connecté comme élève 1204 : GET /resultats/1205 → 200 OK, notes de l’élève 1205.\n→ Faille de contrôle d’accès (IDOR).'],
['Lire le code', 'public function show($id) { return view("resultats", ["eleve" => Eleve::findOrFail($id)]); }\n→ Aucune vérification que l’élève connecté est celui demandé.'],
['Corriger', 'Policy EleveResultatsPolicy : un élève voit ses résultats, un professeur ceux de ses classes, la vie scolaire tout l’établissement.\npublic function show(Eleve $eleve) { $this->authorize("voirResultats", $eleve); … }\n→ Réponse 403 pour un autre élève.'],
['Tester et vérifier les autres routes', '→ Test automatisé ajouté ; revue de toutes les routes prenant un identifiant : deux autres corrigées (exports, devoirs rendus).'],
['Traiter les conséquences', '→ Analyse des journaux d’accès : consultations anormales par deux comptes. Information du DPO, qui évalue la notification à la CNIL et l’information des familles concernées.']
] },
erreurs: ['Contrôler l’accès seulement dans l’interface', 'Requêtes SQL construites par concaténation', 'Utiliser {!! !!} sur une saisie utilisateur', 'APP_DEBUG=true en production', 'Pas de tests d’autorisation', 'Codes HTTP incohérents dans l’API'],
memo: [['Commande / élément', 'Usage'], ['php artisan route:list', 'lister les routes'], ['php artisan make:model -mcr Devoir', 'modèle, migration, contrôleur'], ['php artisan migrate', 'appliquer les migrations'], ['php artisan make:policy', 'politique d’autorisation'], ['$this->authorize()', 'vérifier un droit'], ['with("relation")', 'éviter le N+1'], ['@csrf', 'jeton de formulaire'], ['php artisan test', 'lancer les tests'], ['php artisan config:cache route:cache', 'optimiser en production'], ['curl -H "Authorization: Bearer …" /api/v1/…', 'tester une API']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer MVC sur un exemple. (3 pts)', 'Concevoir une API REST. (5 pts)', 'Écrire un contrôleur avec autorisation. (5 pts)', 'Identifier et corriger trois failles dans un code fourni. (5 pts)', 'Décrire un déploiement. (2 pts)'], r: ['voir cours', 'ressources et codes', 'authorize + policy', 'IDOR, XSS, injection', 'composer, migrate, cache'] },
prof: { deroule: [['Ouverture', 'Démonstration d’une faille IDOR sur une application de lab'], ['Cours', 'MVC et routage'], ['Pratique', 'Modèle et contrôleur'], ['Cours', 'API et sécurité'], ['Pratique', 'Policies et tests'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : squelette fourni. Élèves avancés : OAuth 2, files d’attente, OpenAPI.', eval: 'Application testée, sujet type.' } },
exemples: [
['Lister les routes', 'Audit d’une application.', 'php artisan route:list --path=api', 'GET|HEAD api/v1/devoirs ...... devoirs.index › DevoirController@index\nPOST api/v1/devoirs ...... devoirs.store › DevoirController@store\nDELETE api/v1/devoirs/{devoir} devoirs.destroy', 'On vérifie que chaque route sensible a un middleware d’authentification.'],
['Détecter un N+1', 'Page lente.', 'DB::enableQueryLog(); $c = Classe::all(); foreach ($c as $x) $x->eleves->count(); count(DB::getQueryLog())', '31', '31 requêtes pour 30 classes. Avec Classe::withCount("eleves")->get(), une seule.'],
['Tester une API', 'Création d’un devoir.', 'curl -s -X POST https://syllabus.local/api/v1/devoirs -H "Authorization: Bearer $T" -H "Content-Type: application/json" -d \'{"titre":"DM 3"}\'', '{"message":"The date limite field is required.","errors":{"date_limite":["…"]}} (HTTP 422)', 'La validation renvoie 422 avec le détail des champs manquants.'],
['Test d’autorisation', 'Non-régression.', 'php artisan test --filter=ResultatsTest', 'PASS Tests\\Feature\\ResultatsTest\n✓ un élève voit ses résultats\n✓ un élève ne voit pas ceux d’un camarade', 'Le test échouerait si la faille réapparaissait.'],
['Vérifier les en-têtes', 'Audit de sécurité.', 'curl -sI https://syllabus.local | grep -iE "content-security|set-cookie"', 'content-security-policy: default-src \'self\'; frame-ancestors \'none\'\nset-cookie: syllabus_session=…; secure; httponly; samesite=lax', 'CSP restrictive et cookie de session protégé.'],
['Déploiement', 'Mise en production.', 'composer install --no-dev -o && php artisan migrate --force && php artisan optimize && php artisan queue:restart', 'INFO Running migrations. 2026_10_01_create_rendus_table ....... DONE\nINFO Caching framework bootstrap, configuration, and metadata.', 'Chaque étape est dans le pipeline CI, avec retour arrière en cas d’échec.']
],
termes: ['MVC', 'modèle', 'vue', 'contrôleur', 'routeur', 'Laravel', 'Blade', 'Eloquent', 'ORM', 'migration', 'N+1', 'API REST', 'JSON', 'OpenAPI', 'session', 'jeton', 'OAuth 2', 'OpenID Connect', 'policy', 'OWASP', 'IDOR', 'XSS', 'CSRF', 'CSP', 'CORS', 'APP_DEBUG']
});
reg({
cours: { id: 'js', match: /JavaScript|TypeScript|DOM|fetch|Accessibilité web|RGAA|WCAG|Performance web/i,
titre: 'JavaScript et TypeScript côté navigateur : DOM, asynchrone, fetch, accessibilité', duree: '',
niveau: 'NSI Première · BTS SIO SLAM · BUT Informatique · Titre pro DWWM',
prerequis: ['Le Web : HTML et CSS', 'Python : bases de la programmation'],
competences: ['NSI · Interactions dans le navigateur', 'BTS SIO SLAM · Développer une interface', 'DWWM · Développer la partie front-end'],
intro: 'Tout ce qui réagit dans une page web, du bouton « Lever la main » au terminal xterm.js des labs, est écrit en JavaScript. Le langage s’exécute dans le navigateur, manipule la page, dialogue avec le serveur sans la recharger et doit rester utilisable par tous, y compris au clavier et avec un lecteur d’écran. Ce module présente le langage moderne, le DOM, la programmation asynchrone avec fetch, TypeScript et les exigences d’accessibilité et de performance.',
cm: [
{ t: 'Le langage moderne', d: '', html: 'On déclare avec <code>const</code> (par défaut) et <code>let</code> ; <code>var</code> est à éviter. Les types de base : nombres, chaînes, booléens, <code>null</code>, <code>undefined</code>, objets et tableaux. Les <b>fonctions fléchées</b> (<code>x => x * 2</code>), la <b>déstructuration</b> (<code>const { nom, classe } = eleve</code>), les <b>gabarits</b> de chaînes (<code>`Bonjour ${nom}`</code>) et les méthodes de tableaux (<code>map</code>, <code>filter</code>, <code>reduce</code>, <code>find</code>) forment le socle quotidien. On compare avec <code>===</code>, qui ne convertit pas les types (<code>"1" == 1</code> est vrai, <code>"1" === 1</code> est faux). Les <b>modules</b> (<code>import</code> / <code>export</code>) organisent le code.' },
{ t: 'Le DOM et les événements', d: '', html: 'Le <b>DOM</b> est la représentation de la page sous forme d’arbre d’objets. On sélectionne un élément (<code>document.querySelector("#liste")</code>), on modifie son contenu (<code>textContent</code>), ses classes (<code>classList.add</code>) ou ses attributs, on en crée (<code>createElement</code>). On réagit aux actions de l’utilisateur avec <code>addEventListener("click", …)</code>. La <b>délégation d’événements</b> place un seul écouteur sur un parent pour gérer de nombreux enfants, y compris ceux ajoutés plus tard.', attention: 'innerHTML avec une donnée saisie par un utilisateur ouvre une faille XSS : le texte peut contenir un script. On utilise textContent pour du texte, ou on échappe explicitement.' },
{ t: 'Asynchrone : promesses et async/await', d: '', html: 'Le navigateur n’exécute qu’un seul fil de JavaScript ; une opération longue (requête réseau, minuterie) ne doit pas le bloquer. Elle renvoie une <b>promesse</b>, résolue plus tard. La syntaxe <code>async</code> / <code>await</code> permet d’écrire ce code de façon lisible, et <code>try</code> / <code>catch</code> gère les erreurs. <code>Promise.all</code> lance plusieurs requêtes en parallèle. La <b>boucle d’événements</b> exécute les rappels quand le fil principal est libre.' },
{ t: 'Dialoguer avec le serveur : fetch', d: '', html: '<code>fetch</code> envoie une requête HTTP et renvoie une promesse de réponse. Il ne rejette la promesse qu’en cas d’erreur réseau : une réponse 404 ou 500 est une réponse « réussie », il faut tester <code>response.ok</code>. On envoie du JSON avec l’en-tête Content-Type et on joint le jeton CSRF ou d’authentification. Pour des mises à jour en temps réel (mains levées, présences du quiz), on utilise les <b>WebSockets</b> ou les Server-Sent Events plutôt qu’interroger le serveur toutes les secondes.', exemple: 'async function leverLaMain(labId) {\n const r = await fetch(`/api/v1/labs/${labId}/mains`, {\n method: "POST",\n headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": jeton },\n body: JSON.stringify({ etape: 4 }),\n });\n if (!r.ok) throw new Error(`Erreur ${r.status}`);\n const { position } = await r.json();\n document.querySelector("#position").textContent = `Vous êtes ${position}e dans la file.`;\n}' },
{ t: 'TypeScript', d: '', html: '<b>TypeScript</b> ajoute un typage statique à JavaScript : on déclare la forme des données (<code>interface Eleve { id: number; nom: string; classe?: string }</code>) et le compilateur signale les erreurs avant l’exécution : propriété mal orthographiée, valeur possiblement undefined, mauvais type d’argument. Le code est compilé en JavaScript standard. Sur un projet de plusieurs milliers de lignes, il réduit fortement les régressions et rend l’éditeur beaucoup plus utile (complétion, renommage sûr).' },
{ t: 'Accessibilité et performance', d: '', html: 'Un site public en France doit respecter le <b>RGAA</b>, fondé sur les normes <b>WCAG</b>. Les règles de base : utiliser les bons éléments HTML (un <code>&lt;button&gt;</code> plutôt qu’un <code>&lt;div&gt;</code> cliquable, qui ne se commande pas au clavier), des étiquettes de formulaire liées, des textes alternatifs, un contraste suffisant (4,5:1 pour le texte), un focus visible, et <code>aria-live</code> pour annoncer les mises à jour dynamiques. Côté <b>performance</b> : réduire le JavaScript chargé, différer ce qui n’est pas indispensable (<code>defer</code>, chargement à la demande), optimiser les images, mettre en cache. Les outils de développement du navigateur et Lighthouse mesurent les deux.' }
],
synthese: ['const/let, fonctions fléchées, déstructuration, ===, modules', 'DOM : querySelector, textContent, addEventListener, délégation', 'Jamais innerHTML avec une saisie utilisateur', 'Promesses, async/await, try/catch, Promise.all', 'fetch : tester response.ok ; WebSockets pour le temps réel', 'TypeScript : interfaces et vérification avant exécution', 'RGAA/WCAG : éléments natifs, clavier, contraste, aria-live ; performance'],
qcm: [
['Que vaut "1" === 1 ?', ['true', 'false', 'undefined', 'erreur'], 1, 'Pas de conversion de type.'],
['Quelle propriété insérer pour afficher un texte saisi par l’utilisateur ?', ['innerHTML', 'textContent', 'outerHTML', 'eval'], 1, 'Pas d’interprétation HTML.'],
['Quand fetch rejette-t-il sa promesse ?', ['Sur un 404', 'Sur un 500', 'En cas d’erreur réseau', 'Jamais'], 2, 'Tester response.ok.'],
['À quoi sert await ?', ['Bloquer le navigateur', 'Attendre le résultat d’une promesse dans une fonction async', 'Créer une boucle', 'Déclarer une variable'], 1, 'Sans bloquer le fil principal.'],
['Pourquoi préférer <button> à une <div> cliquable ?', ['Esthétique', 'Accessible au clavier et aux lecteurs d’écran', 'Plus rapide', 'Obligatoire en HTML5'], 1, 'Comportement natif.'],
['Quel est l’apport principal de TypeScript ?', ['Plus rapide à l’exécution', 'Détecter des erreurs de types avant l’exécution', 'Remplacer HTML', 'Chiffrer le code'], 1, 'Typage statique.']
],
td: [
{ n: 'Application', c: 'À partir d’un tableau d’élèves, afficher dans une liste ceux dont la moyenne est inférieure à 10, triés par moyenne, avec textContent.', r: 'eleves.filter(e => e.moyenne < 10).sort((a, b) => a.moyenne - b.moyenne).forEach(e => { const li = document.createElement("li"); li.textContent = `${e.nom} : ${e.moyenne}`; liste.append(li); });' },
{ n: 'Application', c: 'Charger en parallèle la liste des classes et celle des devoirs, et afficher un message d’erreur accessible en cas d’échec.', r: 'try { const [c, d] = await Promise.all([fetch("/api/v1/classes"), fetch("/api/v1/devoirs")]); if (!c.ok || !d.ok) throw … } catch (e) { zone.textContent = "Chargement impossible"; } avec zone en aria-live="polite".' },
{ n: 'Approfondissement', c: 'Auditer l’accessibilité d’un formulaire fourni et proposer cinq corrections.', r: 'label lié à chaque champ · message d’erreur associé (aria-describedby) · bouton réel · contraste · ordre de tabulation et focus visible.' }
],
tp: { type: 'IDE web', url: 'apercu-ide.html', desc: 'Page interactive avec API simulée.', etapes: ['Manipuler le DOM', 'Gérer les événements par délégation', 'Appeler une API avec fetch', 'Gérer les erreurs', 'Typer avec TypeScript', 'Audit Lighthouse accessibilité'] },
plusLoin: [['MDN · JavaScript (fr)', 'https://developer.mozilla.org/fr/docs/Web/JavaScript'], ['TypeScript Handbook', 'https://www.typescriptlang.org/docs/'], ['RGAA 4', 'https://accessibilite.numerique.gouv.fr/'], ['freeCodeCamp · JavaScript', 'https://www.freecodecamp.org/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Le bouton « Lever la main » affiche parfois « Vous êtes undefinede dans la file » et, pour un élève malvoyant, ne produit aucune annonce.', etapes: [
['Reproduire', 'Console : POST /api/v1/labs/7/mains → 419\n→ Le serveur refuse la requête (jeton CSRF expiré) mais le code lit quand même le JSON.'],
['Lire le code', 'const r = await fetch(…); const { position } = await r.json();\n→ Aucun test de r.ok : la réponse d’erreur n’a pas de champ position.'],
['Corriger la logique', 'if (!r.ok) { afficherErreur(r.status === 419 ? "Session expirée, rechargez la page." : "Envoi impossible."); return; }\n→ Message clair selon l’erreur.'],
['Corriger l’accessibilité', 'Zone de message : <p id="position" aria-live="polite"></p> et bouton réel avec aria-pressed.\n→ Le lecteur d’écran annonce la position et l’état du bouton.'],
['Tester', '→ Test automatisé du cas 419 et vérification avec NVDA. Ajout du typage TypeScript de la réponse (interface ReponseMain { position: number }).']
] },
erreurs: ['== au lieu de ===', 'innerHTML avec des données utilisateur', 'Ne pas tester response.ok', 'Oublier await', 'Div cliquable au lieu d’un bouton', 'Mises à jour non annoncées aux lecteurs d’écran', 'Charger tout le JavaScript au démarrage'],
memo: [['Élément', 'Usage'], ['document.querySelector(sel)', 'sélectionner'], ['el.textContent = …', 'afficher du texte sûr'], ['el.addEventListener("click", f)', 'réagir'], ['async / await / try…catch', 'asynchrone'], ['fetch(url, { method, headers, body })', 'requête HTTP'], ['response.ok · response.json()', 'lire la réponse'], ['Promise.all([...])', 'requêtes parallèles'], ['aria-live="polite"', 'annoncer une mise à jour'], ['<script type="module" defer>', 'charger sans bloquer'], ['tsc --noEmit', 'vérifier les types']],
examen: { titre: 'Sujet type · 20 points', q: ['Manipuler le DOM pour un besoin donné. (5 pts)', 'Écrire un appel fetch avec gestion d’erreur. (5 pts)', 'Expliquer la faille XSS liée à innerHTML. (3 pts)', 'Typer des données avec TypeScript. (3 pts)', 'Corriger quatre problèmes d’accessibilité. (4 pts)'], r: ['querySelector, createElement, textContent', 'async, ok, json, catch', 'interprétation du HTML', 'interface', 'label, bouton, contraste, aria-live'] },
prof: { deroule: [['Ouverture', 'Naviguer sur un site au clavier seul'], ['Cours', 'Langage et DOM'], ['Pratique', 'Liste dynamique'], ['Cours', 'Asynchrone et fetch'], ['Pratique', 'Appel d’API'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : exercices guidés MDN. Élèves avancés : TypeScript strict, WebSockets.', eval: 'Page rendue et auditée, sujet type.' } },
exemples: [
['Égalité stricte', 'Bug de comparaison.', 'console.log("0" == false, "0" === false)', 'true false', '== convertit les types de façon surprenante ; on utilise toujours ===.'],
['Filtrer et trier', 'Élèves en difficulté.', 'eleves.filter(e => e.moyenne < 10).map(e => e.nom)', "[ 'Martin', 'Petit' ]", 'Deux élèves sous la moyenne.'],
['Réponse d’erreur', 'Diagnostic dans la console.', 'const r = await fetch("/api/v1/devoirs/999"); console.log(r.ok, r.status)', 'false 404', 'La promesse est résolue même en cas de 404 : il faut tester ok.'],
['Requêtes parallèles', 'Accélérer le chargement.', 'console.time("p"); await Promise.all([fetch(a), fetch(b)]); console.timeEnd("p")', 'p: 212 ms (séquentiel : 405 ms)', 'Les deux requêtes partent en même temps.'],
['Vérifier les types', 'Avant la mise en production.', 'npx tsc --noEmit', "src/main.ts:14:23 - error TS2339: Property 'posiiton' does not exist on type 'ReponseMain'.", 'Faute de frappe détectée avant l’exécution.'],
['Audit d’accessibilité', 'Page de connexion.', 'npx lighthouse https://syllabus.local/connexion --only-categories=accessibility --quiet', 'Accessibility: 82\n- Form elements do not have associated labels\n- Background and foreground colors do not have a sufficient contrast ratio', 'Deux corrections prioritaires : étiquettes et contraste.']
],
termes: ['JavaScript', 'const', 'let', '===', 'fonction fléchée', 'DOM', 'querySelector', 'textContent', 'innerHTML', 'addEventListener', 'délégation', 'promesse', 'async', 'await', 'fetch', 'response.ok', 'JSON', 'WebSocket', 'TypeScript', 'interface', 'RGAA', 'WCAG', 'aria-live', 'Lighthouse']
});
reg({
cours: { id: 'langc', match: /C : bases|pointeurs|compilation|make|Java ou C#|Sockets réseau/i,
titre: 'Langage C : compilation, pointeurs, mémoire et sockets réseau', duree: '',
niveau: 'BTS CIEL IR · BUT R&T · BUT Informatique · école d’ingénieurs',
prerequis: ['Algorithmique de base', 'Binaire et hexadécimal', 'Architecture des ordinateurs'],
competences: ['BTS CIEL · C05 · Développer un logiciel embarqué ou réseau', 'BUT R&T · Programmer des applications réseau', 'BUT Info · Réaliser un développement proche du système'],
intro: 'Le noyau Linux, les pilotes, les micrologiciels des objets connectés et une grande partie des outils réseau sont écrits en C. C’est un langage proche de la machine : il donne un contrôle total sur la mémoire, au prix d’une responsabilité totale. Comprendre le C, c’est comprendre ce que font réellement les langages de plus haut niveau, et pourquoi tant de failles de sécurité viennent de débordements de mémoire. Ce module couvre la compilation, les types, les pointeurs, l’allocation, les outils de détection d’erreurs et la programmation réseau avec les sockets.',
cm: [
{ t: 'De la source à l’exécutable', d: '', html: 'Un programme C passe par plusieurs étapes. Le <b>préprocesseur</b> traite les directives (<code>#include</code>, <code>#define</code>). Le <b>compilateur</b> traduit chaque fichier .c en fichier objet .o (code machine). L’<b>éditeur de liens</b> assemble les objets et les bibliothèques en un exécutable. <code>gcc -Wall -Wextra -g -o prog main.c</code> compile avec les avertissements et les informations de débogage. Un <b>Makefile</b> décrit les dépendances pour ne recompiler que ce qui a changé.', exemple: '#include <stdio.h>\n\nint main(void) {\n int notes[] = {12, 15, 9, 18};\n int n = sizeof notes / sizeof notes[0];\n int total = 0;\n for (int i = 0; i < n; i++) total += notes[i];\n printf("Moyenne : %.2f\\n", (double) total / n);\n return 0;\n}' },
{ t: 'Types, mémoire et tableaux', d: '', html: 'Chaque type occupe une taille fixe (<code>char</code> 1 octet, <code>int</code> généralement 4, <code>double</code> 8), consultable avec <code>sizeof</code> ; <code>stdint.h</code> fournit des types de taille garantie (<code>uint8_t</code>, <code>int32_t</code>) indispensables pour les protocoles et l’embarqué. Un <b>tableau</b> est une zone contiguë ; le C ne vérifie <b>jamais</b> les bornes : écrire en dehors corrompt la mémoire voisine. Une <b>chaîne</b> est un tableau de char terminé par <code>\'\\0\'</code> : oublier la place de ce caractère est une erreur classique.' },
{ t: 'Pointeurs', d: '', html: 'Un <b>pointeur</b> contient une <b>adresse mémoire</b>. <code>&amp;x</code> donne l’adresse de x, <code>*p</code> la valeur pointée. Les pointeurs permettent de passer une variable à une fonction pour qu’elle la modifie (passage par adresse), de parcourir un tableau (le nom d’un tableau se convertit en pointeur vers son premier élément) et de manipuler des structures allouées dynamiquement. Un pointeur non initialisé ou <code>NULL</code> déréférencé provoque une erreur de segmentation (segfault).', exemple: 'void echanger(int *a, int *b) {\n int tmp = *a;\n *a = *b;\n *b = tmp;\n}\n/* appel : echanger(&x, &y); */' },
{ t: 'Allocation dynamique', d: '', html: 'Les variables locales vivent sur la <b>pile</b> et disparaissent à la fin de la fonction. Pour une taille connue seulement à l’exécution, on alloue sur le <b>tas</b> avec <code>malloc</code> (en vérifiant le retour NULL) et on libère avec <code>free</code>. Trois familles d’erreurs dominent : la <b>fuite</b> (oublier free), l’<b>utilisation après libération</b> (use after free) et le <b>débordement de tampon</b> (buffer overflow), à l’origine d’une grande partie des failles historiques. <b>Valgrind</b> et les sanitizers (<code>-fsanitize=address</code>) les détectent à l’exécution.', attention: 'Les fonctions gets et strcpy n’ont aucune limite de taille : on utilise fgets et snprintf. Les agences de sécurité recommandent désormais les langages sûrs en mémoire (Rust, Go) pour les nouveaux développements critiques.' },
{ t: 'Structures et modularité', d: '', html: 'Une <b>struct</b> regroupe des champs de types différents : <code>struct eleve { char nom[40]; float moyenne; };</code>. On accède aux champs avec <code>.</code>, ou <code>-></code> à travers un pointeur. Un programme se découpe en modules : un fichier d’en-tête .h déclare les fonctions et les types, le fichier .c les implémente. Les tableaux de structures et les listes chaînées de structures sont la base de nombreux programmes système.' },
{ t: 'Programmation réseau : sockets', d: '', html: 'L’API des <b>sockets</b> BSD, disponible en C sur tous les systèmes, est celle qu’utilisent tous les serveurs réseau. Un serveur TCP enchaîne <code>socket</code>, <code>bind</code> (adresse et port), <code>listen</code>, <code>accept</code> (bloquant, renvoie une socket par client), puis <code>recv</code> et <code>send</code>. Un client fait <code>socket</code> puis <code>connect</code>. Les nombres transmis doivent être convertis dans l’<b>ordre réseau</b> (big-endian) avec <code>htons</code> et <code>htonl</code>. Pour UDP, <code>sendto</code> et <code>recvfrom</code> échangent des datagrammes sans connexion. On retrouve exactement les états TCP vus avec ss.', exemple: 'int s = socket(AF_INET, SOCK_STREAM, 0);\nstruct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(5000), .sin_addr.s_addr = htonl(INADDR_ANY) };\nbind(s, (struct sockaddr *)&a, sizeof a);\nlisten(s, 16);\nint c = accept(s, NULL, NULL);\nchar buf[256];\nssize_t n = recv(c, buf, sizeof buf - 1, 0);\nif (n > 0) { buf[n] = \'\\0\'; send(c, "OK\\n", 3, 0); }\nclose(c); close(s);' }
],
synthese: ['Préprocesseur → compilateur → éditeur de liens ; gcc -Wall -Wextra -g ; make', 'Tailles de types, stdint.h ; aucune vérification des bornes ; chaînes terminées par \\0', 'Pointeurs : &, *, passage par adresse, NULL', 'malloc/free : fuites, use after free, débordements ; Valgrind et sanitizers', 'struct, en-têtes .h et modules', 'Sockets : socket, bind, listen, accept, recv, send ; htons ; UDP avec sendto/recvfrom'],
qcm: [
['Quelle étape assemble les fichiers objets en exécutable ?', ['Préprocesseur', 'Compilateur', 'Éditeur de liens', 'Makefile'], 2, 'Linker.'],
['Que contient un pointeur ?', ['Une valeur entière quelconque', 'Une adresse mémoire', 'Un fichier', 'Une chaîne'], 1, 'Adresse.'],
['Taille nécessaire pour stocker « SIO1 » ?', ['4', '5', '8', '1'], 1, '4 caractères + \\0.'],
['Que fait free ?', ['Alloue', 'Libère la mémoire allouée par malloc', 'Initialise', 'Copie'], 1, 'À appeler une seule fois.'],
['Pourquoi htons ?', ['Chiffrer', 'Convertir en ordre d’octets réseau', 'Compresser', 'Vérifier'], 1, 'Big-endian.'],
['Quel outil détecte les fuites mémoire ?', ['gcc', 'Valgrind', 'make', 'gdb uniquement'], 1, 'Et -fsanitize=address.']
],
td: [
{ n: 'Application', c: 'Écrire une fonction qui calcule la moyenne d’un tableau d’entiers et renvoie -1 si le tableau est vide.', r: 'double moyenne(const int *t, size_t n) { if (n == 0) return -1; long s = 0; for (size_t i = 0; i < n; i++) s += t[i]; return (double) s / n; }' },
{ n: 'Application', c: 'Allouer dynamiquement un tableau de n structures eleve lu au clavier, puis le libérer. Vérifier avec Valgrind.', r: 'struct eleve *t = malloc(n * sizeof *t); if (!t) return 1; … free(t); · valgrind --leak-check=full ./prog → 0 bytes lost.' },
{ n: 'Approfondissement', c: 'Écrire un client TCP qui se connecte au serveur du cours, envoie « PING » et affiche la réponse, puis observer la connexion avec ss et tcpdump.', r: 'socket, connect sur 127.0.0.1:5000, send, recv · ss -tn state established ; tcpdump -ni lo port 5000 montre SYN, SYN-ACK, ACK puis PSH.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Compilation et débogage dans une VM Debian.', etapes: ['Compiler avec gcc et make', 'Pointeurs et passage par adresse', 'Allocation dynamique et Valgrind', 'Provoquer puis corriger un débordement avec -fsanitize=address', 'Serveur et client TCP', 'Observer l’échange avec tcpdump'] },
plusLoin: [['Beej’s Guide to Network Programming', 'https://beej.us/guide/bgnet/'], ['Documentation GCC', 'https://gcc.gnu.org/onlinedocs/'], ['ANSSI · Règles de programmation pour le développement sécurisé en C', 'https://cyber.gouv.fr/publications/regles-de-programmation-pour-le-developpement-securise-de-logiciels-en-langage-c']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Le petit serveur de collecte des capteurs de CO₂, écrit en C, plante de façon aléatoire après quelques heures.', etapes: [
['Reproduire', 'journalctl -u collecteur | tail -1\ncollecteur.service: Main process exited, code=killed, status=11/SEGV\n→ Erreur de segmentation.'],
['Compiler avec les sanitizers', 'gcc -g -fsanitize=address -o collecteur collecteur.c && ./collecteur\nERROR: AddressSanitizer: stack-buffer-overflow … WRITE of size 71 … in handle_message collecteur.c:48\n→ Écriture de 71 octets dans un tampon plus petit.'],
['Lire le code', 'char salle[32];\nstrcpy(salle, message + 5);\n→ Un nom de topic plus long que prévu déborde du tableau.'],
['Corriger', 'snprintf(salle, sizeof salle, "%s", message + 5);\n+ contrôle de la longueur et rejet des messages invalides.\n→ Plus aucun débordement, même avec des messages malformés.'],
['Prévenir', '→ Compilation systématique avec -Wall -Wextra -Werror en CI, tests avec -fsanitize=address et un jeu de messages malformés (fuzzing simple). Ce type de faille est exploitable à distance : correction déployée en priorité.']
] },
erreurs: ['Ignorer les avertissements du compilateur', 'Oublier la place du \\0', 'Utiliser gets ou strcpy', 'Ne pas vérifier le retour de malloc', 'Oublier free ou libérer deux fois', 'Oublier htons pour le port', 'Ne pas vérifier les retours de recv et send'],
memo: [['Commande / fonction', 'Usage'], ['gcc -Wall -Wextra -g -o p f.c', 'compiler'], ['make', 'recompiler selon les dépendances'], ['gdb ./p', 'déboguer'], ['valgrind --leak-check=full ./p', 'fuites et erreurs mémoire'], ['-fsanitize=address,undefined', 'détecter les débordements'], ['sizeof', 'taille d’un type ou d’un tableau'], ['malloc / free', 'allouer / libérer'], ['fgets · snprintf', 'lectures et copies bornées'], ['socket bind listen accept', 'serveur TCP'], ['htons · htonl · ntohs', 'ordre réseau']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire les étapes de compilation. (3 pts)', 'Écrire une fonction avec pointeurs. (4 pts)', 'Allouer et libérer une structure dynamique. (4 pts)', 'Identifier un débordement de tampon et le corriger. (4 pts)', 'Écrire les étapes d’un serveur TCP. (5 pts)'], r: ['préprocesseur, compilateur, éditeur de liens', 'passage par adresse', 'malloc, vérification, free', 'strcpy → snprintf', 'socket, bind, listen, accept, recv, send'] },
prof: { deroule: [['Ouverture', 'Même programme en Python et en C : temps et mémoire'], ['Cours', 'Compilation et types'], ['Pratique', 'Pointeurs'], ['Cours', 'Mémoire et sécurité'], ['Pratique', 'Sockets'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : schémas de mémoire. Élèves avancés : serveur multi-clients avec poll, comparaison avec Rust.', eval: 'Programmes sans erreur Valgrind, sujet type.' } },
exemples: [
['Compiler avec avertissements', 'Premier programme.', 'gcc -Wall -Wextra -o moyenne moyenne.c', 'moyenne.c:6:9: warning: unused variable ‘i’ [-Wunused-variable]', 'Les avertissements signalent souvent de vraies erreurs ; on les corrige tous.'],
['Tailles des types', 'Protocole binaire.', 'printf("%zu %zu %zu\\n", sizeof(char), sizeof(int), sizeof(double));', '1 4 8', 'Pour un format de trame, on préfère uint8_t, uint16_t, uint32_t, de taille garantie.'],
['Erreur de segmentation', 'Programme qui plante.', 'gdb -q ./prog -ex run -ex bt', 'Program received signal SIGSEGV, Segmentation fault.\n#0 afficher (e=0x0) at eleves.c:21', 'Le pointeur e vaut NULL à la ligne 21 : un retour de recherche non vérifié.'],
['Détecter une fuite', 'Contrôle qualité.', 'valgrind --leak-check=full ./prog | tail -3', '==4012== LEAK SUMMARY:\n==4012== definitely lost: 1,280 bytes in 32 blocks', '32 blocs jamais libérés : un free manquant dans la boucle.'],
['Tester le serveur TCP', 'Après compilation.', './serveur & echo "PING" | nc 127.0.0.1 5000', 'OK', 'Le serveur répond ; ss -tln montre LISTEN sur 0.0.0.0:5000.'],
['Ordre des octets', 'Port mal configuré.', 'printf("%u\\n", (unsigned) htons(5000));', '34835', '5000 = 0x1388 devient 0x8813 en mémoire sur une machine little-endian : sans htons, le serveur écouterait sur le port 34835.']
],
termes: ['préprocesseur', 'compilateur', 'éditeur de liens', 'gcc', 'Makefile', 'sizeof', 'stdint.h', 'pointeur', 'adresse', 'NULL', 'segfault', 'malloc', 'free', 'pile', 'tas', 'fuite mémoire', 'use after free', 'débordement de tampon', 'Valgrind', 'AddressSanitizer', 'struct', 'socket', 'bind', 'listen', 'accept', 'htons']
});
})();

View file

@ -0,0 +1,281 @@
// Cours complets · lot 11 : Ethernet et commutation, routage dynamique, Wi-Fi, transmission, QoS
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'ethernet', match: /Histoire et vocabulaire : LAN|topologies|Ethernet, adresses MAC|commutation|Spanning tree|STP/i,
titre: 'Réseaux locaux : topologies, Ethernet, commutation et Spanning Tree', duree: '',
niveau: 'SNT · Bac pro CIEL · BTS SIO / CIEL · BUT R&T S1 · TSSR',
prerequis: ['Modèle OSI et TCP/IP'],
competences: ['BTS SIO · Bloc 1 · Exploiter les éléments d’un réseau', 'BTS CIEL · C03 · Installer un réseau', 'BUT R&T · Compétence « Connecter »'],
intro: 'Avant les adresses IP et le routage, il y a les câbles, les prises murales, les switchs et les trames Ethernet qui circulent entre eux. C’est à ce niveau que se produisent les pannes les plus spectaculaires d’un réseau d’établissement : une boucle de câblage qui sature tout le bâtiment en quelques secondes, un switch qui ne retrouve plus les postes, un port bloqué en semi-duplex. Ce module présente le vocabulaire des réseaux, la trame Ethernet, le fonctionnement d’un switch et le protocole Spanning Tree qui protège contre les boucles.',
cm: [
{ t: 'Vocabulaire et topologies', d: '', html: 'Un <b>LAN</b> (réseau local) couvre un bâtiment ou un site ; un <b>WAN</b> (réseau étendu) relie des sites distants par des liaisons d’opérateur ; un <b>MAN</b> couvre une agglomération ; Internet est l’interconnexion de milliers de réseaux autonomes. La <b>topologie physique</b> décrit le câblage : en <b>étoile</b> (tous les postes reliés à un switch, la norme actuelle), en <b>étoile étendue</b> ou arbre (switchs d’étage reliés à un cœur de réseau), en <b>maillage</b> (liens redondants entre équipements de cœur). Le bus et l’anneau sont historiques. Dans un établissement, le câblage suit une organisation normalisée : prises murales, baies de brassage par étage (répartiteurs), liaisons montantes en fibre vers le local technique principal.' },
{ t: 'La trame Ethernet', d: '', html: 'Ethernet (IEEE 802.3) transporte les données en <b>trames</b>. Une trame contient l’adresse <b>MAC</b> de destination et de source (6 octets chacune), le champ <b>EtherType</b> (0x0800 pour IPv4, 0x86DD pour IPv6, 0x0806 pour ARP, 0x8100 pour un tag VLAN), les données (46 à 1 500 octets, la <b>MTU</b>), et une somme de contrôle <b>FCS</b> qui permet de détecter les erreurs de transmission. Une adresse MAC s’écrit en hexadécimal (3c:52:82:1a:0f:9b) ; les trois premiers octets (OUI) identifient le fabricant. L’adresse ff:ff:ff:ff:ff:ff est la <b>diffusion</b>.' },
{ t: 'Comment fonctionne un switch', d: '', html: 'Un switch <b>apprend</b> : à chaque trame reçue, il note l’adresse MAC source et le port d’arrivée dans sa <b>table d’adresses MAC</b> (table CAM). Pour transmettre, il cherche l’adresse de destination : si elle est connue, il envoie la trame sur ce seul port ; si elle est inconnue, ou s’il s’agit d’une diffusion, il l’<b>inonde</b> sur tous les ports sauf celui d’arrivée. Les entrées vieillissent (300 secondes par défaut). Chaque port forme son propre <b>domaine de collision</b> en duplex intégral : il n’y a plus de collisions sur un réseau commuté moderne. Tous les ports d’un même VLAN forment un <b>domaine de diffusion</b>.', attention: 'Un port négocié en semi-duplex face à un équipement en duplex intégral (souvent après un réglage manuel d’un seul côté) provoque des erreurs et des collisions tardives : débit effondré sans panne visible. On laisse l’autonégociation des deux côtés ou on force les deux.' },
{ t: 'Le danger des boucles', d: '', html: 'Pour la redondance, on relie souvent les switchs par plusieurs chemins. Mais Ethernet n’a pas de durée de vie comme le TTL d’IP : une trame de diffusion qui entre dans une boucle tourne indéfiniment et se multiplie à chaque switch. En quelques secondes, c’est la <b>tempête de diffusion</b> : tous les liens sont saturés, les processeurs des switchs à 100 %, les tables MAC instables, et plus rien ne fonctionne dans le bâtiment. La cause classique : un câble branché entre deux prises murales, ou un petit switch de bureau relié deux fois.' },
{ t: 'Spanning Tree', d: '', html: 'Le protocole <b>STP</b> (802.1D), et sa version rapide <b>RSTP</b> (802.1w), construit un arbre sans boucle à partir de la topologie redondante. Les switchs échangent des messages <b>BPDU</b>, élisent un <b>pont racine</b> (le plus petit identifiant : priorité puis adresse MAC), calculent pour chaque switch le meilleur chemin vers la racine, et <b>bloquent</b> les ports qui créeraient une boucle. Si un lien actif tombe, un port bloqué passe en transmission : avec RSTP, en une à deux secondes. On fixe soi-même la racine sur le switch de cœur (priorité basse) plutôt que de laisser l’élection au hasard. Sur les ports d’accès, <b>PortFast</b> (edge port) fait passer immédiatement le port en transmission, et <b>BPDU Guard</b> désactive le port si un switch y est branché.', exemple: 'Switch(config)# spanning-tree mode rapid-pvst\nSwitch(config)# spanning-tree vlan 1-4094 priority 4096\nSwitch(config)# interface range fa0/1 - 24\nSwitch(config-if-range)# spanning-tree portfast\nSwitch(config-if-range)# spanning-tree bpduguard enable\n\n# Linux : bridge avec STP\nip link add br0 type bridge stp_state 1\nbridge link show' },
{ t: 'Diagnostiquer la couche 2', d: '', html: 'Les compteurs d’interface révèlent les problèmes physiques : erreurs CRC (câble ou connecteur défectueux, perturbations), collisions tardives (duplex), pertes. <code>ethtool</code> sous Linux affiche la vitesse et le duplex négociés. La table MAC du switch indique où se trouve un poste ; si une même adresse MAC « bascule » sans cesse entre deux ports (MAC flapping), il y a une boucle ou un équipement en double. Pendant une tempête, on repère les ports dont le trafic de diffusion explose, et on les coupe un par un.' }
],
synthese: ['LAN, WAN ; topologie en étoile étendue avec cœur de réseau', 'Trame : MAC destination et source, EtherType, données (MTU 1 500), FCS', 'Le switch apprend les MAC source et inonde l’inconnu et la diffusion', 'Boucle = tempête de diffusion', 'STP/RSTP : pont racine, ports bloqués, BPDU ; racine fixée sur le cœur', 'PortFast et BPDU Guard sur les ports d’accès', 'CRC, duplex, MAC flapping : signatures des pannes de couche 2'],
qcm: [
['Que fait un switch d’une trame dont la MAC de destination est inconnue ?', ['Il la jette', 'Il l’inonde sur tous les ports sauf celui d’arrivée', 'Il l’envoie au routeur', 'Il demande par ARP'], 1, 'Inondation.'],
['Quel EtherType pour IPv4 ?', ['0x0806', '0x0800', '0x86DD', '0x8100'], 1, '0x0806 est ARP.'],
['Quelle est la cause d’une tempête de diffusion ?', ['Un DNS en panne', 'Une boucle de couche 2', 'Un mauvais masque', 'Un virus'], 1, 'Les trames tournent sans fin.'],
['Comment STP choisit-il le pont racine ?', ['Au hasard', 'Plus petit identifiant (priorité puis MAC)', 'Plus grand nombre de ports', 'Plus récent'], 1, 'On fixe la priorité.'],
['Rôle de BPDU Guard ?', ['Accélérer', 'Désactiver un port d’accès qui reçoit des BPDU', 'Chiffrer', 'Choisir la racine'], 1, 'Protège contre les switchs branchés par erreur.'],
['Des erreurs CRC sur un port indiquent plutôt :', ['un problème DNS', 'un câble ou un connecteur défectueux', 'un VLAN incorrect', 'une boucle'], 1, 'Couche physique.']
],
td: [
{ n: 'Échauffement', c: 'Remplir la table MAC d’un switch à quatre ports après les échanges : A (port 1) → B, B (port 2) → A, C (port 3) → diffusion.', r: 'Après A→B : A sur 1, trame inondée sur 2, 3, 4 · après B→A : B sur 2, envoi sur 1 seulement · après C diffusion : C sur 3, inondée sur 1, 2, 4.' },
{ n: 'Application', c: 'Trois switchs reliés en triangle. Le switch S1 a la priorité 4096, S2 et S3 la priorité par défaut 32768. Indiquer la racine, les ports racine et le port bloqué (liens de même coût, S2 a la plus petite MAC).', r: 'Racine S1 · ports racine : S2 et S3 vers S1 · sur le lien S2–S3, le port de S3 (identifiant le plus grand) est bloqué.' },
{ n: 'Approfondissement', c: 'Rédiger la procédure d’intervention face à une tempête de diffusion signalée dans un bâtiment.', r: 'Repérer le switch dont les ports ont un taux de diffusion anormal · déconnecter les liaisons d’accès une par une ou consulter les journaux STP · trouver le câble en boucle · rebrancher · activer BPDU Guard et le contrôle de tempête (storm-control) sur les ports d’accès.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Bridges Linux reliés en boucle.', etapes: ['Capturer des trames et lire les en-têtes', 'Observer l’apprentissage d’un bridge', 'Créer une boucle sans STP et constater', 'Activer STP et lire l’état des ports', 'Couper un lien et mesurer la reconvergence'] },
plusLoin: [['IEEE 802.3 Ethernet', 'https://www.ieee802.org/3/'], ['Cisco Networking Academy · Switching', 'https://www.netacad.com/'], ['man bridge (iproute2)', 'https://man7.org/linux/man-pages/man8/bridge.8.html']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Lundi 9 h 20 : tout le bâtiment B perd le réseau ; les voyants des switchs clignotent en permanence.', etapes: [
['Observer', 'show interfaces counters | include Gi0/1\nGi0/1 InBcastPkts 48 213 908\n→ Des dizaines de millions de trames de diffusion en quelques minutes : tempête.'],
['Localiser', 'show mac address-table | include 0015.5d0a.1107\n20 0015.5d0a.1107 DYNAMIC Fa0/12\n(une seconde plus tard) 20 0015.5d0a.1107 DYNAMIC Gi0/2\n→ La même adresse bascule entre deux ports : boucle entre Fa0/12 et la liaison montante.'],
['Intervenir', '→ shutdown sur Fa0/12 : le trafic redevient normal en quelques secondes. Sur place, un petit switch de bureau est relié à deux prises murales de la salle B112.'],
['Corriger', 'interface range fa0/1 - 24\n spanning-tree portfast\n spanning-tree bpduguard enable\n storm-control broadcast level 5\n→ Le prochain branchement en boucle désactivera automatiquement le port concerné.'],
['Documenter', '→ Compte rendu : cause, durée d’indisponibilité, configuration déployée sur tous les switchs d’accès, et rappel aux utilisateurs de ne pas brancher de switch personnel.']
] },
erreurs: ['Laisser l’élection du pont racine au hasard', 'Pas de BPDU Guard sur les ports d’accès', 'Forcer le duplex d’un seul côté', 'Câbles de brassage non étiquetés', 'Confondre domaine de collision et domaine de diffusion'],
memo: [['Commande', 'Usage'], ['show mac address-table', 'table MAC'], ['show spanning-tree', 'état STP et racine'], ['show interfaces counters errors', 'erreurs par port'], ['ethtool ens18', 'vitesse et duplex sous Linux'], ['ip -s link', 'compteurs Linux'], ['bridge link show', 'état des ports d’un bridge Linux'], ['bridge fdb show', 'table MAC d’un bridge'], ['tcpdump -eni ens18', 'voir les en-têtes Ethernet']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire une trame Ethernet. (4 pts)', 'Simuler l’apprentissage d’un switch. (4 pts)', 'Expliquer la tempête de diffusion et STP. (6 pts)', 'Déterminer l’arbre STP d’une topologie fournie. (4 pts)', 'Proposer la configuration des ports d’accès. (2 pts)'], r: ['MAC, EtherType, données, FCS', 'table MAC', 'boucle, BPDU, ports bloqués', 'racine, ports racine, bloqués', 'PortFast, BPDU Guard'] },
prof: { deroule: [['Ouverture', 'Visite de la baie de brassage'], ['Cours', 'Trame et switch'], ['Pratique', 'Capture et table MAC'], ['Cours', 'Boucles et STP'], ['Pratique', 'Boucle en lab'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : schéma à compléter. Élèves avancés : MSTP, agrégation de liens.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Voir les en-têtes Ethernet', 'Analyse de trames.', 'sudo tcpdump -eni ens18 -c 1 arp', 'bc:24:11:5e:2a:10 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 10.62.40.1 tell 10.62.40.20', 'Diffusion ARP, EtherType 0x0806.'],
['Vitesse et duplex', 'Poste lent.', 'ethtool ens18 | grep -E "Speed|Duplex"', 'Speed: 100Mb/s\nDuplex: Half', '100 Mbit/s en semi-duplex : câble abîmé ou négociation forcée. Un port gigabit sain affiche 1000Mb/s Full.'],
['Erreurs physiques', 'Coupures intermittentes.', 'ip -s link show ens18 | grep -A1 RX', 'RX: bytes packets errors dropped\n 9831022 71233 1842 0', '1 842 erreurs en réception : câble ou connecteur à remplacer.'],
['Table MAC d’un bridge', 'Où est ce poste ?', 'bridge fdb show br br0 | grep 52:54:00:12:34', '52:54:00:12:34:56 dev tap1 master br0', 'La VM est sur le port tap1.'],
['État STP', 'Après activation.', 'bridge -d link show | grep -E "tap|state"', '4: tap1 state forwarding\n5: tap2 state forwarding\n6: tap3 state blocking', 'Le port tap3 est bloqué : la boucle est neutralisée.'],
['Racine STP', 'Vérifier l’élection.', 'show spanning-tree vlan 20 | include Root', 'Root ID Priority 4116 Address 0015.c7a1.0001 This bridge is the root', 'Le switch de cœur est bien la racine.']
],
termes: ['LAN', 'WAN', 'topologie', 'étoile', 'Ethernet', 'trame', 'MAC', 'EtherType', 'FCS', 'MTU', 'switch', 'table MAC', 'inondation', 'domaine de collision', 'domaine de diffusion', 'duplex', 'boucle', 'tempête de diffusion', 'STP', 'RSTP', 'BPDU', 'pont racine', 'PortFast', 'BPDU Guard', 'CRC']
});
reg({
cours: { id: 'routagedyn', match: /\bRIP\b|OSPF|BGP|MPLS|VRRP|Keepalived|routage dynamique|Réseaux avancés|SDN|Réseaux programmables|FRR|CCNA|ROM/i,
titre: 'Routage dynamique et réseaux opérateurs : RIP, OSPF, BGP, MPLS, VRRP et SDN', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T parcours ROM · école d’ingénieurs · Master réseaux',
prerequis: ['Routage statique', 'Algorithmique : graphes et Dijkstra'],
competences: ['BTS SIO SISR · Bloc 2 · Concevoir une infrastructure réseau', 'BUT R&T ROM · Compétence « Connecter » niveau 3', 'Master · Concevoir des réseaux d’opérateurs'],
intro: 'Le routage statique ne passe pas à l’échelle : avec dix routeurs et des liens redondants, maintenir les routes à la main devient impossible, et aucune panne n’est contournée automatiquement. Les protocoles de routage dynamique permettent aux routeurs d’échanger leurs connaissances et de recalculer les chemins en quelques secondes. Ce module présente RIP, OSPF pour l’intérieur d’un réseau, BGP qui relie les réseaux d’Internet, MPLS des opérateurs, VRRP pour la passerelle redondante, et l’approche SDN, avec mise en pratique sur FRRouting.',
cm: [
{ t: 'IGP, EGP et métriques', d: '', html: 'Un <b>système autonome</b> (AS) est un réseau sous une même administration, identifié par un numéro. À l’intérieur, on utilise un <b>IGP</b> (protocole interne : RIP, OSPF, IS-IS) qui cherche le meilleur chemin selon une <b>métrique</b>. Entre AS, on utilise un <b>EGP</b>, en pratique <b>BGP</b>, qui applique des politiques. Chaque protocole a une <b>distance administrative</b> : en cas de routes concurrentes vers le même préfixe, le routeur préfère la source la plus fiable (connectée, statique, OSPF, RIP…).' },
{ t: 'RIP : vecteur de distance', d: '', html: '<b>RIP</b> est un protocole à <b>vecteur de distance</b> : chaque routeur envoie périodiquement (toutes les 30 secondes) sa table à ses voisins, avec un nombre de sauts comme métrique, limité à 15 (16 = inaccessible). Simple, il converge lentement et souffre du problème du comptage à l’infini, atténué par l’horizon partagé et l’empoisonnement de route. On le rencontre encore dans de petits réseaux et dans les programmes pour son intérêt pédagogique ; RIPv2 transporte les masques (CIDR) et s’authentifie.' },
{ t: 'OSPF : état de liens', d: '', html: '<b>OSPF</b> est un protocole à <b>état de liens</b> : chaque routeur décrit ses liens et leur coût dans des annonces (LSA) diffusées à tous les routeurs de la zone, qui construisent ainsi la même carte du réseau, la base LSDB. Chacun calcule ensuite les plus courts chemins avec l’algorithme de <b>Dijkstra</b> (SPF). Le coût par défaut est inversement proportionnel au débit (référence / débit). Les voisins se découvrent par des messages <b>Hello</b> (multicast 224.0.0.5) ; sur un réseau partagé, un routeur désigné (DR) limite les échanges. Pour les grands réseaux, on découpe en <b>zones</b> autour d’une zone 0 (backbone), ce qui limite la taille des bases et permet de résumer les routes. La convergence se fait en quelques secondes, voire moins avec BFD.', exemple: '# FRRouting (vtysh)\nrouter ospf\n ospf router-id 10.255.0.1\n network 10.62.0.0/16 area 0\n passive-interface ens19.20\n!\n# vérifier\nshow ip ospf neighbor\nshow ip route ospf' },
{ t: 'BGP : le routage d’Internet', d: '', html: '<b>BGP</b> relie les quelque 75 000 systèmes autonomes d’Internet. Les routeurs établissent des <b>sessions</b> TCP (port 179) avec leurs voisins, configurés explicitement : <b>eBGP</b> entre AS différents, <b>iBGP</b> à l’intérieur d’un AS. Ils s’échangent des préfixes accompagnés d’<b>attributs</b> : le chemin d’AS traversés (AS_PATH, qui évite les boucles), le prochain saut, la préférence locale, le MED. Le choix du meilleur chemin suit un ordre précis d’attributs et traduit des <b>politiques</b> commerciales (préférer le transitaire le moins cher, ne pas faire transiter le trafic d’un concurrent). Les erreurs de configuration BGP ont provoqué des pannes mondiales ; la validation d’origine <b>RPKI</b> réduit le risque de détournement de préfixes.' },
{ t: 'MPLS', d: '', html: '<b>MPLS</b> commute les paquets selon une <b>étiquette</b> courte ajoutée à l’entrée du réseau de l’opérateur, au lieu de consulter la table de routage IP à chaque saut. Les routeurs de bordure (PE) ajoutent et retirent les étiquettes, les routeurs de cœur (P) les échangent. Le principal usage est le <b>VPN de niveau 3</b> (L3VPN, avec MP-BGP et VRF) que les opérateurs vendent aux entreprises et aux administrations pour relier leurs sites de façon isolée, ainsi que l’ingénierie de trafic. Les réseaux régionaux ou académiques qui relient les établissements reposent souvent sur ce type de service.' },
{ t: 'VRRP : la passerelle redondante', d: '', html: 'Les postes n’ont qu’une passerelle par défaut : si ce routeur tombe, tout un VLAN perd l’accès aux autres réseaux. <b>VRRP</b> (RFC 5798) fait partager à deux routeurs une <b>adresse IP virtuelle</b> et une adresse MAC virtuelle : le routeur <b>maître</b>, de plus haute priorité, répond ; le <b>secondaire</b> écoute ses annonces et prend le relais en quelques secondes si elles cessent. Sous Linux, <b>Keepalived</b> implémente VRRP et peut surveiller un service pour basculer aussi quand il tombe. HSRP est l’équivalent propriétaire de Cisco.', exemple: 'vrrp_instance VLAN20 {\n state MASTER\n interface ens19.20\n virtual_router_id 20\n priority 150\n advert_int 1\n authentication { auth_type PASS; auth_pass s1llabus }\n virtual_ipaddress { 10.62.20.1/23 }\n}' },
{ t: 'SDN et automatisation', d: '', html: 'Le <b>SDN</b> (réseau défini par logiciel) sépare le <b>plan de contrôle</b> (les décisions) du <b>plan de données</b> (l’acheminement), et centralise le contrôle dans un <b>contrôleur</b> programmable par API. Les équipements reçoivent leurs règles du contrôleur (OpenFlow historiquement, ou des API propres). Dans la pratique, les réseaux de campus et de datacenter combinent fabriques EVPN-VXLAN, contrôleurs et <b>automatisation</b> (Ansible, NETCONF/YANG, gNMI) : on décrit l’état voulu du réseau et l’outil configure les équipements, ce qui réduit les erreurs de configuration manuelle.' }
],
synthese: ['IGP dans un AS, BGP entre AS ; distance administrative', 'RIP : sauts, 15 max, convergence lente', 'OSPF : LSA, LSDB, Dijkstra, coût, zone 0, Hello', 'BGP : sessions TCP 179, eBGP/iBGP, AS_PATH, politiques, RPKI', 'MPLS : étiquettes, PE et P, L3VPN', 'VRRP et Keepalived : IP virtuelle, maître et secondaire', 'SDN : contrôle centralisé et programmable, automatisation'],
qcm: [
['Quel algorithme utilise OSPF ?', ['Bellman-Ford', 'Dijkstra', 'Tri fusion', 'Aucun'], 1, 'SPF.'],
['Métrique de RIP ?', ['Débit', 'Nombre de sauts', 'Délai', 'Coût'], 1, '15 au maximum.'],
['Port de BGP ?', ['UDP 520', 'TCP 179', 'IP 89', 'TCP 22'], 1, 'OSPF est le protocole IP 89.'],
['À quoi sert l’AS_PATH ?', ['Chiffrer', 'Éviter les boucles et choisir', 'Compter les sauts IP', 'Authentifier'], 1, 'Liste des AS traversés.'],
['Que fournit VRRP ?', ['Une route', 'Une passerelle virtuelle partagée par deux routeurs', 'Un VLAN', 'Un DNS'], 1, 'Redondance de passerelle.'],
['Zone obligatoire en OSPF multizone ?', ['Zone 1', 'Zone 0', 'Zone 255', 'Aucune'], 1, 'Backbone.'],
['Principal usage commercial de MPLS ?', ['Wi-Fi', 'VPN de niveau 3 inter-sites', 'DNS', 'VoIP'], 1, 'L3VPN.']
],
td: [
{ n: 'Application', c: 'Calculer le coût OSPF d’un lien à 100 Mbit/s et à 10 Gbit/s avec une référence de 100 Gbit/s, puis le chemin choisi entre deux trajets fournis.', r: '100 000 / 100 = 1 000 ; 100 000 / 10 000 = 10 · on additionne les coûts de chaque trajet et on retient le plus faible.' },
{ n: 'Application', c: 'Configurer OSPF sur trois routeurs FRR en zone 0 et vérifier la table de routage.', r: 'router ospf · ospf router-id · network … area 0 sur chacun · show ip ospf neighbor (état Full) · show ip route ospf.' },
{ n: 'Approfondissement', c: 'Concevoir la redondance de passerelle de deux VLAN avec Keepalived, en répartissant la charge (chaque routeur maître d’un VLAN).', r: 'R1 priorité 150 sur VLAN 10, 100 sur VLAN 20 ; R2 l’inverse · vérification par arrêt de R1 : l’IP virtuelle du VLAN 10 passe sur R2.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Quatre routeurs FRRouting en maillage.', etapes: ['Configurer RIP et observer la convergence', 'Remplacer par OSPF', 'Couper un lien et mesurer la reconvergence', 'Établir une session eBGP entre deux AS', 'Mettre en place Keepalived'] },
plusLoin: [['FRRouting · documentation', 'https://docs.frrouting.org/'], ['RFC 2328 · OSPFv2', 'https://www.rfc-editor.org/rfc/rfc2328'], ['RFC 4271 · BGP-4', 'https://www.rfc-editor.org/rfc/rfc4271'], ['MANRS · bonnes pratiques de routage', 'https://www.manrs.org/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Après le remplacement d’un routeur, le site annexe ne joint plus les serveurs du site principal, alors que les liens sont actifs.', etapes: [
['Vérifier le voisinage', 'show ip ospf neighbor\n(aucun voisin)\n→ Le nouveau routeur n’établit pas d’adjacence OSPF.'],
['Lire les journaux', 'debug ospf packet hello\nOSPF: Hello from 10.255.0.2 … mismatched hello parameters: dead interval 40 / 120\n→ Les intervalles Hello/Dead diffèrent entre les deux routeurs.'],
['Corriger', 'interface ens18\n ip ospf hello-interval 10\n ip ospf dead-interval 40\n→ Paramètres alignés sur le reste du réseau.'],
['Vérifier', 'show ip ospf neighbor → 10.255.0.2 Full/DR\nshow ip route ospf → routes du site principal présentes\nping des serveurs : réponses.'],
['Prévenir', '→ Configuration des routeurs générée par Ansible à partir d’un modèle unique, et contrôle automatique des adjacences par la supervision.']
] },
erreurs: ['Paramètres Hello/Dead ou zone différents entre voisins', 'Oublier passive-interface sur les VLAN d’utilisateurs', 'Annoncer en BGP des préfixes qui ne vous appartiennent pas', 'Même priorité VRRP sur les deux routeurs', 'Mélanger statique et dynamique sans comprendre la distance administrative'],
memo: [['Commande (FRR)', 'Usage'], ['vtysh', 'console de configuration'], ['show ip ospf neighbor', 'adjacences OSPF'], ['show ip ospf database', 'base d’états de liens'], ['show ip route', 'table de routage avec origine'], ['show ip rip', 'routes RIP'], ['show bgp summary', 'sessions BGP'], ['show bgp ipv4 unicast <préfixe>', 'chemins BGP'], ['systemctl status keepalived', 'état VRRP'], ['ip addr | grep secondary', 'IP virtuelle portée']],
examen: { titre: 'Sujet type · 20 points', q: ['Comparer vecteur de distance et état de liens. (4 pts)', 'Calculer des coûts OSPF et un chemin. (4 pts)', 'Configurer OSPF sur une topologie. (5 pts)', 'Expliquer le rôle de BGP et de l’AS_PATH. (4 pts)', 'Proposer une passerelle redondante. (3 pts)'], r: ['RIP / OSPF', 'référence / débit', 'router ospf, network area', 'inter-AS, politiques, boucles', 'VRRP'] },
prof: { deroule: [['Ouverture', 'Couper un lien en routage statique : rien ne se passe'], ['Cours', 'RIP et OSPF'], ['Pratique', 'FRR'], ['Cours', 'BGP, MPLS, VRRP, SDN'], ['Pratique', 'Keepalived'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : topologie préconfigurée. Élèves avancés : BGP avec filtres, EVPN.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Voisins OSPF', 'Vérifier les adjacences.', 'vtysh -c "show ip ospf neighbor"', 'Neighbor ID Pri State Dead Time Address Interface\n10.255.0.2 1 Full/DR 35.112s 10.255.0.2 ens18', 'Adjacence complète.'],
['Routes apprises', 'Origine des routes.', 'vtysh -c "show ip route" | grep -E "^O|^S"', 'O>* 10.63.0.0/16 [110/20] via 10.255.0.2, ens18\nS>* 0.0.0.0/0 [1/0] via 203.0.113.1, wan0', 'O = OSPF, distance 110, coût 20 ; S = statique.'],
['Mesurer la reconvergence', 'Panne simulée.', 'ping -D -i 0.2 10.63.1.10 (puis coupure du lien principal)', '[…10.4] 64 bytes …\n[…10.6] (pas de réponse)\n…\n[…14.2] 64 bytes …', 'Environ 4 secondes de coupure avant le passage par le chemin de secours.'],
['Session BGP', 'Lab inter-AS.', 'vtysh -c "show bgp summary"', 'Neighbor V AS MsgRcvd MsgSent Up/Down State/PfxRcd\n198.51.100.1 4 64510 42 40 00:18:12 3', 'Session établie, trois préfixes reçus.'],
['Chemin BGP', 'Pourquoi ce chemin ?', 'vtysh -c "show bgp ipv4 unicast 192.0.2.0/24"', 'Paths: (2 available, best #1)\n 64510 64520\n 64511 64530 64520', 'Le premier chemin traverse moins d’AS : il est préféré.'],
['Bascule VRRP', 'Test de redondance.', 'journalctl -u keepalived | tail -1', 'VRRP_Instance(VLAN20) Entering MASTER STATE', 'Le secondaire est devenu maître après l’arrêt du principal.']
],
termes: ['système autonome', 'IGP', 'EGP', 'métrique', 'distance administrative', 'RIP', 'vecteur de distance', 'OSPF', 'état de liens', 'LSA', 'LSDB', 'zone 0', 'Hello', 'BGP', 'eBGP', 'iBGP', 'AS_PATH', 'RPKI', 'MPLS', 'étiquette', 'L3VPN', 'VRRP', 'Keepalived', 'SDN', 'FRRouting']
});
reg({
cours: { id: 'wifi', match: /Wi-Fi : normes|WPA2|WPA3|802\.11|sans fil/i,
titre: 'Wi-Fi : normes 802.11, canaux, sécurité WPA2/WPA3 et 802.1X', duree: '',
niveau: 'SNT · Bac pro CIEL · BTS SIO / CIEL · BUT R&T · TSSR',
prerequis: ['Ethernet', 'VLAN', 'Décibels'],
competences: ['BTS SIO · Bloc 1 · Exploiter un réseau sans fil', 'BTS CIEL · C03 · Installer un réseau', 'BUT R&T · Compétence « Connecter »'],
intro: 'Dans un établissement, le Wi-Fi est souvent le réseau le plus utilisé et le plus critiqué : salles où ça ne passe pas, connexions qui tombent pendant les évaluations en ligne, réseau invités mal isolé. Le sans-fil obéit à la physique des ondes et partage un support commun entre tous les appareils. Ce module présente les normes, la planification des canaux, la sécurité WPA2 et WPA3, l’authentification 802.1X et le diagnostic.',
cm: [
{ t: 'Les normes 802.11', d: '', html: 'Le Wi-Fi suit la famille de normes <b>IEEE 802.11</b>, commercialisées sous des numéros : <b>Wi-Fi 4</b> (802.11n), <b>Wi-Fi 5</b> (802.11ac, 5 GHz), <b>Wi-Fi 6</b> et <b>6E</b> (802.11ax, avec OFDMA et la bande des 6 GHz), <b>Wi-Fi 7</b> (802.11be). Les débits annoncés sont théoriques et partagés entre tous les appareils associés à une borne : dans une salle de 35 élèves, le débit utile par appareil est une fraction du débit affiché. Le Wi-Fi 6 améliore surtout l’efficacité quand beaucoup d’appareils sont connectés, ce qui correspond exactement aux salles de classe.' },
{ t: 'Bandes et canaux', d: '', html: 'La bande des <b>2,4 GHz</b> porte loin mais n’offre que trois canaux sans recouvrement (1, 6, 11 en Europe), et elle est encombrée (Bluetooth, micro-ondes). La bande des <b>5 GHz</b> offre beaucoup plus de canaux, avec des débits supérieurs mais une portée plus courte et une moindre traversée des murs ; certains canaux sont soumis au <b>DFS</b> (détection des radars). La bande des <b>6 GHz</b> (Wi-Fi 6E) est encore plus large et moins encombrée. La <b>planification</b> consiste à attribuer des canaux différents aux bornes voisines et à régler leur puissance pour éviter qu’elles se brouillent : plus de bornes à puissance réduite valent mieux que quelques bornes à pleine puissance.' },
{ t: 'Accès au support', d: '', html: 'Tous les appareils d’un même canal partagent le support : un seul émet à la fois. Le Wi-Fi utilise <b>CSMA/CA</b> : écouter avant d’émettre, attendre un délai aléatoire, et chaque trame est acquittée. Un appareil lointain qui émet à faible débit occupe le canal longtemps et ralentit tous les autres. D’où l’intérêt d’imposer un débit minimal, de favoriser la bande des 5 GHz (band steering) et d’assurer une bonne couverture. La puissance reçue se mesure en <b>dBm</b> : au-delà de −65 dBm la qualité est bonne, en dessous de −75 dBm elle se dégrade.' },
{ t: 'Sécurité : WPA2 et WPA3', d: '', html: 'Le <b>WEP</b> et le WPA d’origine sont cassés et ne doivent plus être utilisés. <b>WPA2</b> chiffre avec AES-CCMP ; en mode <b>Personnel</b> (PSK), une clé partagée unique est vulnérable aux attaques par dictionnaire hors ligne si elle est courte, et tout le monde la connaît. <b>WPA3</b> remplace le PSK par <b>SAE</b>, résistant aux attaques par dictionnaire, et impose la protection des trames de gestion. Pour un réseau invités ouvert, <b>OWE</b> (Enhanced Open) chiffre sans mot de passe.' },
{ t: '802.1X et WPA Entreprise', d: '', html: 'Dans un établissement, un mot de passe partagé pour 1 500 personnes n’a aucun sens : il fuit, et on ne peut révoquer personne. Le mode <b>Entreprise</b> repose sur <b>802.1X</b> : chaque utilisateur s’authentifie avec ses propres identifiants (ou un certificat) auprès d’un serveur <b>RADIUS</b> (FreeRADIUS, NPS), lui-même relié à l’annuaire. La borne n’autorise l’accès qu’après la réponse du serveur, qui peut aussi attribuer un <b>VLAN</b> selon le profil (élève, enseignant, administration). Le serveur présente un certificat que les appareils doivent vérifier, sinon un faux point d’accès peut capter les identifiants. <b>eduroam</b> utilise ce mécanisme pour permettre aux étudiants et chercheurs de se connecter dans les établissements partenaires du monde entier.' },
{ t: 'Diagnostiquer', d: '', html: 'Les plaintes sur le Wi-Fi ont des causes variées : couverture insuffisante, brouillage entre bornes sur le même canal, trop d’appareils sur une borne, appareil ancien qui ne gère que le 2,4 GHz, authentification RADIUS en échec, DHCP saturé. Sous Linux, <code>iw dev wlan0 link</code> donne le signal et le débit, <code>iw dev wlan0 scan</code> les réseaux voisins et leurs canaux ; un analyseur de spectre ou une application de relevé de site permet de cartographier la couverture. Les contrôleurs de bornes centralisent ces indicateurs.' }
],
synthese: ['Wi-Fi 4, 5, 6/6E, 7 ; débit partagé entre appareils', '2,4 GHz : canaux 1, 6, 11 ; 5 et 6 GHz : plus de canaux, moins de portée', 'CSMA/CA, un émetteur à la fois ; signal en dBm', 'WEP et WPA cassés ; WPA2-AES, WPA3-SAE, OWE', '802.1X + RADIUS : identifiants individuels, VLAN dynamique, certificat vérifié', 'Diagnostic : couverture, canaux, charge, authentification, DHCP'],
qcm: [
['Canaux sans recouvrement à 2,4 GHz en Europe ?', ['1, 2, 3', '1, 6, 11', '36, 40, 44', 'Tous'], 1, 'Trois canaux.'],
['Pourquoi éviter un mot de passe Wi-Fi partagé pour tout un établissement ?', ['Trop long', 'Il fuit et on ne peut révoquer personne', 'Interdit', 'Plus lent'], 1, 'Mode Entreprise.'],
['Que remplace SAE dans WPA3 ?', ['AES', 'Le PSK de WPA2', 'RADIUS', 'Le DHCP'], 1, 'Résiste au dictionnaire hors ligne.'],
['Signal de −80 dBm :', ['excellent', 'faible, qualité dégradée', 'impossible', 'trop fort'], 1, 'Viser au-dessus de −65.'],
['Quel serveur authentifie en 802.1X ?', ['DNS', 'RADIUS', 'DHCP', 'NTP'], 1, 'Relié à l’annuaire.'],
['Pourquoi réduire la puissance des bornes ?', ['Économie seule', 'Limiter le brouillage entre bornes voisines', 'Obligation', 'Sécurité'], 1, 'Meilleure réutilisation des canaux.']
],
td: [
{ n: 'Application', c: 'Attribuer les canaux 2,4 GHz à neuf bornes réparties en grille 3 × 3 pour limiter le brouillage.', r: 'Alternance 1, 6, 11 en ligne et en colonne, de sorte que deux bornes adjacentes n’aient jamais le même canal.' },
{ n: 'Application', c: 'Concevoir les réseaux Wi-Fi d’un lycée : élèves, personnels, invités, objets.', r: 'Un SSID 802.1X avec VLAN attribué par RADIUS selon le profil (élèves, personnels) · un SSID invités OWE ou portail, VLAN isolé, accès Internet seul · un SSID objets WPA2/3-PSK par usage, VLAN dédié.' },
{ n: 'Approfondissement', c: 'Une salle se plaint de lenteurs pendant les évaluations en ligne. Proposer une démarche de diagnostic chiffrée.', r: 'Mesurer le signal et le débit sur plusieurs postes · compter les appareils associés à la borne · vérifier canal et voisins · consulter les journaux RADIUS et DHCP · solution possible : borne supplémentaire, 5 GHz forcé, débit minimal.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'FreeRADIUS et point d’accès hostapd simulé.', etapes: ['Lire les réseaux voisins', 'Configurer WPA2-PSK puis WPA3-SAE', 'Installer FreeRADIUS', 'Authentifier en 802.1X', 'Attribuer un VLAN selon l’utilisateur'] },
plusLoin: [['Wi-Fi Alliance', 'https://www.wi-fi.org/'], ['ANSSI · Recommandations de sécurité relatives aux réseaux Wi-Fi', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-aux-reseaux-wi-fi'], ['eduroam France', 'https://www.eduroam.fr/'], ['FreeRADIUS', 'https://freeradius.org/documentation/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Depuis la rentrée, les élèves de la salle C204 perdent le Wi-Fi plusieurs fois par heure, alors que la salle voisine fonctionne.', etapes: [
['Mesurer', 'iw dev wlan0 link\nsignal: -72 dBm\ntx bitrate: 6.5 MBit/s\n→ Signal faible et débit très bas.'],
['Observer l’environnement', 'iw dev wlan0 scan | grep -E "SSID|freq|signal"\nfreq: 2437 signal: -58 dBm SSID: Lycee\nfreq: 2437 signal: -61 dBm SSID: Lycee\n→ Deux bornes sur le même canal 6 à 2,4 GHz se brouillent.'],
['Consulter le contrôleur', '→ 41 appareils associés à la borne du couloir, aucun à celle de la salle, éteinte depuis des travaux.'],
['Corriger', '→ Remise en service de la borne de la salle, canal 36 à 5 GHz, réduction de la puissance de la borne du couloir, débit minimal de 12 Mbit/s.'],
['Vérifier', 'signal: -54 dBm · tx bitrate: 433 MBit/s · aucune déconnexion sur une heure de test.\n→ Le plan des bornes et des canaux est mis à jour.']
] },
erreurs: ['Utiliser WEP ou WPA', 'Clé partagée unique pour tout l’établissement', 'Toutes les bornes à pleine puissance', 'Bornes voisines sur le même canal', 'Ne pas vérifier le certificat RADIUS', 'Réseau invités non isolé'],
memo: [['Commande / réglage', 'Usage'], ['iw dev wlan0 link', 'signal et débit'], ['iw dev wlan0 scan', 'réseaux et canaux voisins'], ['nmcli dev wifi', 'liste des réseaux'], ['iw reg get', 'réglementation du pays'], ['hostapd -dd', 'débogage d’un point d’accès'], ['radtest user mdp localhost 0 secret', 'tester FreeRADIUS'], ['journalctl -u freeradius', 'authentifications'], ['wpa_cli status', 'état côté client']],
examen: { titre: 'Sujet type · 20 points', q: ['Comparer les bandes 2,4, 5 et 6 GHz. (4 pts)', 'Planifier les canaux d’un étage. (4 pts)', 'Comparer WPA2-PSK, WPA3-SAE et WPA Entreprise. (5 pts)', 'Décrire une authentification 802.1X. (4 pts)', 'Diagnostiquer un cas fourni. (3 pts)'], r: ['portée, canaux, débit', 'alternance', 'voir cours', 'client, borne, RADIUS, annuaire', 'signal, canal, charge'] },
prof: { deroule: [['Ouverture', 'Relevé du signal dans la salle'], ['Cours', 'Normes et canaux'], ['Pratique', 'Scan et planification'], ['Cours', 'Sécurité et 802.1X'], ['Pratique', 'FreeRADIUS'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : plan d’étage fourni. Élèves avancés : EAP-TLS, roaming 802.11r.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Qualité de connexion', 'Poste lent.', 'iw dev wlan0 link', 'Connected to 3c:52:82:aa:10:01 (on wlan0)\n\tfreq: 5180\n\tsignal: -58 dBm\n\ttx bitrate: 866.7 MBit/s VHT-MCS 9 80MHz', 'Bonne connexion en 5 GHz, canal 36.'],
['Réseaux voisins', 'Planification.', 'nmcli -f SSID,CHAN,SIGNAL,SECURITY dev wifi', 'SSID CHAN SIGNAL SECURITY\nLycee 36 82 WPA2 802.1X\nLycee 6 54 WPA2 802.1X\nFreebox-xx 6 47 WPA2', 'Le canal 6 est partagé avec une box voisine.'],
['Tester RADIUS', 'Nouvelle configuration.', 'radtest lea.martin "MotDePasse" 127.0.0.1 0 testing123', 'Received Access-Accept Id 14 from 127.0.0.1:1812\n\tTunnel-Private-Group-Id:0 = "20"', 'Authentification acceptée, VLAN 20 attribué.'],
['Échec d’authentification', 'Un élève ne se connecte pas.', 'journalctl -u freeradius | grep -i reject | tail -1', 'Login incorrect (mschap: MS-CHAP2-Response is incorrect): [eleve.x] (from client borne-c2 port 0 cli 98-3B-…)', 'Mot de passe incorrect, ou mot de passe changé sans mise à jour sur l’appareil.'],
['Réglementation', 'Canaux disponibles.', 'iw reg get | head -3', 'country FR: DFS-ETSI\n\t(2400 - 2483 @ 40), (N/A, 20), (N/A)\n\t(5170 - 5250 @ 80), (N/A, 23), (N/A), AUTO-BW', 'Puissances maximales autorisées en France par bande.'],
['Sécurité annoncée', 'Audit du réseau invités.', 'nmcli -f SSID,SECURITY dev wifi | grep Invites', 'Lycee-Invites --', 'Réseau ouvert sans chiffrement : passer en OWE et isoler le VLAN.']
],
termes: ['Wi-Fi', '802.11', 'Wi-Fi 6', '2,4 GHz', '5 GHz', '6 GHz', 'canal', 'DFS', 'CSMA/CA', 'dBm', 'SSID', 'WEP', 'WPA2', 'WPA3', 'SAE', 'OWE', 'PSK', '802.1X', 'RADIUS', 'FreeRADIUS', 'eduroam', 'band steering']
});
reg({
cours: { id: 'transmission', match: /Couche physique|câbles, fibre|Codage en ligne|multiplexage|Fibre optique, réseaux radio|4G et 5G|Transmission|Électronique \(option ER\)/i,
titre: 'Transmission : câbles, fibre optique, codage, multiplexage et réseaux mobiles', duree: '',
niveau: 'Bac pro CIEL · BTS CIEL IR / ER · BUT R&T · BUT GEII',
prerequis: ['Mathématiques : décibels, signal'],
competences: ['BTS CIEL · C02 · Analyser une transmission', 'BUT R&T · Compétence « Connecter » · couche physique'],
intro: 'Tout ce qui circule sur un réseau finit en signal électrique, lumineux ou radio. Le choix d’un câble, d’une fibre ou d’une technologie radio détermine le débit, la portée et la fiabilité d’une liaison, et la plupart des pannes « mystérieuses » ont une cause physique. Ce module présente les supports de transmission, le codage et le multiplexage, la fibre optique et ses mesures, et l’architecture des réseaux mobiles 4G et 5G.',
cm: [
{ t: 'Paires torsadées', d: '', html: 'Le câble <b>à paires torsadées</b> relie les postes aux switchs. La torsade réduit la diaphonie et les perturbations. Les <b>catégories</b> fixent les performances : Cat 5e (1 Gbit/s), Cat 6 (1 Gbit/s, 10 Gbit/s sur 55 m), Cat 6A (10 Gbit/s sur 100 m), Cat 8 (25 à 40 Gbit/s sur 30 m, en datacenter). Le blindage (U/UTP, F/UTP, S/FTP) protège des perturbations. La longueur maximale d’un lien Ethernet cuivre est de <b>100 mètres</b> (90 m de câble fixe et 10 m de cordons). Le <b>PoE</b> (802.3af, at, bt) alimente bornes Wi-Fi, téléphones et caméras par le même câble. Les liens se certifient avec un testeur : affaiblissement, diaphonie, longueur, câblage croisé.' },
{ t: 'Fibre optique', d: '', html: 'La fibre transporte la lumière dans un cœur de verre, par réflexion totale. La fibre <b>multimode</b> (cœur de 50 µm, OM3/OM4/OM5) convient aux courtes distances, quelques centaines de mètres, avec des émetteurs peu coûteux ; la fibre <b>monomode</b> (cœur de 9 µm, OS2) porte sur des dizaines de kilomètres. On l’utilise pour les liaisons entre bâtiments et entre baies (insensible aux perturbations électriques et aux différences de potentiel), ainsi que pour les accès FTTH. Les connecteurs courants sont LC et SC ; les modules <b>SFP</b> dans les switchs convertissent le signal. Le <b>bilan optique</b> additionne les pertes (atténuation par kilomètre, connecteurs, épissures) ; un <b>réflectomètre</b> (OTDR) localise un défaut sur la fibre.', attention: 'Ne jamais regarder l’extrémité d’une fibre ou d’un module en fonctionnement : les lasers utilisés, souvent infrarouges donc invisibles, peuvent endommager la rétine.' },
{ t: 'Codage et débit', d: '', html: 'Le <b>codage en ligne</b> transforme les bits en signal adapté au support : NRZ, Manchester (Ethernet 10 Mbit/s, une transition par bit qui permet la synchronisation), codages par blocs comme 8b/10b ou 64b/66b qui ajoutent quelques bits pour garantir assez de transitions, et modulations à plusieurs niveaux (PAM-4) pour les hauts débits. Le <b>débit</b> en bit/s se distingue de la <b>rapidité de modulation</b> en bauds (symboles par seconde) : avec 16 niveaux par symbole, chaque symbole porte 4 bits. La <b>formule de Shannon</b> borne le débit d’un canal : C = B·log₂(1 + S/B), avec la bande passante B et le rapport signal sur bruit.' },
{ t: 'Multiplexage', d: '', html: 'Le <b>multiplexage</b> partage un support entre plusieurs communications. <b>Temporel</b> (TDM) : chacun son créneau de temps. <b>Fréquentiel</b> (FDM) : chacun sa bande de fréquences, comme les canaux radio. En longueur d’onde (<b>WDM</b>) sur fibre : plusieurs couleurs de lumière dans la même fibre, jusqu’à des dizaines de canaux en DWDM. L’<b>OFDM</b> découpe un canal en centaines de sous-porteuses orthogonales : c’est la base du Wi-Fi, de la 4G, de la 5G et de la TNT. L’<b>OFDMA</b> attribue ces sous-porteuses à des utilisateurs différents en même temps.' },
{ t: 'Réseaux mobiles 4G et 5G', d: '', html: 'Un réseau mobile se compose d’un <b>réseau d’accès radio</b> (antennes : eNodeB en 4G, gNodeB en 5G) et d’un <b>cœur de réseau</b> qui gère l’authentification par la carte SIM, la mobilité et la connexion à Internet (EPC en 4G, 5G Core). La 4G (LTE) utilise l’OFDMA et le MIMO. La 5G ajoute de nouvelles bandes (3,5 GHz en France, et les ondes millimétriques à 26 GHz), une latence réduite, et le <b>découpage en tranches</b> (network slicing) qui offre des réseaux virtuels aux caractéristiques garanties. Les réseaux dédiés aux objets (NB-IoT, LTE-M) en sont une déclinaison basse consommation. Les fréquences sont attribuées par l’<b>Arcep</b>.' },
{ t: 'Diagnostiquer la couche physique', d: '', html: 'Une liaison qui fonctionne « parfois » a souvent une cause physique : câble écrasé, connecteur mal serti, longueur excessive, fibre sale ou trop courbée, module SFP incompatible. Les indicateurs : erreurs CRC sur le port, vitesse négociée inférieure à l’attendu, puissance optique reçue hors plage (lue sur le module avec <code>ethtool -m</code>). On nettoie systématiquement les connecteurs de fibre avant de les brancher.' }
],
synthese: ['Cuivre : catégories, 100 m maximum, PoE, certification', 'Fibre multimode (courte distance) et monomode (longue) ; SFP, bilan optique, OTDR ; sécurité laser', 'Codage en ligne ; débit en bit/s et rapidité en bauds ; Shannon', 'Multiplexage TDM, FDM, WDM, OFDM/OFDMA', '4G/5G : accès radio et cœur, bandes, tranches ; Arcep', 'CRC, vitesse négociée, puissance optique'],
qcm: [
['Longueur maximale d’un lien Ethernet cuivre ?', ['50 m', '100 m', '500 m', '2 km'], 1, '90 m + cordons.'],
['Quelle fibre pour relier deux bâtiments distants de 5 km ?', ['Multimode OM3', 'Monomode OS2', 'Cat 6A', 'Coaxial'], 1, 'Longue distance.'],
['16 niveaux par symbole à 1 000 bauds donnent :', ['1 000 bit/s', '4 000 bit/s', '16 000 bit/s', '250 bit/s'], 1, 'log₂16 = 4 bits par symbole.'],
['Technique de multiplexage du Wi-Fi et de la 4G ?', ['TDM', 'OFDM', 'Manchester', 'WDM'], 1, 'Sous-porteuses orthogonales.'],
['Qui attribue les fréquences en France ?', ['La CNIL', 'L’Arcep', 'L’ANSSI', 'L’Afnic'], 1, 'Régulateur des télécoms.'],
['Que permet le PoE ?', ['Chiffrer', 'Alimenter un appareil par le câble réseau', 'Doubler le débit', 'Allonger le câble'], 1, 'Bornes, caméras, téléphones.']
],
td: [
{ n: 'Application', c: 'Calculer le bilan d’une liaison fibre monomode de 8 km (0,35 dB/km), avec 2 connecteurs (0,5 dB chacun) et 3 épissures (0,1 dB), et la comparer au budget du module (puissance émise −3 dBm, sensibilité −20 dBm).', r: '8 × 0,35 + 2 × 0,5 + 3 × 0,1 = 4,1 dB · puissance reçue ≈ −7,1 dBm, budget 17 dB : marge de 12,9 dB.' },
{ n: 'Application', c: 'Appliquer la formule de Shannon à un canal de 20 MHz avec un rapport signal sur bruit de 30 dB.', r: 'S/B = 1 000 · C = 20 × 10⁶ × log₂(1 001) ≈ 199 Mbit/s.' },
{ n: 'Approfondissement', c: 'Choisir et justifier les supports pour un nouveau bâtiment : postes, bornes Wi-Fi 6E, caméras, liaison vers le bâtiment principal à 300 m.', r: 'Cat 6A pour les postes et bornes (10 Gbit/s possible, PoE++ pour les bornes) · caméras en PoE · liaison en fibre OM4 ou monomode avec deux paires pour la redondance.' }
],
tp: { type: 'Atelier', url: 'apercu-td-imprimable.html', desc: 'Câblage, certification et mesures.', etapes: ['Sertir et tester un câble', 'Lire un rapport de certification', 'Calculer un bilan optique', 'Lire les informations d’un module SFP', 'Mesurer un débit réel'] },
plusLoin: [['Arcep · fréquences et réseaux mobiles', 'https://www.arcep.fr/'], ['3GPP · spécifications 4G et 5G', 'https://www.3gpp.org/'], ['Fibre optique : guide FTTH (Arcep)', 'https://www.arcep.fr/la-regulation/grands-dossiers-reseaux-fixes/la-fibre.html']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'La liaison fibre entre le bâtiment principal et le gymnase tombe plusieurs fois par jour depuis des travaux de voirie.', etapes: [
['Lire l’état du port', 'show interfaces gi1/0/48 | include errors|rate\n1532 input errors, 1530 CRC\n→ Erreurs nombreuses sur la liaison.'],
['Mesurer la puissance optique', 'show interfaces gi1/0/48 transceiver\nRx Power: -23.4 dBm (seuil bas -20.0)\n→ La puissance reçue est sous la sensibilité du module.'],
['Localiser', '→ Mesure au réflectomètre : événement de forte atténuation à 182 m, au niveau de la tranchée des travaux.'],
['Réparer', '→ Intervention du prestataire : fibre écrasée, épissure réalisée. Puissance reçue après réparation : −6,8 dBm.'],
['Prévenir', '→ Seconde liaison par un autre cheminement pour la redondance, supervision de la puissance optique avec alerte à −15 dBm.']
] },
erreurs: ['Dépasser 100 m en cuivre', 'Mélanger multimode et monomode', 'Brancher une fibre sans la nettoyer', 'Regarder une fibre allumée', 'Confondre bauds et bit/s', 'Ignorer les erreurs CRC'],
memo: [['Élément', 'À retenir'], ['Cat 6A', '10 Gbit/s sur 100 m'], ['OM4 · OS2', 'multimode courte distance · monomode longue'], ['LC · SC', 'connecteurs de fibre'], ['SFP, SFP+, QSFP', 'modules optiques'], ['ethtool -m ens18', 'informations du module optique'], ['C = B·log₂(1 + S/B)', 'Shannon'], ['débit = rapidité × log₂(niveaux)', 'bits par seconde'], ['OFDM · WDM', 'multiplexages'], ['eNodeB · gNodeB', 'antennes 4G · 5G']],
examen: { titre: 'Sujet type · 20 points', q: ['Choisir les supports d’un cas donné. (5 pts)', 'Calculer un bilan optique. (4 pts)', 'Calculer un débit à partir de la rapidité et des niveaux. (3 pts)', 'Comparer quatre multiplexages. (4 pts)', 'Décrire l’architecture d’un réseau 5G. (4 pts)'], r: ['cuivre, fibre, PoE', 'somme des pertes', 'R × log₂ n', 'TDM, FDM, WDM, OFDM', 'accès radio, cœur, tranches'] },
prof: { deroule: [['Ouverture', 'Démonter un câble Cat 6A et une jarretière fibre'], ['Cours', 'Supports'], ['Atelier', 'Sertissage et tests'], ['Cours', 'Codage et multiplexage'], ['Cours', '4G et 5G'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : fiches matériel. Élèves avancés : calculs de Shannon, OTDR.', eval: 'Atelier et sujet type.' } },
exemples: [
['Informations d’un module optique', 'Diagnostic fibre.', 'sudo ethtool -m ens1f0 | grep -E "Identifier|Wavelength|Rcvr signal"', 'Identifier: 0x03 (SFP)\nLaser wavelength: 1310nm\nReceiver signal average optical power: 0.2081 mW / -6.82 dBm', 'Module monomode 1310 nm, puissance reçue correcte.'],
['Vitesse négociée', 'Lien gigabit attendu.', 'ethtool ens18 | grep Speed', 'Speed: 100Mb/s', 'Une paire défectueuse limite souvent le lien à 100 Mbit/s : retester le câble.'],
['Erreurs physiques', 'Lien instable.', 'ip -s link show ens18 | sed -n "4p"', ' 9821331 71001 1240 0', '1 240 erreurs en réception.'],
['Débit réel', 'Mesurer un lien.', 'iperf3 -c 10.62.40.20 -t 10', '[ 5] 0.00-10.00 sec 1.09 GBytes 939 Mbits/sec sender', '939 Mbit/s sur un lien gigabit : normal compte tenu des en-têtes.'],
['Puissance PoE', 'Borne qui redémarre.', 'show power inline gi1/0/12', 'Interface Admin Oper Power(W) Class\nGi1/0/12 auto on 25.4 4', 'La borne consomme 25,4 W, classe 4 (802.3at) : vérifier le budget PoE total du switch.'],
['Calcul de Shannon', 'Exercice.', 'python3 -c "import math; print(20e6*math.log2(1+1000)/1e6)"', '199.3', 'Environ 199 Mbit/s au maximum.']
],
termes: ['paire torsadée', 'Cat 6A', 'PoE', 'fibre optique', 'multimode', 'monomode', 'SFP', 'bilan optique', 'OTDR', 'codage en ligne', 'Manchester', 'baud', 'Shannon', 'multiplexage', 'TDM', 'FDM', 'WDM', 'OFDM', 'OFDMA', '4G', '5G', 'gNodeB', 'network slicing', 'Arcep']
});
reg({
cours: { id: 'qos', match: /QoS : files|marquage, tc|qualité de service/i,
titre: 'Qualité de service : files d’attente, marquage DSCP et mise en forme avec tc', duree: '',
niveau: 'BTS SIO SISR · BUT R&T parcours ROM · école d’ingénieurs',
prerequis: ['Téléphonie sur IP', 'Pare-feu'],
competences: ['BUT R&T ROM · Garantir la qualité des services', 'BTS SIO SISR · Optimiser l’infrastructure'],
intro: 'Quand un lien est saturé, tout le monde ralentit : la visioconférence se hache, la téléphonie devient inaudible et les évaluations en ligne expirent, pendant qu’une mise à jour système télécharge des gigaoctets. La qualité de service consiste à décider qui passe en premier. Ce module présente le marquage, les files d’attente, la limitation de débit et leur mise en œuvre sous Linux avec tc.',
cm: [
{ t: 'Le problème : la congestion', d: '', html: 'Un lien sature quand le trafic entrant dépasse sa capacité, typiquement à la sortie vers Internet. Les paquets s’accumulent dans des <b>files d’attente</b> : la latence augmente, puis les files débordent et des paquets sont perdus. Sans politique, la file est unique et premier arrivé, premier servi : une voix qui supporte mal 30 ms de retard attend derrière des paquets de téléchargement. Des files trop grandes provoquent aussi le <b>bufferbloat</b>, des latences de plusieurs centaines de millisecondes.' },
{ t: 'Classifier et marquer', d: '', html: 'On commence par <b>classifier</b> le trafic (par port, adresse, VLAN, application) puis on le <b>marque</b> pour que les équipements suivants le reconnaissent. En IP, le champ <b>DSCP</b> (6 bits de l’en-tête) porte la classe : <b>EF</b> (46) pour la voix, <b>AF41</b> pour la visioconférence, <b>CS6</b> pour les protocoles de routage, <b>0</b> (best effort) par défaut, <b>CS1</b> pour le trafic de moindre priorité (sauvegardes, mises à jour). En Ethernet, le champ PCP du tag VLAN porte une priorité de 0 à 7. On marque au plus près de la source et on ne fait confiance qu’aux marquages des équipements maîtrisés.' },
{ t: 'Ordonnancer : les files', d: '', html: 'L’<b>ordonnanceur</b> décide quelle file sert en premier. Une <b>file prioritaire</b> stricte sert la voix avant tout le reste, avec une limite pour qu’elle ne puisse pas affamer les autres. Le <b>partage pondéré</b> (WFQ, HTB) garantit à chaque classe une part du débit et lui permet d’emprunter le débit inutilisé. Les algorithmes de gestion active de file comme <b>fq_codel</b> et <b>CAKE</b> limitent le bufferbloat en maintenant des files courtes et en traitant équitablement les flux.' },
{ t: 'Limiter et mettre en forme', d: '', html: 'La <b>limitation</b> (policing) jette ce qui dépasse un débit, la <b>mise en forme</b> (shaping) retarde pour lisser. Le principe du seau à jetons : chaque paquet consomme des jetons, qui se rechargent à un débit fixé, avec une rafale autorisée. Pour que la QoS ait un effet sur un lien vers une box ou un opérateur, il faut <b>mettre en forme légèrement sous le débit réel</b> du lien : sinon la file se forme dans l’équipement de l’opérateur, où l’on n’a aucune maîtrise.' },
{ t: 'tc sous Linux', d: '', html: 'L’outil <code>tc</code> configure le contrôle du trafic du noyau : des <b>qdisc</b> (disciplines de file), éventuellement hiérarchiques, avec des <b>classes</b> et des <b>filtres</b>. Le plus simple et souvent le plus efficace aujourd’hui est <b>CAKE</b>, qui combine mise en forme, équité entre flux et prise en compte du DSCP en une ligne.', exemple: '# Mise en forme sortante à 95 Mbit/s pour une sortie de 100 Mbit/s, prise en compte du DSCP\nsudo tc qdisc replace dev wan0 root cake bandwidth 95mbit diffserv4 nat\ntc -s qdisc show dev wan0\n\n# Variante HTB : 20 % garantis à la voix, le reste partagé\ntc qdisc add dev wan0 root handle 1: htb default 30\ntc class add dev wan0 parent 1: classid 1:1 htb rate 95mbit\ntc class add dev wan0 parent 1:1 classid 1:10 htb rate 20mbit ceil 95mbit prio 0\ntc class add dev wan0 parent 1:1 classid 1:30 htb rate 75mbit ceil 95mbit prio 1\ntc filter add dev wan0 parent 1: protocol ip u32 match ip dsfield 0xb8 0xfc flowid 1:10' },
{ t: 'Mesurer', d: '', html: 'Une politique de QoS se vérifie par la mesure : latence sous charge (ping pendant un téléchargement saturant), gigue et pertes de la voix (statistiques RTP), compteurs de chaque classe (<code>tc -s class show</code>). L’objectif est une latence quasi constante même lorsque le lien est saturé.' }
],
synthese: ['Congestion : files, latence, pertes, bufferbloat', 'Classifier, marquer DSCP (EF voix, AF41 visio, CS1 faible priorité)', 'File prioritaire limitée, partage pondéré, fq_codel et CAKE', 'Policing jette, shaping retarde ; mettre en forme sous le débit réel', 'tc : qdisc, classes, filtres ; CAKE en une ligne', 'Mesurer la latence sous charge'],
qcm: [
['Marquage DSCP de la voix ?', ['0', 'EF (46)', 'CS1', 'AF11'], 1, 'Expedited Forwarding.'],
['Qu’est-ce que le bufferbloat ?', ['Une attaque', 'Une latence excessive due à des files trop grandes', 'Un câble', 'Un codec'], 1, 'Files saturées.'],
['Différence entre policing et shaping ?', ['Aucune', 'Le policing jette, le shaping retarde', 'Le shaping chiffre', 'Le policing accélère'], 1, 'Seau à jetons.'],
['Pourquoi mettre en forme sous le débit réel du lien ?', ['Économie', 'Pour que la file se forme sur notre équipement, où la politique s’applique', 'Obligation', 'Pour le Wi-Fi'], 1, 'Sinon chez l’opérateur.'],
['Quelle qdisc combine mise en forme et équité entre flux ?', ['pfifo', 'CAKE', 'noqueue', 'ingress'], 1, 'Simple et efficace.'],
['Classe pour les sauvegardes et mises à jour ?', ['EF', 'CS1', 'CS6', 'AF41'], 1, 'Moindre priorité.']
],
td: [
{ n: 'Application', c: 'Proposer une politique de QoS pour la sortie Internet d’un lycée (200 Mbit/s) : voix, visioconférence, évaluations en ligne, navigation, mises à jour.', r: 'Voix EF, file prioritaire limitée à 10 % · visio AF41 et évaluations (destinations connues) 30 % garantis · navigation 40 % · mises à jour CS1 10 % garantis, empruntable · mise en forme à 190 Mbit/s.' },
{ n: 'Application', c: 'Mesurer le bufferbloat d’une liaison : protocole de mesure.', r: 'ping continu vers une cible proche · lancer un téléchargement saturant (iperf3) · relever la latence pendant la charge · répéter après activation de CAKE.' },
{ n: 'Approfondissement', c: 'Interpréter les compteurs tc -s d’une classe dont les « dropped » augmentent fortement.', r: 'La classe dépasse son débit garanti sans pouvoir emprunter : soit la politique est trop restrictive, soit un flux non prévu y est classé ; vérifier les filtres et ajuster rate/ceil.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Lien saturé entre deux VM.', etapes: ['Mesurer la latence sous charge', 'Marquer le trafic voix', 'Appliquer CAKE', 'Construire une hiérarchie HTB', 'Comparer les mesures'] },
plusLoin: [['man tc · tc-cake', 'https://man7.org/linux/man-pages/man8/tc-cake.8.html'], ['Bufferbloat.net', 'https://www.bufferbloat.net/'], ['RFC 4594 · classes de service DiffServ', 'https://www.rfc-editor.org/rfc/rfc4594']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Pendant l’épreuve d’examen en ligne, les visioconférences de la salle des professeurs se figent et des élèves perdent leur session.', etapes: [
['Mesurer', 'ping 1.1.1.1 pendant la journée : 12 ms ; à 10 h : 480 ms\n→ Latence multipliée par 40 : bufferbloat sur la sortie.'],
['Identifier', 'iftop -i wan0 : 160 Mbit/s vers des serveurs de mises à jour\n→ Les postes d’une salle ont lancé une mise à jour du système d’exploitation.'],
['Corriger', 'tc qdisc replace dev wan0 root cake bandwidth 190mbit diffserv4 nat\n+ marquage CS1 des serveurs de mises à jour\n→ Les mises à jour ne passent plus qu’avec le débit inutilisé.'],
['Vérifier', 'ping sous charge : 18 ms · visioconférences fluides · aucune session d’examen perdue.'],
['Prévenir', '→ Mises à jour planifiées hors heures de cours et cache local de mises à jour ; politique de QoS documentée.']
] },
erreurs: ['Configurer la QoS sans mettre en forme sous le débit réel', 'Faire confiance aux marquages des postes', 'File prioritaire sans limite', 'Ne pas mesurer avant et après', 'Classer tout en haute priorité'],
memo: [['Commande', 'Usage'], ['tc qdisc show dev wan0', 'discipline en place'], ['tc -s qdisc show dev wan0', 'statistiques'], ['tc qdisc replace … root cake bandwidth Xmbit', 'mise en forme simple'], ['tc -s class show dev wan0', 'compteurs par classe'], ['tc filter show dev wan0', 'filtres'], ['iperf3 -c hôte', 'saturer et mesurer'], ['ping -i 0.2', 'latence sous charge'], ['iftop -i wan0', 'flux en cours']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer congestion et bufferbloat. (3 pts)', 'Proposer une politique de classes pour un cas donné. (6 pts)', 'Comparer policing et shaping. (3 pts)', 'Écrire une configuration tc. (5 pts)', 'Proposer un protocole de mesure. (3 pts)'], r: ['files', 'classes et DSCP', 'jeter / retarder', 'cake ou htb', 'latence sous charge'] },
prof: { deroule: [['Ouverture', 'Appel VoIP pendant un téléchargement saturant'], ['Cours', 'Classes et files'], ['Pratique', 'CAKE'], ['Pratique', 'HTB'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : CAKE seulement. Élèves avancés : HTB et filtres.', eval: 'Mesures avant/après, sujet type.' } },
exemples: [
['Latence sous charge', 'Diagnostic du bufferbloat.', 'ping -c 5 1.1.1.1 (pendant iperf3 saturant)', 'time=412 ms\ntime=455 ms\ntime=398 ms', 'Latence très élevée sous charge.'],
['Activer CAKE', 'Correction.', 'sudo tc qdisc replace dev wan0 root cake bandwidth 95mbit diffserv4 nat', '(aucune sortie)', 'Mise en forme à 95 % du débit.'],
['Après correction', 'Même mesure.', 'ping -c 5 1.1.1.1 (pendant iperf3)', 'time=17 ms\ntime=19 ms\ntime=16 ms', 'La latence reste basse malgré la saturation.'],
['Statistiques CAKE', 'Contrôle.', 'tc -s qdisc show dev wan0 | grep -A4 Tin', ' Bulk Best Effort Video Voice\n pkts 8121 912334 44012 18233\n drops 84 2210 3 0', 'Aucune perte dans la classe Voix.'],
['Vérifier le marquage', 'La voix est-elle marquée ?', 'sudo tcpdump -ni ens18 -v -c 1 udp portrange 10000-20000 | grep tos', 'IP (tos 0xb8, …)', '0xb8 = DSCP EF.'],
['Flux en cours', 'Qui sature ?', 'sudo iftop -i wan0 -t -s 5 | head -4', '1 10.62.20.57 => 151.101.2.132 48.2Mb\n2 10.62.20.58 => 151.101.2.132 47.9Mb', 'Plusieurs postes téléchargent depuis le même serveur de mises à jour.']
],
termes: ['QoS', 'congestion', 'file d’attente', 'bufferbloat', 'DSCP', 'EF', 'AF41', 'CS1', 'best effort', 'PCP', 'ordonnanceur', 'file prioritaire', 'WFQ', 'HTB', 'fq_codel', 'CAKE', 'policing', 'shaping', 'seau à jetons', 'tc', 'qdisc']
});
})();

View file

@ -0,0 +1,282 @@
// Cours complets · lot 12 : modélisation des données, virtualisation et cloud, logique et arithmétique, big data, compétences professionnelles
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'modelisation', match: /Modélisation : MCD|Merise|UML|MCD et MLD|Normalisation|1FN|Réplication|Entrepôt de données|modèle en étoile|Bases de données avancées|Génie logiciel|patrons de conception|urbanisation/i,
titre: 'Modéliser les données et le logiciel : Merise, UML, normalisation, réplication et entrepôts', duree: '',
niveau: 'NSI Terminale · BTS SIO SLAM · BUT Informatique · MIAGE · école d’ingénieurs',
prerequis: ['SQL'],
competences: ['BTS SIO SLAM · Bloc 2 · Concevoir une base de données', 'BUT Info · Concevoir', 'MIAGE · Urbaniser le système d’information'],
intro: 'Une base de données mal conçue se paie pendant des années : informations en double qui se contredisent, requêtes impossibles, mises à jour qui corrompent les données. La modélisation précède le code : elle traduit les règles de gestion en un schéma clair, partagé avec le métier. Ce module couvre Merise (MCD, MLD), les diagrammes UML essentiels, la normalisation, puis l’exploitation à grande échelle : réplication et entrepôts de données.',
cm: [
{ t: 'Des règles de gestion au MCD', d: '', html: 'On part des <b>règles de gestion</b> exprimées par le métier : « un élève appartient à une seule classe pour une année », « un devoir peut être donné à plusieurs classes », « un rendu concerne un élève et un devoir ». Le <b>modèle conceptuel des données</b> (MCD, méthode Merise) les représente par des <b>entités</b> (Élève, Classe, Devoir) avec leurs <b>attributs</b> et un <b>identifiant</b>, et des <b>associations</b> (Appartenir, Rendre) avec leurs <b>cardinalités</b> minimales et maximales : 0,n, 1,1, 1,n. Le MCD est indépendant de tout logiciel.' },
{ t: 'Du MCD au MLD et au SQL', d: '', html: 'Le <b>modèle logique</b> (MLD) traduit le MCD en tables par des règles simples. Chaque entité devient une table, son identifiant une clé primaire. Une association de type <b>1,n – 0,1 / 1,1</b> devient une <b>clé étrangère</b> du côté de la cardinalité maximale 1. Une association <b>n,n</b> devient une <b>table de liaison</b> dont la clé primaire combine les deux clés étrangères, et qui porte les attributs de l’association (la note d’un rendu). Le <b>modèle physique</b> ajoute les types, les index et les contraintes, et s’écrit en SQL.', exemple: 'ELEVE (id_eleve, nom, prenom, #id_classe)\nCLASSE (id_classe, libelle, annee)\nDEVOIR (id_devoir, titre, date_limite)\nRENDU (#id_eleve, #id_devoir, date_depot, note)\n\nCREATE TABLE rendu (\n id_eleve INT REFERENCES eleve(id_eleve),\n id_devoir INT REFERENCES devoir(id_devoir),\n date_depot TIMESTAMP NOT NULL,\n note NUMERIC(4,2) CHECK (note BETWEEN 0 AND 20),\n PRIMARY KEY (id_eleve, id_devoir)\n);' },
{ t: 'UML pour le logiciel', d: '', html: '<b>UML</b> propose des diagrammes complémentaires. Le <b>diagramme de cas d’utilisation</b> montre les acteurs (élève, professeur, CPE) et ce qu’ils font avec le système. Le <b>diagramme de classes</b> décrit les classes, leurs attributs, méthodes et relations (association, composition, héritage) : il sert à la fois de modèle de données et de structure du code orienté objet. Le <b>diagramme de séquence</b> montre l’enchaînement des messages pour un scénario (« l’élève lève la main »). Le <b>diagramme d’activité</b> décrit un processus. En pratique, on dessine ce qui aide à se comprendre, pas tous les diagrammes possibles.' },
{ t: 'La normalisation', d: '', html: 'Les <b>formes normales</b> éliminent les redondances et les anomalies de mise à jour. <b>1FN</b> : chaque attribut est atomique, pas de liste dans une cellule (pas de colonne « matières : maths, réseaux »). <b>2FN</b> : en 1FN, et chaque attribut non clé dépend de toute la clé primaire, pas d’une partie (dans RENDU(id_eleve, id_devoir, nom_eleve, note), nom_eleve ne dépend que de id_eleve). <b>3FN</b> : en 2FN, et aucun attribut non clé ne dépend d’un autre attribut non clé (le nom du professeur principal ne doit pas être stocké dans ELEVE, mais dans CLASSE). On peut <b>dénormaliser</b> volontairement pour la performance, en le documentant.' },
{ t: 'Réplication et haute disponibilité', d: '', html: 'La <b>réplication</b> copie les modifications d’un serveur <b>primaire</b> vers un ou plusieurs <b>réplicas</b>. Elle sert à la haute disponibilité (un réplica devient primaire si celui-ci tombe), à la répartition de la lecture et aux sauvegardes sans charger le primaire. Elle peut être <b>asynchrone</b> (rapide, mais quelques transactions peuvent être perdues lors d’une bascule) ou <b>synchrone</b> (aucune perte, plus lente). PostgreSQL utilise la réplication en flux des journaux (WAL) ; MariaDB les journaux binaires. La réplication ne remplace pas la sauvegarde : une suppression est répliquée instantanément.' },
{ t: 'Entrepôts de données', d: '', html: 'Une base <b>transactionnelle</b> (OLTP) est optimisée pour de nombreuses petites écritures. Pour l’analyse (OLAP), on construit un <b>entrepôt de données</b> alimenté par des traitements <b>ETL</b> ou ELT (extraire, transformer, charger). Le <b>modèle en étoile</b> place au centre une table de <b>faits</b> (les mesures : notes, absences, heures de lab) entourée de tables de <b>dimensions</b> (temps, élève, classe, matière, établissement). Les requêtes analytiques deviennent simples et rapides ; les bases en colonnes (ClickHouse, DuckDB) et les tableaux de bord (Metabase, Superset) s’appuient sur ce modèle.' }
],
synthese: ['Règles de gestion → MCD : entités, associations, cardinalités', 'MLD : 1,n → clé étrangère ; n,n → table de liaison', 'UML : cas d’utilisation, classes, séquence, activité', '1FN atomique, 2FN toute la clé, 3FN pas de dépendance transitive', 'Réplication primaire/réplicas, synchrone ou asynchrone ; pas une sauvegarde', 'Entrepôt : ETL, faits et dimensions, modèle en étoile'],
qcm: [
['Une association n,n devient en MLD :', ['une colonne', 'une table de liaison', 'un index', 'rien'], 1, 'Clé composée des deux clés étrangères.'],
['Une colonne « matières : maths, réseaux » viole :', ['1FN', '2FN', '3FN', 'aucune'], 0, 'Attribut non atomique.'],
['Quel diagramme UML montre les acteurs et leurs usages ?', ['Classes', 'Cas d’utilisation', 'Séquence', 'Déploiement'], 1, 'Vue fonctionnelle.'],
['Risque de la réplication asynchrone ?', ['Lenteur', 'Perte des dernières transactions à la bascule', 'Aucune', 'Corruption systématique'], 1, 'Les réplicas ont un léger retard.'],
['Dans un modèle en étoile, les notes sont :', ['une dimension', 'des faits', 'un index', 'une vue'], 1, 'Mesures au centre.'],
['La réplication remplace-t-elle la sauvegarde ?', ['Oui', 'Non, une suppression est répliquée', 'Seulement en synchrone', 'Oui si chiffrée'], 1, 'Il faut des sauvegardes historisées.']
],
td: [
{ n: 'Application', c: 'À partir des règles de gestion des labs (un lab a plusieurs étapes ; un élève réalise plusieurs labs ; pour chaque étape réalisée, on garde le nombre d’indices et la date), établir le MCD puis le MLD.', r: 'LAB (id_lab, titre) · ETAPE (id_etape, rang, consigne, #id_lab) · ELEVE (id_eleve, nom) · REALISER (#id_eleve, #id_etape, date, nb_indices).' },
{ n: 'Application', c: 'Normaliser la table INSCRIPTION(id_eleve, nom_eleve, id_classe, libelle_classe, nom_prof_principal).', r: 'ELEVE(id_eleve, nom, #id_classe) · CLASSE(id_classe, libelle, #id_prof_principal) · PROF(id_prof, nom).' },
{ n: 'Approfondissement', c: 'Concevoir le modèle en étoile d’un entrepôt de suivi pédagogique académique.', r: 'Faits : activité (heures de lab, nb de labs réussis, note moyenne) · dimensions : temps (jour, semaine, trimestre), élève (pseudonymisé), classe, filière, établissement, compétence.' }
],
tp: { type: 'IDE SQL', url: 'apercu-ide.html', desc: 'Du modèle à la base.', etapes: ['MCD à partir de règles de gestion', 'Passage au MLD', 'Création en SQL avec contraintes', 'Normalisation d’une table fournie', 'Requêtes d’analyse sur un modèle en étoile'] },
plusLoin: [['Documentation PostgreSQL · réplication', 'https://www.postgresql.org/docs/current/high-availability.html'], ['UML (OMG)', 'https://www.omg.org/spec/UML/'], ['DuckDB', 'https://duckdb.org/docs/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Dans l’application de vie scolaire, le nom du professeur principal d’une classe n’est pas le même selon les élèves.', etapes: [
['Constater', 'SELECT DISTINCT id_classe, prof_principal FROM eleve WHERE id_classe = 12;\n12 | M. Belkacemi\n12 | Mme Roche\n→ La même classe a deux professeurs principaux.'],
['Identifier la cause', '→ Le professeur principal est stocké dans chaque ligne de la table ELEVE : lors du changement en cours d’année, seuls les nouveaux élèves ont reçu la nouvelle valeur. Anomalie de mise à jour due à une table en 2FN mais pas en 3FN.'],
['Corriger le schéma', 'ALTER TABLE classe ADD id_prof_principal INT REFERENCES prof(id_prof);\nUPDATE classe SET id_prof_principal = … ;\nALTER TABLE eleve DROP COLUMN prof_principal;\n→ L’information n’existe plus qu’à un seul endroit.'],
['Adapter les requêtes', 'SELECT e.nom, p.nom FROM eleve e JOIN classe c USING (id_classe) JOIN prof p ON p.id_prof = c.id_prof_principal;\n→ Jointure au lieu de la colonne dupliquée.'],
['Prévenir', '→ Revue de modèle avant toute nouvelle fonctionnalité, migrations versionnées et tests.']
] },
erreurs: ['Stocker une liste dans une colonne', 'Dupliquer une information qui dépend d’une autre table', 'Oublier la table de liaison d’une association n,n', 'Identifiants métier modifiables comme clé primaire', 'Confondre réplication et sauvegarde', 'Analyser directement sur la base de production'],
memo: [['Règle', 'Traduction'], ['Entité', 'table, identifiant → clé primaire'], ['Association 1,n', 'clé étrangère côté 1'], ['Association n,n', 'table de liaison'], ['1FN', 'valeurs atomiques'], ['2FN', 'dépendance de toute la clé'], ['3FN', 'pas de dépendance transitive'], ['Cas d’utilisation', 'acteurs et usages'], ['Diagramme de classes', 'structure et relations'], ['Faits / dimensions', 'mesures / axes d’analyse']],
examen: { titre: 'Sujet type · 20 points', q: ['Établir un MCD à partir de règles de gestion. (6 pts)', 'Le traduire en MLD et en SQL. (5 pts)', 'Normaliser une table fournie. (4 pts)', 'Dessiner un diagramme de cas d’utilisation. (3 pts)', 'Comparer réplication et sauvegarde. (2 pts)'], r: ['entités, associations, cardinalités', 'règles de passage', '1FN à 3FN', 'acteurs', 'voir cours'] },
prof: { deroule: [['Ouverture', 'Une table pleine d’incohérences'], ['Cours', 'MCD et MLD'], ['Pratique', 'Modéliser les labs'], ['Cours', 'Normalisation et UML'], ['Cours', 'Réplication et entrepôts'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : MCD à compléter. Élèves avancés : patrons de conception, modèle en flocon.', eval: 'Dossier de modélisation, sujet type.' } },
exemples: [
['Contrainte de clé étrangère', 'Insertion invalide.', "INSERT INTO rendu VALUES (9999, 3, now(), 12);", 'ERROR: insert or update on table "rendu" violates foreign key constraint "rendu_id_eleve_fkey"', 'L’élève 9999 n’existe pas : la base protège l’intégrité.'],
['Détecter une redondance', 'Audit d’un schéma.', 'SELECT id_classe, COUNT(DISTINCT prof_principal) FROM eleve GROUP BY id_classe HAVING COUNT(DISTINCT prof_principal) > 1;', '12 | 2', 'Signe d’une table non normalisée.'],
['État de la réplication', 'Contrôle du primaire.', 'SELECT client_addr, state, replay_lag FROM pg_stat_replication;', '10.62.40.32 | streaming | 00:00:00.12', 'Le réplica suit avec 120 ms de retard.'],
['Requête analytique', 'Entrepôt.', 'SELECT t.trimestre, c.filiere, AVG(f.note) FROM faits f JOIN dim_temps t USING (id_temps) JOIN dim_classe c USING (id_classe) GROUP BY 1, 2;', 'T1 | SIO | 11.8\nT1 | CIEL | 12.3', 'Le modèle en étoile rend la requête directe.'],
['Analyse avec DuckDB', 'Fichier d’export.', "duckdb -c \"SELECT filiere, COUNT(*) FROM 'activite.parquet' GROUP BY 1\"", '┌─────────┬──────────────┐\n│ filiere │ count_star() │\n│ SIO │ 18234 │', 'Analyse directe d’un fichier Parquet sans serveur.'],
['Clé composée', 'Doublon de rendu.', "INSERT INTO rendu VALUES (1204, 3, now(), 14);", 'ERROR: duplicate key value violates unique constraint "rendu_pkey"', 'Un élève ne peut rendre qu’une fois le même devoir.']
],
termes: ['règle de gestion', 'MCD', 'MLD', 'Merise', 'entité', 'association', 'cardinalité', 'identifiant', 'table de liaison', 'UML', 'cas d’utilisation', 'diagramme de classes', 'diagramme de séquence', 'normalisation', '1FN', '2FN', '3FN', 'réplication', 'WAL', 'entrepôt de données', 'ETL', 'modèle en étoile', 'faits', 'dimensions']
});
reg({
cours: { id: 'virtucloud', match: /Virtualisation : hyperviseurs|KVM|Proxmox|Infrastructure en code|Terraform|OpenTofu|Cloud : IaaS|OpenStack|Systèmes distribués|tolérance aux pannes|Automatisation et infrastructure|Administrateur systèmes, réseaux et cloud|Concevoir une solution d’évolution/i,
titre: 'Virtualisation, cloud et infrastructure en code : KVM, Proxmox, IaaS, Terraform', duree: '',
niveau: 'BTS SIO SISR · BUT R&T DevCloud · AIS · Bachelor · Master cloud',
prerequis: ['Linux : processus, stockage', 'Réseau : VLAN, routage'],
competences: ['BTS SIO SISR · Bloc 2 · Virtualiser une infrastructure', 'AIS · Administrer une infrastructure virtualisée', 'Master · Concevoir des systèmes distribués'],
intro: 'Les machines virtuelles qui font tourner les labs de Syllabus, les serveurs de l’établissement et les offres des hébergeurs reposent sur la virtualisation. Le cloud la rend disponible à la demande, et l’infrastructure en code la décrit dans des fichiers versionnés. Ce module explique le fonctionnement des hyperviseurs, l’exploitation de KVM et Proxmox, les modèles de cloud, et Terraform/OpenTofu, avec les principes de conception des systèmes distribués tolérants aux pannes.',
cm: [
{ t: 'Hyperviseurs', d: '', html: 'Un <b>hyperviseur</b> partage une machine physique entre plusieurs machines virtuelles isolées. Le <b>type 1</b> (natif) s’exécute directement sur le matériel : KVM intégré au noyau Linux, VMware ESXi, Hyper-V, Xen. Le <b>type 2</b> s’exécute comme une application sur un système hôte : VirtualBox, VMware Workstation, adapté au poste de travail. Les processeurs modernes offrent des instructions de virtualisation (Intel VT-x, AMD-V) qui rendent l’exécution quasi native. Les périphériques <b>virtio</b> (disque, réseau) sont paravirtualisés : le système invité sait qu’il est virtualisé et communique efficacement avec l’hyperviseur.' },
{ t: 'KVM, libvirt et Proxmox', d: '', html: '<b>KVM</b> transforme le noyau Linux en hyperviseur ; <b>QEMU</b> émule le matériel ; <b>libvirt</b> fournit une interface commune (<code>virsh</code>). <b>Proxmox VE</b>, distribution Debian, ajoute une interface web, la gestion de clusters, la migration à chaud, les sauvegardes et les conteneurs LXC : il est très répandu dans les établissements et les PME. Les notions à maîtriser : formats de disque (qcow2 avec instantanés, raw plus rapide), réseau en pont (bridge Linux) avec VLAN, <b>instantanés</b> (utiles avant une mise à jour, pas une sauvegarde), <b>modèles</b> (templates) et <b>cloud-init</b> pour créer des VM configurées automatiquement.' },
{ t: 'Les modèles de cloud', d: '', html: 'Le cloud fournit des ressources à la demande, facturées à l’usage. <b>IaaS</b> : machines virtuelles, réseaux, stockage ; le client gère le système et au-dessus. <b>PaaS</b> : plateforme d’exécution (bases de données gérées, hébergement d’applications) ; le client gère son code et ses données. <b>SaaS</b> : logiciel prêt à l’emploi (messagerie, ENT). Le <b>modèle de responsabilité partagée</b> fixe qui sécurise quoi. <b>OpenStack</b> est la plateforme libre de cloud IaaS utilisée par plusieurs hébergeurs européens. Pour les données publiques et sensibles, la doctrine française « cloud au centre » et la qualification <b>SecNumCloud</b> de l’ANSSI orientent vers des offres hébergées et opérées dans l’Union européenne, protégées des lois extraterritoriales.' },
{ t: 'Infrastructure en code', d: '', html: '<b>Terraform</b> et son fork libre <b>OpenTofu</b> décrivent l’infrastructure (VM, réseaux, enregistrements DNS, bases) dans des fichiers déclaratifs (HCL). Le cycle : <code>init</code> (télécharger les fournisseurs), <code>plan</code> (calculer les différences entre le code et la réalité), <code>apply</code> (appliquer). L’<b>état</b> (state) mémorise ce qui a été créé ; il contient souvent des secrets et doit être stocké de façon partagée, verrouillée et protégée. On combine Terraform pour créer les ressources et Ansible pour configurer les systèmes.', exemple: 'resource "proxmox_vm_qemu" "lab" {\n count = 30\n name = "lab-eleve-${count.index + 1}"\n target_node = "pve1"\n clone = "debian12-template"\n cores = 2\n memory = 2048\n network { model = "virtio" bridge = "vmbr0" tag = 120 }\n}\n\n$ tofu plan\nPlan: 30 to add, 0 to change, 0 to destroy.' },
{ t: 'Concevoir des systèmes distribués', d: '', html: 'Dès qu’un service repose sur plusieurs machines, des pannes partielles surviennent : un nœud lent, un réseau qui se coupe entre deux sites. Le <b>théorème CAP</b> rappelle qu’en cas de partition réseau, il faut choisir entre cohérence et disponibilité. Les principes de conception : éliminer les points uniques de défaillance, rendre les services <b>sans état</b> pour pouvoir les multiplier, placer l’état dans des systèmes répliqués (base, stockage objet), utiliser un <b>consensus</b> (Raft, utilisé par etcd) pour les décisions critiques, prévoir des délais d’expiration et des nouvelles tentatives avec temporisation croissante, et tester les pannes volontairement.' },
{ t: 'Dimensionner et exploiter', d: '', html: 'On dimensionne un hôte de virtualisation à partir des besoins : processeurs (un léger <b>surengagement</b> est acceptable), mémoire (à éviter de surengager), stockage (IOPS plus que capacité), réseau. On surveille la charge de l’hôte, le temps d’attente processeur des VM (steal time), la latence disque. Un cluster de trois nœuds avec stockage partagé ou répliqué (Ceph) permet la haute disponibilité : une VM redémarre sur un autre nœud si le sien tombe. Les sauvegardes des VM (Proxmox Backup Server) s’ajoutent aux sauvegardes applicatives.' }
],
synthese: ['Type 1 (KVM, ESXi) et type 2 (VirtualBox) ; VT-x/AMD-V ; virtio', 'KVM + QEMU + libvirt ; Proxmox : cluster, migration, sauvegardes, LXC', 'Instantanés ≠ sauvegardes ; templates et cloud-init', 'IaaS, PaaS, SaaS ; responsabilité partagée ; SecNumCloud', 'Terraform/OpenTofu : init, plan, apply, état protégé', 'Distribué : pas de point unique, sans état, consensus, CAP', 'Dimensionner mémoire et IOPS ; haute disponibilité en cluster'],
qcm: [
['KVM est un hyperviseur de type :', ['1', '2', '3', 'aucun'], 0, 'Intégré au noyau.'],
['Un instantané de VM est-il une sauvegarde ?', ['Oui', 'Non, il dépend du disque d’origine', 'Seulement en qcow2', 'Oui s’il est récent'], 1, 'Il disparaît avec le stockage.'],
['Dans un IaaS, qui met à jour le système de la VM ?', ['Le fournisseur', 'Le client', 'Personne', 'L’ANSSI'], 1, 'Responsabilité partagée.'],
['Que fait terraform plan ?', ['Crée tout', 'Montre les changements à appliquer', 'Supprime l’état', 'Installe Terraform'], 1, 'Aucune modification.'],
['Pourquoi protéger le fichier d’état Terraform ?', ['Il est gros', 'Il peut contenir des secrets et doit rester cohérent', 'Obligation', 'Il est lent'], 1, 'Stockage distant verrouillé.'],
['Que dit le théorème CAP ?', ['Tout est possible', 'En cas de partition, choisir entre cohérence et disponibilité', 'Le cloud est sûr', 'Les VM sont isolées'], 1, 'Brewer.']
],
td: [
{ n: 'Application', c: 'Dimensionner un hôte Proxmox pour 30 VM de labs (2 vCPU, 2 Go, 20 Go chacune), avec une marge de 30 %.', r: 'Mémoire : 60 Go + 30 % ≈ 80 Go (+ hôte) → 96 Go · CPU : 60 vCPU, surengagement 3:1 → 20 cœurs · disque : 600 Go en qcow2 léger sur SSD.' },
{ n: 'Application', c: 'Écrire un fichier OpenTofu qui crée 30 VM de lab à partir d’un modèle, dans le VLAN 120.', r: 'Voir l’exemple du cours : resource avec count = 30, clone du template, réseau tag 120 ; tofu plan puis apply.' },
{ n: 'Approfondissement', c: 'Concevoir l’architecture d’un service de labs tolérant à la panne d’un serveur.', r: 'Cluster de trois nœuds, stockage Ceph répliqué ×3, orchestrateur de VM avec haute disponibilité, application web sans état derrière deux répartiteurs avec IP virtuelle, base PostgreSQL répliquée, sauvegardes externes.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Virtualisation imbriquée sous KVM.', etapes: ['Créer une VM avec virsh', 'Configurer un pont réseau et un VLAN', 'Prendre et restaurer un instantané', 'Créer une VM par cloud-init', 'Décrire l’infrastructure avec OpenTofu', 'Mesurer la charge de l’hôte'] },
plusLoin: [['Proxmox VE · documentation', 'https://pve.proxmox.com/pve-docs/'], ['OpenTofu', 'https://opentofu.org/docs/'], ['ANSSI · SecNumCloud', 'https://cyber.gouv.fr/secnumcloud-pour-les-fournisseurs-de-services-cloud'], ['libvirt', 'https://libvirt.org/docs.html']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Les VM des labs deviennent très lentes l’après-midi, alors que l’hôte Proxmox affiche un processeur à 45 %.', etapes: [
['Mesurer côté VM', 'vmstat 1 dans une VM : st (steal) = 2, wa = 61\n→ Peu d’attente processeur mais beaucoup d’attente d’entrées-sorties.'],
['Mesurer côté hôte', 'iostat -x 1 sur l’hôte : sda %util 100, await 180 ms\n→ Le disque de l’hôte est saturé.'],
['Identifier', '→ Toutes les VM sont sur un disque mécanique, et la sauvegarde planifiée à 14 h lit l’intégralité des disques pendant les cours.'],
['Corriger', '→ Sauvegarde déplacée à 2 h, VM de labs migrées à chaud vers le stockage SSD, limite de bande passante disque pour la sauvegarde.'],
['Vérifier', 'await : 3 ms · wa dans les VM : 1 %\n→ Règle de dimensionnement documentée : les VM interactives sur SSD, sauvegardes hors des heures de cours.']
] },
erreurs: ['Prendre les instantanés pour des sauvegardes', 'Surengager la mémoire', 'Dimensionner le disque en capacité sans regarder les IOPS', 'Fichier d’état Terraform sur un poste et dans Git', 'Point unique de défaillance', 'Modifier à la main une ressource gérée par Terraform'],
memo: [['Commande', 'Usage'], ['virsh list --all', 'VM et état'], ['virsh snapshot-create-as vm nom', 'instantané'], ['qm list · qm migrate', 'Proxmox : VM, migration'], ['pvesh get /cluster/resources', 'ressources du cluster'], ['tofu init · plan · apply', 'infrastructure en code'], ['tofu state list', 'ressources gérées'], ['iostat -x 1', 'charge disque'], ['vmstat 1 (colonne st)', 'temps volé à la VM']],
examen: { titre: 'Sujet type · 20 points', q: ['Comparer hyperviseurs de type 1 et 2. (3 pts)', 'Dimensionner un hôte pour un besoin donné. (5 pts)', 'Comparer IaaS, PaaS, SaaS et la responsabilité partagée. (4 pts)', 'Écrire une configuration Terraform simple. (5 pts)', 'Expliquer le théorème CAP. (3 pts)'], r: ['natif / hébergé', 'CPU, mémoire, IOPS', 'voir cours', 'resource, plan, apply', 'cohérence / disponibilité'] },
prof: { deroule: [['Ouverture', 'Créer 30 VM à la main ou en une commande'], ['Cours', 'Hyperviseurs'], ['Pratique', 'KVM et instantanés'], ['Cours', 'Cloud et IaC'], ['Pratique', 'OpenTofu'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : interface Proxmox. Élèves avancés : Ceph, consensus.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Lister les VM', 'Inventaire.', 'virsh list --all', ' Id Name State\n 3 lab-eleve-1 running\n - debian12-tpl shut off', 'Une VM de lab en cours et le modèle arrêté.'],
['Vérifier la virtualisation matérielle', 'Nouvel hôte.', 'grep -cE "vmx|svm" /proc/cpuinfo', '24', 'Les instructions de virtualisation sont présentes sur les 24 processeurs logiques.'],
['Planifier une infrastructure', 'Avant application.', 'tofu plan | tail -1', 'Plan: 30 to add, 0 to change, 0 to destroy.', 'Aucune ressource existante n’est modifiée.'],
['Détecter une dérive', 'Une VM modifiée à la main.', 'tofu plan | grep "~"', ' ~ memory = 4096 -> 2048', 'La mémoire a été changée hors du code ; apply la ramènerait à 2 Go.'],
['Temps volé', 'VM lente.', 'vmstat 1 3 | tail -1', ' r b … us sy id wa st\n 2 0 … 20 5 40 0 35', '35 % de steal : l’hôte surengage le processeur.'],
['Migration à chaud', 'Maintenance d’un nœud.', 'qm migrate 104 pve2 --online', 'migration finished successfully (duration 00:00:21)', 'La VM a changé d’hôte sans interruption.']
],
termes: ['hyperviseur', 'type 1', 'type 2', 'KVM', 'QEMU', 'libvirt', 'virsh', 'Proxmox', 'virtio', 'qcow2', 'instantané', 'template', 'cloud-init', 'LXC', 'IaaS', 'PaaS', 'SaaS', 'responsabilité partagée', 'OpenStack', 'SecNumCloud', 'Terraform', 'OpenTofu', 'state', 'CAP', 'consensus', 'Raft', 'Ceph']
});
reg({
cours: { id: 'logique', match: /Algèbre de Boole|Karnaugh|Portes logiques|circuits combinatoires|additionneur|Arithmétique : division euclidienne|PGCD|Bézout|congruences|Fonctions : dérivées|exponentielle|logarithme|RSA, logique|Numération, codage, logique combinatoire|Numération, modulo/i,
titre: 'Logique et arithmétique pour l’informatique : Boole, circuits, modulo et RSA', duree: '',
niveau: 'NSI · STI2D SIN · BTS SIO (maths) · BTS CIEL · BUT · classes préparatoires',
prerequis: ['Binaire'],
competences: ['NSI · Architectures matérielles : portes logiques', 'BTS SIO · Mathématiques pour l’informatique', 'STI2D SIN · Logique combinatoire'],
intro: 'Un processeur n’est qu’un immense assemblage de portes logiques ; une connexion HTTPS repose sur de l’arithmétique modulaire ; une boucle qui double à chaque tour suit une exponentielle. Ce module rassemble les bases mathématiques directement utilisées en informatique : algèbre de Boole et simplification, circuits combinatoires jusqu’à l’additionneur, arithmétique des entiers jusqu’au chiffrement RSA, et le comportement des fonctions exponentielle et logarithme.',
cm: [
{ t: 'Algèbre de Boole', d: '', html: 'Une variable booléenne vaut 0 ou 1. Trois opérations de base : <b>ET</b> (produit, a·b), <b>OU</b> (somme, a + b) et <b>NON</b> (complément, ā). Les lois usuelles permettent de simplifier : commutativité, associativité, distributivité, a + ā = 1, a·ā = 0, a + a·b = a (absorption), et les <b>lois de De Morgan</b> : NON(a ET b) = ā OU b̄, NON(a OU b) = ā ET b̄. Une fonction se décrit par sa <b>table de vérité</b> ; on en tire une expression en somme de produits (une ligne à 1 = un produit).' },
{ t: 'Tableaux de Karnaugh', d: '', html: 'Le <b>tableau de Karnaugh</b> présente la table de vérité de façon que deux cases voisines ne diffèrent que d’une variable (ordre de Gray : 00, 01, 11, 10). On regroupe les 1 en rectangles de 1, 2, 4 ou 8 cases, les plus grands possible, éventuellement en passant par les bords. Chaque groupe donne un produit où ne restent que les variables qui ne changent pas. La somme des groupes est l’expression simplifiée, qui demande moins de portes. Les cas indifférents (« X ») peuvent être inclus dans un groupe s’ils l’agrandissent.' },
{ t: 'Portes et circuits combinatoires', d: '', html: 'Chaque opération correspond à une <b>porte logique</b> : ET, OU, NON, et leurs combinaisons NON-ET (NAND), NON-OU (NOR), OU exclusif (XOR, vrai si les entrées diffèrent). La porte NAND seule suffit à construire toutes les autres : elle est dite universelle. Un <b>circuit combinatoire</b> a des sorties qui ne dépendent que des entrées : multiplexeur, décodeur, comparateur. Le <b>demi-additionneur</b> additionne deux bits : somme = a XOR b, retenue = a ET b. L’<b>additionneur complet</b> prend aussi une retenue entrante ; en chaînant n additionneurs complets, on additionne deux nombres de n bits : c’est le cœur de l’UAL.', exemple: 'Additionneur complet :\nS = a XOR b XOR r_entrée\nr_s = (a ET b) OU (r_entrée ET (a XOR b))' },
{ t: 'Division euclidienne et congruences', d: '', html: 'Pour deux entiers a et b > 0, il existe un unique couple (q, r) tel que a = b·q + r avec 0 ≤ r < b : c’est la <b>division euclidienne</b> ; en programmation, <code>a // b</code> et <code>a % b</code>. On dit que a est <b>congru</b> à r modulo b. L’arithmétique <b>modulaire</b> est partout : heures sur 24, jours de la semaine, numéros de clé de contrôle (RIB, INE, numéro de sécurité sociale), répartition de charge par hachage, et cryptographie. On peut additionner et multiplier les congruences, ce qui permet de calculer de grandes puissances modulaires sans manipuler d’énormes nombres (exponentiation rapide).' },
{ t: 'PGCD, Bézout et RSA', d: '', html: 'L’<b>algorithme d’Euclide</b> calcule le PGCD par divisions successives : PGCD(a, b) = PGCD(b, a mod b), jusqu’à un reste nul. Le <b>théorème de Bézout</b> affirme qu’il existe des entiers u et v tels que a·u + b·v = PGCD(a, b) ; l’algorithme d’Euclide étendu les calcule, ce qui permet de trouver l’<b>inverse modulaire</b> d’un nombre premier avec le module.<br><br><b>RSA</b> repose sur ces outils : on choisit deux grands nombres premiers p et q, n = p·q, φ = (p − 1)(q − 1), un exposant e premier avec φ, et d l’inverse de e modulo φ. La clé publique est (n, e), la clé privée d. Chiffrer : c = m<sup>e</sup> mod n ; déchiffrer : m = c<sup>d</sup> mod n. La sécurité repose sur la difficulté de factoriser n quand p et q ont des centaines de chiffres.', exemple: 'p = 61, q = 53, n = 3233, φ = 3120\ne = 17 (premier avec 3120), d = 2753 (17 × 2753 = 46801 ≡ 1 mod 3120)\nm = 65 → c = 65¹⁷ mod 3233 = 2790 → 2790²⁷⁵³ mod 3233 = 65' },
{ t: 'Exponentielle et logarithme', d: '', html: 'La fonction <b>exponentielle</b> modélise les croissances proportionnelles à la valeur atteinte : un nombre d’utilisateurs qui augmente de 5 % par mois, un algorithme en O(2ⁿ). Le <b>logarithme</b> est sa réciproque : log₂(n) est le nombre de fois qu’on peut diviser n par deux, d’où la complexité de la recherche dichotomique, la profondeur d’un arbre équilibré, ou le nombre de bits nécessaires pour coder n valeurs. Propriétés utiles : log(a·b) = log a + log b, log(aⁿ) = n·log a. La <b>dérivée</b> mesure une vitesse de variation : elle sert à la descente de gradient des réseaux de neurones et à l’étude des variations d’une fonction de coût.' }
],
synthese: ['Boole : ET, OU, NON ; De Morgan ; table de vérité', 'Karnaugh : ordre de Gray, groupes de 2ⁿ cases', 'Portes ET, OU, NON, NAND, NOR, XOR ; NAND universelle', 'Demi-additionneur et additionneur complet : base de l’UAL', 'Division euclidienne, congruences, exponentiation rapide', 'Euclide, Bézout, inverse modulaire, RSA', 'Exponentielle pour les croissances, logarithme pour les divisions par deux'],
qcm: [
['NON(a ET b) est égal à :', ['ā ET b̄', 'ā OU b̄', 'a OU b', 'a XOR b'], 1, 'De Morgan.'],
['Somme du demi-additionneur ?', ['a ET b', 'a OU b', 'a XOR b', 'NON a'], 2, 'Retenue = a ET b.'],
['Quelle porte est universelle ?', ['XOR', 'NAND', 'OU', 'NON'], 1, 'Tout se construit avec des NAND.'],
['17 mod 5 ?', ['3', '2', '1', '0'], 1, '17 = 5 × 3 + 2.'],
['PGCD(84, 36) ?', ['6', '12', '18', '4'], 1, '84 = 2×36 + 12, 36 = 3×12.'],
['Sur quoi repose la sécurité de RSA ?', ['La vitesse', 'La difficulté de factoriser n', 'Le secret de l’algorithme', 'Le hachage'], 1, 'Grands nombres premiers.'],
['log₂(1024) ?', ['8', '10', '12', '512'], 1, '2¹⁰ = 1024.']
],
td: [
{ n: 'Application', c: 'Simplifier par Karnaugh la fonction à 3 variables valant 1 pour les combinaisons abc = 001, 011, 101, 111.', r: 'Les quatre 1 forment un groupe où c = 1 : F = c.' },
{ n: 'Application', c: 'Construire la table de vérité de l’additionneur complet et vérifier les expressions de S et de la retenue.', r: '8 lignes ; S vaut 1 quand le nombre d’entrées à 1 est impair ; la retenue vaut 1 quand au moins deux entrées valent 1.' },
{ n: 'Application', c: 'Calculer avec l’algorithme d’Euclide étendu l’inverse de 17 modulo 3120.', r: '3120 = 17 × 183 + 9 ; 17 = 9 + 8 ; 9 = 8 + 1 → en remontant : 1 = 2 × 3120 − 367 × 17 → inverse ≡ −367 ≡ 2753.' },
{ n: 'Approfondissement', c: 'Écrire en Python l’exponentiation modulaire rapide et la comparer à pow(m, e, n).', r: 'def expmod(m, e, n):\n r = 1\n while e:\n if e & 1: r = r * m % n\n m = m * m % n\n e >>= 1\n return r' }
],
tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'Logique et arithmétique en code.', etapes: ['Générer une table de vérité', 'Simuler un additionneur 4 bits', 'Algorithme d’Euclide étendu', 'RSA avec de petits nombres', 'Exponentiation rapide et mesure'] },
plusLoin: [['Logisim Evolution (simulateur de circuits)', 'https://github.com/logisim-evolution/logisim-evolution'], ['Eduscol · NSI', 'https://eduscol.education.fr/'], ['Nand to Tetris', 'https://www.nand2tetris.org/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Un script de contrôle des numéros INE saisis dans l’application rejette des numéros valides.', etapes: [
['Comprendre la règle', '→ Les identifiants comportent une clé de contrôle calculée par une opération modulo sur la partie numérique ; le script doit recalculer la clé et la comparer.'],
['Tester', 'controle("070123456AB") → False alors que le numéro figure dans l’annuaire officiel.'],
['Lire le code', 'cle = int(partie) % 23\n→ Le script utilise un modulo différent de celui de la spécification, et convertit la lettre de clé avec un décalage erroné.'],
['Corriger', '→ Réécriture selon la documentation de la règle de calcul, avec des tests sur une liste de numéros valides et invalides fournie.'],
['Retenir', '→ Toute règle de contrôle par modulo se vérifie avec un jeu de tests issus de cas réels, avant la mise en production.']
] },
erreurs: ['Oublier une ligne de la table de vérité', 'Groupes de Karnaugh de 3 ou 6 cases', 'Confondre OU et OU exclusif', 'Calculer mᵉ puis mod n sur de grands nombres', 'Croire que RSA est sûr avec de petits nombres', 'Confondre log₂ et log₁₀'],
memo: [['Formule', 'Usage'], ['NON(a·b) = ā + b̄', 'De Morgan'], ['a + a·b = a', 'absorption'], ['S = a ⊕ b ⊕ r', 'somme d’un additionneur'], ['a = bq + r, 0 ≤ r < b', 'division euclidienne'], ['PGCD(a, b) = PGCD(b, a mod b)', 'Euclide'], ['au + bv = PGCD(a, b)', 'Bézout'], ['c = mᵉ mod n · m = cᵈ mod n', 'RSA'], ['pow(m, e, n)', 'exponentiation modulaire en Python'], ['log(ab) = log a + log b', 'logarithme']],
examen: { titre: 'Sujet type · 20 points', q: ['Simplifier une fonction par Karnaugh. (5 pts)', 'Dessiner un additionneur complet. (4 pts)', 'Calculer un PGCD et un inverse modulaire. (5 pts)', 'Dérouler RSA sur de petits nombres. (4 pts)', 'Utiliser un logarithme pour une complexité. (2 pts)'], r: ['groupes', 'XOR, ET, OU', 'Euclide étendu', 'n, φ, e, d', 'log₂ n'] },
prof: { deroule: [['Ouverture', 'Construire un additionneur dans Logisim'], ['Cours', 'Boole et Karnaugh'], ['Pratique', 'Circuits'], ['Cours', 'Modulo, Euclide, RSA'], ['Pratique', 'RSA en Python'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : tables guidées. Élèves avancés : exponentiation rapide, test de primalité.', eval: 'Sujet type.' } },
exemples: [
['Table de vérité', 'XOR.', 'python3 -c "print([(a, b, a ^ b) for a in (0, 1) for b in (0, 1)])"', '[(0, 0, 0), (0, 1, 1), (1, 0, 1), (1, 1, 0)]', 'Vrai si les entrées diffèrent.'],
['Addition binaire', 'Vérifier un additionneur.', 'python3 -c "print(bin(0b1011 + 0b0110))"', '0b10001', '11 + 6 = 17 : la dernière retenue produit un cinquième bit.'],
['PGCD', 'Euclide.', 'python3 -c "import math; print(math.gcd(84, 36))"', '12', 'Comme le calcul à la main.'],
['Inverse modulaire', 'Clé privée RSA.', 'python3 -c "print(pow(17, -1, 3120))"', '2753', 'Python 3.8+ calcule directement l’inverse.'],
['Chiffrer et déchiffrer', 'RSA jouet.', 'python3 -c "c = pow(65, 17, 3233); print(c, pow(c, 2753, 3233))"', '2790 65', 'Le message revient intact.'],
['Nombre de bits', 'Coder 1 500 identifiants.', 'python3 -c "import math; print(math.ceil(math.log2(1500)))"', '11', '11 bits suffisent (2¹¹ = 2048).']
],
termes: ['algèbre de Boole', 'ET', 'OU', 'NON', 'XOR', 'NAND', 'NOR', 'De Morgan', 'table de vérité', 'Karnaugh', 'code de Gray', 'porte logique', 'circuit combinatoire', 'demi-additionneur', 'additionneur complet', 'division euclidienne', 'modulo', 'congruence', 'PGCD', 'Euclide', 'Bézout', 'inverse modulaire', 'RSA', 'logarithme', 'exponentielle']
});
reg({
cours: { id: 'bigdata', match: /Kafka|Spark|Traitement distribué|Flux temps réel|big data|Ingénierie des données/i,
titre: 'Ingénierie des données à grande échelle : pipelines, Kafka et Spark', duree: '',
niveau: 'BUT SD / Informatique · école d’ingénieurs · Master data',
prerequis: ['Analyse de données avec pandas', 'SQL', 'Docker'],
competences: ['BUT SD · Traiter des données massives', 'Master · Concevoir un pipeline de données'],
intro: 'Quand les données dépassent la mémoire d’une machine ou arrivent en continu, pandas et une base unique ne suffisent plus. Le data engineer conçoit des pipelines qui collectent, transportent, transforment et stockent ces données de façon fiable. Ce module présente l’architecture d’un pipeline, le transport d’événements avec Apache Kafka, le traitement distribué avec Apache Spark, et les formats et pratiques qui rendent l’ensemble exploitable.',
cm: [
{ t: 'Architecture d’un pipeline', d: '', html: 'Un pipeline suit des étapes : <b>ingestion</b> (bases, fichiers, API, capteurs, journaux), <b>stockage brut</b> (lac de données, souvent en stockage objet S3), <b>transformation</b> (nettoyage, jointures, agrégats), <b>stockage préparé</b> (entrepôt ou couche « gold »), puis <b>exposition</b> (tableaux de bord, modèles, API). Le traitement par <b>lots</b> (batch) s’exécute périodiquement ; le traitement en <b>flux</b> (streaming) traite chaque événement à son arrivée. Un <b>orchestrateur</b> (Airflow, Dagster) planifie les tâches et leurs dépendances, et rejoue celles qui échouent.' },
{ t: 'Formats et stockage', d: '', html: 'Le CSV ne porte pas de types et se lit lentement. Les formats en <b>colonnes</b> comme <b>Parquet</b> sont typés, compressés, et permettent de ne lire que les colonnes utiles. Les formats de tables comme <b>Delta Lake</b> ou <b>Apache Iceberg</b> ajoutent sur Parquet des transactions, l’historique des versions et l’évolution du schéma. On <b>partitionne</b> les données (par date, par établissement) pour ne lire que les partitions nécessaires.' },
{ t: 'Kafka : le journal d’événements', d: '', html: '<b>Apache Kafka</b> est un <b>journal distribué</b> : des <b>producteurs</b> écrivent des messages dans des <b>topics</b>, découpés en <b>partitions</b> répliquées sur plusieurs <b>brokers</b> ; des <b>consommateurs</b> les lisent à leur rythme en suivant leur position (<b>offset</b>). Les messages sont conservés pendant une durée configurée, ce qui permet de relire l’historique. L’ordre est garanti à l’intérieur d’une partition ; la clé du message détermine sa partition. Les consommateurs d’un même <b>groupe</b> se répartissent les partitions pour monter en charge. Kafka découple les systèmes : la source d’événements (connexions, mains levées, mesures de capteurs) n’a pas à connaître ses consommateurs.' },
{ t: 'Spark : le calcul distribué', d: '', html: '<b>Apache Spark</b> répartit un calcul sur un cluster. Un <b>pilote</b> (driver) découpe le travail en tâches exécutées par des <b>exécuteurs</b>. Le <b>DataFrame</b> de Spark ressemble à celui de pandas, mais ses opérations sont <b>paresseuses</b> : elles construisent un plan, optimisé puis exécuté seulement lors d’une action (count, write). Les transformations qui regroupent ou joignent des données provoquent un <b>shuffle</b>, échange réseau coûteux entre machines. Spark SQL permet d’écrire les traitements en SQL, et Structured Streaming applique le même modèle aux flux, par exemple depuis Kafka.', exemple: 'from pyspark.sql import SparkSession, functions as F\nspark = SparkSession.builder.appName("activite").getOrCreate()\ndf = spark.read.parquet("s3a://lac/activite/annee=2026/")\n(df.filter(F.col("type") == "lab")\n .groupBy("etablissement", "semaine")\n .agg(F.countDistinct("eleve").alias("eleves"), F.avg("duree").alias("duree_moy"))\n .write.mode("overwrite").partitionBy("semaine").parquet("s3a://gold/activite_hebdo/"))' },
{ t: 'Fiabilité et qualité', d: '', html: 'Un pipeline de production doit être <b>idempotent</b> : rejouer un traitement ne doit pas dupliquer les données (écriture par partition en remplacement, clés uniques). Il faut gérer les <b>données tardives</b> en streaming (fenêtres et filigranes), les <b>garanties de livraison</b> (au moins une fois, exactement une fois), et contrôler la <b>qualité</b> à chaque étape : schéma attendu, valeurs manquantes, volumes anormaux (Great Expectations, tests dbt). La <b>traçabilité</b> (lignage) indique d’où vient chaque donnée, utile pour le RGPD et pour corriger une erreur à la source.' },
{ t: 'Données personnelles et sobriété', d: '', html: 'Les données d’élèves restent des données personnelles même dans un lac de données : on pseudonymise au plus tôt, on applique des durées de conservation, on limite les accès par rôle et on journalise. Les traitements massifs ont aussi un coût énergétique : on ne garde que les données utiles, on choisit des formats compressés, on dimensionne les clusters à la demande et on préfère souvent une seule machine bien dimensionnée (DuckDB, Polars) à un cluster quand le volume le permet.' }
],
synthese: ['Ingestion → brut → transformation → préparé → exposition ; lots ou flux ; orchestrateur', 'Parquet, Delta/Iceberg, partitionnement', 'Kafka : topics, partitions, offsets, groupes, rétention', 'Spark : driver, exécuteurs, évaluation paresseuse, shuffle, Spark SQL, Structured Streaming', 'Idempotence, données tardives, garanties de livraison, qualité, lignage', 'Pseudonymiser, conserver le nécessaire, dimensionner sobrement'],
qcm: [
['Où l’ordre des messages est-il garanti dans Kafka ?', ['Dans tout le topic', 'Dans une partition', 'Nulle part', 'Par broker'], 1, 'La clé choisit la partition.'],
['Qu’est-ce qu’un offset ?', ['Une clé', 'La position d’un consommateur dans une partition', 'Un serveur', 'Un format'], 1, 'Permet de reprendre.'],
['Quand Spark exécute-t-il réellement les transformations ?', ['Immédiatement', 'Lors d’une action', 'Jamais', 'Au démarrage'], 1, 'Évaluation paresseuse.'],
['Qu’est-ce qu’un shuffle ?', ['Un tri local', 'Un échange de données entre machines pour regrouper ou joindre', 'Une erreur', 'Un format'], 1, 'Opération coûteuse.'],
['Pourquoi un pipeline doit-il être idempotent ?', ['Vitesse', 'Le rejouer ne doit pas dupliquer les données', 'Obligation', 'Sécurité'], 1, 'Reprises sans risque.'],
['Pourquoi préférer Parquet au CSV ?', ['Lisible', 'Typé, compressé, lecture par colonnes', 'Obligatoire', 'Plus petit en mémoire'], 1, 'Format colonne.']
],
td: [
{ n: 'Application', c: 'Concevoir le pipeline qui alimente chaque nuit le tableau de bord académique des labs à partir des bases de 40 établissements.', r: 'Extraction incrémentale (depuis la dernière date) → Parquet partitionné par jour et établissement dans le lac → pseudonymisation → agrégats Spark ou DuckDB → tables gold → tableau de bord ; orchestration Airflow avec reprise et contrôles de volume.' },
{ n: 'Application', c: 'Choisir le nombre de partitions et la clé d’un topic Kafka « mains-levees » consommé par 4 instances, l’ordre devant être respecté par salle.', r: 'Clé = identifiant de la salle (ordre par salle) · au moins 4 partitions, idéalement 8 à 12 pour la croissance · réplication 3.' },
{ n: 'Approfondissement', c: 'Un traitement Spark de 10 Go met 40 minutes. Proposer des pistes d’optimisation.', r: 'Lire seulement les colonnes et partitions utiles · filtrer avant de joindre · diffuser la petite table (broadcast join) · réduire les shuffles · vérifier le déséquilibre des clés · ou utiliser DuckDB sur une seule machine.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Kafka et Spark en conteneurs.', etapes: ['Démarrer Kafka avec Compose', 'Produire et consommer des événements', 'Lire un flux avec Structured Streaming', 'Agréger par fenêtre', 'Écrire en Parquet partitionné', 'Comparer avec DuckDB'] },
plusLoin: [['Apache Kafka · documentation', 'https://kafka.apache.org/documentation/'], ['Apache Spark · documentation', 'https://spark.apache.org/docs/latest/'], ['DataCamp · Big Data with PySpark', 'https://www.datacamp.com/tracks/big-data-with-pyspark'], ['Data Engineering Zoomcamp', 'https://github.com/DataTalksClub/data-engineering-zoomcamp']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Le tableau de bord affiche deux fois plus d’heures de labs que la réalité pour la semaine dernière.', etapes: [
['Comparer avec la source', 'SELECT SUM(duree) FROM activite WHERE semaine = 39 (base source) → 1 840 h ; tableau de bord → 3 680 h\n→ Exactement le double.'],
['Lire l’historique de l’orchestrateur', '→ La tâche du lundi a échoué à mi-parcours puis a été relancée ; elle écrivait en mode ajout (append).'],
['Identifier', '→ La première exécution partielle et la seconde complète ont toutes deux été écrites : traitement non idempotent.'],
['Corriger', '.write.mode("overwrite").option("partitionOverwriteMode", "dynamic").partitionBy("semaine")\n→ Chaque exécution remplace la partition qu’elle traite ; on recalcule la semaine 39.'],
['Prévenir', '→ Contrôle automatique : le total d’heures doit correspondre à la source à 1 % près, sinon la publication est bloquée et une alerte est envoyée.']
] },
erreurs: ['Écrire en mode ajout un traitement rejouable', 'Stocker en CSV non typé', 'Clé Kafka mal choisie qui casse l’ordre ou déséquilibre les partitions', 'Collecter un gros DataFrame sur le pilote', 'Pas de contrôle de qualité', 'Données personnelles non pseudonymisées dans le lac'],
memo: [['Commande / API', 'Usage'], ['kafka-topics.sh --create --partitions 8 --replication-factor 3', 'créer un topic'], ['kafka-console-producer.sh / consumer.sh', 'tester'], ['kafka-consumer-groups.sh --describe --group g', 'retard des consommateurs'], ['spark.read.parquet(...)', 'lire'], ['df.explain()', 'plan d’exécution'], ['.write.partitionBy(...).mode("overwrite")', 'écrire de façon idempotente'], ['F.window("ts", "5 minutes")', 'fenêtres en streaming'], ['duckdb', 'analyse locale de Parquet']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire l’architecture d’un pipeline. (4 pts)', 'Expliquer topics, partitions, offsets et groupes Kafka. (5 pts)', 'Écrire un traitement Spark. (5 pts)', 'Expliquer l’idempotence et la corriger dans un cas. (4 pts)', 'Mesures RGPD d’un lac de données. (2 pts)'], r: ['étapes', 'voir cours', 'read, groupBy, write', 'overwrite par partition', 'pseudonymisation, conservation'] },
prof: { deroule: [['Ouverture', 'Un fichier qui ne tient pas en mémoire'], ['Cours', 'Pipelines et formats'], ['Pratique', 'Kafka'], ['Cours', 'Spark'], ['Pratique', 'Streaming'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : notebooks guidés. Élèves avancés : Iceberg, exactly-once.', eval: 'Pipeline fonctionnel, sujet type.' } },
exemples: [
['Créer un topic', 'Mise en place.', 'kafka-topics.sh --bootstrap-server kafka:9092 --create --topic mains-levees --partitions 8 --replication-factor 3', 'Created topic mains-levees.', 'Huit partitions répliquées trois fois.'],
['Retard des consommateurs', 'Le tableau de bord n’est plus à jour.', 'kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group tableau', 'TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG\nmains-levees 0 18233 18233 0\nmains-levees 3 9120 52218 43098', 'La partition 3 accumule du retard : consommateur bloqué ou clé déséquilibrée.'],
['Plan Spark', 'Comprendre la lenteur.', 'df.groupBy("etablissement").count().explain()', '== Physical Plan ==\nHashAggregate(keys=[etablissement]…)\n+- Exchange hashpartitioning(etablissement, 200)\n +- HashAggregate(…)\n +- FileScan parquet […] PushedFilters: [], ReadSchema: struct<etablissement:string>', 'Exchange = shuffle ; seule la colonne utile est lue.'],
['Volume réel', 'Faut-il un cluster ?', 'du -sh /lac/activite/annee=2026/', '6.2G', '6 Go de Parquet tiennent sur une seule machine avec DuckDB ou Polars.'],
['Analyse locale', 'Même calcul sans cluster.', "duckdb -c \"SELECT etablissement, COUNT(DISTINCT eleve) FROM '/lac/activite/annee=2026/*/*.parquet' GROUP BY 1 ORDER BY 2 DESC LIMIT 3\"", '0750012A | 1840\n0930045K | 1612\n…', 'Résultat en quelques secondes.'],
['Consommer un flux', 'Test.', 'kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic mains-levees --from-beginning --max-messages 2', '{"salle":"B204","eleve":"a3f9c1","etape":4,"ts":"2026-10-01T10:14:02"}\n{"salle":"B204","eleve":"7b20e1","etape":2,"ts":"2026-10-01T10:14:09"}', 'Événements pseudonymisés, ordonnés par salle.']
],
termes: ['pipeline', 'ingestion', 'lac de données', 'batch', 'streaming', 'orchestrateur', 'Airflow', 'Parquet', 'Delta Lake', 'Iceberg', 'partitionnement', 'Kafka', 'topic', 'partition', 'broker', 'producteur', 'consommateur', 'offset', 'groupe', 'Spark', 'driver', 'exécuteur', 'évaluation paresseuse', 'shuffle', 'idempotent', 'lignage']
});
reg({
cours: { id: 'pro', match: /Rédiger une documentation|compte rendu|Veille technologique|Relation client|support utilisateur|Anglais technique|CEJM et anglais|Assister les utilisateurs|Comptabilité et gestion d’entreprise/i,
titre: 'Compétences professionnelles : documentation, compte rendu, veille, support et anglais technique', duree: '',
niveau: 'Bac pro CIEL · BTS SIO / CIEL · BUT · Titres pro TSSR / AIS',
prerequis: ['Aucun'],
competences: ['BTS SIO · Bloc 1 · Organiser son développement professionnel, répondre aux demandes', 'TSSR · Assister les utilisateurs', 'Tous référentiels · Communiquer'],
intro: 'Un technicien est jugé autant sur ce qu’il sait faire que sur la façon dont il l’explique, le documente et le partage. Une procédure claire évite d’être appelé pendant ses congés, un compte rendu précis désamorce une crise, une veille régulière évite d’être surpris par une faille, et une bonne relation avec les utilisateurs rend tout le reste plus simple. Ce module présente ces compétences, évaluées dans toutes les épreuves professionnelles, et l’anglais technique indispensable pour lire la documentation.',
cm: [
{ t: 'La documentation technique', d: '', html: 'On distingue plusieurs documents. La <b>documentation d’architecture</b> décrit l’ensemble : schéma réseau, plan d’adressage, flux, dépendances. La <b>procédure</b> décrit une tâche pas à pas, pour qu’un collègue puisse l’exécuter sans aide : prérequis, étapes numérotées avec les commandes exactes, résultat attendu à chaque étape, vérification, retour arrière. Le <b>guide utilisateur</b> explique un usage avec des mots simples et des captures. Une bonne documentation est datée, versionnée (Git, wiki), relue, et tenue à jour à chaque changement : une documentation fausse est pire qu’une absence de documentation.' },
{ t: 'Le compte rendu d’incident ou d’intervention', d: '', html: 'Un compte rendu répond aux questions essentielles : <b>quoi</b> (symptôme constaté, impact), <b>quand</b> (heures de début, de détection, de résolution), <b>qui</b> (personnes concernées, intervenants), <b>cause</b> (racine, démontrée par les éléments recueillis), <b>actions</b> (ce qui a été fait, dans l’ordre), <b>résultat</b> et <b>actions préventives</b> avec un responsable et une échéance. On écrit des faits vérifiables, sans chercher de coupable : l’objectif est que l’incident ne se reproduise pas. La démarche « post-mortem sans blâme » des équipes d’exploitation formalise cette approche.' },
{ t: 'La veille technologique', d: '', html: 'La veille consiste à suivre de façon organisée les évolutions de son domaine : vulnérabilités (avis du CERT-FR), nouvelles versions et fins de support des logiciels utilisés, évolutions réglementaires (CNIL, ANSSI), nouvelles technologies. On choisit des <b>sources fiables</b> (sites officiels, documentations, publications d’éditeurs, revues spécialisées), on les agrège (flux RSS, lettres d’information), on <b>trie et vérifie</b> (croiser les sources, dater l’information), puis on <b>synthétise</b> et on <b>diffuse</b> ce qui est utile à l’équipe. Au BTS SIO, la veille fait partie du portfolio présenté à l’épreuve E5.' },
{ t: 'Le support utilisateur', d: '', html: 'Le support suit un circuit : <b>réception</b> de la demande (de préférence par un outil de tickets, pas par un couloir), <b>qualification</b> (incident ou demande, priorité selon l’impact et l’urgence), <b>diagnostic</b>, <b>résolution</b> ou <b>escalade</b> vers un niveau supérieur, <b>clôture</b> après validation par l’utilisateur, et capitalisation dans une base de connaissances. Côté relation : écouter jusqu’au bout, reformuler, poser des questions fermées pour préciser (« le message apparaît-il au démarrage ou à l’ouverture de l’ENT ? »), expliquer sans jargon, annoncer un délai réaliste et le tenir, et rester courtois même face à un utilisateur agacé, dont la frustration vise rarement la personne qui répond.' },
{ t: 'Communiquer à l’oral', d: '', html: 'Présenter une solution à un responsable ou un jury suppose d’adapter le niveau de détail à l’auditoire : le proviseur veut connaître l’impact, le coût et le risque ; le collègue technique veut les choix et la configuration. Une structure simple fonctionne : contexte et besoin, solution retenue et alternatives écartées, mise en œuvre, résultats mesurés, limites et suites. On s’appuie sur une démonstration ou un schéma plutôt que sur des diapositives chargées de texte.' },
{ t: 'L’anglais technique', d: '', html: 'Documentations, messages d’erreur, forums, normes et RFC sont presque toujours en anglais. Il faut lire couramment ce vocabulaire et savoir rédiger un message clair, par exemple un rapport de bogue sur un projet libre ou une demande au support d’un éditeur : contexte (versions, système), étapes pour reproduire, résultat attendu, résultat obtenu, journaux pertinents. Quelques faux amis fréquents : <i>eventually</i> (finalement), <i>actually</i> (en fait), <i>library</i> (bibliothèque), <i>to deprecate</i> (déconseiller, avant suppression), <i>legacy</i> (hérité, ancien).', table: [['Anglais', 'Français'], ['deprecated', 'déconseillé, voué à disparaître'], ['workaround', 'contournement'], ['root cause', 'cause racine'], ['rollback', 'retour arrière'], ['throughput', 'débit'], ['outage', 'panne, indisponibilité'], ['to troubleshoot', 'diagnostiquer, dépanner'], ['release notes', 'notes de version'], ['breaking change', 'changement incompatible']] }
],
synthese: ['Architecture, procédure, guide utilisateur ; datés, versionnés, à jour', 'Compte rendu : quoi, quand, qui, cause, actions, résultat, prévention ; sans blâme', 'Veille : sources fiables, tri, synthèse, diffusion', 'Support : réception, qualification, diagnostic, résolution ou escalade, clôture, capitalisation', 'Oral : adapter au public, structure claire, démonstration', 'Anglais : lire la documentation, rédiger un rapport de bogue, faux amis'],
qcm: [
['Qu’est-ce qu’une bonne procédure doit permettre ?', ['Impressionner', 'Qu’un collègue exécute la tâche sans aide', 'Remplacer la formation', 'Être courte à tout prix'], 1, 'Étapes, résultat attendu, retour arrière.'],
['Objectif d’un post-mortem sans blâme ?', ['Trouver un coupable', 'Éviter que l’incident se reproduise', 'Sanctionner', 'Rassurer'], 1, 'Faits et actions préventives.'],
['Que signifie « deprecated » ?', ['Supprimé', 'Déconseillé, voué à disparaître', 'Nouveau', 'Rapide'], 1, 'Il fonctionne encore.'],
['Première étape du support ?', ['Résoudre', 'Recevoir et qualifier la demande', 'Escalader', 'Clôturer'], 1, 'Priorité selon impact et urgence.'],
['Source fiable pour les vulnérabilités en France ?', ['Un forum', 'Le CERT-FR', 'Un réseau social', 'Une vidéo'], 1, 'Avis officiels.'],
['Que contient un rapport de bogue utile ?', ['Une plainte', 'Versions, étapes pour reproduire, attendu, obtenu, journaux', 'Une capture seule', 'Un titre'], 1, 'Reproductible.']
],
td: [
{ n: 'Application', c: 'Rédiger la procédure « ajouter une salle de TP au réseau » à partir des cours VLAN et DHCP.', r: 'Prérequis (plan d’adressage, accès aux switchs) · 1. créer le VLAN · 2. l’autoriser sur les trunks · 3. créer la passerelle · 4. créer l’étendue DHCP · 5. affecter les ports · 6. tester depuis un poste · retour arrière.' },
{ n: 'Application', c: 'Transformer les notes brutes d’une intervention (fournies) en compte rendu structuré.', r: 'Titre, date · symptôme et impact · chronologie horodatée · cause racine démontrée · actions · résultat · prévention avec responsable et échéance.' },
{ n: 'Application', c: 'Rédiger en anglais un rapport de bogue pour un projet libre dont le paquet échoue à démarrer après mise à jour.', r: 'Title · Environment (Debian 12, version x.y) · Steps to reproduce · Expected behavior · Actual behavior (with log excerpt) · Workaround if any.' },
{ n: 'Approfondissement', c: 'Mettre en place une veille sur les logiciels de l’établissement : sources, outil, rythme, diffusion.', r: 'CERT-FR, notes de version des éditeurs, flux RSS dans un agrégateur · lecture hebdomadaire · synthèse mensuelle et alerte immédiate pour les failles critiques · archivage dans le wiki.' }
],
tp: { type: 'Atelier', url: 'apercu-aide.html', desc: 'Mise en situation de support et de documentation.', etapes: ['Jeu de rôle : appel d’un utilisateur', 'Créer et qualifier un ticket', 'Rédiger la fiche de base de connaissances', 'Rédiger un compte rendu d’incident', 'Présenter oralement la solution en 3 minutes'] },
plusLoin: [['CERT-FR · avis et alertes', 'https://www.cert.ssi.gouv.fr/'], ['Google SRE Book · Postmortem Culture', 'https://sre.google/sre-book/postmortem-culture/'], ['Documentation GLPI · assistance', 'https://glpi-project.org/fr/documentation/'], ['Write the Docs', 'https://www.writethedocs.org/guide/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Une enseignante appelle, très agacée : « Rien ne marche, je dois faire cours dans dix minutes. »', etapes: [
['Accueillir', '→ « Je comprends, on va regarder ça tout de suite. » Laisser expliquer sans interrompre.'],
['Qualifier', '→ Questions fermées : quel appareil ? quelle salle ? quel message ? Résultat : le vidéoprojecteur de la salle C112 n’affiche rien. Impact élevé, urgence immédiate : priorité haute.'],
['Diagnostiquer et contourner', '→ À distance, guider : source HDMI sélectionnée, câble branché. Sans succès : prêt immédiat d’un vidéoprojecteur mobile pour que le cours ait lieu.'],
['Résoudre', '→ Après le cours : lampe du vidéoprojecteur en fin de vie (compteur dépassé), remplacement.'],
['Clôturer et capitaliser', '→ Ticket clôturé après validation de l’enseignante ; fiche de base de connaissances « vidéoprojecteur sans image » ; ajout du suivi des heures de lampe dans l’inventaire GLPI pour anticiper.']
] },
erreurs: ['Procédure sans résultat attendu ni retour arrière', 'Compte rendu qui cherche un coupable', 'Documentation jamais mise à jour', 'Veille sans sources fiables ni synthèse', 'Jargon avec un utilisateur', 'Promettre un délai intenable', 'Traiter une demande sans ticket'],
memo: [['Document', 'Contenu clé'], ['Procédure', 'prérequis, étapes, résultat attendu, vérification, retour arrière'], ['Compte rendu', 'quoi, quand, qui, cause, actions, résultat, prévention'], ['Fiche de veille', 'source, date, résumé, impact pour nous, action'], ['Ticket', 'demandeur, catégorie, priorité, description, résolution'], ['Rapport de bogue (EN)', 'environment, steps, expected, actual, logs'], ['Priorité', 'impact × urgence'], ['Escalade', 'quand la résolution dépasse son niveau ou son délai']],
examen: { titre: 'Sujet type · 20 points', q: ['Rédiger une procédure pour une tâche donnée. (6 pts)', 'Rédiger un compte rendu à partir de notes. (5 pts)', 'Qualifier trois demandes de support. (3 pts)', 'Présenter une démarche de veille. (3 pts)', 'Traduire et expliquer un extrait de documentation en anglais. (3 pts)'], r: ['étapes vérifiables', 'structure du cours', 'impact et urgence', 'sources, tri, diffusion', 'vocabulaire'] },
prof: { deroule: [['Ouverture', 'Exécuter une procédure mal écrite'], ['Cours', 'Documentation et compte rendu'], ['Atelier', 'Rédaction'], ['Cours', 'Support et veille'], ['Jeu de rôle', 'Appel d’utilisateur'], ['Clôture', 'Oral de 3 minutes']], diff: 'Tous niveaux : modèles fournis. Élèves avancés : post-mortem complet, oral en anglais.', eval: 'Documents rédigés, oral, sujet type.' } },
exemples: [
['Versionner une procédure', 'Mise à jour du wiki.', 'git log --oneline -3 docs/procedures/ajout-salle.md', 'a91c2e3 Ajout de l’étape DHCP snooping\n4f0b7d1 Correction du VLAN natif\n1c22e90 Création de la procédure', 'L’historique montre qui a modifié quoi et quand.'],
['Lire des notes de version', 'Veille.', 'curl -s https://www.postgresql.org/support/versioning/ | grep -o "16[^<]*November 2028"', '16 … November 2028', 'Fin de support connue : la migration se planifie.'],
['Suivre les avis CERT-FR', 'Veille automatisée.', 'curl -s https://www.cert.ssi.gouv.fr/avis/feed/ | grep -oE "<title>[^<]+" | head -3', '<title>CERT-FR – Avis\n<title>Multiples vulnérabilités dans …\n<title>Vulnérabilité dans …', 'Flux à intégrer dans un agrégateur.'],
['Chronologie d’un incident', 'Rédaction du compte rendu.', 'journalctl -u nginx --since "08:00" --until "09:00" | grep -E "start|stop|error" | head -3', '08:12:03 nginx[612]: [emerg] …\n08:12:03 systemd: nginx.service: Failed\n08:41:55 systemd: Started nginx.service', 'Les horodatages des journaux fournissent la chronologie factuelle.'],
['Statistiques du support', 'Bilan mensuel.', 'Requête GLPI : tickets par catégorie, septembre', 'Mot de passe oublié 142 · Vidéoprojecteur 37 · Accès ENT 29', 'Les mots de passe oubliés dominent : mettre en place la réinitialisation en libre-service.'],
['Rapport de bogue', 'Projet libre.', 'Title: Service fails to start after upgrade to 2.4.0 on Debian 12', 'Steps to reproduce · Expected · Actual · Logs', 'Un titre précis et des étapes reproductibles augmentent fortement les chances de réponse.']
],
termes: ['documentation', 'procédure', 'retour arrière', 'compte rendu', 'cause racine', 'post-mortem', 'veille', 'CERT-FR', 'flux RSS', 'support', 'ticket', 'qualification', 'priorité', 'escalade', 'base de connaissances', 'deprecated', 'workaround', 'rollback', 'outage', 'release notes', 'breaking change']
});
})();

View file

@ -0,0 +1,228 @@
// Cours complets · lot 6 : IPv6, gestion de projet, mathématiques pour l’informatique, services de fichiers et de temps
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'ipv6', match: /IPv6|SLAAC|NDP|double pile/i,
titre: 'IPv6 : adresses, autoconfiguration, NDP et double pile', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T S2-S3 · TSSR',
prerequis: ['Adressage IPv4', 'Hexadécimal', 'ARP et diagnostic réseau'],
competences: ['BTS SIO SISR · Bloc 2 · Concevoir une infrastructure réseau', 'BUT R&T · Compétence « Connecter »'],
intro: 'Les adresses IPv4 publiques sont épuisées depuis des années et une part croissante du trafic Internet passe en IPv6 ; les opérateurs français fournissent l’IPv6 sur la plupart de leurs offres et les réseaux mobiles l’utilisent massivement. Un technicien qui l’ignore ne comprend pas pourquoi un service répond différemment selon le poste, ou pourquoi un pare-feu IPv4 bien réglé laisse passer du trafic. Ce module présente la structure des adresses, l’autoconfiguration, le protocole NDP qui remplace ARP, et la coexistence avec IPv4.',
cm: [
{ t: 'Adresses de 128 bits', d: '', html: 'Une adresse IPv6 compte <b>128 bits</b>, écrits en huit groupes de quatre chiffres hexadécimaux séparés par deux-points : <code>2001:0db8:0000:0040:0000:0000:0000:0010</code>. Deux règles abrègent l’écriture : on supprime les zéros en tête de chaque groupe, et on remplace <b>une seule</b> suite de groupes nuls par <code>::</code>. L’adresse devient <code>2001:db8:0:40::10</code>. Dans une URL, on l’écrit entre crochets : <code>http://[2001:db8:0:40::10]:8080/</code>.<br><br>Le préfixe s’écrit en notation CIDR, comme en IPv4. Un réseau local est presque toujours un <b>/64</b> : 64 bits de préfixe, 64 bits d’identifiant d’interface. Un site reçoit typiquement un /48 ou un /56 de son opérateur, soit 65 536 ou 256 réseaux /64.' },
{ t: 'Types d’adresses', d: '', html: 'Il n’y a plus de diffusion (broadcast) en IPv6 : on utilise l’unicast, le multicast et l’anycast.', table: [['Préfixe', 'Type', 'Usage'], ['2000::/3', 'globale unicast', 'adresses publiques routées sur Internet'], ['fe80::/10', 'lien local', 'présente sur chaque interface, non routée'], ['fc00::/7 (fd00::/8)', 'unique locale (ULA)', 'équivalent des adresses privées'], ['ff00::/8', 'multicast', 'ff02::1 tous les nœuds, ff02::2 tous les routeurs'], ['::1/128', 'boucle locale', 'équivalent de 127.0.0.1'], ['::/128', 'non spécifiée', 'équivalent de 0.0.0.0']], attention: 'Une interface IPv6 possède toujours plusieurs adresses : au minimum une adresse de lien local fe80::, souvent une ou plusieurs adresses globales. Une adresse fe80:: s’utilise avec l’interface précisée : ping fe80::1%ens18.' },
{ t: 'NDP : le voisinage', d: '', html: 'Le protocole <b>NDP</b> (Neighbor Discovery, RFC 4861), porté par ICMPv6, remplace ARP et bien plus. <b>Neighbor Solicitation / Advertisement</b> résolvent une adresse IPv6 en adresse MAC, par multicast ciblé plutôt que par diffusion. <b>Router Solicitation / Advertisement</b> permettent aux hôtes de découvrir les routeurs et les préfixes du réseau. La détection d’adresse dupliquée (<b>DAD</b>) vérifie qu’une adresse est libre avant de l’utiliser.<br><br>Conséquence pratique : bloquer ICMPv6 sur un pare-feu casse IPv6. On filtre finement, mais on laisse passer les messages NDP sur le lien local et les messages d’erreur comme Packet Too Big, indispensables car les routeurs IPv6 ne fragmentent pas.' },
{ t: 'Autoconfiguration : SLAAC et DHCPv6', d: '', html: 'Avec <b>SLAAC</b> (autoconfiguration sans état), le routeur annonce le préfixe /64 dans ses Router Advertisements ; chaque hôte construit lui-même son identifiant d’interface, aujourd’hui de façon aléatoire et stable par réseau (RFC 7217), et souvent des <b>adresses temporaires</b> renouvelées pour limiter le traçage. Aucun serveur n’est nécessaire.<br><br><b>DHCPv6</b> distribue des adresses ou seulement des options (serveurs DNS) ; les drapeaux M et O des annonces indiquent aux hôtes ce qu’ils doivent demander. Les serveurs DNS peuvent aussi être annoncés directement dans les RA (option RDNSS). Android n’implémentant pas DHCPv6 pour les adresses, un réseau Wi-Fi mixte s’appuie en général sur SLAAC.' },
{ t: 'Configurer et diagnostiquer sous Linux', d: '', html: 'Les mêmes outils servent, avec l’option <code>-6</code> : <code>ip -6 addr</code>, <code>ip -6 route</code>, <code>ip -6 neigh</code> pour le cache NDP, <code>ping -6</code> (ou ping6), <code>traceroute -6</code>, <code>ss -6</code>. Un routeur Linux annonce ses préfixes avec <b>radvd</b> ou systemd-networkd.', exemple: '# /etc/radvd.conf\ninterface ens19 {\n AdvSendAdvert on;\n prefix 2001:db8:62:20::/64 { AdvOnLink on; AdvAutonomous on; };\n RDNSS 2001:db8:62:40::10 { };\n};\n\n$ ip -6 addr show ens18 | grep inet6\n inet6 2001:db8:62:20:5d3a:9f10:77c2:1b04/64 scope global dynamic mngtmpaddr\n inet6 fe80::be24:11ff:fe5e:2a10/64 scope link' },
{ t: 'Coexistence et sécurité', d: '', html: 'La transition passe par la <b>double pile</b> : chaque machine a une adresse IPv4 et une adresse IPv6, et les applications préfèrent IPv6 quand le DNS renvoie un enregistrement AAAA (algorithme Happy Eyeballs, qui bascule vite en IPv4 si IPv6 échoue). Sur des réseaux uniquement IPv6, <b>NAT64</b> et <b>DNS64</b> permettent de joindre les services restés en IPv4.<br><br>IPv6 supprime le NAT : chaque appareil a une adresse globale. La sécurité repose alors sur un <b>pare-feu à état</b> qui bloque par défaut les connexions entrantes. Erreur fréquente : des règles soignées dans la table IPv4 et une politique accept dans la table IPv6, activée par défaut sur les systèmes récents. Avec nftables, une table de famille <code>inet</code> applique les mêmes règles aux deux protocoles. Sur le réseau local, le <b>RA Guard</b> des switchs empêche un poste de se faire passer pour le routeur, équivalent du DHCP snooping.' }
],
synthese: ['128 bits en hexadécimal, zéros de tête omis, :: une seule fois', 'Réseau local en /64, site en /48 ou /56', 'Lien local fe80::, globale 2000::/3, ULA fd00::/8, multicast ff00::/8, pas de broadcast', 'NDP (ICMPv6) remplace ARP : ne pas bloquer ICMPv6', 'SLAAC par annonces de routeur, DHCPv6 et RDNSS pour les options', 'Double pile, AAAA, Happy Eyeballs ; NAT64/DNS64', 'Pas de NAT : pare-feu à état obligatoire, table inet, RA Guard'],
qcm: [
['Forme abrégée de 2001:0db8:0000:0000:0000:0000:0000:0001 ?', ['2001:db8::1', '2001:db8:0:1', '2001::db8::1', '2001:db8:1::'], 0, 'Une seule suite de zéros remplacée par ::'],
['Quel préfixe désigne les adresses de lien local ?', ['2000::/3', 'fe80::/10', 'ff00::/8', 'fd00::/8'], 1, 'Présentes sur toutes les interfaces.'],
['Quel protocole remplace ARP ?', ['DHCPv6', 'NDP', 'SLAAC', 'RIPng'], 1, 'Neighbor Discovery, sur ICMPv6.'],
['Taille standard d’un réseau local IPv6 ?', ['/24', '/48', '/64', '/128'], 2, 'Nécessaire à SLAAC.'],
['Pourquoi ne pas bloquer tout ICMPv6 ?', ['Il est inoffensif', 'NDP et Packet Too Big en dépendent', 'Obligation légale', 'Pour le ping'], 1, 'IPv6 cesserait de fonctionner.'],
['Les règles IPv4 d’un pare-feu nftables en table ip s’appliquent-elles à IPv6 ?', ['Oui', 'Non, il faut une table ip6 ou inet', 'Seulement en sortie', 'Seulement avec NAT'], 1, 'Famille inet pour les deux.'],
['Qui annonce le préfixe en SLAAC ?', ['Le serveur DHCP', 'Le routeur, par Router Advertisement', 'Le DNS', 'Le switch'], 1, 'RA.']
],
td: [
{ n: 'Échauffement', c: 'Abréger : 2001:0db8:0062:0020:0000:0000:0000:0001 ; fe80:0000:0000:0000:be24:11ff:fe5e:2a10 ; ff02:0000:0000:0000:0000:0000:0000:0002.', r: '2001:db8:62:20::1 · fe80::be24:11ff:fe5e:2a10 · ff02::2' },
{ n: 'Application', c: 'Le lycée reçoit 2001:db8:62::/48. Proposer un plan d’adressage /64 pour les VLAN 10, 20, 30, 40 en reprenant le numéro de VLAN dans le préfixe.', r: '2001:db8:62:10::/64 · 2001:db8:62:20::/64 · 2001:db8:62:30::/64 · 2001:db8:62:40::/64 (passerelle ::1 dans chacun)' },
{ n: 'Application', c: 'Écrire une chaîne nftables input en famille inet qui autorise SSH, les connexions établies et les messages ICMPv6 indispensables, et bloque le reste.', r: 'type filter hook input priority 0; policy drop;\nct state established,related accept\niif lo accept\nicmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert, packet-too-big, echo-request } accept\ntcp dport 22 accept' },
{ n: 'Approfondissement', c: 'Un site web répond lentement uniquement depuis certains postes. Le DNS renvoie A et AAAA. Formuler l’hypothèse et la vérifier.', r: 'La route IPv6 est défaillante et les clients attendent avant de basculer en IPv4. curl -6 -v et curl -4 -v pour comparer, traceroute -6, vérifier les RA reçues et la route par défaut IPv6.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Routeur Linux avec radvd et deux clients en double pile.', etapes: ['Configurer l’adresse du routeur', 'Annoncer le préfixe avec radvd', 'Observer SLAAC sur les clients', 'Lire le cache NDP', 'Filtrer en famille inet', 'Tester un service en IPv6 et IPv4'] },
plusLoin: [['RFC 8200 · IPv6', 'https://www.rfc-editor.org/rfc/rfc8200'], ['RFC 4861 · Neighbor Discovery', 'https://www.rfc-editor.org/rfc/rfc4861'], ['Arcep · Baromètre de la transition vers IPv6', 'https://www.arcep.fr/'], ['ANSSI · Guide de recommandations pour IPv6', 'https://cyber.gouv.fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Un audit révèle que le serveur web interne est joignable depuis Internet sur le port de sa base de données, alors que le pare-feu la bloque. Trouver et corriger.', etapes: [
['Reproduire', 'Depuis l’extérieur : nc -zv 203.0.113.10 5432 → timeout ; nc -zv 2001:db8:62:40::20 5432 → succeeded\n→ Bloqué en IPv4, ouvert en IPv6.'],
['Lire le pare-feu', 'nft list tables\ntable ip filtre\n→ Seule une table IPv4 existe : le trafic IPv6 n’est pas filtré.'],
['Comprendre', '→ L’opérateur a activé IPv6 ; le routeur annonce un préfixe global, le serveur a obtenu une adresse publique par SLAAC. Sans NAT et sans règles IPv6, tout est accessible.'],
['Corriger', 'Conversion de la table en famille inet (mêmes règles pour IPv4 et IPv6), politique drop en forward et input, ICMPv6 indispensable autorisé.\nnft -c -f /etc/nftables.conf && systemctl reload nftables\n→ Test : IPv6 5432 → timeout, 443 → succeeded.'],
['Prévenir', '→ Inventaire des adresses IPv6 de tous les serveurs, supervision des ports ouverts en IPv4 et en IPv6, et vérification IPv6 ajoutée à la procédure de mise en service.']
] },
erreurs: ['Utiliser :: deux fois dans une adresse', 'Bloquer tout ICMPv6', 'Filtrer IPv4 et oublier IPv6', 'Découper un LAN en préfixe plus long que /64', 'Oublier l’interface avec une adresse fe80::', 'Désactiver IPv6 au lieu de le configurer'],
memo: [['Commande', 'Usage'], ['ip -6 addr', 'adresses IPv6'], ['ip -6 route', 'routes et passerelle IPv6'], ['ip -6 neigh', 'cache NDP'], ['ping -6 <adresse>', 'tester'], ['ping ff02::1%ens18', 'voir tous les voisins du lien'], ['traceroute -6', 'chemin IPv6'], ['rdisc6 ens18', 'solliciter les annonces de routeur'], ['dig AAAA nom', 'adresse IPv6 d’un nom'], ['curl -6 · curl -4', 'comparer les deux protocoles'], ['nft list ruleset', 'vérifier les tables inet / ip6']],
examen: { titre: 'Sujet type · 20 points', q: ['Abréger et identifier le type de cinq adresses. (5 pts)', 'Proposer un plan d’adressage à partir d’un /48. (4 pts)', 'Expliquer SLAAC et NDP. (5 pts)', 'Écrire un filtrage IPv6 minimal. (4 pts)', 'Expliquer le risque de l’absence de NAT. (2 pts)'], r: ['règles d’abréviation', 'un /64 par VLAN', 'RS/RA, NS/NA, DAD', 'table inet', 'adresses globales joignables'] },
prof: { deroule: [['Ouverture', 'curl -6 ifconfig.co : qui a une IPv6 ?'], ['Cours', 'Adresses et types'], ['Cours', 'NDP, SLAAC'], ['Pratique', 'Lab radvd'], ['Incident', 'Serveur exposé en IPv6'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : fiche d’abréviation. Élèves avancés : NAT64/DNS64, RA Guard.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Lister ses adresses', 'Première observation.', 'ip -6 -br addr', 'lo UNKNOWN ::1/128\nens18 UP 2001:db8:62:20:5d3a:9f10:77c2:1b04/64 fe80::be24:11ff:fe5e:2a10/64', 'Une adresse globale obtenue par SLAAC et une adresse de lien local.'],
['Voir les voisins', 'Cache NDP.', 'ip -6 neigh', 'fe80::1 dev ens18 lladdr 3c:52:82:1a:0f:9b router REACHABLE', 'Le routeur est identifié comme tel (router) et joignable.'],
['Découvrir le routeur', 'Aucune adresse globale.', 'rdisc6 ens18', 'Soliciting ff02::2 (ff02::2) on ens18...\nTimed out.', 'Aucune annonce reçue : radvd arrêté ou RA filtrées. Le poste n’a que son adresse fe80::.'],
['Pinger tout le lien', 'Inventaire rapide.', 'ping -c 2 ff02::1%ens18', '64 bytes from fe80::be24:11ff:fe5e:2a10%ens18\n64 bytes from fe80::1%ens18\n64 bytes from fe80::215:5dff:fe0a:1107%ens18', 'Le multicast tous-nœuds fait répondre chaque machine du lien.'],
['Comparer IPv4 et IPv6', 'Service lent.', 'curl -6 -o /dev/null -s -w "%{time_total}\\n" https://ent.lycee-exemple.fr ; curl -4 -o /dev/null -s -w "%{time_total}\\n" https://ent.lycee-exemple.fr', '3.842\n0.118', 'IPv6 est trente fois plus lent : chemin IPv6 défaillant. traceroute -6 pour localiser.'],
['Vérifier le filtrage', 'Audit.', 'nft list ruleset | grep -E "^table"', 'table ip filtre\ntable ip nat', 'Aucune table inet ou ip6 : IPv6 n’est pas filtré.']
],
termes: ['IPv6', '::', '/64', 'fe80::', 'ULA', 'multicast', 'ff02::1', 'NDP', 'ICMPv6', 'Router Advertisement', 'Neighbor Solicitation', 'DAD', 'SLAAC', 'DHCPv6', 'RDNSS', 'radvd', 'double pile', 'AAAA', 'Happy Eyeballs', 'NAT64', 'RA Guard', 'inet']
});
reg({
cours: { id: 'projet', match: /gestion de projet|Scrum|Kanban|agile|cycle en V|ITIL|conduire un projet|management de projet|lean/i,
titre: 'Gestion de projet informatique : cycle en V, agilité, Scrum, Kanban et ITIL', duree: '',
niveau: 'BTS SIO · BTS CIEL · BUT · École d’ingénieurs · Master',
prerequis: ['Aucun'],
competences: ['BTS SIO · Bloc 1 · Travailler en mode projet', 'BUT · Compétence « Conduire un projet »', 'École d’ingénieurs · Piloter un projet'],
intro: 'La majorité des projets informatiques qui échouent ne butent pas sur la technique mais sur l’organisation : besoin mal compris, périmètre qui gonfle, délais irréalistes, utilisateurs découverts à la fin. Ce module présente les deux grandes familles de méthodes, prédictives et agiles, les pratiques concrètes de Scrum et Kanban, le pilotage par les indicateurs et, pour l’exploitation, les processus ITIL de gestion des services.',
cm: [
{ t: 'Le triangle et les parties prenantes', d: '', html: 'Tout projet arbitre entre <b>périmètre</b>, <b>délai</b> et <b>coût</b>, sous contrainte de <b>qualité</b> : on ne peut pas tout augmenter à budget constant. Les <b>parties prenantes</b> sont toutes les personnes concernées : le <b>maître d’ouvrage</b> (MOA), qui exprime le besoin et porte le projet, et le <b>maître d’œuvre</b> (MOE), qui réalise ; les utilisateurs, la direction, l’équipe d’exploitation, le délégué à la protection des données. Les identifier tôt évite de découvrir une exigence bloquante la veille de la mise en service.' },
{ t: 'Méthodes prédictives : cascade et cycle en V', d: '', html: 'Les méthodes <b>prédictives</b> planifient tout au départ. Dans le <b>cycle en V</b>, chaque phase de conception (expression du besoin, spécifications, conception générale, conception détaillée) a sa phase de validation en miroir (recette, tests de validation, d’intégration, unitaires). Le <b>cahier des charges</b> fixe le besoin, le <b>planning de Gantt</b> les tâches et leurs dépendances, le <b>chemin critique</b> la suite de tâches qui fixe la durée totale.<br><br>Ces méthodes conviennent quand le besoin est stable et bien connu (déploiement d’une infrastructure, migration réglementaire). Leur faiblesse : les utilisateurs ne voient le résultat qu’à la fin, et une erreur de compréhension coûte d’autant plus cher qu’elle est découverte tard.' },
{ t: 'L’agilité et Scrum', d: '', html: 'Le <b>Manifeste agile</b> (2001) privilégie les individus, le logiciel qui fonctionne, la collaboration avec le client et l’adaptation au changement. On livre par petites <b>itérations</b> et on ajuste à partir des retours.<br><br><b>Scrum</b> organise le travail en <b>sprints</b> de une à quatre semaines. Trois rôles : le <b>Product Owner</b> porte la vision et priorise le <b>Product Backlog</b> ; les <b>développeurs</b> réalisent ; le <b>Scrum Master</b> facilite et lève les obstacles. Quatre événements : planification du sprint, mêlée quotidienne de 15 minutes, revue de sprint avec démonstration aux parties prenantes, rétrospective d’amélioration. Les besoins s’écrivent en <b>user stories</b> (« En tant que professeur, je veux voir les mains levées dans l’ordre, afin d’aider chacun à son tour ») avec des <b>critères d’acceptation</b> et une <b>définition de terminé</b> commune.' },
{ t: 'Kanban et le flux', d: '', html: '<b>Kanban</b> visualise le travail sur un tableau (à faire, en cours, en revue, terminé) et <b>limite le travail en cours</b> (WIP) dans chaque colonne. Moins de tâches commencées en parallèle, c’est moins de changements de contexte et un délai de livraison plus court. On mesure le <b>temps de cycle</b> (du démarrage à la fin) et le <b>débit</b>. Kanban convient bien à un service support ou à une équipe d’exploitation, où les demandes arrivent en continu. Le <b>lean</b>, dont il est issu, traque les gaspillages : attentes, retouches, fonctionnalités inutiles.' },
{ t: 'Piloter : estimations, indicateurs, risques', d: '', html: 'En agile, on estime en <b>points d’effort</b> relatifs (souvent par planning poker) et on suit la <b>vélocité</b> de l’équipe et le <b>burndown</b> (reste à faire). En prédictif, on suit l’avancement réel contre le planning. Dans tous les cas, on tient un <b>registre des risques</b> (probabilité, impact, action de réduction, responsable) et un <b>relevé de décisions</b>. Un <b>RACI</b> clarifie qui réalise, qui décide, qui est consulté, qui est informé pour chaque livrable.' },
{ t: 'Exploiter : ITIL', d: '', html: '<b>ITIL</b> rassemble les bonnes pratiques de gestion des services informatiques. Les processus clés au quotidien : la <b>gestion des incidents</b> (rétablir le service au plus vite), la <b>gestion des problèmes</b> (trouver et supprimer la cause des incidents récurrents), la <b>gestion des changements</b> (évaluer et planifier toute modification, avec retour arrière), la <b>gestion des demandes</b> (création de compte, installation), et le <b>catalogue de services</b> avec ses niveaux de service (<b>SLA</b> : délai de prise en charge, de résolution). Des outils comme GLPI, très répandu dans les établissements, outillent ces processus.' }
],
synthese: ['Périmètre, délai, coût sous contrainte de qualité ; MOA et MOE', 'Cycle en V : besoin stable, validation en miroir, Gantt et chemin critique', 'Agile : itérations courtes et retours ; Scrum : rôles, sprint, backlog, user stories', 'Kanban : visualiser, limiter le travail en cours, mesurer le flux', 'Vélocité, burndown, registre des risques, RACI', 'ITIL : incidents, problèmes, changements, demandes, SLA'],
qcm: [
['Qui priorise le Product Backlog ?', ['Le Scrum Master', 'Le Product Owner', 'Le directeur', 'Les développeurs seuls'], 1, 'Il porte la valeur du produit.'],
['Durée d’un sprint Scrum ?', ['Un jour', 'Une à quatre semaines', 'Six mois', 'Illimitée'], 1, 'Durée fixe.'],
['Que limite Kanban ?', ['Le budget', 'Le travail en cours', 'Le nombre de réunions', 'L’équipe'], 1, 'WIP.'],
['Quand préférer le cycle en V ?', ['Besoin incertain', 'Besoin stable et bien connu', 'Toujours', 'Jamais'], 1, 'Méthode prédictive.'],
['Différence incident / problème en ITIL ?', ['Aucune', 'L’incident rétablit le service, le problème traite la cause', 'Le problème est plus rapide', 'L’incident est planifié'], 1, 'Deux processus distincts.'],
['Que contient un RACI ?', ['Les coûts', 'Qui réalise, décide, est consulté, est informé', 'Le planning', 'Les risques'], 1, 'Responsabilités.']
],
td: [
{ n: 'Application', c: 'Rédiger trois user stories avec critères d’acceptation pour la fonction « Lever la main » de Syllabus.', r: 'En tant qu’élève, je veux lever la main depuis mon lab afin d’être aidé. Critères : un clic, confirmation visible, position dans la file.\nEn tant que professeur, je veux voir les mains levées dans l’ordre… Critères : ordre d’arrivée, nom, étape du lab.\nEn tant que professeur, je veux baisser une main une fois l’élève aidé…' },
{ n: 'Application', c: 'Construire le Gantt du déploiement d’une salle de TP (commande, câblage, installation des postes, image système, tests, formation) et identifier le chemin critique.', r: 'Commande → câblage → installation → image → tests → mise en service, formation en parallèle des tests. Chemin critique : la suite commande-câblage-installation-image-tests.' },
{ n: 'Approfondissement', c: 'Remplir le registre des risques du passage d’un établissement à un nouvel ENT (au moins quatre risques).', r: 'Reprise de données incomplète · indisponibilité le jour de la rentrée · refus des utilisateurs · non-conformité RGPD. Pour chacun : probabilité, impact, action (migration à blanc, bascule en vacances, formation, AIPD), responsable.' }
],
tp: { type: 'Projet', url: 'apercu-devoirs.html', desc: 'Projet en équipe sur trois sprints.', etapes: ['Rédiger le backlog', 'Planifier le sprint 1', 'Tenir le tableau Kanban', 'Démontrer en revue de sprint', 'Rétrospective et ajustements'] },
plusLoin: [['Manifeste agile (fr)', 'https://agilemanifesto.org/iso/fr/manifesto.html'], ['Scrum Guide (fr)', 'https://scrumguides.org/'], ['GLPI', 'https://glpi-project.org/fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'À trois semaines de la date prévue, le projet d’application de réservation des salles accumule du retard, les utilisateurs découvrent des écrans qui ne leur conviennent pas et l’équipe travaille sur huit fonctionnalités à la fois.', etapes: [
['Faire l’état des lieux', '→ Tableau : 8 fonctionnalités en cours, 1 terminée ; aucune démonstration aux utilisateurs depuis deux mois ; le cahier des charges n’a pas été relu depuis le lancement.'],
['Réduire le travail en cours', '→ Limite de 3 tâches en cours ; on termine avant de commencer. En une semaine, 3 fonctionnalités passent en terminé.'],
['Reprioriser avec la MOA', '→ Atelier avec le Product Owner (proviseure adjointe) : les fonctionnalités sont classées selon la méthode MoSCoW ; l’export statistique passe en « plus tard ».'],
['Montrer tôt', '→ Démonstration hebdomadaire à trois enseignants ; deux écrans sont simplifiés dès la première séance.'],
['Décider', '→ Mise en service à la date prévue avec le périmètre essentiel, le reste en livraisons suivantes. Rétrospective : démonstrations régulières et limite de travail en cours adoptées pour la suite.']
] },
erreurs: ['Ne montrer le produit qu’à la fin', 'Commencer beaucoup de tâches en parallèle', 'Oublier des parties prenantes (DPO, exploitation)', 'User stories sans critères d’acceptation', 'Aucun registre des risques', 'Confondre incident et problème'],
memo: [['Outil', 'Usage'], ['Cahier des charges', 'fixer le besoin (prédictif)'], ['Gantt et chemin critique', 'planifier et identifier les tâches déterminantes'], ['User story + critères d’acceptation', 'exprimer un besoin (agile)'], ['Product Backlog', 'liste priorisée'], ['Tableau Kanban + limite WIP', 'piloter le flux'], ['Burndown · vélocité', 'suivre l’avancement'], ['MoSCoW', 'prioriser'], ['RACI', 'répartir les responsabilités'], ['Registre des risques', 'anticiper'], ['SLA', 'engagement de service (ITIL)']],
examen: { titre: 'Sujet type · 20 points', q: ['Choisir et justifier une méthode pour deux projets donnés. (4 pts)', 'Rédiger quatre user stories. (4 pts)', 'Construire un Gantt et identifier le chemin critique. (5 pts)', 'Remplir un RACI. (3 pts)', 'Distinguer incident, problème et changement ITIL. (4 pts)'], r: ['besoin stable ou non', 'format + critères', 'dépendances', 'une seule A par ligne', 'voir cours'] },
prof: { deroule: [['Ouverture', 'Jeu : construire une tour en cascade puis en itérations'], ['Cours', 'Prédictif et agile'], ['Pratique', 'User stories et backlog'], ['Cours', 'Kanban et ITIL'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : modèles de user stories. Élèves avancés : SAFe, indicateurs de flux.', eval: 'Projet par sprints, sujet type.' } },
exemples: [
['Lire un burndown', 'Revue de mi-sprint.', 'Reste à faire : J1 34 pts · J5 30 pts · J8 27 pts (idéal J8 : 14 pts)', 'Écart de 13 points', 'L’équipe ne terminera pas le sprint : on retire des éléments avec le Product Owner plutôt que d’allonger le sprint.'],
['Calculer la vélocité', 'Planification.', 'Sprints précédents : 21, 24, 19 points', 'Moyenne ≈ 21 points', 'On engage environ 21 points au prochain sprint, pas 35.'],
['Temps de cycle Kanban', 'Service support.', 'Tickets terminés : 2, 3, 9, 2, 14, 3 jours', 'Médiane 3 jours, deux tickets très longs', 'Les deux tickets longs attendaient une validation extérieure : on ajoute une colonne « en attente » pour rendre ce blocage visible.'],
['Chemin critique', 'Déploiement de salle.', 'Commande 10 j → câblage 3 j → installation 2 j → image 1 j → tests 1 j ; formation 2 j en parallèle des tests', 'Durée totale : 17 jours', 'Tout retard de commande décale la mise en service ; la formation a une marge.'],
['Priorisation MoSCoW', 'Périmètre trop large.', 'Must : réserver, annuler · Should : notifications · Could : statistiques · Won’t : application mobile', 'Livraison 1 : Must + Should', 'Le périmètre essentiel tient dans le délai.'],
['Suivi d’un SLA', 'Rapport mensuel du support.', 'Incidents prioritaires : 42, dont 39 résolus en moins de 4 h', '92,9 % contre un objectif de 95 %', 'Analyse des trois dépassements (tous liés à des pièces à commander) : stock minimal de pièces critiques.']
],
termes: ['MOA', 'MOE', 'cahier des charges', 'cycle en V', 'Gantt', 'chemin critique', 'agile', 'Scrum', 'sprint', 'Product Owner', 'Scrum Master', 'Product Backlog', 'user story', 'critères d’acceptation', 'Kanban', 'WIP', 'vélocité', 'burndown', 'MoSCoW', 'RACI', 'ITIL', 'incident', 'problème', 'changement', 'SLA', 'GLPI']
});
reg({
cours: { id: 'maths', match: /Matrices|Suites|Nombres complexes|Trigonométrie|Intégrales|Fourier|Probabilités|Équations différentielles|Ensembles, relations|Maths :|Décibels|Modulation|échantillonnage|signal/i,
titre: 'Mathématiques pour l’informatique et les réseaux : suites, matrices, complexes, probabilités, signal', duree: '',
niveau: 'BTS SIO (maths approfondies) · BTS CIEL · BUT R&T / Informatique · Classes préparatoires',
prerequis: ['Mathématiques du lycée'],
competences: ['BTS SIO · Mathématiques pour l’informatique', 'BTS CIEL · Mathématiques', 'BUT R&T · Ressources mathématiques et signal'],
intro: 'Les mathématiques ne sont pas un passage obligé déconnecté du métier : les suites décrivent la croissance d’un stockage ou d’un nombre d’utilisateurs, les matrices font tourner les images et les réseaux de neurones, les nombres complexes et Fourier décrivent le signal qui circule sur un câble ou dans l’air, les probabilités mesurent la fiabilité d’un serveur et la sécurité d’un mot de passe. Ce module présente chaque notion avec son application directe en informatique ou en réseaux.',
cm: [
{ t: 'Suites : prévoir une croissance', d: '', html: 'Une suite <b>arithmétique</b> ajoute une raison constante : u<sub>n</sub> = u<sub>0</sub> + n·r. Une suite <b>géométrique</b> multiplie par une raison : u<sub>n</sub> = u<sub>0</sub>·q<sup>n</sup>. Un stockage qui gagne 200 Go par mois suit une croissance arithmétique ; un nombre d’utilisateurs qui augmente de 8 % par mois suit une croissance géométrique, beaucoup plus rapide à long terme. La somme des termes d’une suite géométrique, S = u<sub>0</sub>·(1 − q<sup>n</sup>)/(1 − q), sert par exemple à calculer le volume cumulé de sauvegardes.', exemple: 'Disque de 10 To, 4 To utilisés, +6 % par mois :\n4 × 1,06ⁿ ≥ 10 ⇔ n ≥ ln(2,5) / ln(1,06) ≈ 15,7\n→ saturation dans environ 16 mois' },
{ t: 'Matrices : transformer et calculer en masse', d: '', html: 'Une <b>matrice</b> est un tableau de nombres. Le produit d’une matrice par un vecteur applique une transformation linéaire : rotation, mise à l’échelle, projection. En infographie, faire tourner une image d’un angle θ revient à multiplier chaque point par la matrice de rotation [[cos θ, −sin θ], [sin θ, cos θ]]. Une couche d’un réseau de neurones est exactement un produit matriciel suivi d’une activation. La matrice <b>inverse</b> permet de résoudre un système linéaire ; la <b>matrice d’adjacence</b> représente un graphe, par exemple un réseau de routeurs, et ses puissances comptent les chemins.' },
{ t: 'Nombres complexes : représenter un signal', d: '', html: 'Un nombre complexe z = a + ib s’écrit aussi sous forme <b>exponentielle</b> z = r·e<sup>iθ</sup>, avec un module r et un argument θ. Cette forme décrit naturellement une onde : amplitude et phase. En électronique, l’impédance d’un circuit en régime sinusoïdal est un nombre complexe ; en télécommunications, la modulation <b>QAM</b> utilisée par le Wi-Fi, la 4G et la fibre code des bits par des points du plan complexe (la constellation) : 256-QAM place 256 points, soit 8 bits par symbole.' },
{ t: 'Trigonométrie, Fourier et échantillonnage', d: '', html: 'Tout signal périodique se décompose en une somme de sinusoïdes : c’est la <b>série de Fourier</b>. La <b>transformée de Fourier</b> généralise cette idée et donne le <b>spectre</b> d’un signal, sa répartition en fréquences : c’est ce que calcule un spectrogramme pour la reconnaissance vocale, ou un analyseur de spectre pour repérer un canal Wi-Fi encombré.<br><br>Pour numériser un signal, on l’<b>échantillonne</b> à intervalles réguliers. Le <b>théorème de Shannon-Nyquist</b> impose une fréquence d’échantillonnage au moins double de la plus haute fréquence du signal, sinon apparaît le repliement (aliasing). La voix téléphonique, limitée à environ 3,4 kHz, est échantillonnée à 8 kHz ; la musique, jusqu’à 20 kHz, à 44,1 kHz.' },
{ t: 'Décibels et bilan de liaison', d: '', html: 'Le <b>décibel</b> exprime un rapport de puissances sur une échelle logarithmique : 10·log<sub>10</sub>(P<sub>2</sub>/P<sub>1</sub>). +3 dB double la puissance, −3 dB la divise par deux, −10 dB la divise par dix. Le <b>dBm</b> exprime une puissance par rapport à 1 mW : 20 dBm = 100 mW, puissance maximale courante d’une borne Wi-Fi à 2,4 GHz en France.<br><br>Le <b>bilan de liaison</b> additionne simplement en dB puissance émise, gains d’antennes et pertes (câbles, affaiblissement en espace libre, murs) pour estimer la puissance reçue et la comparer à la sensibilité du récepteur. C’est ainsi qu’on dimensionne une liaison Wi-Fi entre deux bâtiments ou un réseau LoRaWAN.' },
{ t: 'Probabilités : fiabilité et sécurité', d: '', html: 'Une variable aléatoire et sa <b>loi</b> décrivent un phénomène incertain ; l’<b>espérance</b> en donne la valeur moyenne. En informatique, les probabilités servent à mesurer la <b>fiabilité</b> : si un serveur est disponible 99 % du temps, deux serveurs redondants indépendants le sont 1 − 0,01² = 99,99 % du temps. Elles mesurent la <b>force d’un mot de passe</b> : 12 caractères parmi 94 donnent 94<sup>12</sup> ≈ 4,8 × 10<sup>23</sup> possibilités. La <b>loi binomiale</b> modélise le nombre de paquets perdus sur n envois, et le <b>paradoxe des anniversaires</b> explique pourquoi une empreinte de 64 bits connaît une collision après seulement environ 2<sup>32</sup> essais.' }
],
synthese: ['Suites arithmétiques et géométriques pour prévoir une croissance', 'Matrices : transformations, réseaux de neurones, graphes', 'Complexes : amplitude et phase, constellations QAM', 'Fourier : spectre ; Shannon-Nyquist : échantillonner à plus du double', 'Décibels : +3 dB = ×2, bilan de liaison additif', 'Probabilités : redondance, force des mots de passe, collisions'],
qcm: [
['Un nombre d’utilisateurs augmente de 5 % par mois. Type de suite ?', ['Arithmétique', 'Géométrique', 'Constante', 'Aucune'], 1, 'Raison 1,05.'],
['Combien de bits par symbole en 64-QAM ?', ['4', '6', '8', '64'], 1, '2⁶ = 64.'],
['Fréquence d’échantillonnage minimale pour un signal jusqu’à 4 kHz ?', ['2 kHz', '4 kHz', '8 kHz', '44,1 kHz'], 2, 'Double de la fréquence maximale.'],
['+6 dB correspond à une puissance :', ['doublée', 'environ quadruplée', 'décuplée', 'divisée par deux'], 1, '3 dB = ×2, donc 6 dB ≈ ×4.'],
['Deux serveurs indépendants disponibles à 99 % en redondance :', ['98 %', '99 %', '99,99 %', '100 %'], 2, '1 − 0,01².'],
['20 dBm vaut :', ['20 mW', '100 mW', '1 W', '2 mW'], 1, '10^(20/10) mW.']
],
td: [
{ n: 'Application', c: 'Un serveur de sauvegarde conserve 30 sauvegardes quotidiennes de 200 Go, chacune augmentant de 1 % par jour. Estimer le volume total avec une somme géométrique.', r: 'S = 200 × (1 − 1,01³⁰)/(1 − 1,01) ≈ 200 × 34,78 ≈ 6,96 To' },
{ n: 'Application', c: 'Faire tourner le point (3, 1) de 90° autour de l’origine avec la matrice de rotation.', r: '[[0, −1], [1, 0]] × (3, 1) = (−1, 3)' },
{ n: 'Application', c: 'Bilan de liaison Wi-Fi : émission 17 dBm, antennes 2 × 12 dBi, câbles 2 × 1,5 dB, espace libre 100 dB. Puissance reçue ? Liaison possible si sensibilité −75 dBm ?', r: '17 + 24 − 3 − 100 = −62 dBm > −75 dBm : liaison possible avec 13 dB de marge.' },
{ n: 'Approfondissement', c: 'Comparer le nombre de combinaisons d’un mot de passe de 8 caractères parmi 94 et d’une phrase de passe de 5 mots tirés d’une liste de 7 776 mots.', r: '94⁸ ≈ 6,1 × 10¹⁵ ; 7 776⁵ ≈ 2,8 × 10¹⁹ : la phrase de passe est environ 4 600 fois plus forte et plus facile à retenir.' }
],
tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'Mathématiques appliquées avec numpy.', etapes: ['Prévoir la saturation d’un disque', 'Rotations d’images par matrices', 'Spectre d’un signal avec numpy.fft', 'Repliement lors d’un sous-échantillonnage', 'Simulation de disponibilité'] },
plusLoin: [['Eduscol · programmes de mathématiques', 'https://eduscol.education.fr/'], ['3Blue1Brown · Essence of linear algebra', 'https://www.3blue1brown.com/topics/linear-algebra'], ['Khan Academy (fr)', 'https://fr.khanacademy.org/math']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'La direction demande pour quand prévoir l’achat de stockage pour l’ENT : 18 To utilisés sur 30 To, avec +0,9 To le mois dernier et +1,1 To le mois précédent l’année d’avant à la même période.', etapes: [
['Choisir le modèle', '→ Relevé sur 12 mois : la croissance mensuelle rapportée au volume est à peu près constante, environ 5 % : modèle géométrique.'],
['Calculer', '18 × 1,05ⁿ ≥ 30 ⇔ n ≥ ln(30/18)/ln(1,05) ≈ 10,5 mois.'],
['Intégrer une marge', '→ Seuil d’alerte à 85 % (25,5 To) : 18 × 1,05ⁿ ≥ 25,5 ⇔ n ≈ 7,1 mois.'],
['Comparer avec le modèle linéaire', '→ À +1 To par mois, la saturation serait dans 12 mois : l’écart de 1,5 mois montre l’importance du bon modèle.'],
['Recommander', '→ Commande dans 5 à 6 mois pour tenir compte des délais de livraison, et supervision prédictive dans Prometheus (predict_linear) pour affiner.']
] },
erreurs: ['Confondre croissance arithmétique et géométrique', 'Additionner des puissances en mW au lieu de travailler en dB', 'Échantillonner à la fréquence du signal', 'Supposer des pannes indépendantes quand elles ne le sont pas (même alimentation)', 'Multiplier des matrices dans le mauvais ordre'],
memo: [['Formule', 'Usage'], ['uₙ = u₀ + n·r', 'suite arithmétique'], ['uₙ = u₀·qⁿ', 'suite géométrique'], ['S = u₀(1 − qⁿ)/(1 − q)', 'somme géométrique'], ['n = ln(a)/ln(q)', 'temps pour atteindre un seuil'], ['R(θ) = [[cos, −sin], [sin, cos]]', 'rotation'], ['z = r·e^{iθ}', 'amplitude et phase'], ['fₑ ≥ 2·f_max', 'Shannon-Nyquist'], ['dB = 10·log₁₀(P₂/P₁)', 'décibels'], ['1 − Π(1 − dispoᵢ)', 'disponibilité en redondance'], ['Nᴸ', 'combinaisons d’un mot de passe']],
examen: { titre: 'Sujet type · 20 points', q: ['Prévoir une saturation par suite géométrique. (5 pts)', 'Appliquer une transformation matricielle. (4 pts)', 'Justifier une fréquence d’échantillonnage. (3 pts)', 'Faire un bilan de liaison. (4 pts)', 'Calculer une disponibilité et une force de mot de passe. (4 pts)'], r: ['ln', 'produit matriciel', 'Nyquist', 'somme en dB', '1 − produit ; Nᴸ'] },
prof: { deroule: [['Ouverture', 'Quand notre disque sera-t-il plein ?'], ['Cours', 'Suites et matrices'], ['Cours', 'Complexes, Fourier, dB'], ['Pratique', 'numpy'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : formulaire, calculs guidés. Élèves avancés : FFT, algèbre linéaire.', eval: 'Sujet type.' } },
exemples: [
['Prévoir une saturation', 'Planification du stockage.', 'python3 -c "import math; print(math.log(30/18)/math.log(1.05))"', '10.47', 'Environ 10 mois et demi avant saturation au rythme de 5 % par mois.'],
['Produit matriciel', 'Couche d’un réseau de neurones.', 'import numpy as np; W = np.array([[0.5, -1],[2, 0.1]]); x = np.array([1, 2]); W @ x', 'array([-1.5, 2.2])', 'Chaque sortie est une combinaison pondérée des entrées.'],
['Spectre d’un signal', 'Analyser un bruit.', 'f = np.fft.rfftfreq(8000, 1/8000); s = np.abs(np.fft.rfft(np.sin(2*np.pi*440*t) + 0.5*np.sin(2*np.pi*1200*t))); f[s.argsort()[-2:]]', 'array([1200., 440.])', 'La FFT retrouve les deux fréquences qui composent le signal.'],
['Conversion dBm', 'Réglage d’une borne.', 'python3 -c "print(10**(20/10), 10**(14/10))"', '100.0 25.1', '20 dBm = 100 mW ; 14 dBm ≈ 25 mW : baisser de 6 dB divise par quatre.'],
['Disponibilité en redondance', 'Justifier un second serveur.', 'python3 -c "print(1 - (1-0.99)**2)"', '0.9999', 'De 3 jours et demi d’indisponibilité par an à moins d’une heure, si les pannes sont indépendantes.'],
['Force d’un mot de passe', 'Politique de mots de passe.', 'python3 -c "import math; print(math.log2(94**12), math.log2(7776**6))"', '78.66 77.55', 'Un mot de passe aléatoire de 12 caractères et une phrase de 6 mots ont une force comparable, environ 78 bits.']
],
termes: ['suite arithmétique', 'suite géométrique', 'raison', 'matrice', 'produit matriciel', 'matrice de rotation', 'nombre complexe', 'module', 'argument', 'QAM', 'série de Fourier', 'transformée de Fourier', 'spectre', 'échantillonnage', 'Shannon-Nyquist', 'repliement', 'décibel', 'dBm', 'bilan de liaison', 'espérance', 'loi binomiale', 'disponibilité']
});
reg({
cours: { id: 'fichiers', match: /NFS|SFTP|FTP|NTP|chrony|serveur de temps|partage de fichiers|GLPI|bonding|agrégation de liens/i,
titre: 'Services d’infrastructure : partage de fichiers (NFS, SMB, SFTP), temps (NTP) et inventaire (GLPI)', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR',
prerequis: ['Droits Unix', 'SSH par clé', 'Annuaires (pour SMB)'],
competences: ['BTS SIO SISR · Bloc 2 · Installer et administrer des services', 'TSSR · Exploiter des serveurs', 'BTS SIO · Bloc 1 · Gérer le patrimoine informatique'],
intro: 'Derrière chaque établissement tournent des services discrets mais indispensables : les partages de fichiers où enseignants et élèves déposent leurs documents, l’heure exacte sans laquelle l’authentification et les journaux deviennent inutilisables, et l’inventaire qui dit quel matériel existe et quels tickets sont ouverts. Ce module les présente avec leurs pièges de production : FTP en clair, NFS mal exporté, horloges qui dérivent, parc non inventorié.',
cm: [
{ t: 'Transférer des fichiers : oublier FTP', d: '', html: '<b>FTP</b> (1971) transmet identifiants et données <b>en clair</b> et utilise deux connexions (commande sur 21, données sur un port négocié), ce qui complique le filtrage et le NAT. On l’abandonne au profit de <b>SFTP</b>, sous-système de SSH : un seul port (22), chiffrement, authentification par clé. Pour un espace de dépôt, on crée un groupe dédié et on enferme ses membres dans un dossier avec la directive <b>ChrootDirectory</b> d’OpenSSH, sans accès shell.', exemple: '# /etc/ssh/sshd_config\nMatch Group depot\n ChrootDirectory /srv/depot/%u\n ForceCommand internal-sftp\n AllowTcpForwarding no\n X11Forwarding no\n# le dossier chroot doit appartenir à root et ne pas être modifiable par l’utilisateur', attention: 'Le dossier désigné par ChrootDirectory doit appartenir à root et ne pas être accessible en écriture par le groupe ou les autres, sinon sshd refuse la connexion (« bad ownership or modes for chroot directory »). On crée un sous-dossier accessible en écriture pour l’utilisateur.' },
{ t: 'NFS entre serveurs Linux', d: '', html: '<b>NFS</b> monte un dossier distant comme s’il était local, entre machines Unix. Le serveur déclare ses exports dans <code>/etc/exports</code> avec les clients autorisés et les options : <code>rw</code> ou <code>ro</code>, <code>root_squash</code> (par défaut, root du client devient nobody sur le serveur), <code>sync</code>. NFSv3 identifie les utilisateurs par leur <b>UID</b> numérique : les UID doivent correspondre entre machines. <b>NFSv4</b> utilise un seul port (2049) et peut s’appuyer sur Kerberos pour authentifier et chiffrer.', exemple: '# /etc/exports\n/srv/sauvegardes 10.62.40.0/24(rw,sync,root_squash,no_subtree_check)\nsudo exportfs -ra ; sudo exportfs -v\n# côté client\nsudo mount -t nfs4 fichiers:/srv/sauvegardes /mnt/sauv\n# /etc/fstab : fichiers:/srv/sauvegardes /mnt/sauv nfs4 defaults,_netdev 0 0' },
{ t: 'SMB pour les postes Windows', d: '', html: '<b>SMB</b> est le protocole de partage des postes Windows, servi par Windows Server ou par <b>Samba</b> sous Linux. Les droits combinent les permissions du partage et celles du système de fichiers ; en domaine, ils s’appuient sur les groupes de l’annuaire. <b>SMBv1</b>, exploité par le rançongiciel WannaCry en 2017, doit être désactivé ; SMB 3 chiffre les échanges. Les <b>clichés instantanés</b> (shadow copies, ou snapshots ZFS et Btrfs exposés par Samba) permettent aux utilisateurs de restaurer eux-mêmes une version précédente d’un fichier.' },
{ t: 'L’heure exacte : NTP et chrony', d: '', html: 'Sans horloge synchronisée, Kerberos refuse les authentifications (écart maximal de 5 minutes), les certificats paraissent invalides, les journaux de plusieurs machines deviennent impossibles à corréler et les tâches planifiées se déclenchent au mauvais moment. <b>NTP</b> (UDP 123) synchronise les horloges sur des serveurs de référence organisés en <b>strates</b> : strate 0 pour l’horloge atomique ou le GPS, strate 1 pour le serveur qui y est relié, et ainsi de suite.<br><br>Sous Debian, <b>chrony</b> est le client et serveur recommandé ; il corrige progressivement la dérive plutôt que de faire sauter l’heure. On configure quelques serveurs internes qui se synchronisent sur des sources publiques (pool.ntp.org, serveurs d’opérateurs), et tous les autres équipements sur ces serveurs internes.', exemple: '# /etc/chrony/chrony.conf (serveur interne)\npool fr.pool.ntp.org iburst\nallow 10.62.0.0/16\n\n$ chronyc tracking\nReference ID : 5B1A2C3D (ntp1.fr.example)\nStratum : 3\nSystem time : 0.000012873 seconds fast of NTP time\nLeap status : Normal' },
{ t: 'Inventaire et assistance : GLPI', d: '', html: '<b>GLPI</b>, logiciel libre français, est très présent dans les établissements et les collectivités. Il tient l’<b>inventaire</b> du parc (ordinateurs, logiciels, licences, imprimantes, contrats), alimenté automatiquement par un agent qui remonte la configuration de chaque poste, et gère l’<b>assistance</b> : tickets d’incident et de demande, catégories, priorités, délais (SLA), base de connaissances. Relier un ticket au matériel concerné permet de repérer un poste qui tombe en panne à répétition et de justifier un remplacement.' },
{ t: 'Redondance des liens : agrégation', d: '', html: 'Un serveur de fichiers critique ne doit pas dépendre d’un seul câble. L’<b>agrégation de liens</b> (bonding sous Linux, LACP 802.3ad côté switch) regroupe plusieurs interfaces physiques en une interface logique : en mode <b>active-backup</b>, une seule sert et l’autre prend le relais en cas de panne ; en mode <b>802.3ad</b>, les deux se partagent le trafic, ce qui augmente le débit total pour plusieurs clients, mais pas le débit d’une connexion unique. Le switch doit être configuré en cohérence.' }
],
synthese: ['FTP en clair : SFTP avec clé et chroot', 'NFS : exports, root_squash, UID cohérents, NFSv4 et Kerberos', 'SMB : Samba ou Windows, SMBv1 désactivé, clichés pour restaurer', 'NTP et chrony : heure exacte indispensable à Kerberos, TLS et journaux', 'GLPI : inventaire automatique et tickets liés au matériel', 'Bonding active-backup ou LACP pour la redondance'],
qcm: [
['Pourquoi abandonner FTP ?', ['Trop lent', 'Identifiants et données en clair', 'Payant', 'Interdit'], 1, 'SFTP chiffre.'],
['Que fait root_squash ?', ['Supprime root', 'Transforme root du client en nobody sur le serveur', 'Chiffre', 'Donne root à tous'], 1, 'Protection par défaut.'],
['Pourquoi les UID doivent-ils concorder en NFSv3 ?', ['Pour la vitesse', 'NFS identifie les utilisateurs par leur UID numérique', 'Pour Kerberos', 'Ils n’ont pas à concorder'], 1, 'Sinon mauvais propriétaire.'],
['Quel protocole de partage exploité par WannaCry ?', ['NFS', 'SMBv1', 'SFTP', 'NTP'], 1, 'À désactiver.'],
['Port de NTP ?', ['TCP 22', 'UDP 123', 'TCP 443', 'UDP 53'], 1, 'UDP 123.'],
['En bonding 802.3ad, une seule copie de fichier va-t-elle deux fois plus vite ?', ['Oui', 'Non, une connexion unique reste sur un lien', 'Parfois', 'Seulement en IPv6'], 1, 'Le débit total augmente pour plusieurs clients.']
],
td: [
{ n: 'Application', c: 'Mettre en place un dépôt SFTP pour que chaque élève de SIO1 dépose ses rendus dans son seul dossier, sans shell.', r: 'groupe depot, utilisateurs dans ce groupe · /srv/depot/<login> root:root 755, sous-dossier rendus appartenant à l’élève · bloc Match Group depot avec ChrootDirectory /srv/depot/%u et ForceCommand internal-sftp · sshd -t puis reload.' },
{ n: 'Application', c: 'Exporter /srv/iso en lecture seule vers tout le réseau serveurs et /srv/sauvegardes en lecture-écriture vers le seul serveur de sauvegarde.', r: '/srv/iso 10.62.40.0/24(ro,root_squash,no_subtree_check)\n/srv/sauvegardes 10.62.40.50(rw,sync,root_squash,no_subtree_check)\nexportfs -ra' },
{ n: 'Approfondissement', c: 'Concevoir la hiérarchie de temps d’un établissement (serveurs, contrôleurs de domaine, switchs, postes).', r: 'Deux serveurs chrony internes synchronisés sur des sources publiques · DC principal synchronisé sur eux, postes Windows sur la hiérarchie du domaine · switchs et Linux sur les serveurs internes · flux UDP 123 sortants limités à ces deux serveurs.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Serveur de fichiers et de temps sur deux VM.', etapes: ['Configurer un dépôt SFTP chrooté', 'Exporter un dossier NFS et le monter', 'Configurer chrony serveur et client', 'Mesurer l’écart avec chronyc', 'Inventorier les VM dans GLPI avec l’agent'] },
plusLoin: [['Debian Wiki · NFS Server Setup', 'https://wiki.debian.org/NFSServerSetup'], ['Documentation chrony', 'https://chrony-project.org/documentation.html'], ['Documentation GLPI', 'https://glpi-project.org/fr/documentation/'], ['ANSSI · Recommandations pour un usage sécurisé d’OpenSSH', 'https://cyber.gouv.fr/publications/openssh-secure-use-recommendations']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Les sauvegardes nocturnes écrites par le serveur de sauvegarde sur un partage NFS échouent avec « Permission denied », alors qu’elles fonctionnaient avant la réinstallation du serveur de sauvegarde.', etapes: [
['Reproduire', 'Sur le serveur de sauvegarde : sudo -u backup touch /mnt/sauv/test\ntouch: cannot touch \'/mnt/sauv/test\': Permission denied'],
['Comparer les identités', 'Sur le client : id backup → uid=998(backup)\nSur le serveur NFS : ls -ln /srv/sauvegardes → propriétaire 1001\n→ Après réinstallation, l’utilisateur backup a reçu un autre UID. NFSv3 compare les numéros, pas les noms.'],
['Corriger', 'Sur le client : usermod -u 1001 backup && find / -xdev -uid 998 -exec chown -h 1001 {} +\n→ L’UID est aligné sur celui du serveur.'],
['Vérifier', 'sudo -u backup touch /mnt/sauv/test && ls -l /mnt/sauv/test\n-rw-r--r-- 1 backup backup 0 … test\n→ La sauvegarde de test passe.'],
['Prévenir', '→ UID des comptes de service fixés dans la procédure d’installation (ou gérés par l’annuaire), passage à NFSv4 avec idmapping par nom, et supervision du succès de chaque sauvegarde.']
] },
erreurs: ['Laisser un serveur FTP en clair', 'Chroot SFTP avec un dossier appartenant à l’utilisateur', 'Exporter NFS vers * ou avec no_root_squash sans raison', 'UID différents entre machines en NFSv3', 'SMBv1 encore actif', 'Machines sans synchronisation d’heure', 'Parc non inventorié'],
memo: [['Commande', 'Usage'], ['sftp -i cle eleve@serveur', 'se connecter en SFTP'], ['sshd -t', 'valider la configuration SSH'], ['exportfs -v', 'exports NFS actifs'], ['showmount -e serveur', 'exports vus depuis un client'], ['mount -t nfs4 …', 'monter un partage'], ['smbclient -L //serveur -U user', 'lister les partages SMB'], ['testparm', 'valider la configuration Samba'], ['chronyc tracking · sources -v', 'état de la synchronisation'], ['timedatectl', 'heure et fuseau du système'], ['cat /proc/net/bonding/bond0', 'état de l’agrégation']],
examen: { titre: 'Sujet type · 20 points', q: ['Remplacer un FTP par un SFTP chrooté. (5 pts)', 'Écrire des exports NFS justifiés. (4 pts)', 'Expliquer l’importance de NTP pour Kerberos et les journaux. (4 pts)', 'Diagnostiquer un problème d’UID NFS. (4 pts)', 'Présenter l’intérêt de GLPI. (3 pts)'], r: ['Match Group, chroot root:root', 'ro/rw, root_squash', '5 minutes, corrélation', 'id et ls -ln', 'inventaire + tickets'] },
prof: { deroule: [['Ouverture', 'Capturer un mot de passe FTP avec tcpdump'], ['Cours', 'SFTP, NFS, SMB'], ['Pratique', 'Lab fichiers'], ['Cours', 'NTP et GLPI'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : configurations fournies. Élèves avancés : NFSv4 Kerberos, bonding LACP.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Voir un mot de passe FTP', 'Démonstration de sécurité.', 'sudo tcpdump -ni ens18 -A port 21 | grep -E "USER|PASS"', 'USER prof.sio\nPASS Rentree2026', 'Le mot de passe circule en clair : raison suffisante pour passer à SFTP.'],
['Chroot refusé', 'Élève qui ne se connecte pas.', 'journalctl -u ssh | tail -1', 'fatal: bad ownership or modes for chroot directory "/srv/depot/lea.martin"', 'Le dossier chroot appartient à l’élève : il doit appartenir à root, avec un sous-dossier inscriptible pour elle.'],
['Voir les exports', 'Client qui ne monte pas.', 'showmount -e fichiers', 'Export list for fichiers:\n/srv/iso 10.62.40.0/24\n/srv/sauvegardes 10.62.40.50', 'Le client 10.62.40.51 n’est pas autorisé sur /srv/sauvegardes.'],
['Vérifier l’heure', 'Kerberos refuse les connexions.', 'chronyc tracking | grep -E "Reference|System time"', 'Reference ID : 00000000 ()\nSystem time : 412.8 seconds slow of NTP time', 'Aucune source joignable et près de 7 minutes de retard : vérifier le flux UDP 123 vers les serveurs internes.'],
['État du bonding', 'Serveur de fichiers critique.', 'grep -E "Mode|Slave Interface|MII Status" /proc/net/bonding/bond0', 'Bonding Mode: IEEE 802.3ad Dynamic link aggregation\nSlave Interface: ens18\nMII Status: up\nSlave Interface: ens19\nMII Status: down', 'Un des deux liens est tombé : le service continue, mais il n’y a plus de redondance. Vérifier le câble et le port du switch.'],
['Lister les partages SMB', 'Audit.', 'smbclient -L //fichiers -U technicien -m SMB3', 'Sharename Type Comment\n--------- ---- -------\nprofs Disk Documents enseignants\neleves Disk Espaces élèves', 'Connexion forcée en SMB 3. Un échec avec -m SMB3 alors que -m NT1 passe révélerait un serveur encore en SMBv1.']
],
termes: ['FTP', 'SFTP', 'ChrootDirectory', 'internal-sftp', 'NFS', 'NFSv4', '/etc/exports', 'root_squash', 'UID', 'SMB', 'SMBv1', 'Samba', 'clichés instantanés', 'NTP', 'chrony', 'strate', 'UDP 123', 'GLPI', 'inventaire', 'ticket', 'bonding', 'LACP', '802.3ad']
});
})();

View file

@ -0,0 +1,225 @@
// Cours complets · lot 7 : pare-feu nftables, VPN WireGuard, Nginx reverse proxy, sauvegardes
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'parefeu', match: /Pare-feu|nftables|iptables|DMZ|segmentation réseau/i,
titre: 'Pare-feu et segmentation : nftables, politique par défaut et DMZ', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T Cyber · TSSR',
prerequis: ['TCP et UDP, ports', 'NAT et PAT', 'VLAN'],
competences: ['BTS SIO · Bloc 3 · Cybersécurité des services informatiques', 'BTS CIEL · C08 · Sécuriser un réseau', 'BUT R&T Cyber · Compétence « Protéger »'],
intro: 'Un pare-feu décide, paquet par paquet, ce qui a le droit de passer. Bien configuré, il réduit la surface d’attaque à quelques services choisis et isole les zones du réseau les unes des autres ; mal configuré, il donne une fausse impression de sécurité. Ce module explique le filtrage à état dans le noyau Linux, l’écriture d’un jeu de règles nftables en liste blanche, l’architecture en zones avec une DMZ, et la méthode pour faire évoluer des règles sans se couper l’accès.',
cm: [
{ t: 'Filtrage sans état et à état', d: '', html: 'Un filtrage <b>sans état</b> examine chaque paquet isolément (adresses, protocole, ports). Pour laisser revenir les réponses d’une connexion sortante, il faudrait ouvrir largement les ports hauts. Un pare-feu <b>à état</b> s’appuie sur le suivi de connexions (conntrack) : une règle autorise le premier paquet d’une connexion, et tous les paquets suivants, dans les deux sens, sont reconnus comme appartenant à une connexion <b>established</b> ou <b>related</b>. C’est le mode normal de nftables, et ce qui permet d’écrire des règles courtes et sûres.' },
{ t: 'Tables, chaînes, règles', d: '', html: 'nftables organise les règles en <b>tables</b> (famille <code>inet</code> pour IPv4 et IPv6 ensemble, <code>ip</code>, <code>ip6</code>, <code>arp</code>, <code>bridge</code>), contenant des <b>chaînes</b> accrochées à un point du parcours des paquets : <b>input</b> (vers la machine elle-même), <b>forward</b> (traversant la machine, rôle de routeur), <b>output</b> (émis par la machine). Chaque chaîne de base a une <b>politique par défaut</b>. Les règles sont évaluées dans l’ordre ; la première qui décide (accept, drop, reject) arrête l’évaluation.<br><br>Les <b>ensembles</b> (sets) et les <b>maps</b> évitent de répéter une règle par adresse ou par port : <code>tcp dport { 22, 80, 443 } accept</code>.' },
{ t: 'La liste blanche : politique drop', d: '', html: 'La seule approche robuste est la <b>liste blanche</b> : politique <b>drop</b> par défaut, puis on autorise explicitement ce qui est nécessaire. Un jeu de règles d’entrée typique pour un serveur web : accepter la boucle locale, les connexions established et related, les messages ICMP et ICMPv6 indispensables, SSH depuis le réseau d’administration seulement, et HTTP/HTTPS depuis partout. Tout le reste est rejeté sans être écrit dans une règle.', exemple: 'table inet filtre {\n set admin { type ipv4_addr; flags interval; elements = { 10.62.99.0/28 } }\n chain input {\n type filter hook input priority filter; policy drop;\n iif lo accept\n ct state established,related accept\n ct state invalid drop\n meta l4proto { icmp, ipv6-icmp } accept\n ip saddr @admin tcp dport 22 accept\n tcp dport { 80, 443 } accept\n limit rate 5/minute log prefix "nft-drop: "\n }\n chain forward { type filter hook forward priority filter; policy drop; }\n chain output { type filter hook output priority filter; policy accept; }\n}', attention: 'Appliquer une politique drop en SSH sans avoir autorisé sa propre connexion coupe l’accès. On teste avec nft -c, on garde une console hors bande (hyperviseur, IPMI) et on programme un retour arrière automatique (at now + 5 minutes : nft -f ancienne-config) le temps de valider.' },
{ t: 'Zones et DMZ', d: '', html: 'On découpe le réseau en <b>zones</b> de confiance différente : Internet, <b>DMZ</b> (services exposés : site web, messagerie, reverse proxy), réseau interne (postes, serveurs internes), administration. Le pare-feu central, souvent le routeur inter-VLAN, applique une matrice de flux : Internet peut joindre la DMZ sur 443 et 25 seulement ; la DMZ peut joindre une base interne sur un seul port ; Internet ne joint jamais le réseau interne ; seul le réseau d’administration ouvre SSH partout. Si un serveur de la DMZ est compromis, l’attaquant reste enfermé dans une zone qui ne voit presque rien.' },
{ t: 'Journaliser et diagnostiquer', d: '', html: 'Une règle <code>log</code> juste avant la politique drop, avec une limite de débit, montre ce qui est refusé. <code>nft list ruleset -a</code> affiche les règles avec leur numéro (handle) pour les supprimer ou insérer précisément ; des <b>compteurs</b> (<code>counter</code>) indiquent combien de paquets chaque règle a traités. <code>nft monitor trace</code>, avec une règle <code>meta nftrace set 1</code>, suit un paquet à travers toutes les chaînes : l’outil le plus efficace pour comprendre pourquoi un flux est bloqué.' },
{ t: 'iptables, UFW et Docker', d: '', html: 'Beaucoup de documentations utilisent encore <b>iptables</b> ; sur Debian récent, la commande iptables est une couche de compatibilité qui écrit dans nftables (iptables-nft). Mélanger des règles écrites des deux façons rend l’ensemble difficile à lire. <b>UFW</b> et <b>firewalld</b> sont des interfaces simplifiées. Docker insère ses propres règles pour publier les ports des conteneurs, avant celles d’UFW : un port publié peut être accessible malgré une règle UFW qui semble le bloquer. On vérifie toujours le résultat réel avec <code>nft list ruleset</code> et un test depuis l’extérieur.' }
],
synthese: ['Filtrage à état : on autorise le premier paquet, conntrack gère le reste', 'Tables inet, chaînes input, forward, output, politique par défaut', 'Liste blanche : policy drop et autorisations explicites', 'Ne jamais se couper l’accès : nft -c, console hors bande, retour arrière programmé', 'Zones et DMZ avec matrice de flux', 'log limité, compteurs, nft monitor trace', 'Vérifier le résultat réel quand iptables, UFW ou Docker interviennent'],
qcm: [
['Que permet le filtrage à état ?', ['Chiffrer', 'Laisser passer automatiquement les réponses des connexions autorisées', 'Filtrer le contenu web', 'Accélérer le routage'], 1, 'ct state established,related.'],
['Quelle chaîne filtre le trafic traversant un routeur ?', ['input', 'forward', 'output', 'prerouting'], 1, 'input concerne la machine elle-même.'],
['Quelle politique par défaut pour un pare-feu en liste blanche ?', ['accept', 'drop', 'log', 'return'], 1, 'Tout ce qui n’est pas autorisé est refusé.'],
['Quelle famille nftables filtre IPv4 et IPv6 avec les mêmes règles ?', ['ip', 'ip6', 'inet', 'bridge'], 2, 'inet.'],
['Rôle d’une DMZ ?', ['Accélérer', 'Isoler les services exposés du réseau interne', 'Chiffrer les postes', 'Remplacer le NAT'], 1, 'Confinement en cas de compromission.'],
['Comment suivre un paquet à travers les règles ?', ['ping', 'nft monitor trace', 'ss', 'ip route'], 1, 'Avec meta nftrace set 1.']
],
td: [
{ n: 'Application', c: 'Écrire la matrice de flux d’un lycée avec zones Internet, DMZ (reverse proxy, messagerie), serveurs internes (ENT, base), postes, administration.', r: 'Internet → DMZ : 443, 25 · DMZ proxy → ENT : 8080 · ENT → base : 5432 · postes → Internet : 80, 443, DNS via résolveur · administration → tout : 22 · tout le reste refusé.' },
{ n: 'Application', c: 'Écrire la chaîne input d’un serveur de base de données : PostgreSQL depuis l’ENT (10.62.40.20) seulement, SSH depuis l’administration, rien d’autre.', r: 'policy drop; iif lo accept; ct state established,related accept; ip saddr 10.62.40.20 tcp dport 5432 accept; ip saddr 10.62.99.0/28 tcp dport 22 accept; ICMP accepté.' },
{ n: 'Approfondissement', c: 'Proposer une procédure pour modifier le pare-feu d’un serveur distant sans risque de perdre l’accès.', r: 'Sauvegarder la configuration actuelle · nft -c -f nouvelle · programmer at now + 5 min : nft -f ancienne · appliquer · tester depuis une seconde session · annuler la tâche at si tout va bien.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Pare-feu Linux à trois zones.', etapes: ['Écrire la chaîne input en liste blanche', 'Programmer un retour arrière', 'Créer la chaîne forward avec la matrice de flux', 'Tester depuis chaque zone', 'Tracer un paquet bloqué avec nft monitor trace'] },
plusLoin: [['Wiki nftables', 'https://wiki.nftables.org/'], ['ANSSI · Recommandations pour la définition d’une politique de filtrage réseau d’un pare-feu', 'https://cyber.gouv.fr/publications/recommandations-pour-la-definition-dune-politique-de-filtrage-reseau-dun-pare-feu'], ['Debian Wiki · nftables', 'https://wiki.debian.org/nftables']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Après l’ajout de règles de filtrage sur le routeur inter-VLAN, les postes élèves n’accèdent plus à Internet, alors que les postes de l’administration fonctionnent.', etapes: [
['Lire les règles de forward', 'nft list chain inet filtre forward -a\n ct state established,related accept # handle 4\n iifname "ens19.10" accept # handle 5\n iifname "ens19.20" oifname "ens19.10" drop # handle 7\n→ Seul le VLAN 10 (administration) a une règle d’autorisation ; le VLAN 20 n’en a aucune vers Internet.'],
['Confirmer par une trace', 'nft insert rule inet filtre forward ip saddr 10.62.20.57 meta nftrace set 1\nnft monitor trace | head -4\ntrace id 7a… inet filtre forward packet: iif "ens19.20" oif "wan0" ip saddr 10.62.20.57 ip daddr 142.250.75.227 tcp dport 443\ntrace id 7a… inet filtre forward policy drop\n→ Le paquet atteint la politique drop.'],
['Corriger', 'nft add rule inet filtre forward iifname "ens19.20" oifname "wan0" tcp dport { 80, 443 } accept\nnft add rule inet filtre forward iifname "ens19.20" ip daddr 10.62.40.10 udp dport 53 accept\n→ Les élèves ont accès au web et au résolveur DNS interne, rien d’autre.'],
['Nettoyer et pérenniser', 'nft delete rule inet filtre forward handle <trace>\nnft list ruleset > /etc/nftables.conf ; nft -c -f /etc/nftables.conf\n→ Les règles de trace sont retirées, la configuration est sauvegardée et validée.'],
['Vérifier', 'Depuis un poste élève : curl -sI https://www.education.gouv.fr | head -1 → HTTP/2 200 ; ssh 10.62.40.20 → délai dépassé (attendu).\n→ Accès rétabli selon la matrice de flux.']
] },
erreurs: ['Politique accept par défaut', 'Règles IPv4 seulement', 'Se couper l’accès SSH', 'Oublier ct state established,related', 'Bloquer tout ICMP', 'Mélanger iptables et nftables sans vérifier le résultat', 'Pas de journalisation des refus'],
memo: [['Commande', 'Usage'], ['nft list ruleset -a', 'règles avec handles'], ['nft -c -f fichier', 'valider sans appliquer'], ['nft -f /etc/nftables.conf', 'appliquer'], ['nft add|insert rule …', 'ajouter à la fin / au début'], ['nft delete rule … handle N', 'supprimer une règle'], ['nft monitor trace', 'suivre un paquet'], ['counter', 'compter les paquets d’une règle'], ['journalctl -k | grep nft-drop', 'voir les refus journalisés'], ['conntrack -L', 'connexions suivies'], ['nmap -Pn -p- serveur', 'vérifier depuis l’extérieur']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer le filtrage à état. (3 pts)', 'Écrire le jeu de règles d’un serveur donné. (6 pts)', 'Construire la matrice de flux d’une architecture avec DMZ. (6 pts)', 'Diagnostiquer un flux bloqué à partir d’une trace. (3 pts)', 'Procédure de modification sans perte d’accès. (2 pts)'], r: ['conntrack', 'policy drop + autorisations', 'zones et ports', 'trace et politique', 'nft -c, retour arrière programmé'] },
prof: { deroule: [['Ouverture', 'nmap d’un serveur sans pare-feu'], ['Cours', 'État, tables, chaînes'], ['Pratique', 'Liste blanche sur un serveur'], ['Cours', 'Zones et DMZ'], ['Incident', 'Élèves sans Internet'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : règles à compléter. Élèves avancés : sets dynamiques, limitation de débit, nftables en pont.', eval: 'Lab validé par tests depuis chaque zone, sujet type.' } },
exemples: [
['Lister les règles', 'Audit.', 'nft list ruleset | head -12', 'table inet filtre {\n chain input {\n type filter hook input priority filter; policy accept;\n tcp dport 22 accept\n }\n}', 'Politique accept : la règle SSH ne sert à rien, tout est ouvert. Il faut passer en policy drop.'],
['Compter les paquets', 'Une règle sert-elle ?', 'nft list chain inet filtre input | grep counter', 'ip saddr @admin tcp dport 22 counter packets 1842 bytes 190322 accept\ntcp dport 8080 counter packets 0 bytes 0 accept', 'La règle 8080 n’a jamais servi : un ancien service à retirer.'],
['Voir les refus', 'Diagnostic.', 'journalctl -k --since "10 min ago" | grep nft-drop | tail -1', 'nft-drop: IN=ens18 SRC=185.220.101.4 DST=203.0.113.10 PROTO=TCP DPT=3389', 'Tentative de bureau à distance depuis Internet, correctement refusée.'],
['Valider avant d’appliquer', 'Modification de la configuration.', 'nft -c -f /etc/nftables.conf', '/etc/nftables.conf:14:19-22: Error: syntax error, unexpected accept', 'Erreur de syntaxe ligne 14 : la configuration n’est pas appliquée, le pare-feu reste dans son état précédent.'],
['Vérifier depuis l’extérieur', 'Après mise en service.', 'nmap -Pn -p 22,80,443,5432 203.0.113.10', 'PORT STATE SERVICE\n22/tcp filtered ssh\n80/tcp open http\n443/tcp open https\n5432/tcp filtered postgresql', 'Seuls les ports web sont ouverts depuis Internet ; SSH et la base sont filtrés.'],
['Programmer un retour arrière', 'Modification à distance.', 'nft list ruleset > /root/nft-avant.conf ; echo "nft flush ruleset; nft -f /root/nft-avant.conf" | at now + 5 minutes', 'job 12 at Thu Oct 1 10:47:00 2026', 'Si la nouvelle configuration coupe l’accès, l’ancienne revient seule au bout de 5 minutes. Sinon : atrm 12.']
],
termes: ['pare-feu', 'filtrage à état', 'conntrack', 'nftables', 'nft', 'table', 'chaîne', 'input', 'forward', 'output', 'policy drop', 'liste blanche', 'established', 'related', 'set', 'DMZ', 'zone', 'matrice de flux', 'nft monitor trace', 'counter', 'iptables', 'UFW']
});
reg({
cours: { id: 'vpn', match: /VPN|WireGuard|OpenVPN|IPsec/i,
titre: 'VPN : relier des sites et des utilisateurs avec WireGuard et IPsec', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T Cyber · TSSR',
prerequis: ['Cryptographie appliquée', 'Routage statique', 'Pare-feu'],
competences: ['BTS SIO · Bloc 3 · Sécuriser les accès distants', 'BUT R&T Cyber · Protéger les communications'],
intro: 'Un technicien d’astreinte qui doit intervenir le soir, un second site qui doit accéder au serveur de fichiers, un prestataire qui maintient une application : tous ont besoin d’un accès au réseau interne sans l’exposer sur Internet. Le VPN crée un tunnel chiffré et authentifié à travers Internet. Ce module présente les principes, la mise en œuvre de WireGuard, les notions d’IPsec, et les pièges de routage et de MTU.',
cm: [
{ t: 'Principe du tunnel', d: '', html: 'Un VPN <b>encapsule</b> des paquets IP internes dans des paquets chiffrés échangés entre deux extrémités publiques. Deux usages : le <b>site à site</b>, qui relie deux réseaux par leurs passerelles, et l’<b>accès nomade</b> (road warrior), qui relie un poste isolé au réseau. Le tunnel garantit la confidentialité et l’intégrité, et authentifie les extrémités ; il ne remplace pas le filtrage : une fois connecté, un utilisateur ne doit accéder qu’à ce dont il a besoin.' },
{ t: 'WireGuard', d: '', html: '<b>WireGuard</b>, intégré au noyau Linux depuis la version 5.6, est volontairement simple : quelques milliers de lignes de code, une cryptographie moderne fixée (Curve25519, ChaCha20-Poly1305), un seul port UDP. Chaque pair possède une <b>paire de clés</b> ; la configuration associe à chaque clé publique de pair la liste des adresses qu’il est autorisé à utiliser (<b>AllowedIPs</b>). Cette liste sert à la fois de table de routage (quels paquets envoyer à ce pair) et de filtre (quelles adresses sources accepter de lui).', exemple: '# serveur /etc/wireguard/wg0.conf\n[Interface]\nAddress = 10.99.0.1/24\nListenPort = 51820\nPrivateKey = <clé privée du serveur>\n\n[Peer] # portable du technicien\nPublicKey = <clé publique du portable>\nAllowedIPs = 10.99.0.10/32\n\n# poste client\n[Interface]\nAddress = 10.99.0.10/32\nPrivateKey = <clé privée du portable>\nDNS = 10.62.40.10\n[Peer]\nPublicKey = <clé publique du serveur>\nEndpoint = vpn.lycee-exemple.fr:51820\nAllowedIPs = 10.62.0.0/16, 10.99.0.0/24\nPersistentKeepalive = 25' },
{ t: 'Routage et tunnel partiel', d: '', html: 'Côté client, AllowedIPs = 10.62.0.0/16 crée un <b>tunnel partiel</b> (split tunnel) : seul le trafic vers le réseau du lycée passe par le VPN. AllowedIPs = 0.0.0.0/0, ::/0 fait passer tout le trafic (tunnel complet), utile sur un Wi-Fi public mais plus lourd pour la passerelle. Côté serveur, il faut activer le routage IP, autoriser les flux dans la chaîne forward et, si le réseau interne ne connaît pas la route vers 10.99.0.0/24, soit ajouter cette route sur la passerelle interne, soit faire du masquerade à la sortie du tunnel.' },
{ t: 'IPsec', d: '', html: '<b>IPsec</b> est le standard historique des VPN site à site entre équipements de marques différentes (pare-feu, routeurs, cloud). La négociation se fait avec <b>IKEv2</b> (UDP 500) : authentification par certificats ou clé prépartagée, puis création d’<b>associations de sécurité</b> (SA). Les données passent en <b>ESP</b> (protocole IP 50), encapsulé en UDP 4500 quand un NAT est présent (NAT-T). Sous Linux, strongSwan l’implémente. Plus complexe que WireGuard, il reste incontournable pour interconnecter un établissement avec un réseau académique ou un fournisseur cloud.' },
{ t: 'MTU et fragmentation', d: '', html: 'L’encapsulation ajoute des en-têtes : 60 octets en IPv4 pour WireGuard, d’où une MTU de tunnel de 1 420 octets par défaut. Si la découverte de MTU échoue (ICMP filtré quelque part), les petits échanges passent mais les gros paquets se perdent : la connexion SSH s’ouvre, mais l’affichage d’un gros fichier ou une page web bloquent. Les remèdes : ne pas filtrer les messages ICMP « fragmentation nécessaire », réduire la MTU de l’interface, ou ajuster la taille maximale des segments TCP (MSS clamping) sur la passerelle.' },
{ t: 'Exploiter un VPN', d: '', html: 'Une clé par personne et par appareil, jamais partagée ; révocation immédiate au départ d’un utilisateur (suppression du pair). Accès limité par le pare-feu selon le groupe (un prestataire n’accède qu’à son serveur). Journalisation des connexions. Pour les accès d’administration, l’ANSSI recommande en outre une authentification forte et des postes d’administration dédiés.' }
],
synthese: ['Tunnel chiffré site à site ou nomade ; il ne remplace pas le filtrage', 'WireGuard : clés par pair, AllowedIPs = routage + filtre, UDP 51820', 'Tunnel partiel ou complet ; routage IP, forward et route retour côté serveur', 'IPsec : IKEv2 UDP 500, ESP, NAT-T UDP 4500', 'MTU réduite : ne pas bloquer ICMP, MSS clamping', 'Une clé par appareil, révocation, accès limité par groupe'],
qcm: [
['Que définit AllowedIPs dans WireGuard ?', ['Les ports', 'Les adresses routées vers ce pair et acceptées de lui', 'Le chiffrement', 'Le DNS'], 1, 'Routage et filtre.'],
['Port par défaut de WireGuard ?', ['TCP 443', 'UDP 51820', 'UDP 500', 'TCP 22'], 1, 'Un seul port UDP.'],
['AllowedIPs = 0.0.0.0/0 côté client signifie :', ['aucun trafic dans le tunnel', 'tout le trafic passe par le tunnel', 'seulement le DNS', 'erreur'], 1, 'Tunnel complet.'],
['Quel port pour IKEv2 ?', ['UDP 500', 'TCP 1194', 'UDP 51820', 'TCP 443'], 0, 'UDP 4500 avec NAT-T.'],
['SSH marche à travers le VPN mais les gros transferts bloquent :', ['DNS', 'problème de MTU', 'clé expirée', 'pare-feu local'], 1, 'Gros paquets perdus.'],
['Au départ d’un prestataire, on :', ['change le mot de passe Wi-Fi', 'supprime son pair et sa clé', 'redémarre le serveur', 'ne fait rien'], 1, 'Révocation.']
],
td: [
{ n: 'Application', c: 'Configurer un VPN WireGuard site à site entre le lycée (LAN 10.62.0.0/16, passerelle publique 203.0.113.10) et une annexe (LAN 10.63.0.0/16).', r: 'Lycée : [Peer] annexe, AllowedIPs = 10.63.0.0/16, 10.99.0.2/32 · Annexe : [Peer] lycée, Endpoint = 203.0.113.10:51820, AllowedIPs = 10.62.0.0/16, 10.99.0.1/32 · ip_forward=1 et règles forward des deux côtés.' },
{ n: 'Application', c: 'Limiter un prestataire au seul serveur 10.62.40.30 en SSH à travers le VPN.', r: 'Pair du prestataire en 10.99.0.50/32 · forward : iifname "wg0" ip saddr 10.99.0.50 ip daddr 10.62.40.30 tcp dport 22 accept ; iifname "wg0" ip saddr 10.99.0.50 drop.' },
{ n: 'Approfondissement', c: 'Diagnostiquer un tunnel établi (handshake récent) où le client ne joint pas le réseau interne.', r: 'wg show (handshake OK) · ip route sur le client (AllowedIPs) · ip_forward et forward sur le serveur · route retour vers 10.99.0.0/24 sur la passerelle interne ou masquerade · tcpdump -ni wg0 puis sur l’interface LAN.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Passerelle WireGuard et client nomade.', etapes: ['Générer les clés', 'Configurer le serveur et le client', 'Établir le tunnel', 'Router vers le LAN', 'Limiter l’accès par le pare-feu', 'Révoquer un pair'] },
plusLoin: [['WireGuard · documentation', 'https://www.wireguard.com/'], ['strongSwan', 'https://docs.strongswan.org/'], ['ANSSI · Recommandations de sécurité relatives à IPsec', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-ipsec-pour-la-protection-des-flux-reseau']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Depuis chez lui, un technicien se connecte au VPN, ouvre une session SSH sur un serveur interne, mais tout se fige dès qu’il affiche un journal un peu long.', etapes: [
['Vérifier le tunnel', 'wg show wg0 | grep -E "handshake|transfer"\nlatest handshake: 12 seconds ago\ntransfer: 48.2 KiB received, 12.1 KiB sent\n→ Le tunnel est établi et échange des données.'],
['Tester la taille des paquets', 'ping -M do -s 1392 10.62.40.30 → réponses\nping -M do -s 1400 10.62.40.30 → aucune réponse\n→ Les paquets de plus de 1 420 octets ne passent pas.'],
['Comprendre', '→ La box du technicien a une MTU réduite (1 492 en PPPoE) et le chemin filtre les ICMP de fragmentation : la découverte de MTU échoue, les gros segments TCP sont perdus.'],
['Corriger', 'Côté client : MTU = 1380 dans [Interface]\nCôté passerelle : nft add rule inet filtre forward oifname "wg0" tcp flags syn tcp option maxseg size set rt mtu\n→ La MSS est ajustée automatiquement à la MTU du chemin.'],
['Vérifier', 'journalctl -n 500 dans la session SSH s’affiche sans blocage ; scp d’un fichier de 200 Mo réussi.\n→ Paramètres ajoutés au modèle de configuration client.']
] },
erreurs: ['Partager une même clé entre plusieurs personnes', 'Oublier la route retour ou le forward', 'Accès VPN à tout le réseau sans filtrage', 'Filtrer tout ICMP et casser la découverte de MTU', 'Ne pas révoquer les accès des partants'],
memo: [['Commande', 'Usage'], ['wg genkey | tee priv | wg pubkey > pub', 'générer une paire de clés'], ['wg-quick up wg0 · down wg0', 'monter ou descendre le tunnel'], ['systemctl enable --now wg-quick@wg0', 'démarrage automatique'], ['wg show', 'pairs, dernier handshake, trafic'], ['wg set wg0 peer <clé> remove', 'révoquer un pair'], ['ping -M do -s 1392 <ip>', 'tester la MTU du tunnel'], ['tcpdump -ni wg0', 'voir le trafic dans le tunnel'], ['swanctl --list-sas', 'associations IPsec actives']],
examen: { titre: 'Sujet type · 20 points', q: ['Comparer VPN site à site et nomade. (3 pts)', 'Configurer WireGuard pour un cas donné. (7 pts)', 'Expliquer AllowedIPs et le tunnel partiel. (4 pts)', 'Diagnostiquer un problème de MTU. (3 pts)', 'Mesures d’exploitation sûre. (3 pts)'], r: ['voir cours', 'deux fichiers wg0.conf', 'routage + filtre', 'ping -M do', 'clés, révocation, filtrage'] },
prof: { deroule: [['Ouverture', 'Capturer du trafic avant et dans le tunnel'], ['Cours', 'Principe et WireGuard'], ['Pratique', 'Lab VPN'], ['Cours', 'IPsec et MTU'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : fichiers de configuration à compléter. Élèves avancés : IPsec strongSwan, tunnel IPv6.', eval: 'Tunnel fonctionnel et filtré, sujet type.' } },
exemples: [
['État du tunnel', 'Un utilisateur dit que le VPN ne marche pas.', 'wg show wg0', 'peer: q8Zc…=\n endpoint: 86.245.12.9:53811\n allowed ips: 10.99.0.10/32\n latest handshake: 3 hours, 12 minutes ago', 'Plus de handshake depuis 3 heures : le client n’est plus connecté ou son paquet UDP n’arrive plus (pare-feu, changement de réseau).'],
['Générer des clés', 'Nouveau technicien.', 'umask 077; wg genkey | tee tech.key | wg pubkey > tech.pub; cat tech.pub', 'B7r1…Xw=', 'La clé privée ne quitte jamais l’appareil ; seule la clé publique est ajoutée au serveur.'],
['Vérifier les routes client', 'Le client ne joint pas le serveur.', 'ip route | grep wg0', '10.99.0.0/24 dev wg0 scope link', 'Seul le réseau du tunnel est routé : il manque 10.62.0.0/16 dans AllowedIPs du client.'],
['Voir si le trafic sort du tunnel', 'Côté serveur.', 'tcpdump -ni ens18 host 10.99.0.10', '10.99.0.10 > 10.62.40.30: ICMP echo request\n(aucune réponse)', 'Le paquet sort bien vers le LAN mais le serveur ne sait pas répondre à 10.99.0.10 : route retour ou masquerade manquant.'],
['Tester la MTU', 'Blocages sur gros fichiers.', 'ping -c1 -M do -s 1400 10.62.40.30', 'ping: local error: message too long, mtu=1420', 'La MTU du tunnel est de 1 420 : 1 400 + 28 d’en-têtes dépasse. On teste 1 392.'],
['Révoquer un accès', 'Fin de contrat d’un prestataire.', 'wg set wg0 peer q8Zc…= remove && wg-quick save wg0', '(aucune sortie)', 'Le pair est supprimé à chaud et la configuration enregistrée.']
],
termes: ['VPN', 'tunnel', 'site à site', 'nomade', 'WireGuard', 'wg', 'wg-quick', 'AllowedIPs', 'Endpoint', 'PersistentKeepalive', 'handshake', 'tunnel partiel', 'IPsec', 'IKEv2', 'ESP', 'NAT-T', 'strongSwan', 'MTU', 'MSS']
});
reg({
cours: { id: 'nginx', match: /Nginx|reverse proxy|Proxy et reverse|répartition de charge|HAProxy|PHP-FPM|LAMP|LEMP|Haute disponibilité/i,
titre: 'Nginx : serveur web, reverse proxy, HTTPS et répartition de charge', duree: '',
niveau: 'BTS SIO SISR / SLAM · BUT R&T / Informatique · TSSR / AIS',
prerequis: ['Le Web : HTTP', 'Cryptographie : certificats', 'DNS'],
competences: ['BTS SIO SISR · Bloc 2 · Mettre en œuvre un service web', 'BTS SIO SLAM · Déployer une application', 'AIS · Assurer la disponibilité des services'],
intro: 'Devant presque chaque application web se trouve un serveur Nginx : il sert les fichiers statiques, termine le chiffrement TLS, transmet les requêtes à l’application (PHP-FPM, Python, Node.js) et répartit la charge entre plusieurs instances. C’est aussi le premier endroit où regarder quand un site renvoie une erreur 502 ou 504. Ce module couvre la configuration de Nginx en production, le reverse proxy, HTTPS, la répartition de charge et la lecture des erreurs.',
cm: [
{ t: 'Architecture et configuration', d: '', html: 'Nginx fonctionne avec un processus maître et des <b>workers</b> événementiels : chaque worker gère des milliers de connexions sans créer un processus par requête, d’où sa faible consommation. La configuration est hiérarchique : bloc <code>http</code>, blocs <code>server</code> (un par site, sélectionné par <code>listen</code> et <code>server_name</code>), blocs <code>location</code> (selon le chemin). Sous Debian, les sites vont dans <code>/etc/nginx/sites-available/</code> et sont activés par un lien dans <code>sites-enabled/</code>. On valide toujours avec <code>nginx -t</code> avant <code>systemctl reload nginx</code>, qui recharge sans couper les connexions.' },
{ t: 'Servir une application PHP', d: '', html: 'Nginx n’exécute pas PHP : il transmet les requêtes .php à <b>PHP-FPM</b> par FastCGI, souvent via un socket Unix. C’est la pile <b>LEMP</b> (Linux, Nginx, MariaDB/MySQL, PHP), variante de LAMP où Apache exécutait PHP lui-même. Pour Laravel, la racine du site est le dossier <code>public</code> et toute requête qui ne correspond pas à un fichier est envoyée à index.php.', exemple: 'server {\n listen 443 ssl;\n http2 on;\n server_name ent.lycee-exemple.fr;\n root /srv/ent/public;\n index index.php;\n ssl_certificate /etc/letsencrypt/live/ent.lycee-exemple.fr/fullchain.pem;\n ssl_certificate_key /etc/letsencrypt/live/ent.lycee-exemple.fr/privkey.pem;\n client_max_body_size 64m;\n location / { try_files $uri $uri/ /index.php?$query_string; }\n location ~ \\.php$ {\n include snippets/fastcgi-php.conf;\n fastcgi_pass unix:/run/php/php8.3-fpm.sock;\n }\n location ~ /\\.(?!well-known) { deny all; }\n}' },
{ t: 'Reverse proxy', d: '', html: 'En <b>reverse proxy</b>, Nginx reçoit la requête du client et la transmet à une application interne (<code>proxy_pass</code>). Il doit lui transmettre les informations d’origine, sinon l’application croit que toutes les requêtes viennent de 127.0.0.1 en HTTP : en-têtes <code>Host</code>, <code>X-Forwarded-For</code>, <code>X-Forwarded-Proto</code>. Pour les WebSockets (terminal xterm.js, notifications en direct), il faut relayer les en-têtes <code>Upgrade</code> et <code>Connection</code>.', exemple: 'location / {\n proxy_pass http://127.0.0.1:8000;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_read_timeout 60s;\n}\nlocation /terminal/ {\n proxy_pass http://127.0.0.1:7681;\n proxy_http_version 1.1;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection "upgrade";\n}' },
{ t: 'HTTPS en production', d: '', html: '<b>Certbot</b> obtient un certificat Let’s Encrypt valable 90 jours et le renouvelle automatiquement (timer systemd). On redirige HTTP vers HTTPS, on n’autorise que TLS 1.2 et 1.3, et on ajoute l’en-tête <b>HSTS</b> pour que les navigateurs refusent ensuite toute connexion non chiffrée. D’autres en-têtes de sécurité complètent : <code>X-Content-Type-Options: nosniff</code>, <code>Content-Security-Policy</code>, <code>Referrer-Policy</code>. Le site de l’ANSSI et des outils comme SSL Labs ou testssl.sh vérifient la configuration.' },
{ t: 'Répartition de charge et haute disponibilité', d: '', html: 'Un bloc <code>upstream</code> déclare plusieurs serveurs d’application ; Nginx répartit les requêtes (tour à tour par défaut, <code>least_conn</code> pour le moins chargé, <code>ip_hash</code> pour garder un client sur le même serveur) et écarte un serveur qui ne répond plus (<code>max_fails</code>, <code>fail_timeout</code>). Pour que Nginx lui-même ne soit pas un point unique de défaillance, on en place deux, avec une adresse IP virtuelle portée par <b>Keepalived</b> (protocole VRRP) : si le premier tombe, le second reprend l’adresse en quelques secondes. <b>HAProxy</b> est l’alternative spécialisée, très utilisée pour la répartition de charge TCP et HTTP.', exemple: 'upstream ent_app {\n least_conn;\n server 10.62.40.21:8000 max_fails=3 fail_timeout=10s;\n server 10.62.40.22:8000 max_fails=3 fail_timeout=10s;\n}\nserver { … location / { proxy_pass http://ent_app; … } }' },
{ t: 'Lire les erreurs', d: '', html: 'Les codes renvoyés par Nginx orientent le diagnostic. <b>502 Bad Gateway</b> : Nginx n’a pas pu parler à l’application (processus arrêté, mauvais port ou socket, droits sur le socket). <b>504 Gateway Timeout</b> : l’application a mis trop longtemps à répondre (requête lente, proxy_read_timeout trop court). <b>413</b> : fichier envoyé plus grand que client_max_body_size. <b>403</b> : droits de fichiers ou règle deny. Le journal d’erreurs (<code>/var/log/nginx/error.log</code>) donne toujours la cause précise.' }
],
synthese: ['Workers événementiels ; http → server → location', 'nginx -t puis reload', 'PHP par PHP-FPM (LEMP) ; try_files pour les frameworks', 'Reverse proxy : proxy_pass + Host, X-Forwarded-For, X-Forwarded-Proto ; Upgrade pour WebSocket', 'HTTPS avec Certbot, TLS 1.2+, HSTS et en-têtes de sécurité', 'upstream, least_conn, max_fails ; Keepalived pour la redondance', '502 : application injoignable ; 504 : trop lente ; 413 : fichier trop gros'],
qcm: [
['Quelle commande valide la configuration ?', ['nginx -s check', 'nginx -t', 'systemctl test nginx', 'nginx --dry'], 1, 'Avant chaque reload.'],
['Comment Nginx exécute-t-il PHP ?', ['Il l’intègre', 'Via PHP-FPM par FastCGI', 'Il ne peut pas', 'Via Apache'], 1, 'fastcgi_pass.'],
['Pourquoi X-Forwarded-For ?', ['Chiffrer', 'Transmettre l’IP réelle du client à l’application', 'Accélérer', 'Répartir la charge'], 1, 'Sinon tout vient du proxy.'],
['Que signifie 502 ?', ['Fichier introuvable', 'Le proxy n’a pas pu joindre l’application', 'Accès interdit', 'Délai dépassé'], 1, 'Application arrêtée ou mauvais port.'],
['Quel mécanisme partage une IP entre deux Nginx ?', ['DNS', 'Keepalived (VRRP)', 'NAT', 'HSTS'], 1, 'Adresse virtuelle.'],
['Pour un terminal web en WebSocket, il faut relayer :', ['Cookie', 'Upgrade et Connection', 'Referer', 'Accept'], 1, 'Changement de protocole.']
],
td: [
{ n: 'Application', c: 'Écrire la configuration Nginx qui redirige HTTP vers HTTPS pour resa.lycee-exemple.fr et transmet à une application sur le port 8000.', r: 'server { listen 80; server_name resa…; return 301 https://$host$request_uri; }\nserver { listen 443 ssl; server_name resa…; ssl_certificate …; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; X-Forwarded-For …; X-Forwarded-Proto $scheme; } }' },
{ n: 'Application', c: 'Répartir la charge entre trois instances en écartant une instance qui échoue trois fois.', r: 'upstream app { least_conn; server a:8000 max_fails=3 fail_timeout=10s; server b:8000 …; server c:8000 …; } puis proxy_pass http://app;' },
{ n: 'Approfondissement', c: 'Les enseignants obtiennent une erreur en déposant des vidéos de 120 Mo dans l’ENT. Diagnostiquer et corriger à tous les niveaux.', r: 'error.log : client intended to send too large body → client_max_body_size 200m dans Nginx · upload_max_filesize et post_max_size dans PHP · éventuellement limite de l’application · reload de Nginx et PHP-FPM.' }
],
tp: { type: 'Lab web', url: 'apercu-lab-web.html', desc: 'Nginx devant deux instances applicatives.', etapes: ['Installer Nginx et valider', 'Publier un site statique', 'Configurer un reverse proxy', 'Activer HTTPS (certificat de test)', 'Répartir la charge sur deux instances', 'Provoquer et lire une erreur 502'] },
plusLoin: [['Documentation Nginx', 'https://nginx.org/en/docs/'], ['Mozilla SSL Configuration Generator', 'https://ssl-config.mozilla.org/'], ['ANSSI · Recommandations de sécurité relatives à TLS', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-tls']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Depuis la mise à jour de PHP de la nuit, l’ENT renvoie « 502 Bad Gateway » sur toutes les pages dynamiques ; les images s’affichent.', etapes: [
['Lire le journal d’erreurs', 'tail -2 /var/log/nginx/error.log\nconnect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or directory) while connecting to upstream\n→ Nginx cherche le socket de PHP 8.2.'],
['Vérifier PHP-FPM', 'systemctl list-units "php*-fpm*" --no-legend\nphp8.3-fpm.service loaded active running\nls /run/php/\nphp8.3-fpm.sock\n→ La mise à jour a installé PHP 8.3 ; l’ancien service et son socket n’existent plus.'],
['Corriger', 'sed -i "s#php8.2-fpm.sock#php8.3-fpm.sock#" /etc/nginx/sites-available/ent\nnginx -t && systemctl reload nginx\n→ Nginx pointe vers le bon socket.'],
['Vérifier', 'curl -sI https://ent.lycee-exemple.fr | head -1\nHTTP/2 200\n→ Les pages dynamiques répondent.'],
['Prévenir', '→ Chemin du socket défini une seule fois dans un fichier inclus (snippets/php.conf) ; test de fumée automatique après chaque mise à jour ; supervision du code HTTP de la page de connexion.']
] },
erreurs: ['Recharger sans nginx -t', 'Oublier les en-têtes X-Forwarded-*', 'Mettre la racine sur le dossier du projet au lieu de public', 'Laisser les fichiers cachés (.env, .git) accessibles', 'Pas de redirection HTTPS ni de HSTS', 'Un seul Nginx pour un service critique'],
memo: [['Commande', 'Usage'], ['nginx -t', 'valider'], ['systemctl reload nginx', 'recharger sans coupure'], ['nginx -T', 'afficher la configuration complète chargée'], ['tail -f /var/log/nginx/error.log', 'cause des erreurs'], ['tail -f /var/log/nginx/access.log', 'requêtes et codes'], ['certbot --nginx -d nom', 'obtenir un certificat'], ['certbot renew --dry-run', 'tester le renouvellement'], ['curl -sI https://site', 'code et en-têtes'], ['ss -xlp | grep php', 'sockets PHP-FPM'], ['ip addr show | grep "scope global secondary"', 'IP virtuelle Keepalived']],
examen: { titre: 'Sujet type · 20 points', q: ['Écrire un bloc server pour une application PHP. (6 pts)', 'Configurer un reverse proxy avec WebSocket. (4 pts)', 'Mettre en place HTTPS et les en-têtes de sécurité. (4 pts)', 'Répartir la charge et assurer la redondance. (4 pts)', 'Diagnostiquer une 502 et une 504. (2 pts)'], r: ['root public, try_files, fastcgi_pass', 'proxy_pass + Upgrade', 'certbot, TLS 1.2+, HSTS', 'upstream + Keepalived', 'socket / délai'] },
prof: { deroule: [['Ouverture', 'Les en-têtes Server de sites connus'], ['Cours', 'Configuration et PHP-FPM'], ['Pratique', 'Site et reverse proxy'], ['Cours', 'HTTPS et répartition'], ['Incident', '502 après mise à jour'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : configuration à trous. Élèves avancés : Keepalived, cache, limitation de débit.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Valider et recharger', 'Nouvelle configuration.', 'nginx -t && systemctl reload nginx', 'nginx: [emerg] unknown directive "proxy_pas" in /etc/nginx/sites-enabled/resa:12\nnginx: configuration file /etc/nginx/nginx.conf test failed', 'Faute de frappe : le reload n’est pas lancé, le site continue de tourner avec l’ancienne configuration.'],
['Lire les codes dans le journal', 'Pic d’erreurs.', "awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -4", ' 18234 200\n 612 304\n 418 502\n 37 404', '418 erreurs 502 : on regarde error.log sur la même période.'],
['Vérifier les en-têtes de sécurité', 'Audit.', 'curl -sI https://ent.lycee-exemple.fr | grep -iE "strict|content-security|x-content"', 'strict-transport-security: max-age=31536000; includeSubDomains\nx-content-type-options: nosniff', 'HSTS et nosniff sont présents ; il manque une Content-Security-Policy.'],
['Tester le renouvellement', 'Préparer l’expiration.', 'certbot renew --dry-run', 'Congratulations, all simulated renewals succeeded:\n /etc/letsencrypt/live/ent.lycee-exemple.fr/fullchain.pem (success)', 'Le renouvellement automatique fonctionnera.'],
['Voir la configuration réellement chargée', 'Une directive semble ignorée.', 'nginx -T 2>/dev/null | grep -n client_max_body_size', '34: client_max_body_size 1m;\n112: client_max_body_size 64m;', 'La valeur 64m est définie dans un location qui ne correspond pas à la route d’envoi : il faut la placer au niveau du server.'],
['Vérifier la bascule Keepalived', 'Test de redondance.', 'systemctl stop nginx (sur proxy1) ; ip addr show ens18 | grep 10.62.40.100 (sur proxy2)', 'inet 10.62.40.100/24 scope global secondary ens18', 'L’adresse virtuelle est passée sur proxy2 : le service continue.']
],
termes: ['Nginx', 'worker', 'server_name', 'location', 'nginx -t', 'reload', 'PHP-FPM', 'FastCGI', 'LEMP', 'LAMP', 'try_files', 'reverse proxy', 'proxy_pass', 'X-Forwarded-For', 'WebSocket', 'Certbot', 'HSTS', 'upstream', 'least_conn', 'Keepalived', 'VRRP', 'HAProxy', '502', '504', '413']
});
reg({
cours: { id: 'sauvegarde', match: /Sauvegardes|rsync|restic|3-2-1|restauration|Archivage|tar/i,
titre: 'Sauvegardes : stratégie 3-2-1, rsync, restic et tests de restauration', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR',
prerequis: ['Le terminal Linux', 'Droits Unix', 'SSH par clé'],
competences: ['BTS SIO · Bloc 1 · Gérer le patrimoine informatique', 'BTS SIO SISR · Bloc 2 · Garantir la disponibilité des données', 'TSSR · Mettre en œuvre une politique de sauvegarde'],
intro: 'La question n’est pas de savoir si un disque tombera, si un utilisateur supprimera un dossier ou si un rançongiciel chiffrera un serveur, mais quand. Ce jour-là, seule compte une sauvegarde récente, complète, hors d’atteinte et dont la restauration a déjà été testée. Ce module construit une politique de sauvegarde, puis la met en œuvre avec tar, rsync et restic, jusqu’au test de restauration.',
cm: [
{ t: 'Objectifs : RPO et RTO', d: '', html: 'Deux indicateurs fixent la politique. Le <b>RPO</b> (Recovery Point Objective) est la quantité de données que l’on accepte de perdre : une sauvegarde par nuit donne un RPO de 24 heures. Le <b>RTO</b> (Recovery Time Objective) est le délai acceptable pour rétablir le service. Ils se décident avec les utilisateurs, service par service : on n’accepte pas la même perte pour les notes d’examen que pour un dossier de travail temporaire.' },
{ t: 'La règle 3-2-1', d: '', html: 'Garder <b>3</b> copies des données (l’original et deux sauvegardes), sur <b>2</b> supports différents, dont <b>1</b> hors site. On y ajoute aujourd’hui une copie <b>hors ligne ou immuable</b> que ni un administrateur compromis ni un rançongiciel ne peuvent effacer (règle 3-2-1-1-0 : et 0 erreur à la vérification). Un RAID n’est <b>pas</b> une sauvegarde : il protège contre la panne d’un disque, pas contre une suppression ou un chiffrement, qui sont répliqués instantanément.' },
{ t: 'Complète, incrémentale, différentielle', d: '', html: 'Une sauvegarde <b>complète</b> copie tout : simple à restaurer, longue et volumineuse. Une <b>incrémentale</b> ne copie que ce qui a changé depuis la dernière sauvegarde, quelle qu’elle soit : rapide, mais la restauration demande la dernière complète et toutes les incrémentales. Une <b>différentielle</b> copie ce qui a changé depuis la dernière complète. Les outils modernes comme restic ou BorgBackup utilisent la <b>déduplication</b> : chaque instantané semble complet, mais seuls les blocs nouveaux sont stockés.' },
{ t: 'tar et rsync', d: '', html: '<code>tar</code> regroupe une arborescence dans une archive en conservant droits et propriétaires, souvent compressée (<code>-z</code> gzip, <code>--zstd</code>). <code>rsync</code> synchronise deux dossiers, localement ou par SSH, en ne transférant que les différences : <code>-a</code> conserve droits, dates et liens, <code>--delete</code> supprime à la destination ce qui a disparu de la source. Attention : un miroir rsync avec --delete n’est pas une sauvegarde historisée, une suppression accidentelle est répliquée au prochain passage ; on combine avec des instantanés (<code>--link-dest</code>) ou on passe à restic.', exemple: 'tar --zstd -cpf /backup/etc-$(date +%F).tar.zst /etc\nrsync -aH --delete --link-dest=/backup/ent/latest /srv/ent/ backup@sauv:/backup/ent/$(date +%F)/' },
{ t: 'restic : sauvegardes chiffrées et dédupliquées', d: '', html: '<b>restic</b> crée un <b>dépôt</b> chiffré (AES-256) sur un disque, un serveur SFTP ou un stockage objet S3, et y enregistre des <b>instantanés</b> dédupliqués. Une <b>politique de rétention</b> (<code>forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12</code>) garde un historique utile sans croissance illimitée, et <code>check</code> vérifie l’intégrité du dépôt. Sans la clé du dépôt, les données sont illisibles : elle se conserve dans un coffre, à part.', exemple: 'export RESTIC_REPOSITORY=sftp:sauv@10.62.40.50:/depots/ent\nexport RESTIC_PASSWORD_FILE=/root/.restic-pass\nrestic init\nrestic backup /srv/ent /etc --exclude /srv/ent/cache\nrestic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune\nrestic check' },
{ t: 'Bases de données et restauration', d: '', html: 'On ne copie pas les fichiers d’une base en cours d’écriture : la copie serait incohérente. On exporte avec l’outil de la base (<code>pg_dump</code>, <code>mariadb-dump --single-transaction</code>) ou on utilise un instantané du système de fichiers, puis on sauvegarde l’export.<br><br>Une sauvegarde jamais restaurée n’est qu’une hypothèse. On planifie des <b>tests de restauration</b> réguliers sur une machine de test, on chronomètre pour vérifier le RTO, et on documente la procédure pas à pas, pour qu’un collègue puisse la suivre le jour où l’auteur est absent.', attention: 'Un rançongiciel qui obtient les droits d’administration cherche d’abord les sauvegardes accessibles pour les chiffrer. Le serveur de sauvegarde doit tirer les données (pull) plutôt que les recevoir avec des droits d’écriture larges, et une copie doit être immuable ou déconnectée.' }
],
synthese: ['RPO : perte acceptable ; RTO : délai de rétablissement', '3-2-1 + une copie hors ligne ou immuable ; le RAID n’est pas une sauvegarde', 'Complète, incrémentale, différentielle ; déduplication', 'tar pour archiver, rsync pour synchroniser (attention à --delete)', 'restic : dépôt chiffré, instantanés, rétention, check', 'Exports cohérents des bases ; restaurations testées et documentées'],
qcm: [
['Une sauvegarde chaque nuit à 2 h donne un RPO de :', ['2 h', '24 h', '1 semaine', '0'], 1, 'Jusqu’à 24 h de données perdues.'],
['Le RAID 1 est-il une sauvegarde ?', ['Oui', 'Non, une suppression est répliquée', 'Seulement en RAID 5', 'Oui s’il est chiffré'], 1, 'Il protège seulement du matériel.'],
['Que signifie le « 1 » final de 3-2-1 ?', ['Une copie par jour', 'Une copie hors site', 'Un seul support', 'Un administrateur'], 1, 'Protection contre sinistre.'],
['Pour restaurer une incrémentale, il faut :', ['la dernière incrémentale seule', 'la dernière complète et toutes les incrémentales suivantes', 'la première sauvegarde', 'rien'], 1, 'Chaîne complète.'],
['Pourquoi pg_dump plutôt qu’une copie des fichiers de la base ?', ['Plus rapide', 'Export cohérent pendant que la base fonctionne', 'Obligatoire', 'Plus petit'], 1, 'Cohérence.'],
['Que vérifie restic check ?', ['La vitesse', 'L’intégrité du dépôt', 'Le réseau', 'Les droits'], 1, 'Blocs et index.']
],
td: [
{ n: 'Application', c: 'Définir RPO et RTO pour l’ENT, le serveur de fichiers et les postes élèves d’un lycée, puis la fréquence de sauvegarde de chacun.', r: 'ENT : RPO 1 h (sauvegarde horaire de la base), RTO 4 h · fichiers : RPO 24 h, RTO 1 j, clichés horaires en journée · postes élèves : pas de sauvegarde, réinstallation par image, données sur le serveur.' },
{ n: 'Application', c: 'Écrire le script de sauvegarde nocturne de l’ENT : export de la base, sauvegarde restic, rétention, vérification, et code de sortie exploitable par la supervision.', r: 'set -euo pipefail ; pg_dump -Fc ent > /var/backups/ent.dump ; restic backup /var/backups/ent.dump /srv/ent/storage /etc ; restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune ; restic check --read-data-subset=5% ; planifié par un timer systemd, échec remonté à la supervision.' },
{ n: 'Approfondissement', c: 'Concevoir une architecture de sauvegarde résistante à un rançongiciel qui aurait les droits administrateur du domaine.', r: 'Serveur de sauvegarde hors domaine, comptes distincts, en mode pull ; dépôt avec verrouillage objet (immuabilité) ou copie hebdomadaire sur support déconnecté ; clé du dépôt dans un coffre ; supervision des suppressions massives ; test de restauration trimestriel.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Serveur applicatif et serveur de sauvegarde.', etapes: ['Archiver /etc avec tar', 'Synchroniser avec rsync par SSH', 'Créer un dépôt restic', 'Sauvegarder base et fichiers', 'Appliquer une rétention', 'Supprimer des données puis restaurer et chronométrer'] },
plusLoin: [['Documentation restic', 'https://restic.readthedocs.io/'], ['ANSSI · Fondamentaux de la sauvegarde des systèmes d’information', 'https://cyber.gouv.fr/publications/les-fondamentaux-de-la-sauvegarde-des-systemes-dinformation'], ['Cybermalveillance.gouv.fr · Les sauvegardes', 'https://www.cybermalveillance.gouv.fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Lundi matin, un enseignant signale que le dossier « Sujets BTS 2026 » a disparu du partage. Les sauvegardes sont faites chaque nuit par rsync vers le serveur de sauvegarde.', etapes: [
['Constater', 'ls /srv/partage/profs/ | grep -i sujets → rien\nls /backup/partage/profs/ | grep -i sujets → rien\n→ Le dossier a aussi disparu de la sauvegarde.'],
['Comprendre', 'grep rsync /etc/cron.d/sauvegarde\nrsync -a --delete /srv/partage/ sauv:/backup/partage/\n→ Miroir avec --delete : la suppression de vendredi a été répliquée samedi à 2 h. Il n’existe aucun historique.'],
['Chercher une autre copie', 'zfs list -t snapshot | grep partage | tail -2\ntank/partage@auto-2026-09-25\ntank/partage@auto-2026-09-26\n→ Par chance, des clichés ZFS quotidiens existent sur le serveur de fichiers.'],
['Restaurer', 'cp -a /srv/partage/.zfs/snapshot/auto-2026-09-25/profs/"Sujets BTS 2026" /srv/partage/profs/\n→ Le dossier est restauré dans l’état de jeudi soir ; l’enseignant vérifie son contenu.'],
['Corriger la politique', '→ Remplacement du miroir rsync par restic avec rétention 7 jours / 4 semaines / 12 mois, copie hebdomadaire hors site, test de restauration mensuel inscrit au planning, et documentation pour les utilisateurs sur la restauration par clichés.']
] },
erreurs: ['Considérer le RAID comme une sauvegarde', 'Miroir rsync --delete sans historique', 'Sauvegardes accessibles en écriture depuis le serveur sauvegardé', 'Copier les fichiers d’une base en fonctionnement', 'Ne jamais tester la restauration', 'Perdre la clé de chiffrement du dépôt', 'Aucune supervision des échecs de sauvegarde'],
memo: [['Commande', 'Usage'], ['tar --zstd -cpf a.tar.zst dossier', 'archiver en conservant les droits'], ['tar -tf a.tar.zst', 'lister le contenu'], ['rsync -aH --dry-run src/ dest/', 'simuler une synchronisation'], ['restic init · backup · snapshots', 'dépôt et instantanés'], ['restic restore <id> --target /tmp/r', 'restaurer'], ['restic forget --keep-daily 7 … --prune', 'rétention'], ['restic check', 'intégrité'], ['pg_dump -Fc base > f.dump', 'export PostgreSQL'], ['mariadb-dump --single-transaction base', 'export MariaDB'], ['systemctl list-timers', 'vérifier la planification']],
examen: { titre: 'Sujet type · 20 points', q: ['Définir RPO et RTO pour un cas donné. (3 pts)', 'Proposer une architecture 3-2-1-1. (5 pts)', 'Écrire un script de sauvegarde avec rétention. (6 pts)', 'Expliquer pourquoi un miroir n’est pas une sauvegarde. (3 pts)', 'Rédiger une procédure de test de restauration. (3 pts)'], r: ['perte et délai acceptables', 'copies, supports, hors site, immuable', 'dump + restic + forget + check', 'suppression répliquée', 'machine de test, chronométrage, vérification'] },
prof: { deroule: [['Ouverture', 'Supprimer un dossier en direct : comment le récupérer ?'], ['Cours', 'RPO, RTO, 3-2-1'], ['Pratique', 'tar et rsync'], ['Pratique', 'restic et restauration'], ['Incident', 'Miroir qui a répliqué la suppression'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : script fourni à compléter. Élèves avancés : immuabilité S3, BorgBackup, sauvegarde de VM.', eval: 'Restauration réussie et chronométrée, sujet type.' } },
exemples: [
['Lister les instantanés', 'Retrouver une version.', 'restic snapshots --compact | tail -4', 'ID Time Host Paths\n4b1c2e7a 2026-09-28 02:00:12 ent /srv/ent /etc\n9ad03f11 2026-09-29 02:00:09 ent /srv/ent /etc\nc71e5b20 2026-09-30 02:00:14 ent /srv/ent /etc', 'Un instantané par nuit : on choisit celui d’avant l’incident.'],
['Restaurer un seul fichier', 'Un enseignant a écrasé un document.', 'restic restore 9ad03f11 --target /tmp/restau --include "/srv/ent/storage/app/sujets/e6.pdf"', 'restoring <Snapshot 9ad03f11> to /tmp/restau\nSummary: Restored 1 files', 'On restaure dans un dossier temporaire, on vérifie, puis on remet le fichier en place.'],
['Simuler rsync', 'Avant une première synchronisation.', 'rsync -aH --delete --dry-run --itemize-changes /srv/partage/ sauv:/backup/partage/ | head -3', '*deleting profs/ancien/\n>f+++++++++ profs/sujets/e5.odt\n.d..t...... profs/', '--dry-run montre qu’un dossier serait supprimé à la destination : on vérifie avant de lancer.'],
['Vérifier le dépôt', 'Contrôle mensuel.', 'restic check --read-data-subset=10%', 'using temporary cache\ncheck snapshots, trees and blobs\nread 10.0% of data packs\nno errors were found', 'Aucune corruption sur l’échantillon lu.'],
['Mesurer la déduplication', 'Justifier l’espace disque.', 'restic stats --mode raw-data ; restic stats --mode restore-size', 'Total Size: 42.8 GiB\nTotal Size: 1.17 TiB', '30 instantanés représentant 1,17 To de données occupent 42,8 Go grâce à la déduplication et à la compression.'],
['Vérifier la planification', 'La sauvegarde a-t-elle tourné ?', 'systemctl list-timers sauvegarde.timer ; journalctl -u sauvegarde --since yesterday | tail -2', 'NEXT Thu 2026-10-01 02:00 LAST Wed 2026-09-30 02:00\nrestic: snapshot c71e5b20 saved\nsauvegarde.service: Deactivated successfully.', 'Dernière exécution réussie cette nuit ; en cas d’échec, le code de sortie doit déclencher une alerte.']
],
termes: ['sauvegarde', 'restauration', 'RPO', 'RTO', '3-2-1', 'immuable', 'RAID', 'complète', 'incrémentale', 'différentielle', 'déduplication', 'tar', 'rsync', '--delete', 'restic', 'dépôt', 'instantané', 'rétention', 'restic check', 'pg_dump', 'rançongiciel']
});
})();

View file

@ -0,0 +1,227 @@
// Cours complets · lot 8 : processus et systemd, disques LVM RAID, SSH avancé, messagerie
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'systemd', match: /Processus|systemd|journalctl|Planification|cron|timers|Journaux : rsyslog|logrotate|Démarrage : BIOS|GRUB|service en échec|Ordonnancement des processus/i,
titre: 'Processus, services systemd, planification et journaux', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR · LPIC-1',
prerequis: ['Le terminal Linux', 'Droits Unix'],
competences: ['BTS SIO SISR · Bloc 2 · Exploiter les serveurs', 'TSSR · Exploiter des serveurs Linux', 'LPIC-1 · Topics 101 à 108'],
intro: 'Un serveur, c’est d’abord un ensemble de processus qui tournent en permanence : serveur web, base de données, agent de supervision, tâches de sauvegarde. Savoir les observer, les arrêter proprement, les faire démarrer au bon moment et lire leurs journaux est le quotidien d’un administrateur. Ce module va du processus au service systemd, en passant par les signaux, la planification et la gestion des journaux, jusqu’au dépannage d’un service qui refuse de démarrer.',
cm: [
{ t: 'Processus, PID et arborescence', d: '', html: 'Chaque programme en cours d’exécution est un <b>processus</b>, identifié par un <b>PID</b> et rattaché à son parent (<b>PPID</b>). Le premier processus, PID 1, est <b>systemd</b> sur les distributions actuelles ; tous les autres en descendent. Un processus a un propriétaire, un état (R en exécution, S en attente, D en attente d’entrée-sortie non interruptible, Z zombie, T arrêté), une priorité et des ressources consommées. <code>ps aux</code> et <code>ps -ef --forest</code> les listent, <code>top</code> et <code>htop</code> les suivent en direct, <code>pgrep</code> et <code>pidof</code> retrouvent un PID par nom.' },
{ t: 'Signaux et priorités', d: '', html: 'On communique avec un processus par des <b>signaux</b>. <b>SIGTERM</b> (15), envoyé par défaut par <code>kill</code>, demande poliment l’arrêt : le programme peut terminer ses écritures. <b>SIGKILL</b> (9) le tue immédiatement, sans lui laisser le choix : à réserver aux processus bloqués, car il peut laisser des fichiers incohérents. <b>SIGHUP</b> (1) demande souvent de relire la configuration. <b>SIGSTOP</b> et <b>SIGCONT</b> suspendent et relancent.<br><br>La <b>priorité</b> d’ordonnancement se règle par la valeur <b>nice</b>, de −20 (très prioritaire) à 19 (très peu) : <code>nice -n 10 commande</code> au lancement, <code>renice</code> ensuite. Une compression de sauvegarde lancée en nice 19 ne ralentit pas le service web. <code>ionice</code> fait de même pour les entrées-sorties.' },
{ t: 'systemd et les unités', d: '', html: 'systemd pilote le démarrage et les services à l’aide d’<b>unités</b> : <code>.service</code> (un démon), <code>.timer</code> (une planification), <code>.socket</code>, <code>.mount</code>, <code>.target</code> (un groupe d’unités, équivalent des anciens niveaux d’exécution : multi-user.target, graphical.target). Les unités fournies par les paquets sont dans <code>/lib/systemd/system</code> ; les unités et surcharges de l’administrateur dans <code>/etc/systemd/system</code>, qui a priorité.<br><br><code>systemctl start|stop|restart|reload</code> agit maintenant ; <code>enable|disable</code> agit au démarrage ; <code>status</code> montre l’état, le PID principal, le cgroup et les dernières lignes de journal. Pour modifier une unité fournie par un paquet, on ne l’édite pas : <code>systemctl edit service</code> crée un fichier de surcharge qui survit aux mises à jour.', exemple: '# /etc/systemd/system/resa.service\n[Unit]\nDescription=Application de réservation\nAfter=network-online.target postgresql.service\nWants=network-online.target\n\n[Service]\nUser=resa\nWorkingDirectory=/srv/resa\nExecStart=/srv/resa/venv/bin/gunicorn -b 127.0.0.1:8000 app:app\nRestart=on-failure\nRestartSec=5\nMemoryMax=512M\nNoNewPrivileges=yes\nProtectSystem=strict\nReadWritePaths=/srv/resa/data\n\n[Install]\nWantedBy=multi-user.target' },
{ t: 'Planifier : cron et timers', d: '', html: '<b>cron</b> exécute des commandes selon cinq champs (minute, heure, jour du mois, mois, jour de la semaine) : <code>30 2 * * 1-5 /usr/local/bin/sauvegarde</code> tourne à 2 h 30 du lundi au vendredi. Les tâches système se placent dans <code>/etc/cron.d/</code>, celles d’un utilisateur avec <code>crontab -e</code>. Pièges classiques : un environnement minimal (PATH réduit), l’absence de sortie visible, et le fuseau horaire.<br><br>Les <b>timers systemd</b> sont l’alternative moderne : journalisation automatique dans journald, rattrapage des exécutions manquées quand la machine était éteinte (<code>Persistent=true</code>), délai aléatoire pour éviter que cent serveurs lancent la même tâche à la même seconde, et dépendances entre unités. <code>systemctl list-timers</code> affiche les prochaines exécutions. <code>at</code> planifie une exécution unique.' },
{ t: 'Journaux : journald, rsyslog, logrotate', d: '', html: '<b>journald</b> collecte les journaux du noyau, des services et du démarrage, avec des métadonnées (unité, PID, priorité). <code>journalctl -u nginx --since "1 hour ago"</code>, <code>-p err</code> pour les erreurs, <code>-b -1</code> pour le démarrage précédent, <code>-f</code> pour suivre. La persistance après redémarrage demande le dossier <code>/var/log/journal</code> (Storage=persistent). <b>rsyslog</b> écrit les journaux texte classiques dans /var/log et peut les envoyer vers un serveur central. <b>logrotate</b> fait tourner les fichiers (compression, nombre de fichiers conservés) pour qu’ils ne remplissent pas le disque ; journald se limite avec SystemMaxUse.' },
{ t: 'Le démarrage', d: '', html: 'Au démarrage, le micrologiciel (<b>UEFI</b>, ou BIOS sur les anciennes machines) charge le chargeur d’amorçage <b>GRUB</b>, qui charge le noyau et l’<b>initramfs</b>, petit système de fichiers temporaire contenant les pilotes nécessaires pour monter la vraie racine. Le noyau lance systemd, qui atteint la cible par défaut. <code>systemd-analyze blame</code> classe les unités par durée de démarrage, <code>systemd-analyze critical-chain</code> montre la chaîne qui retarde le démarrage. En cas de panne, on démarre en <b>mode secours</b> (rescue.target) en ajoutant <code>systemd.unit=rescue.target</code> à la ligne du noyau dans GRUB.' }
],
synthese: ['PID, PPID, états ; ps, top, pgrep', 'SIGTERM avant SIGKILL ; nice et ionice pour les tâches lourdes', 'Unités systemd : start/enable, status, surcharge avec systemctl edit', 'Durcissement d’un service : User, NoNewPrivileges, ProtectSystem, MemoryMax', 'cron ou timers systemd (Persistent, journalisation)', 'journalctl -u -p -b ; logrotate et SystemMaxUse', 'UEFI → GRUB → noyau + initramfs → systemd ; systemd-analyze, rescue.target'],
qcm: [
['Quel signal envoyer en premier pour arrêter un processus ?', ['SIGKILL', 'SIGTERM', 'SIGSTOP', 'SIGHUP'], 1, 'Il laisse le programme terminer proprement.'],
['Différence entre systemctl start et enable ?', ['Aucune', 'start agit maintenant, enable au démarrage', 'enable démarre immédiatement', 'start est obsolète'], 1, 'On fait souvent enable --now.'],
['Comment modifier une unité fournie par un paquet ?', ['Éditer /lib/systemd/system', 'systemctl edit, qui crée une surcharge', 'Recompiler', 'Impossible'], 1, 'La surcharge survit aux mises à jour.'],
['Que signifie 30 2 * * 1-5 dans cron ?', ['2 h 30 tous les jours', '2 h 30 du lundi au vendredi', 'Toutes les 30 minutes', 'Le 2 de chaque mois'], 1, 'Minute, heure, jour, mois, jour de semaine.'],
['Quel avantage d’un timer avec Persistent=true ?', ['Plus rapide', 'Rattrape l’exécution manquée si la machine était éteinte', 'Chiffre la tâche', 'Aucun'], 1, 'Pratique pour les postes et VM.'],
['Quelle commande montre les erreurs du démarrage précédent ?', ['dmesg', 'journalctl -b -1 -p err', 'systemctl status', 'cat /var/log/boot'], 1, '-b -1 = démarrage précédent.'],
['Un processus en état Z est :', ['en exécution', 'zombie, terminé mais non récupéré par son parent', 'suspendu', 'prioritaire'], 1, 'Il ne consomme presque rien, le parent doit le récupérer.']
],
td: [
{ n: 'Échauffement', c: 'Trouver les trois processus qui consomment le plus de mémoire, puis baisser la priorité du plus gros sans l’arrêter.', r: 'ps aux --sort=-%mem | head -4 · sudo renice 10 -p <PID> (et ionice -c3 -p <PID>)' },
{ n: 'Application', c: 'Écrire l’unité systemd d’une application Python qui doit tourner sous l’utilisateur resa, redémarrer en cas d’échec, être limitée à 512 Mo et démarrer après PostgreSQL.', r: 'Voir l’exemple du cours : User=resa, After=postgresql.service, Restart=on-failure, MemoryMax=512M, WantedBy=multi-user.target · systemctl daemon-reload && systemctl enable --now resa' },
{ n: 'Application', c: 'Remplacer la tâche cron « 0 3 * * * /usr/local/bin/sauvegarde » par un timer systemd qui rattrape les exécutions manquées.', r: 'sauvegarde.service (Type=oneshot, ExecStart=…) · sauvegarde.timer : [Timer] OnCalendar=*-*-* 03:00 · Persistent=true · RandomizedDelaySec=10m · [Install] WantedBy=timers.target · systemctl enable --now sauvegarde.timer' },
{ n: 'Approfondissement', c: 'Le démarrage d’un serveur prend 2 minutes. Identifier l’unité responsable.', r: 'systemd-analyze · systemd-analyze blame | head · systemd-analyze critical-chain → souvent un montage réseau ou un service qui attend network-online, à corriger (nofail, _netdev, dépendances).' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Exploitation d’un serveur Debian.', etapes: ['Observer et signaler des processus', 'Écrire et durcir une unité de service', 'Remplacer un cron par un timer', 'Lire les journaux par unité et priorité', 'Analyser le temps de démarrage'] },
plusLoin: [['Documentation systemd (man systemd.service, systemd.timer)', 'https://www.freedesktop.org/software/systemd/man/latest/'], ['Debian Administrator’s Handbook (fr)', 'https://debian-handbook.info/browse/fr-FR/stable/'], ['LPI · objectifs LPIC-1', 'https://www.lpi.org/fr/our-certifications/lpic-1-overview/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Après une modification de configuration, le service de l’application de réservation ne démarre plus et le site renvoie une erreur 502.', etapes: [
['Lire l’état', 'systemctl status resa\n● resa.service - Application de réservation\n Active: failed (Result: exit-code) since …\n Process: 2231 ExecStart=… (code=exited, status=1/FAILURE)\n→ Le processus principal s’est terminé avec le code 1.'],
['Lire le journal de l’unité', 'journalctl -u resa -n 20 --no-pager\nPermissionError: [Errno 13] Permission denied: \'/srv/resa/data/app.db\'\n→ L’application ne peut plus écrire dans son dossier de données.'],
['Chercher le changement', 'systemctl cat resa | grep -E "Protect|ReadWrite"\nProtectSystem=strict\n→ La surcharge de durcissement ajoutée hier rend tout le système en lecture seule, et ReadWritePaths a été oublié.'],
['Corriger', 'systemctl edit resa\n[Service]\nReadWritePaths=/srv/resa/data\nsystemctl restart resa && systemctl is-active resa\nactive\n→ Le durcissement est conservé, avec une exception limitée au dossier de données.'],
['Vérifier et prévenir', 'curl -sI http://127.0.0.1:8000/health | head -1 → HTTP/1.1 200 OK\n→ Procédure : toute modification d’unité est testée en préproduction, et systemd-analyze security resa mesure le niveau de durcissement.']
] },
erreurs: ['kill -9 par réflexe', 'Éditer directement les unités de /lib/systemd/system', 'Oublier daemon-reload après une modification', 'Tâches cron qui dépendent du PATH de l’utilisateur', 'Journaux sans rotation qui remplissent le disque', 'Démarrer un service sans enable et le perdre au redémarrage'],
memo: [['Commande', 'Usage'], ['ps -ef --forest', 'arborescence des processus'], ['pgrep -a nom', 'retrouver un PID'], ['kill -TERM PID · kill -KILL PID', 'arrêter proprement, puis de force'], ['renice 10 -p PID', 'baisser la priorité'], ['systemctl status|start|enable --now', 'piloter un service'], ['systemctl edit unité', 'surcharger une unité'], ['systemctl list-timers', 'planifications'], ['journalctl -u unité -p err --since today', 'erreurs d’un service'], ['journalctl -b -1', 'démarrage précédent'], ['systemd-analyze blame', 'durées de démarrage'], ['systemd-analyze security unité', 'niveau de durcissement']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer SIGTERM, SIGKILL et SIGHUP. (3 pts)', 'Écrire une unité de service durcie. (6 pts)', 'Convertir une tâche cron en timer. (4 pts)', 'Diagnostiquer un service en échec à partir de sorties fournies. (4 pts)', 'Décrire la séquence de démarrage. (3 pts)'], r: ['arrêt poli, forcé, relecture', 'User, Restart, MemoryMax, Protect…', 'service oneshot + timer Persistent', 'status puis journalctl -u', 'UEFI, GRUB, noyau, initramfs, systemd'] },
prof: { deroule: [['Ouverture', 'Tuer un serveur de base de données en SIGKILL et constater la récupération'], ['Cours', 'Processus et signaux'], ['Pratique', 'Unité systemd'], ['Cours', 'Planification et journaux'], ['Incident', 'Service en échec'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : unité à compléter. Élèves avancés : durcissement systemd, analyse du démarrage.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Arborescence des processus', 'Qui a lancé ce processus ?', 'ps -ef --forest | grep -B2 gunicorn | head -4', 'root 1 0 … /sbin/init\nresa 842 1 … gunicorn: master [app:app]\nresa 901 842 … gunicorn: worker [app:app]', 'Le maître gunicorn est lancé par systemd (PID 1) et crée ses workers.'],
['État d’un service', 'Vérification rapide.', 'systemctl status nginx --no-pager | head -5', '● nginx.service - A high performance web server\n Loaded: loaded (/lib/systemd/system/nginx.service; enabled)\n Active: active (running) since Thu 2026-10-01 07:12:03 CEST\n Main PID: 612 (nginx)', 'Actif et activé au démarrage.'],
['Erreurs du jour', 'Point du matin.', 'journalctl -p err --since today --no-pager | tail -3', 'oct. 01 03:00:12 srv sauvegarde.sh[4410]: restic: repository is already locked\noct. 01 06:41:55 srv kernel: EXT4-fs warning (device sdb1): …', 'Une sauvegarde bloquée par un verrou restant et un avertissement disque à surveiller.'],
['Prochaines exécutions', 'Contrôle des planifications.', 'systemctl list-timers --no-pager | head -4', 'NEXT LEFT UNIT\nThu 2026-10-01 03:00:00 CEST 7h left sauvegarde.timer\nThu 2026-10-01 06:34:00 CEST 10h left apt-daily-upgrade.timer', 'La sauvegarde et les mises à jour automatiques sont planifiées.'],
['Temps de démarrage', 'Serveur long à démarrer.', 'systemd-analyze blame | head -3', '1min 30.112s systemd-networkd-wait-online.service\n 4.211s postgresql@16-main.service', 'L’attente du réseau dure 90 secondes : une interface configurée mais non câblée. On la marque comme facultative.'],
['Niveau de durcissement', 'Audit d’un service.', 'systemd-analyze security resa | tail -1', '→ Overall exposure level for resa.service: 3.9 OK', 'Le score est passé de 9,6 (très exposé) à 3,9 après ajout des options de protection.']
],
termes: ['processus', 'PID', 'PPID', 'SIGTERM', 'SIGKILL', 'SIGHUP', 'nice', 'renice', 'ionice', 'systemd', 'unité', 'systemctl', 'enable', 'daemon-reload', 'target', 'timer', 'cron', 'crontab', 'journalctl', 'journald', 'rsyslog', 'logrotate', 'UEFI', 'GRUB', 'initramfs', 'rescue.target', 'systemd-analyze']
});
reg({
cours: { id: 'disques', match: /Disques|partitions|fstab|LVM|RAID|mdadm|systèmes de fichiers|disque plein/i,
titre: 'Stockage : partitions, systèmes de fichiers, LVM et RAID logiciel', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR · LPIC-1',
prerequis: ['Le terminal Linux', 'Processus et systemd'],
competences: ['BTS SIO SISR · Bloc 2 · Administrer le stockage', 'TSSR · Exploiter des serveurs', 'LPIC-1 · Topic 104'],
intro: '« Disque plein » est l’une des causes les plus fréquentes de panne en production : la base de données s’arrête, les journaux ne s’écrivent plus, les utilisateurs ne peuvent plus enregistrer. Et un disque qui meurt sans redondance, c’est un serveur à reconstruire. Ce module présente la chaîne complète du stockage sous Linux : disques et partitions, systèmes de fichiers et montage, LVM pour agrandir à chaud, RAID logiciel pour survivre à une panne, et la méthode pour libérer de l’espace en urgence.',
cm: [
{ t: 'Disques et partitions', d: '', html: 'Linux nomme les disques <code>/dev/sda</code>, <code>/dev/sdb</code> (SATA, SCSI, USB, disques virtuels) ou <code>/dev/nvme0n1</code> (NVMe) ; les partitions ajoutent un numéro (<code>sda1</code>, <code>nvme0n1p1</code>). La table de partitions <b>GPT</b> remplace l’ancienne table MBR : plus de quatre partitions primaires, disques de plus de 2 To, et une partition système EFI pour démarrer en UEFI. <code>lsblk</code> affiche l’arborescence disques, partitions et points de montage ; <code>fdisk</code>, <code>gdisk</code> ou <code>parted</code> modifient les partitions.' },
{ t: 'Systèmes de fichiers et montage', d: '', html: 'Un <b>système de fichiers</b> organise les données sur une partition : <b>ext4</b>, robuste et universel ; <b>XFS</b>, performant sur de gros volumes, agrandissable mais non réductible ; <b>Btrfs</b> et <b>ZFS</b>, avec instantanés, sommes de contrôle et compression. On le crée avec <code>mkfs.ext4</code>, on le <b>monte</b> dans l’arborescence avec <code>mount</code>, et on rend le montage permanent dans <code>/etc/fstab</code>, en désignant la partition par son <b>UUID</b> (stable) plutôt que par /dev/sdb1 (qui peut changer).', exemple: '$ sudo mkfs.ext4 -L donnees /dev/sdb1\n$ sudo blkid /dev/sdb1\n/dev/sdb1: LABEL="donnees" UUID="3f9c…" TYPE="ext4"\n# /etc/fstab\nUUID=3f9c… /srv/donnees ext4 defaults,noatime 0 2\n$ sudo mount -a && findmnt /srv/donnees', attention: 'Une erreur dans /etc/fstab peut empêcher le démarrage. On teste toujours avec mount -a (ou findmnt --verify) avant de redémarrer, et on ajoute nofail pour un disque non indispensable au démarrage.' },
{ t: 'Espace et inodes', d: '', html: '<code>df -h</code> affiche l’espace par système de fichiers, <code>du -sh</code> la taille d’un dossier. Un système de fichiers peut aussi être plein en <b>inodes</b> (une structure par fichier) alors qu’il reste de l’espace : des millions de petits fichiers de session ou de cache en sont la cause habituelle, visible avec <code>df -i</code>. Un fichier supprimé mais encore ouvert par un processus continue d’occuper l’espace jusqu’à la fermeture : <code>lsof +L1</code> les révèle. ext4 réserve 5 % de l’espace à root, ce qui permet encore d’intervenir sur un disque « plein ».' },
{ t: 'LVM : des volumes souples', d: '', html: 'Le <b>LVM</b> (Logical Volume Manager) ajoute une couche d’abstraction : des <b>volumes physiques</b> (PV, disques ou partitions) sont regroupés en un <b>groupe de volumes</b> (VG), dans lequel on découpe des <b>volumes logiques</b> (LV) qui accueillent les systèmes de fichiers. On peut agrandir un LV à chaud, ajouter un disque au VG, déplacer des données d’un disque à un autre ou prendre des <b>instantanés</b>. C’est la configuration par défaut de nombreuses installations serveur.', exemple: '$ sudo pvcreate /dev/sdc\n$ sudo vgextend vg_data /dev/sdc\n$ sudo lvextend -r -L +200G /dev/vg_data/lv_partage\n Size of logical volume vg_data/lv_partage changed from 500.00 GiB to 700.00 GiB.\n resize2fs … The filesystem on /dev/mapper/vg_data-lv_partage is now 183500800 blocks long.' },
{ t: 'RAID logiciel', d: '', html: 'Le <b>RAID</b> combine plusieurs disques pour la performance ou la tolérance aux pannes. <b>RAID 0</b> répartit les données : rapide, mais la perte d’un disque perd tout. <b>RAID 1</b> duplique : on survit à la perte d’un disque sur deux. <b>RAID 5</b> répartit avec une parité : on perd un disque de capacité et on survit à une panne, mais la reconstruction de gros disques est longue et risquée. <b>RAID 6</b> supporte deux pannes. <b>RAID 10</b> combine miroir et répartition : performant et robuste, au prix de la moitié de la capacité.<br><br>Sous Linux, <b>mdadm</b> gère le RAID logiciel ; <code>/proc/mdstat</code> en montre l’état. On surveille les disques avec <b>SMART</b> (<code>smartctl</code>) pour les remplacer avant la panne. Rappel : le RAID n’est pas une sauvegarde.', table: [['Niveau', 'Disques min.', 'Capacité utile', 'Pannes tolérées'], ['RAID 0', '2', '100 %', '0'], ['RAID 1', '2', '50 %', '1'], ['RAID 5', '3', '(n−1)/n', '1'], ['RAID 6', '4', '(n−2)/n', '2'], ['RAID 10', '4', '50 %', '1 par miroir']] },
{ t: 'Remplacer un disque en panne', d: '', html: 'Quand un disque d’un RAID 1 ou 5 tombe, l’ensemble passe en mode <b>dégradé</b> : il fonctionne, mais une seconde panne serait fatale. La procédure : identifier le disque (numéro de série avec <code>smartctl -i</code>, pour ne pas retirer le mauvais), le marquer défaillant et le retirer de l’ensemble, le remplacer physiquement, recréer la même table de partitions, l’ajouter à l’ensemble, puis suivre la reconstruction. Sur un serveur qui démarre sur ce RAID, on réinstalle aussi le chargeur d’amorçage sur le nouveau disque.' }
],
synthese: ['/dev/sdX, /dev/nvme0n1 ; GPT ; lsblk', 'ext4, XFS, Btrfs, ZFS ; fstab par UUID, tester avec mount -a', 'df -h, du -sh, df -i pour les inodes, lsof +L1 pour les fichiers supprimés ouverts', 'LVM : PV → VG → LV, lvextend -r à chaud', 'RAID 0, 1, 5, 6, 10 ; mdadm, /proc/mdstat, SMART', 'Disque en panne : identifier par numéro de série, retirer, remplacer, reconstruire'],
qcm: [
['Pourquoi utiliser l’UUID dans fstab ?', ['Plus court', 'Il reste stable même si l’ordre des disques change', 'Obligatoire', 'Plus rapide'], 1, 'sdb peut devenir sdc.'],
['df -h indique 60 % mais impossible de créer un fichier. Cause probable ?', ['Disque en panne', 'Inodes épuisés', 'Droits', 'RAID dégradé'], 1, 'df -i.'],
['Quelle commande agrandit un LV et son système de fichiers ?', ['lvextend -r', 'pvcreate', 'mkfs', 'resize2fs seul'], 0, '-r redimensionne aussi le système de fichiers.'],
['Combien de pannes tolère un RAID 5 ?', ['0', '1', '2', 'Autant que de disques'], 1, 'Une parité.'],
['Quel RAID ne tolère aucune panne ?', ['RAID 0', 'RAID 1', 'RAID 5', 'RAID 10'], 0, 'Répartition seule.'],
['Espace non libéré après suppression d’un gros journal :', ['bug', 'un processus garde le fichier ouvert', 'RAID', 'LVM'], 1, 'lsof +L1 puis redémarrer le service.']
],
td: [
{ n: 'Application', c: 'Un nouveau disque /dev/sdb doit devenir /srv/partage en ext4, monté au démarrage. Écrire toutes les commandes.', r: 'parted /dev/sdb mklabel gpt mkpart primary ext4 0% 100% · mkfs.ext4 -L partage /dev/sdb1 · mkdir /srv/partage · blkid · ligne UUID=… /srv/partage ext4 defaults 0 2 dans fstab · mount -a · findmnt /srv/partage' },
{ n: 'Application', c: 'Calculer la capacité utile de 6 disques de 4 To en RAID 5, RAID 6 et RAID 10.', r: 'RAID 5 : 20 To · RAID 6 : 16 To · RAID 10 : 12 To' },
{ n: 'Approfondissement', c: 'Le volume /srv/partage (LVM) est plein. Proposer deux solutions sans interruption de service.', r: 'Espace libre dans le VG : lvextend -r -L +100G. Sinon ajouter un disque : pvcreate, vgextend, puis lvextend -r. Dans les deux cas, chercher aussi la cause de la croissance.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'VM avec quatre disques virtuels.', etapes: ['Partitionner et formater un disque', 'Monter par UUID dans fstab', 'Créer un VG et un LV, puis l’agrandir à chaud', 'Créer un RAID 1 avec mdadm', 'Simuler une panne et reconstruire', 'Trouver ce qui remplit un disque'] },
plusLoin: [['Debian Wiki · LVM', 'https://wiki.debian.org/LVM'], ['Linux RAID wiki', 'https://raid.wiki.kernel.org/'], ['man fstab · man mdadm', 'https://man7.org/linux/man-pages/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'La base de données de l’ENT s’est arrêtée à 11 h avec « No space left on device ». Rétablir le service en urgence, puis traiter la cause.', etapes: [
['Identifier le système de fichiers plein', 'df -h | awk \'$5+0 > 90\'\n/dev/mapper/vg0-var 40G 40G 0 100% /var\n→ /var est plein.'],
['Trouver ce qui occupe l’espace', 'du -xh --max-depth=2 /var | sort -h | tail -3\n2.1G /var/lib/postgresql\n34G /var/log/ent\n→ Les journaux de l’application occupent 34 Go.'],
['Libérer sans casser', 'ls -lh /var/log/ent | tail -2\n-rw-r----- 1 ent ent 33G oct. 1 11:02 debug.log\n: > /var/log/ent/debug.log\n→ On vide le fichier au lieu de le supprimer : l’application le garde ouvert, une suppression ne libérerait rien.'],
['Redémarrer et vérifier', 'systemctl start postgresql && df -h /var\n/dev/mapper/vg0-var 40G 6.8G 31G 18% /var\n→ La base redémarre.'],
['Traiter la cause', '→ Le niveau de journalisation était resté en debug après un dépannage. Retour au niveau info, règle logrotate quotidienne avec compression et 14 jours de conservation, alerte de supervision à 85 % et prédiction de remplissage. /var agrandi de 20 Go dans le VG par sécurité.']
] },
erreurs: ['Utiliser /dev/sdb1 dans fstab', 'Redémarrer sans tester fstab', 'Supprimer un journal ouvert au lieu de le vider', 'Retirer le mauvais disque d’un RAID', 'Considérer le RAID comme une sauvegarde', 'Oublier la supervision SMART'],
memo: [['Commande', 'Usage'], ['lsblk -f', 'disques, partitions, systèmes de fichiers'], ['blkid', 'UUID et types'], ['df -h · df -i', 'espace et inodes'], ['du -xh --max-depth=1 / | sort -h', 'trouver les gros dossiers'], ['lsof +L1', 'fichiers supprimés encore ouverts'], ['mount -a · findmnt --verify', 'tester fstab'], ['pvs · vgs · lvs', 'état LVM'], ['lvextend -r -L +50G …', 'agrandir à chaud'], ['cat /proc/mdstat · mdadm --detail /dev/md0', 'état du RAID'], ['smartctl -a /dev/sda', 'santé d’un disque']],
examen: { titre: 'Sujet type · 20 points', q: ['Préparer et monter un nouveau disque. (5 pts)', 'Expliquer LVM et agrandir un volume. (4 pts)', 'Comparer les niveaux de RAID et calculer des capacités. (5 pts)', 'Procédure de remplacement d’un disque en panne. (3 pts)', 'Libérer de l’espace sur un disque plein. (3 pts)'], r: ['parted, mkfs, fstab UUID', 'PV/VG/LV, lvextend -r', 'tableau du cours', 'identifier, fail, remove, add', 'du, lsof, vider plutôt que supprimer'] },
prof: { deroule: [['Ouverture', 'Remplir un disque de VM et observer la base qui s’arrête'], ['Cours', 'Partitions, systèmes de fichiers, fstab'], ['Pratique', 'Montage et LVM'], ['Cours', 'RAID'], ['Incident', 'Disque plein'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : commandes guidées. Élèves avancés : ZFS, instantanés LVM.', eval: 'Lab validé, sujet type.' } },
exemples: [
['Voir la structure des disques', 'Inventaire.', 'lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT', 'NAME SIZE TYPE FSTYPE MOUNTPOINT\nsda 40G disk\n├─sda1 512M part vfat /boot/efi\n└─sda2 39.5G part LVM2_member\n ├─vg0-root 15G lvm ext4 /\n └─vg0-var 20G lvm ext4 /var\nsdb 500G disk', 'Le second disque sdb n’est pas encore utilisé.'],
['Inodes épuisés', 'Impossible de créer un fichier.', 'df -i /var', 'Filesystem Inodes IUsed IFree IUse% Mounted on\n/dev/vg0/var 1310720 1310720 0 100% /var', 'Des millions de fichiers de session PHP : on les purge et on corrige leur nettoyage automatique.'],
['Espace non libéré', 'Fichier supprimé, espace toujours occupé.', 'lsof +L1 | head -2', 'COMMAND PID USER FD SIZE/OFF NLINK NAME\njava 1204 ent 5w 34359738368 0 /var/log/ent/debug.log (deleted)', 'Le processus 1204 écrit encore dans le fichier supprimé ; un redémarrage du service libère les 32 Go.'],
['État LVM', 'Reste-t-il de la place ?', 'vgs', 'VG #PV #LV VSize VFree\nvg0 1 3 39.50g 4.50g', '4,5 Go libres dans le groupe : suffisant pour un petit agrandissement.'],
['RAID dégradé', 'Alerte de supervision.', 'cat /proc/mdstat', 'md0 : active raid1 sdb1[0] sdc1[1](F)\n 976630464 blocks super 1.2 [2/1] [U_]', '[U_] : un seul disque actif, sdc est marqué défaillant (F). Remplacement urgent.'],
['Santé d’un disque', 'Maintenance préventive.', 'smartctl -A /dev/sdb | grep -E "Reallocated|Pending"', ' 5 Reallocated_Sector_Ct 0x0033 098 098 010 Pre-fail Always - 312\n197 Current_Pending_Sector 0x0012 100 100 000 Old_age Always - 16', 'Des secteurs réalloués et en attente : le disque se dégrade, on le remplace avant la panne.']
],
termes: ['partition', 'GPT', 'lsblk', 'ext4', 'XFS', 'Btrfs', 'ZFS', 'mkfs', 'mount', 'fstab', 'UUID', 'df', 'du', 'inode', 'lsof', 'LVM', 'PV', 'VG', 'LV', 'lvextend', 'RAID', 'mdadm', '/proc/mdstat', 'SMART', 'smartctl', 'dégradé']
});
reg({
cours: { id: 'sshav', match: /SSH avancé|tunnels|rebonds|sshd_config|Durcissement Linux|fail2ban|force brute|CIS|Lynis/i,
titre: 'SSH avancé et durcissement : clés, tunnels, rebonds, fail2ban', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T Cyber · TSSR / AIS',
prerequis: ['Cryptographie appliquée', 'Pare-feu'],
competences: ['BTS SIO · Bloc 3 · Sécuriser les accès', 'BUT R&T Cyber · Protéger', 'AIS · Sécuriser l’administration'],
intro: 'SSH est la porte d’entrée de tous les serveurs Linux, et donc la première cible des attaquants : un serveur exposé reçoit des milliers de tentatives de connexion par jour. Au-delà du simple `ssh utilisateur@serveur`, l’administrateur doit savoir organiser ses clés, rebondir par un bastion, créer des tunnels, durcir la configuration et bloquer la force brute. Ce module couvre ces pratiques, jusqu’à l’audit de durcissement d’un serveur.',
cm: [
{ t: 'Authentification par clé et agent', d: '', html: 'La clé <b>Ed25519</b> est le choix actuel : courte, rapide, sûre. La clé privée est protégée par une <b>phrase de passe</b> ; l’<b>agent SSH</b> la garde déverrouillée en mémoire pendant la session pour éviter de la retaper. La clé publique est déposée dans <code>~/.ssh/authorized_keys</code> sur le serveur, avec éventuellement des <b>restrictions</b> : <code>from="10.62.99.0/28"</code>, <code>command="…"</code>, <code>no-port-forwarding</code>. Lors de la première connexion, on vérifie l’<b>empreinte</b> du serveur ; un changement ultérieur (« REMOTE HOST IDENTIFICATION HAS CHANGED ») signale une réinstallation ou une attaque, jamais à ignorer.' },
{ t: 'Le fichier ~/.ssh/config', d: '', html: 'Le fichier de configuration client évite de retaper les options et documente l’accès au parc : alias, utilisateur, port, clé, rebond.', exemple: 'Host bastion\n HostName bastion.lycee-exemple.fr\n User admin\n IdentityFile ~/.ssh/id_ed25519\nHost srv-*\n User admin\n ProxyJump bastion\nHost srv-ent\n HostName 10.62.40.20\nHost srv-db\n HostName 10.62.40.31\n\n$ ssh srv-db # passe automatiquement par le bastion' },
{ t: 'Bastion et rebond', d: '', html: 'Plutôt que d’exposer chaque serveur, on n’expose qu’un <b>bastion</b> durci et journalisé. Les administrateurs s’y connectent, puis rebondissent vers les serveurs internes avec <code>ssh -J bastion srv-db</code> ou la directive <b>ProxyJump</b>. La clé privée reste sur le poste de l’administrateur : contrairement au transfert d’agent (<code>-A</code>), ProxyJump ne l’expose pas sur le bastion. Les serveurs internes n’acceptent SSH que depuis l’adresse du bastion.' },
{ t: 'Tunnels', d: '', html: 'SSH transporte d’autres flux dans sa connexion chiffrée. La <b>redirection locale</b> (<code>-L 5433:10.62.40.31:5432</code>) rend accessible sur le poste, au port 5433, la base de données interne, sans l’exposer. La <b>redirection distante</b> (<code>-R</code>) fait l’inverse : un port du serveur renvoie vers le poste. La <b>redirection dynamique</b> (<code>-D 1080</code>) crée un proxy SOCKS. Ces tunnels sont précieux pour l’administration, et dangereux s’ils permettent de contourner le pare-feu : on les désactive pour les comptes qui n’en ont pas besoin.' },
{ t: 'Durcir sshd_config', d: '', html: 'Les réglages essentiels du serveur : <code>PermitRootLogin no</code>, <code>PasswordAuthentication no</code> et <code>KbdInteractiveAuthentication no</code> (clés uniquement), <code>AllowGroups admins</code>, <code>MaxAuthTries 3</code>, <code>AllowTcpForwarding no</code> sauf besoin, <code>X11Forwarding no</code>, des algorithmes modernes, et un bandeau d’avertissement légal. On place ses réglages dans <code>/etc/ssh/sshd_config.d/</code>, on valide avec <code>sshd -t</code> et on garde une session ouverte pendant le rechargement.', exemple: '# /etc/ssh/sshd_config.d/10-durcissement.conf\nPermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nAllowGroups admins\nMaxAuthTries 3\nLoginGraceTime 30\nAllowTcpForwarding no\nX11Forwarding no\n\n$ sudo sshd -t && sudo systemctl reload ssh' },
{ t: 'fail2ban et audit de durcissement', d: '', html: '<b>fail2ban</b> lit les journaux, repère les échecs répétés d’authentification et bannit temporairement l’adresse par une règle de pare-feu. Il réduit le bruit et ralentit la force brute, sans remplacer l’authentification par clé. Pour l’ensemble du serveur, on s’appuie sur des référentiels de durcissement : guide de l’ANSSI pour GNU/Linux, benchmarks CIS, et des outils d’audit comme <b>Lynis</b>, qui produit un score et une liste de recommandations (mises à jour automatiques de sécurité, droits des fichiers sensibles, services inutiles, journalisation).' }
],
synthese: ['Clé Ed25519 avec phrase de passe et agent ; vérifier les empreintes', 'authorized_keys avec restrictions from et command', '~/.ssh/config : alias, ProxyJump', 'Bastion unique ; ProxyJump plutôt que transfert d’agent', 'Tunnels -L, -R, -D ; les désactiver sans besoin', 'sshd_config : pas de root, pas de mot de passe, AllowGroups ; sshd -t', 'fail2ban, Lynis, guides ANSSI et CIS'],
qcm: [
['Quel type de clé choisir aujourd’hui ?', ['DSA', 'RSA 1024', 'Ed25519', 'Aucune'], 2, 'Moderne et sûre.'],
['Pourquoi préférer ProxyJump au transfert d’agent ?', ['Plus rapide', 'La clé n’est pas exposée sur le bastion', 'Obligatoire', 'Pas de différence'], 1, 'Le bastion ne peut pas utiliser votre agent.'],
['Que fait ssh -L 5433:10.62.40.31:5432 bastion ?', ['Expose la base sur Internet', 'Rend la base accessible sur le port 5433 du poste local via le tunnel', 'Change le port de la base', 'Rien'], 1, 'Redirection locale.'],
['Quelle commande valide sshd_config ?', ['sshd -t', 'ssh -t', 'systemctl check ssh', 'sshd --dry'], 0, 'Avant chaque reload.'],
['Que fait fail2ban ?', ['Chiffre SSH', 'Bannit temporairement les adresses après échecs répétés', 'Remplace les clés', 'Change le port'], 1, 'Lecture des journaux.'],
['Message « REMOTE HOST IDENTIFICATION HAS CHANGED » :', ['à ignorer', 'vérifier : réinstallation ou attaque', 'mettre à jour ssh', 'redémarrer'], 1, 'Jamais ignoré sans vérification.']
],
td: [
{ n: 'Application', c: 'Écrire le ~/.ssh/config d’un technicien qui accède à trois serveurs internes par un bastion.', r: 'Host bastion (HostName, User, IdentityFile) · Host srv-* ProxyJump bastion · un bloc Host par serveur avec HostName.' },
{ n: 'Application', c: 'Accéder à l’interface web d’administration d’un switch (10.62.99.2:443) depuis chez soi sans l’exposer.', r: 'ssh -L 8443:10.62.99.2:443 bastion puis https://localhost:8443 (vérifier le certificat du switch).' },
{ n: 'Approfondissement', c: 'Durcir un serveur exposé et prouver l’amélioration.', r: 'Mesure initiale lynis audit system (score) · sshd_config durci · fail2ban · unattended-upgrades · pare-feu en liste blanche · suppression des services inutiles · nouvelle mesure Lynis et comparaison.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Bastion, deux serveurs internes et un poste.', etapes: ['Générer une clé et la déployer', 'Configurer ProxyJump', 'Créer un tunnel vers une base interne', 'Durcir sshd_config', 'Installer fail2ban et observer un bannissement', 'Mesurer avec Lynis'] },
plusLoin: [['ANSSI · Recommandations pour un usage sécurisé d’OpenSSH', 'https://cyber.gouv.fr/publications/openssh-secure-use-recommendations'], ['ANSSI · Recommandations de sécurité relatives à un système GNU/Linux', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-un-systeme-gnulinux'], ['Lynis', 'https://cisofy.com/lynis/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Après l’application d’une configuration SSH durcie, plus personne ne peut se connecter au serveur de fichiers, y compris les administrateurs.', etapes: [
['Accéder autrement', '→ Connexion par la console de l’hyperviseur (accès hors bande).'],
['Lire les journaux', 'journalctl -u ssh -n 5\nUser admin from 10.62.99.5 not allowed because none of user\'s groups are listed in AllowGroups\n→ La directive AllowGroups admins refuse les administrateurs.'],
['Vérifier les groupes', 'id admin\nuid=1000(admin) gid=1000(admin) groupes=1000(admin),27(sudo)\n→ Le groupe admins n’existe pas sur ce serveur ; les administrateurs sont dans sudo.'],
['Corriger', 'groupadd admins && usermod -aG admins admin\nsshd -t && systemctl reload ssh\n→ Connexion de test depuis une seconde session : acceptée.'],
['Prévenir', '→ Procédure : toute modification de SSH se fait avec une session ouverte et une console hors bande disponible, et se teste d’abord sur une VM de préproduction. La configuration est déployée par Ansible avec un test de connexion après application.']
] },
erreurs: ['Laisser l’authentification par mot de passe sur un serveur exposé', 'Clé privée sans phrase de passe partagée entre plusieurs personnes', 'Transfert d’agent vers un serveur non maîtrisé', 'Ignorer un changement d’empreinte', 'Recharger sshd sans session de secours', 'Exposer tous les serveurs au lieu d’un bastion'],
memo: [['Commande', 'Usage'], ['ssh-keygen -t ed25519 -C "nom"', 'créer une clé'], ['ssh-copy-id -i cle.pub hôte', 'déposer la clé'], ['ssh-add · ssh-add -l', 'agent : ajouter, lister'], ['ssh -J bastion hôte', 'rebond'], ['ssh -L port:hôte:port bastion', 'redirection locale'], ['ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub', 'empreinte du serveur'], ['sshd -t · sshd -T', 'valider · afficher la configuration effective'], ['fail2ban-client status sshd', 'adresses bannies'], ['lynis audit system', 'audit de durcissement']],
examen: { titre: 'Sujet type · 20 points', q: ['Mettre en place l’authentification par clé avec restrictions. (4 pts)', 'Écrire un ~/.ssh/config avec bastion. (4 pts)', 'Expliquer et utiliser un tunnel local. (4 pts)', 'Durcir sshd_config et justifier chaque directive. (5 pts)', 'Rôle et limites de fail2ban. (3 pts)'], r: ['ssh-keygen, authorized_keys from=', 'ProxyJump', '-L', 'root, mot de passe, groupes', 'ralentit sans remplacer les clés'] },
prof: { deroule: [['Ouverture', 'journalctl -u ssh d’un serveur exposé : les tentatives'], ['Cours', 'Clés, config, bastion'], ['Pratique', 'Rebond et tunnel'], ['Cours', 'Durcissement'], ['Incident', 'Verrouillage après durcissement'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : configurations fournies. Élèves avancés : certificats SSH, clés matérielles FIDO2.', eval: 'Lab validé, score Lynis amélioré, sujet type.' } },
exemples: [
['Tentatives sur un serveur exposé', 'Pourquoi durcir.', 'journalctl -u ssh --since today | grep -c "Invalid user"', '4127', 'Plus de 4 000 tentatives avec des noms d’utilisateurs inventés en une journée.'],
['Vérifier une empreinte', 'Première connexion.', 'ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub (sur le serveur, par la console)', '256 SHA256:q1Vt…fX8 root@srv-ent (ED25519)', 'On compare avec l’empreinte affichée par le client avant d’accepter.'],
['Configuration effective', 'Une directive est-elle appliquée ?', 'sudo sshd -T | grep -E "^(permitrootlogin|passwordauthentication|allowgroups)"', 'permitrootlogin no\npasswordauthentication no\nallowgroups admins', 'sshd -T affiche la configuration réellement retenue après lecture de tous les fichiers.'],
['Rebond par le bastion', 'Accès à un serveur interne.', 'ssh -v srv-db 2>&1 | grep -E "Connecting to|Authenticated"', 'debug1: Connecting to bastion.lycee-exemple.fr port 22.\nAuthenticated to 10.62.40.31 (via proxy) using "publickey".', 'La connexion passe par le bastion et s’authentifie par clé sur la cible.'],
['Adresses bannies', 'Contrôle de fail2ban.', 'sudo fail2ban-client status sshd', 'Status for the jail: sshd\n|- Currently failed: 3\n`- Actions\n |- Currently banned: 27\n `- Banned IP list: 185.220.101.4 45.143.220.9 …', '27 adresses bannies temporairement.'],
['Audit de durcissement', 'Avant et après.', 'sudo lynis audit system --quick | grep "Hardening index"', 'Hardening index : 58 [########### ]\n(après corrections) Hardening index : 79 [################ ]', 'Le score passe de 58 à 79 ; les recommandations restantes sont listées dans /var/log/lynis-report.dat.']
],
termes: ['SSH', 'Ed25519', 'clé publique', 'clé privée', 'phrase de passe', 'ssh-agent', 'authorized_keys', 'empreinte', '~/.ssh/config', 'bastion', 'ProxyJump', 'tunnel', '-L', '-R', '-D', 'sshd_config', 'PermitRootLogin', 'PasswordAuthentication', 'AllowGroups', 'sshd -t', 'fail2ban', 'Lynis', 'CIS']
});
reg({
cours: { id: 'messagerie', match: /Messagerie|Postfix|Dovecot|SPF|DKIM|DMARC|SMTP/i,
titre: 'Messagerie : SMTP, Postfix, Dovecot, SPF, DKIM et DMARC', duree: '',
niveau: 'BTS SIO SISR · BUT R&T · TSSR / AIS',
prerequis: ['DNS : enregistrements MX et TXT', 'Certificats TLS'],
competences: ['BTS SIO SISR · Bloc 2 · Installer un service de messagerie', 'BUT R&T · Administrer les services'],
intro: 'Le courriel reste le principal canal de communication officiel d’un établissement avec les familles, et le premier vecteur d’attaque (hameçonnage, usurpation). Envoyer un courriel qui arrive vraiment, et pas dans les indésirables, suppose de maîtriser SMTP, la réputation de l’expéditeur et les mécanismes d’authentification du domaine. Ce module décrit l’acheminement d’un message, la configuration de Postfix et Dovecot, et surtout SPF, DKIM et DMARC, désormais exigés par les grands fournisseurs de messagerie.',
cm: [
{ t: 'Le trajet d’un courriel', d: '', html: 'Le client de messagerie (MUA) remet le message à un serveur de soumission (port <b>587</b> avec authentification et STARTTLS, ou 465 en TLS direct). Le serveur d’envoi (MTA) cherche l’enregistrement <b>MX</b> du domaine destinataire et lui transmet le message en <b>SMTP</b> sur le port <b>25</b>. Le serveur de destination le dépose dans la boîte, que l’utilisateur consulte en <b>IMAP</b> (993 en TLS). Chaque serveur traversé ajoute un en-tête <code>Received:</code> : lus de bas en haut, ils retracent le parcours du message.' },
{ t: 'Postfix et Dovecot', d: '', html: '<b>Postfix</b> est le MTA de référence sous Linux : il reçoit, met en file d’attente et achemine. Sa configuration principale est <code>/etc/postfix/main.cf</code> (nom d’hôte, domaines acceptés, réseaux autorisés à relayer, TLS) et <code>master.cf</code> (services, dont la soumission sur 587). <b>Dovecot</b> fournit l’accès IMAP aux boîtes et l’authentification que Postfix utilise pour la soumission (SASL). Le point le plus critique est de ne <b>jamais être un relais ouvert</b> : seuls les utilisateurs authentifiés ou les réseaux internes peuvent envoyer vers l’extérieur.', exemple: '# /etc/postfix/main.cf (extrait)\nmyhostname = mx1.lycee-exemple.fr\nmydestination = $myhostname, localhost\nvirtual_mailbox_domains = lycee-exemple.fr\nmynetworks = 127.0.0.0/8 [::1]/128\nsmtpd_tls_cert_file = /etc/letsencrypt/live/mx1.lycee-exemple.fr/fullchain.pem\nsmtpd_tls_key_file = /etc/letsencrypt/live/mx1.lycee-exemple.fr/privkey.pem\nsmtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination' },
{ t: 'SPF', d: '', html: 'SMTP n’authentifie pas l’expéditeur : n’importe qui peut écrire « From: proviseur@lycee-exemple.fr ». <b>SPF</b> publie dans un enregistrement TXT la liste des serveurs autorisés à envoyer pour le domaine : <code>v=spf1 mx include:_spf.prestataire.fr -all</code>. Le serveur destinataire vérifie que l’adresse IP de l’émetteur y figure. <code>-all</code> demande de rejeter le reste, <code>~all</code> de le marquer suspect. SPF vérifie l’enveloppe (MAIL FROM), pas l’adresse visible par l’utilisateur, et il est cassé par les transferts automatiques.' },
{ t: 'DKIM', d: '', html: '<b>DKIM</b> signe cryptographiquement chaque message sortant : le serveur calcule une signature sur certains en-têtes et sur le corps avec une clé privée, et publie la clé publique dans le DNS sous un <b>sélecteur</b> (<code>2026._domainkey.lycee-exemple.fr</code>). Le destinataire vérifie la signature : le message vient bien d’un serveur autorisé par le domaine et n’a pas été modifié en route. Sous Linux, OpenDKIM ou rspamd signent pour Postfix.' },
{ t: 'DMARC', d: '', html: '<b>DMARC</b> relie SPF et DKIM à l’adresse visible par l’utilisateur (From) : il exige qu’au moins l’un des deux réussisse <b>et</b> soit aligné sur le domaine du From. L’enregistrement <code>_dmarc.lycee-exemple.fr</code> indique la politique à appliquer en cas d’échec (<code>p=none</code> pour observer, <code>quarantine</code>, puis <code>reject</code>) et une adresse de réception des <b>rapports</b> (rua), qui listent tous les serveurs envoyant au nom du domaine. On déploie progressivement : none, analyse des rapports pour découvrir les services oubliés (ENT, logiciel de vie scolaire, prestataire de newsletters), puis quarantine et reject. Les grands fournisseurs de messagerie exigent désormais SPF, DKIM et DMARC pour les expéditeurs en volume.', exemple: '_dmarc.lycee-exemple.fr. TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@lycee-exemple.fr; adkim=s; aspf=r"' },
{ t: 'Délivrabilité et sécurité', d: '', html: 'Au-delà de SPF, DKIM et DMARC, la délivrabilité dépend d’une <b>résolution inverse</b> (PTR) cohérente avec le nom du serveur, d’une adresse IP absente des listes de blocage, de TLS entre serveurs (MTA-STS pour l’imposer), et de volumes d’envoi réguliers. Côté réception, un filtre (rspamd, SpamAssassin) note les messages, et un antivirus analyse les pièces jointes. La sensibilisation des utilisateurs à l’hameçonnage reste indispensable : DMARC empêche l’usurpation exacte du domaine, pas les domaines ressemblants.' }
],
synthese: ['MUA → soumission 587 authentifiée → MX → SMTP 25 → IMAP 993', 'Postfix achemine, Dovecot sert les boîtes ; jamais de relais ouvert', 'SPF : serveurs autorisés (TXT) ; vérifie l’enveloppe', 'DKIM : signature, clé publique sous un sélecteur', 'DMARC : alignement sur le From, politique none → quarantine → reject, rapports', 'PTR, listes de blocage, TLS, filtrage et sensibilisation'],
qcm: [
['Sur quel port un client soumet-il un message avec authentification ?', ['25', '587', '110', '143'], 1, '25 sert entre serveurs.'],
['Qu’est-ce qu’un relais ouvert ?', ['Un serveur rapide', 'Un serveur qui accepte d’acheminer pour n’importe qui', 'Un serveur IMAP', 'Un MX secondaire'], 1, 'Exploité par les spammeurs.'],
['Que publie SPF ?', ['Une signature', 'Les serveurs autorisés à envoyer pour le domaine', 'La politique de rejet', 'Le certificat'], 1, 'Enregistrement TXT.'],
['Où se trouve la clé publique DKIM ?', ['Dans le message', 'Dans le DNS, sous sélecteur._domainkey', 'Sur le poste', 'Dans le MX'], 1, 'Le destinataire la récupère.'],
['Que vérifie DMARC en plus de SPF et DKIM ?', ['La taille', 'L’alignement avec l’adresse From visible', 'Le contenu', 'L’antivirus'], 1, 'C’est ce que voit l’utilisateur.'],
['Première étape du déploiement DMARC ?', ['p=reject', 'p=none et analyse des rapports', 'Supprimer SPF', 'Changer de MX'], 1, 'Découvrir tous les émetteurs légitimes.']
],
td: [
{ n: 'Application', c: 'Le lycée envoie depuis son serveur mx1 et via un prestataire de lettres d’information (include:_spf.newsletter.example). Écrire SPF, et les enregistrements DKIM et DMARC de départ.', r: 'TXT @ "v=spf1 mx include:_spf.newsletter.example -all" · TXT 2026._domainkey "v=DKIM1; k=ed25519; p=<clé>" (ou rsa) · TXT _dmarc "v=DMARC1; p=none; rua=mailto:dmarc@lycee-exemple.fr"' },
{ n: 'Application', c: 'Lire les en-têtes Authentication-Results d’un message reçu et dire s’il est authentique.', r: 'spf=pass, dkim=pass header.d=lycee-exemple.fr, dmarc=pass → authentique. dmarc=fail avec From du lycée → usurpation probable.' },
{ n: 'Approfondissement', c: 'Proposer le calendrier de passage de DMARC à p=reject sans bloquer d’envois légitimes.', r: 'Mois 1 : p=none, collecte des rapports · mois 2 : correction des émetteurs non alignés (ENT, vie scolaire) · mois 3 : p=quarantine; pct=25 puis 100 · mois 4 : p=reject, surveillance continue des rapports.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Postfix, Dovecot et OpenDKIM avec un DNS de lab.', etapes: ['Installer Postfix et Dovecot', 'Configurer la soumission authentifiée', 'Vérifier qu’il n’y a pas de relais ouvert', 'Publier SPF', 'Signer avec DKIM', 'Lire les en-têtes d’authentification'] },
plusLoin: [['ANSSI · Recommandations de sécurité pour la mise en œuvre d’un système de messagerie', 'https://cyber.gouv.fr/'], ['Documentation Postfix', 'https://www.postfix.org/documentation.html'], ['RFC 7489 · DMARC', 'https://www.rfc-editor.org/rfc/rfc7489'], ['Cybermalveillance.gouv.fr · hameçonnage', 'https://www.cybermalveillance.gouv.fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Les parents signalent que les convocations envoyées par le logiciel de vie scolaire arrivent dans leurs courriers indésirables, alors que les messages des enseignants arrivent normalement.', etapes: [
['Lire les en-têtes d’un message reçu', 'Authentication-Results: mx.fournisseur.example;\n spf=fail (ip 198.51.100.77 not permitted) smtp.mailfrom=lycee-exemple.fr;\n dkim=none; dmarc=fail (p=quarantine) header.from=lycee-exemple.fr\n→ Le logiciel envoie depuis les serveurs de son éditeur, non déclarés dans SPF, et sans signature DKIM.'],
['Vérifier le SPF', 'dig +short TXT lycee-exemple.fr | grep spf\n"v=spf1 mx -all"\n→ Seul le serveur du lycée est autorisé.'],
['Corriger SPF', 'Ajout de l’include fourni par l’éditeur :\n"v=spf1 mx include:_spf.editeur-vie-scolaire.example -all"\n→ Les serveurs de l’éditeur deviennent autorisés.'],
['Ajouter DKIM', '→ Activation de la signature DKIM dans l’interface de l’éditeur avec le sélecteur vs2026 ; publication de vs2026._domainkey.lycee-exemple.fr.'],
['Vérifier', 'Nouveau message test : spf=pass, dkim=pass header.d=lycee-exemple.fr, dmarc=pass\n→ Les convocations arrivent en boîte de réception. Les rapports DMARC sont désormais analysés chaque semaine pour repérer tout nouvel émetteur.']
] },
erreurs: ['Relais ouvert', 'Oublier des émetteurs légitimes dans SPF', 'Plus de 10 résolutions DNS dans SPF (erreur permerror)', 'Passer DMARC directement à reject', 'Pas de PTR cohérent', 'Ignorer les rapports DMARC'],
memo: [['Commande', 'Usage'], ['dig +short MX domaine', 'serveurs de messagerie'], ['dig +short TXT domaine', 'SPF'], ['dig +short TXT sel._domainkey.domaine', 'clé DKIM'], ['dig +short TXT _dmarc.domaine', 'politique DMARC'], ['postconf -n', 'configuration Postfix non par défaut'], ['postqueue -p', 'file d’attente'], ['postsuper -d ALL deferred', 'purger les messages en échec'], ['tail -f /var/log/mail.log', 'journal de messagerie'], ['swaks --to x --server mx1 --port 587 --auth', 'tester l’envoi'], ['openssl s_client -starttls smtp -connect mx1:25', 'tester TLS']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire le trajet d’un courriel avec les ports. (4 pts)', 'Configurer Postfix contre le relais ouvert. (4 pts)', 'Écrire SPF, DKIM et DMARC pour un cas donné. (6 pts)', 'Analyser des en-têtes Authentication-Results. (3 pts)', 'Proposer un plan de déploiement DMARC. (3 pts)'], r: ['587, MX, 25, 993', 'smtpd_relay_restrictions', 'TXT', 'pass/fail et alignement', 'none → quarantine → reject'] },
prof: { deroule: [['Ouverture', 'Envoyer un faux courriel au nom du proviseur en lab'], ['Cours', 'SMTP, Postfix, Dovecot'], ['Pratique', 'Lab messagerie'], ['Cours', 'SPF, DKIM, DMARC'], ['Incident', 'Convocations en indésirables'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : enregistrements à compléter. Élèves avancés : MTA-STS, rspamd.', eval: 'Message signé et authentifié, sujet type.' } },
exemples: [
['Serveurs de messagerie d’un domaine', 'Diagnostic de réception.', 'dig +short MX lycee-exemple.fr', '10 mx1.lycee-exemple.fr.\n20 mx2.lycee-exemple.fr.', 'mx1 est essayé en premier, mx2 en secours.'],
['Tester le relais ouvert', 'Audit.', 'swaks --to test@exemple.org --from x@exemple.org --server mx1.lycee-exemple.fr', '<** 554 5.7.1 <test@exemple.org>: Relay access denied', 'Le serveur refuse de relayer pour un inconnu : correct.'],
['Lire la file d’attente', 'Messages bloqués.', 'postqueue -p | tail -3', 'A1B2C3D4E5 2048 Thu Oct 1 08:12:03 vie-scolaire@lycee-exemple.fr\n(host mx.fournisseur.example said: 421 4.7.0 Try again later, IP temporarily deferred)', 'Le fournisseur retarde nos envois : réputation ou authentification à vérifier.'],
['Vérifier DKIM', 'Signature publiée ?', 'dig +short TXT 2026._domainkey.lycee-exemple.fr', '"v=DKIM1; k=rsa; p=MIIBIjANBgkqh…"', 'La clé publique est en place.'],
['Lire DMARC', 'Politique actuelle.', 'dig +short TXT _dmarc.lycee-exemple.fr', '"v=DMARC1; p=none; rua=mailto:dmarc@lycee-exemple.fr"', 'Mode observation : aucun message usurpé n’est encore bloqué.'],
['Suivre un envoi', 'Un parent n’a rien reçu.', 'grep "to=<parent@fournisseur.example>" /var/log/mail.log | tail -1', 'postfix/smtp[3310]: 7F2E…: to=<parent@fournisseur.example>, relay=mx.fournisseur.example[…]:25, status=sent (250 2.0.0 OK)', 'Le message a été accepté par le serveur du destinataire : il faut chercher chez lui (indésirables, filtre).']
],
termes: ['SMTP', 'MX', 'MTA', 'MUA', 'IMAP', '587', '25', '993', 'Postfix', 'Dovecot', 'relais ouvert', 'SASL', 'STARTTLS', 'SPF', 'DKIM', 'sélecteur', 'DMARC', 'alignement', 'p=reject', 'PTR', 'MTA-STS', 'rspamd', 'hameçonnage']
});
})();

View file

@ -0,0 +1,225 @@
// Cours complets · lot 9 : cybersécurité (menaces et hygiène, vulnérabilités, détection et réponse, gouvernance et droit)
(function () {
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
};
reg({
cours: { id: 'menaces', match: /Hygiène numérique|Menaces|hameçonnage|rançongiciel|ingénierie sociale|42 mesures/i,
titre: 'Menaces et hygiène numérique : hameçonnage, rançongiciels et mesures essentielles', duree: '',
niveau: 'SNT · BTS SIO / CIEL · BUT R&T · Titres pro · sensibilisation des personnels',
prerequis: ['Aucun'],
competences: ['BTS SIO · Bloc 3 · Protéger les données et les usages', 'BUT R&T Cyber · Protéger', 'PIX · Protéger et sécuriser'],
intro: 'Les attaques qui touchent les établissements scolaires et les collectivités ne commencent presque jamais par une prouesse technique : un courriel piégé, un mot de passe réutilisé, un serveur jamais mis à jour. Le rançongiciel qui chiffre tout le réseau un lundi matin est l’aboutissement de plusieurs semaines d’intrusion silencieuse. Ce module décrit les menaces réelles, le déroulé d’une attaque, et les mesures d’hygiène qui en bloquent l’essentiel, telles que les recommande l’ANSSI.',
cm: [
{ t: 'Qui attaque, pourquoi', d: '', html: 'La grande majorité des attaques est <b>opportuniste et lucrative</b> : des groupes criminels balaient Internet à la recherche de failles connues et de mots de passe faibles, puis revendent l’accès ou déploient un rançongiciel. S’y ajoutent l’<b>espionnage</b> (vol de données), la <b>déstabilisation</b> (défiguration de site, déni de service) et les actes internes (vengeance, curiosité d’un élève). Pour un établissement, la question n’est pas « qui voudrait nous attaquer ? » mais « sommes-nous plus faciles que les autres ? ».' },
{ t: 'L’hameçonnage et l’ingénierie sociale', d: '', html: 'L’<b>hameçonnage</b> (phishing) imite un expéditeur de confiance pour obtenir des identifiants ou faire ouvrir une pièce jointe piégée : faux message de l’ENT, faux colis, fausse facture. Le <b>harponnage</b> (spear phishing) cible une personne précise avec des détails réalistes. La <b>fraude au président</b> fait croire à un virement urgent demandé par la direction. Signes d’alerte : urgence, demande inhabituelle, adresse ou lien qui ne correspond pas au domaine officiel, pièce jointe inattendue. La parade repose sur la technique (filtrage, DMARC, authentification multifacteur) et sur un réflexe humain : vérifier par un autre canal.' },
{ t: 'Anatomie d’une attaque par rançongiciel', d: '', html: 'Le schéma type suit plusieurs étapes. <b>Accès initial</b> : hameçonnage, accès à distance exposé (RDP, VPN non corrigé) ou identifiants volés. <b>Installation</b> d’un outil de contrôle à distance. <b>Reconnaissance</b> et <b>élévation de privilèges</b> jusqu’aux comptes d’administration, souvent via l’annuaire. <b>Déplacement latéral</b> vers les serveurs. <b>Exfiltration</b> des données, pour faire pression par la menace de publication (double extorsion). <b>Destruction des sauvegardes</b> accessibles, puis <b>chiffrement</b> simultané de tout le parc. Chaque étape est une occasion de détecter et d’arrêter l’attaque.', attention: 'On ne paie pas la rançon : rien ne garantit la restitution, cela finance les attaquants et désigne la victime comme payeuse. On isole, on alerte (hiérarchie, CERT, dépôt de plainte, CNIL en cas de violation de données) et on restaure à partir de sauvegardes saines.' },
{ t: 'Les mesures d’hygiène essentielles', d: '', html: 'Le guide d’hygiène informatique de l’ANSSI regroupe ses mesures en thèmes. Les plus efficaces au regard des attaques réelles : <b>connaître son parc</b> (inventaire à jour) ; <b>mettre à jour</b> systèmes et logiciels, en priorité ce qui est exposé sur Internet ; <b>authentification</b> : mots de passe longs et uniques, gestionnaire de mots de passe, <b>multifacteur</b> sur la messagerie, les accès distants et l’administration ; <b>moindre privilège</b> : comptes d’administration séparés et réservés à l’administration ; <b>cloisonnement</b> du réseau et filtrage ; <b>sauvegardes</b> hors ligne testées ; <b>journalisation</b> et supervision ; <b>sensibilisation</b> des utilisateurs ; et une <b>procédure de crise</b> connue de tous.' },
{ t: 'Mots de passe et multifacteur', d: '', html: 'Un mot de passe doit être <b>long</b> (au moins 12 caractères, ou une phrase de passe de plusieurs mots), <b>unique</b> par service et stocké dans un <b>gestionnaire</b> (KeePassXC, Bitwarden). La réutilisation est le vrai danger : un mot de passe fuité sur un site est testé automatiquement sur tous les autres (bourrage d’identifiants). L’<b>authentification multifacteur</b> ajoute un second élément (application d’authentification, clé physique FIDO2) : même si le mot de passe est volé, le compte reste protégé. Les recommandations de la CNIL et de l’ANSSI privilégient désormais la longueur à la complexité imposée et au renouvellement systématique.' },
{ t: 'Réagir et signaler', d: '', html: 'Face à un incident, les premiers gestes comptent : ne pas éteindre brutalement (des traces utiles sont en mémoire), <b>déconnecter du réseau</b> la machine suspecte, prévenir immédiatement le référent, noter les heures et les observations. En France, <b>cybermalveillance.gouv.fr</b> accompagne les victimes et oriente vers des prestataires ; les établissements publics disposent aussi de leur CERT académique ou ministériel. Une violation de données personnelles doit être notifiée à la <b>CNIL</b> sous 72 heures lorsqu’elle présente un risque pour les personnes.' }
],
synthese: ['Attaques surtout opportunistes : être plus difficile que les autres', 'Hameçonnage : urgence, lien douteux ; vérifier par un autre canal', 'Rançongiciel : accès initial → privilèges → déplacement → exfiltration → destruction des sauvegardes → chiffrement', 'Hygiène : inventaire, mises à jour, MFA, moindre privilège, cloisonnement, sauvegardes hors ligne, journaux', 'Mots de passe longs, uniques, gestionnaire', 'Isoler, alerter, ne pas payer ; CNIL sous 72 h en cas de violation'],
qcm: [
['Signe typique d’hameçonnage ?', ['Message attendu', 'Urgence et lien vers un domaine inhabituel', 'Signature du proviseur', 'Pièce jointe PDF'], 1, 'L’urgence empêche de réfléchir.'],
['Quelle mesure protège un compte même si le mot de passe a fuité ?', ['Changer d’écran', 'L’authentification multifacteur', 'Un antivirus', 'Un mot de passe complexe'], 1, 'Second facteur.'],
['Pourquoi les attaquants détruisent-ils les sauvegardes avant de chiffrer ?', ['Par erreur', 'Pour empêcher la restauration et forcer le paiement', 'Pour gagner de la place', 'Ils ne le font pas'], 1, 'D’où les sauvegardes hors ligne.'],
['Premier geste face à un poste suspect ?', ['L’éteindre de force', 'Le déconnecter du réseau et prévenir', 'Formater', 'Payer'], 1, 'Isoler sans détruire les traces.'],
['Délai de notification d’une violation de données à la CNIL ?', ['24 h', '72 h', '1 mois', 'Aucun'], 1, 'RGPD, article 33.'],
['Quel est le vrai danger des mots de passe ?', ['Leur longueur', 'La réutilisation entre services', 'Les chiffres', 'Les majuscules'], 1, 'Bourrage d’identifiants.']
],
td: [
{ n: 'Application', c: 'Analyser trois courriels fournis (faux ENT, faux colis, vraie convocation) et justifier pour chacun le verdict.', r: 'Vérifier l’adresse réelle de l’expéditeur, le domaine des liens au survol, les en-têtes d’authentification, le ton d’urgence et la cohérence de la demande.' },
{ n: 'Application', c: 'Classer par ordre de priorité dix mesures d’hygiène pour un lycée qui part de zéro, en justifiant.', r: 'MFA messagerie et accès distants · mises à jour des services exposés · sauvegardes hors ligne · comptes d’administration séparés · inventaire · cloisonnement · gestionnaire de mots de passe · journalisation · sensibilisation · procédure de crise.' },
{ n: 'Approfondissement', c: 'Rédiger la fiche réflexe « rançongiciel » affichée dans la salle des professeurs.', r: 'Ne pas éteindre · débrancher le câble ou couper le Wi-Fi · appeler le référent (numéro) · noter heure et message affiché · ne pas payer · ne rien supprimer.' }
],
tp: { type: 'Quiz', url: 'apercu-quiz-projection.html', desc: 'Atelier de reconnaissance d’hameçonnage et audit d’hygiène.', etapes: ['Quiz flash sur des captures de courriels', 'Vérifier ses mots de passe sur haveibeenpwned', 'Installer un gestionnaire de mots de passe', 'Activer la MFA sur un compte de test', 'Audit d’hygiène de la salle de TP'] },
plusLoin: [['ANSSI · Guide d’hygiène informatique', 'https://cyber.gouv.fr/publications/guide-dhygiene-informatique'], ['Cybermalveillance.gouv.fr', 'https://www.cybermalveillance.gouv.fr/'], ['CNIL · Notifier une violation de données', 'https://www.cnil.fr/fr/notifier-une-violation-de-donnees-personnelles'], ['PIX · Protéger et sécuriser', 'https://pix.fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Une enseignante signale avoir saisi ses identifiants sur une page « ENT » reçue par courriel. Une heure plus tard, des messages frauduleux partent de son compte vers tous les parents de sa classe.', etapes: [
['Contenir', '→ Désactivation temporaire du compte, révocation des sessions en cours, réinitialisation du mot de passe par le support, vérification des règles de transfert ajoutées dans la messagerie (souvent créées par l’attaquant).'],
['Évaluer', '→ Journaux de connexion : connexions depuis une adresse étrangère à 10 h 12 ; 214 messages envoyés ; aucun autre service accédé. Le mot de passe était aussi utilisé pour sa messagerie personnelle : elle est prévenue.'],
['Informer', '→ Message aux parents depuis l’adresse officielle de l’établissement pour signaler le courriel frauduleux ; signalement du site d’hameçonnage ; information de la hiérarchie et du DPO, qui évalue la nécessité d’une notification à la CNIL.'],
['Bloquer la campagne', '→ Blocage du domaine d’hameçonnage au niveau du résolveur DNS et du filtre de messagerie ; recherche des autres destinataires du courriel initial et vérification de leurs comptes.'],
['Prévenir', '→ Activation de l’authentification multifacteur pour tous les personnels, campagne de sensibilisation, et rappel : l’ENT ne demande jamais de saisir ses identifiants par un lien reçu par courriel.']
] },
erreurs: ['Penser « personne ne nous visera »', 'Réutiliser un mot de passe', 'Administrer avec son compte du quotidien', 'Sauvegardes toujours connectées', 'Éteindre ou formater une machine compromise avant analyse', 'Payer la rançon', 'Ne pas signaler par honte'],
memo: [['Mesure', 'Effet'], ['MFA messagerie et accès distants', 'bloque l’usage d’identifiants volés'], ['Mises à jour des services exposés', 'ferme les failles exploitées en masse'], ['Comptes d’administration séparés', 'limite l’élévation de privilèges'], ['Sauvegardes hors ligne testées', 'permet de restaurer sans payer'], ['Cloisonnement réseau', 'limite le déplacement latéral'], ['Gestionnaire de mots de passe', 'mots de passe longs et uniques'], ['Journaux centralisés', 'détecter et comprendre'], ['Sensibilisation régulière', 'réduit le succès de l’hameçonnage'], ['Fiche réflexe et contacts', 'réaction rapide'], ['cybermalveillance.gouv.fr · CNIL', 'signalement et accompagnement']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire les étapes d’une attaque par rançongiciel. (5 pts)', 'Analyser un courriel d’hameçonnage fourni. (4 pts)', 'Proposer six mesures d’hygiène prioritaires et justifier. (6 pts)', 'Rédiger les premiers gestes face à un incident. (3 pts)', 'Obligations en cas de violation de données. (2 pts)'], r: ['accès, privilèges, latéral, exfiltration, sauvegardes, chiffrement', 'expéditeur, liens, urgence', 'MFA, mises à jour…', 'isoler, alerter, noter', 'CNIL 72 h, information des personnes'] },
prof: { deroule: [['Ouverture', 'Quiz flash : vrai ou faux courriel'], ['Cours', 'Menaces et déroulé d’attaque'], ['Cours', 'Mesures d’hygiène'], ['Pratique', 'Mots de passe et MFA'], ['Étude de cas', 'Compte d’enseignant compromis'], ['Clôture', 'Fiche réflexe rédigée par la classe']], diff: 'Tous niveaux : cas concrets. Élèves avancés : analyse d’en-têtes, matrice MITRE ATT&CK.', eval: 'Analyse de courriels, fiche réflexe, sujet type.' } },
exemples: [
['Vérifier un lien', 'Courriel « Votre ENT va expirer ».', 'Survol du lien → https://ent-lycee-exemple.fr-connexion.info/login', 'Domaine réel : fr-connexion.info', 'Le domaine officiel n’apparaît qu’en début de nom ; le vrai domaine est celui qui précède l’extension finale.'],
['Lire les en-têtes', 'Courriel suspect au nom du proviseur.', 'Authentication-Results: … dmarc=fail header.from=lycee-exemple.fr', 'Échec DMARC', 'Le message prétend venir du lycée mais n’en vient pas : usurpation. Si le domaine était ressemblant (lycee-exemp1e.fr), DMARC ne protégerait pas.'],
['Tester une fuite de mot de passe', 'Sensibilisation.', 'curl -s https://api.pwnedpasswords.com/range/$(printf "Rentree2026" | sha1sum | cut -c1-5) | grep -i $(printf "Rentree2026" | sha1sum | cut -c6-40)', 'D2C7…:142', 'Ce mot de passe apparaît 142 fois dans des fuites publiques. Seuls les 5 premiers caractères du hachage sont envoyés au service.'],
['Règles de transfert suspectes', 'Compte compromis.', 'Recherche dans la messagerie : règles de la boîte de l’enseignante', 'Règle « . » : transférer tout à zz.recup@exemple.net, marquer comme lu, supprimer', 'Règle typique créée par un attaquant pour continuer à lire les messages après le changement de mot de passe : à supprimer.'],
['Services exposés', 'Audit de surface d’attaque.', 'nmap -Pn -p 3389,22,445,443 203.0.113.0/29', '203.0.113.12 3389/tcp open ms-wbt-server', 'Un bureau à distance est exposé sur Internet : principale porte d’entrée des rançongiciels. On le ferme et on passe par un VPN avec MFA.'],
['Mises à jour en attente', 'Contrôle d’un serveur.', 'apt list --upgradable 2>/dev/null | grep -i security | wc -l', '23', '23 mises à jour de sécurité non appliquées : activer unattended-upgrades pour les correctifs de sécurité.']
],
termes: ['hameçonnage', 'harponnage', 'ingénierie sociale', 'fraude au président', 'rançongiciel', 'double extorsion', 'déplacement latéral', 'élévation de privilèges', 'exfiltration', 'MFA', 'gestionnaire de mots de passe', 'bourrage d’identifiants', 'moindre privilège', 'cloisonnement', 'ANSSI', 'CNIL', 'cybermalveillance.gouv.fr', 'CERT']
});
reg({
cours: { id: 'vulns', match: /Scan et reconnaissance|nmap|Vulnérabilités|CVE|CVSS|Élévation de privilèges|CTF/i,
titre: 'Vulnérabilités : reconnaissance avec nmap, CVE, CVSS et gestion des correctifs', duree: '',
niveau: 'BTS SIO / CIEL option cyber · BUT R&T Cyber · Master cybersécurité',
prerequis: ['TCP et UDP', 'Pare-feu', 'Terminal Linux'],
competences: ['BTS SIO · Bloc 3 · Cybersécurité des services informatiques', 'BUT R&T Cyber · Surveiller et auditer', 'Master · Évaluer la sécurité d’un système'],
intro: 'On ne protège bien que ce que l’on connaît. Un administrateur doit voir son réseau comme un attaquant le voit : quelles machines répondent, quels services sont exposés, dans quelles versions, avec quelles failles publiées. Ce module présente la reconnaissance avec nmap, l’univers des vulnérabilités (CVE, CVSS, bulletins du CERT-FR), la priorisation et le déploiement des correctifs, dans un cadre légal strict.',
cm: [
{ t: 'Le cadre légal d’abord', d: '', html: 'En France, accéder ou se maintenir frauduleusement dans un système, ou en entraver le fonctionnement, est puni par les articles 323-1 et suivants du Code pénal (loi <b>Godfrain</b>). Scanner, tester ou exploiter une faille n’est licite que sur ses propres systèmes ou avec une <b>autorisation écrite</b> précisant le périmètre et la période. En TP, on travaille exclusivement sur les machines du lab. Découvrir une faille chez un tiers se signale (divulgation responsable), sans l’exploiter.' },
{ t: 'Reconnaissance avec nmap', d: '', html: '<b>nmap</b> découvre les hôtes actifs (<code>-sn</code>), les ports ouverts, les services et leurs versions (<code>-sV</code>) et parfois le système (<code>-O</code>). Le scan SYN (<code>-sS</code>, par défaut en root) envoie un SYN et interprète la réponse : SYN-ACK = <b>ouvert</b>, RST = <b>fermé</b>, rien = <b>filtré</b>. <code>-p-</code> teste les 65 535 ports TCP, <code>-sU</code> les ports UDP (lent). Les scripts NSE (<code>--script vuln</code>, <code>ssl-enum-ciphers</code>) approfondissent. On compare régulièrement le résultat à ce qui devrait être exposé.', exemple: '$ sudo nmap -sS -sV -p- --min-rate 500 10.62.40.0/24 -oA audit-serveurs\nNmap scan report for 10.62.40.20\nPORT STATE SERVICE VERSION\n22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3\n443/tcp open ssl/http nginx 1.22.1\n5432/tcp open postgresql PostgreSQL DB 15.6' },
{ t: 'CVE, CVSS et bulletins', d: '', html: 'Chaque vulnérabilité publique reçoit un identifiant <b>CVE</b> (CVE-2024-3094, par exemple, la porte dérobée de xz). Le score <b>CVSS</b> (0 à 10) estime sa gravité selon le vecteur d’attaque (réseau ou local), la complexité, les privilèges requis, l’interaction utilisateur et l’impact. Le <b>CERT-FR</b> publie des avis et des alertes en français ; le catalogue <b>KEV</b> de la CISA liste les vulnérabilités effectivement exploitées. La priorité ne dépend pas seulement du score : une faille notée 7 exploitée activement sur un service exposé passe avant une faille notée 9 sur un service interne inaccessible.' },
{ t: 'Scanner les vulnérabilités', d: '', html: 'Des scanners comme <b>OpenVAS / Greenbone</b> comparent les versions et le comportement des services à des bases de vulnérabilités, et produisent un rapport priorisé. Pour les conteneurs et les dépendances, <b>Trivy</b> ou <b>Grype</b> analysent images et fichiers de paquets. Ces outils produisent des faux positifs (version rétroportée par Debian, par exemple) et des faux négatifs : un rapport se vérifie, il ne se transmet pas tel quel.' },
{ t: 'Gérer les correctifs', d: '', html: 'Un processus de gestion des correctifs comprend : l’<b>inventaire</b> (sans lui, on ne sait pas qui est concerné), la <b>veille</b> (abonnement aux avis CERT-FR et des éditeurs), la <b>priorisation</b> (exposition, exploitation connue, criticité du système), le <b>test</b> en préproduction, le <b>déploiement</b> planifié et la <b>vérification</b>. Sous Debian, <code>unattended-upgrades</code> applique automatiquement les correctifs de sécurité ; <code>needrestart</code> signale les services à redémarrer pour charger les bibliothèques corrigées. Quand aucun correctif n’existe, on applique des <b>mesures de contournement</b> (désactiver la fonction, filtrer l’accès).' },
{ t: 'Élévation de privilèges et CTF', d: '', html: 'Une fois un accès limité obtenu, un attaquant cherche à devenir root : binaire SUID mal choisi, règle sudo trop large, tâche cron modifiable, service tournant en root avec un fichier de configuration inscriptible, noyau non corrigé. Ces vérifications font aussi partie de l’<b>audit</b> défensif (<code>sudo -l</code>, <code>find / -perm -4000</code>, droits de /etc/cron.d). Les <b>CTF</b> (Capture The Flag), sur des plateformes comme Root-Me ou HackTheBox, entraînent légalement à ces techniques sur des machines prévues pour.' }
],
synthese: ['Tester uniquement ses systèmes ou avec autorisation écrite (loi Godfrain)', 'nmap : ouvert (SYN-ACK), fermé (RST), filtré (rien) ; -sV pour les versions', 'CVE = identifiant, CVSS = gravité, CERT-FR et KEV = exploitation réelle', 'Scanners : vérifier les faux positifs', 'Correctifs : inventaire, veille, priorisation, test, déploiement, vérification', 'Élévation de privilèges : sudo -l, SUID, cron ; CTF pour s’entraîner légalement'],
qcm: [
['Dans quel cadre peut-on scanner un réseau ?', ['Toujours', 'Sur ses systèmes ou avec autorisation écrite', 'La nuit', 'Si on ne casse rien'], 1, 'Code pénal art. 323-1.'],
['nmap indique « filtered » : cela signifie', ['port ouvert', 'aucune réponse, un pare-feu bloque probablement', 'port fermé', 'service arrêté'], 1, 'Pas de SYN-ACK ni de RST.'],
['Que représente un CVE ?', ['Un score', 'Un identifiant unique de vulnérabilité', 'Un correctif', 'Un antivirus'], 1, 'Le CVSS est le score.'],
['Quelle faille corriger en premier ?', ['CVSS 9 sur un service interne isolé', 'CVSS 7 exploitée activement sur un service exposé', 'La plus ancienne', 'Au hasard'], 1, 'Exposition et exploitation réelle.'],
['Quel outil Debian applique automatiquement les correctifs de sécurité ?', ['apt-cache', 'unattended-upgrades', 'dpkg -l', 'lynis'], 1, 'Avec needrestart.'],
['Quelle commande montre les droits sudo d’un utilisateur ?', ['sudo -l', 'id', 'whoami', 'groups'], 0, 'À vérifier en audit.']
],
td: [
{ n: 'Application', c: 'Sur le lab, découvrir les hôtes du réseau 10.99.1.0/24, lister leurs services et comparer avec la liste des services autorisés fournie.', r: 'nmap -sn 10.99.1.0/24 · nmap -sS -sV -p- hôtes · tableau écarts : service non prévu → à fermer ou justifier.' },
{ n: 'Application', c: 'Interpréter le vecteur CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.', r: 'Attaque réseau, faible complexité, sans privilège ni interaction, impact total sur confidentialité, intégrité et disponibilité : score 9,8, critique.' },
{ n: 'Approfondissement', c: 'Rédiger la procédure de gestion d’une alerte CERT-FR critique touchant le VPN de l’établissement.', r: 'Vérifier la version installée · si vulnérable : appliquer le correctif ou la mesure de contournement dans la journée · rechercher des traces de compromission antérieure (journaux) · vérifier · documenter.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Réseau de lab volontairement vulnérable.', etapes: ['Découverte des hôtes', 'Inventaire des services et versions', 'Recherche des CVE correspondantes', 'Priorisation', 'Correction et nouveau scan', 'Défi d’élévation de privilèges'] },
plusLoin: [['CERT-FR · avis et alertes', 'https://www.cert.ssi.gouv.fr/'], ['Nmap Reference Guide', 'https://nmap.org/book/man.html'], ['FIRST · CVSS', 'https://www.first.org/cvss/'], ['Root-Me', 'https://www.root-me.org/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Le CERT-FR publie une alerte sur une vulnérabilité critique activement exploitée dans le logiciel de VPN utilisé par l’établissement.', etapes: [
['Vérifier l’exposition', 'Inventaire : passerelle VPN version 7.0.12 ; l’avis concerne les versions antérieures à 7.0.14.\n→ L’établissement est vulnérable et le service est exposé sur Internet.'],
['Mesure immédiate', '→ En attendant la fenêtre de mise à jour : restriction de l’accès au portail VPN aux adresses connues des personnels itinérants et désactivation de la fonction vulnérable indiquée dans l’avis.'],
['Corriger', '→ Application du correctif 7.0.14 le soir même, après sauvegarde de la configuration. Vérification de la version et test de connexion.'],
['Chercher une compromission', '→ Analyse des journaux du VPN sur les 30 jours précédents à la recherche des indicateurs publiés dans l’avis (adresses, requêtes caractéristiques). Aucune trace trouvée.'],
['Documenter', '→ Fiche de suivi : date de l’alerte, délai de correction (9 heures), mesures, vérifications. Abonnement confirmé aux avis de l’éditeur.']
] },
erreurs: ['Scanner sans autorisation', 'Se fier au seul score CVSS', 'Transmettre un rapport de scanner sans vérification', 'Oublier de redémarrer les services après une mise à jour de bibliothèque', 'Pas d’inventaire', 'Aucune veille'],
memo: [['Commande', 'Usage'], ['nmap -sn réseau', 'hôtes actifs'], ['nmap -sS -sV -p- hôte', 'ports et versions'], ['nmap -sU --top-ports 50 hôte', 'principaux ports UDP'], ['nmap --script vuln hôte', 'scripts de vulnérabilités'], ['nmap -oA fichier', 'enregistrer les résultats'], ['apt list --upgradable', 'mises à jour disponibles'], ['unattended-upgrades --dry-run -d', 'tester les mises à jour automatiques'], ['needrestart -r l', 'services à redémarrer'], ['trivy image nom', 'analyser une image'], ['sudo -l · find / -perm -4000', 'audit de privilèges']],
examen: { titre: 'Sujet type · 20 points', q: ['Rappeler le cadre légal d’un test. (2 pts)', 'Interpréter une sortie nmap. (5 pts)', 'Interpréter un vecteur CVSS et prioriser trois failles. (6 pts)', 'Décrire un processus de gestion des correctifs. (4 pts)', 'Citer trois vérifications d’élévation de privilèges. (3 pts)'], r: ['autorisation écrite', 'ouvert, fermé, filtré, versions', 'exposition, exploitation', 'inventaire → vérification', 'sudo -l, SUID, cron'] },
prof: { deroule: [['Ouverture', 'Cadre légal et charte du lab'], ['Cours', 'nmap'], ['Pratique', 'Reconnaissance du lab'], ['Cours', 'CVE, CVSS, correctifs'], ['Pratique', 'Correction et nouveau scan'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : scans guidés. Élèves avancés : CTF Root-Me, OpenVAS.', eval: 'Rapport d’audit, sujet type.' } },
exemples: [
['Découvrir les hôtes', 'Inventaire du lab.', 'sudo nmap -sn 10.99.1.0/24', 'Nmap scan report for 10.99.1.1\nNmap scan report for 10.99.1.20\nNmap scan report for 10.99.1.33\nNmap done: 256 IP addresses (3 hosts up)', 'Trois machines actives.'],
['Interpréter les états', 'Pare-feu en place ?', 'sudo nmap -p 22,80,3306 10.99.1.20', '22/tcp open ssh\n80/tcp closed http\n3306/tcp filtered mysql', 'SSH ouvert, rien sur 80 (RST), 3306 bloqué par un pare-feu.'],
['Version d’un service', 'Recherche de failles.', 'sudo nmap -sV -p 21 10.99.1.33', '21/tcp open ftp vsftpd 2.3.4', 'Cette version historique contient une porte dérobée célèbre : machine de lab volontairement vulnérable.'],
['Correctifs de sécurité', 'Serveur Debian.', 'apt list --upgradable 2>/dev/null | grep security | head -2', 'openssl/stable-security 3.0.14-1~deb12u2 amd64 [upgradable from: 3.0.13-1~deb12u1]\nlinux-image-amd64/stable-security 6.1.106-3 amd64', 'OpenSSL et le noyau ont des correctifs en attente ; le noyau demandera un redémarrage.'],
['Services à redémarrer', 'Après mise à jour d’OpenSSL.', 'sudo needrestart -r l -b | grep NEEDRESTART-SVC', 'NEEDRESTART-SVC: nginx.service\nNEEDRESTART-SVC: postgresql@15-main.service', 'Ces services utilisent encore l’ancienne bibliothèque en mémoire.'],
['Audit sudo', 'Élévation de privilèges.', 'sudo -l -U stagiaire', 'User stagiaire may run the following commands:\n (root) NOPASSWD: /usr/bin/vim', 'vim en root permet d’ouvrir un shell root : règle à supprimer ou à remplacer par sudoedit.']
],
termes: ['loi Godfrain', 'autorisation', 'nmap', 'scan SYN', 'open', 'closed', 'filtered', '-sV', 'NSE', 'CVE', 'CVSS', 'CERT-FR', 'KEV', 'OpenVAS', 'Trivy', 'correctif', 'unattended-upgrades', 'needrestart', 'élévation de privilèges', 'SUID', 'sudo -l', 'CTF']
});
reg({
cours: { id: 'detection', match: /IDS|Suricata|SIEM|SOC|Wazuh|forensique|Réponse à incident|Observabilité|Centralisation des journaux/i,
titre: 'Détection et réponse à incident : IDS, SIEM, analyse et retour d’expérience', duree: '',
niveau: 'BTS SIO / CIEL option cyber · BUT R&T Cyber · Master · analyste SOC',
prerequis: ['Menaces et hygiène', 'Supervision', 'Analyse de trames'],
competences: ['BUT R&T Cyber · Détecter et répondre', 'Master · Piloter la réponse à incident', 'BTS SIO · Bloc 3 · Surveiller la sécurité'],
intro: 'Aucune protection n’est parfaite : un attaquant finira par entrer. La différence se joue sur le temps qu’il met à être détecté et sur la qualité de la réponse. Ce module présente la détection réseau (IDS), la corrélation des journaux (SIEM), le travail d’un SOC, puis la méthode de réponse à incident, de l’analyse des premières traces au retour d’expérience.',
cm: [
{ t: 'Détecter sur le réseau : IDS', d: '', html: 'Un <b>IDS</b> (système de détection d’intrusion) analyse le trafic et lève des alertes quand il reconnaît une <b>signature</b> d’attaque (motif connu) ou un comportement anormal. Un <b>IPS</b> va plus loin et bloque. <b>Suricata</b>, libre, s’installe sur un port miroir ou un tap et applique des jeux de règles (ET Open) ; il produit aussi des journaux détaillés de flux, de requêtes DNS, HTTP et TLS au format JSON (eve.json), précieux pour les investigations.', exemple: 'alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"Téléchargement d’un exécutable depuis un domaine récent"; flow:established,to_server; http.uri; content:".exe"; endswith; sid:1000001; rev:1;)' },
{ t: 'Corréler : SIEM', d: '', html: 'Un <b>SIEM</b> collecte les journaux de toutes les sources (pare-feu, annuaire, serveurs, VPN, IDS, postes), les normalise et applique des <b>règles de corrélation</b> : dix échecs de connexion suivis d’une réussite depuis la même adresse ; un compte qui se connecte depuis deux pays en une heure ; un compte d’administration utilisé sur un poste élève. <b>Wazuh</b> associe un SIEM libre et des agents qui surveillent l’intégrité des fichiers, les configurations et les vulnérabilités. La qualité de la détection dépend d’abord de la qualité des journaux collectés et de l’horodatage synchronisé.' },
{ t: 'Le SOC et le tri des alertes', d: '', html: 'Un <b>SOC</b> (centre opérationnel de sécurité) surveille en continu. Le niveau 1 trie les alertes : vrai positif, faux positif, à approfondir. Le niveau 2 investigue, le niveau 3 traite les incidents complexes. Le cadre <b>MITRE ATT&CK</b> décrit les techniques des attaquants et sert à classer les alertes et à évaluer la couverture de détection. Trop de faux positifs épuisent les analystes : chaque règle doit être ajustée à l’environnement.' },
{ t: 'La réponse à incident', d: '', html: 'Le déroulé classique suit six phases. <b>Préparation</b> : contacts, procédures, outils, journaux disponibles. <b>Identification</b> : confirmer l’incident, en évaluer le périmètre. <b>Confinement</b> : isoler les machines, bloquer les comptes et les flux, sans détruire les preuves. <b>Éradication</b> : supprimer l’accès de l’attaquant (comptes, portes dérobées, failles). <b>Restauration</b> : remettre en service à partir de sources saines, sous surveillance renforcée. <b>Retour d’expérience</b> : comprendre, corriger les causes, améliorer la détection. On tient un <b>journal de bord</b> horodaté de toutes les actions.' },
{ t: 'Analyse : chronologie et traces', d: '', html: 'L’analyse reconstitue une <b>chronologie</b> à partir des journaux (authentification, commandes, connexions réseau), des fichiers (dates de modification, nouveaux binaires, tâches planifiées, clés SSH ajoutées) et, si possible, de la mémoire vive. On travaille sur des <b>copies</b> et on calcule les empreintes des éléments collectés pour préserver leur valeur probante. Sur Linux : <code>last</code> et <code>journalctl</code> pour les connexions, <code>find / -newer</code> pour les fichiers récents, <code>ss -tnp</code> et <code>ps</code> pour l’activité en cours, <code>crontab -l</code> et les unités systemd pour la persistance.' },
{ t: 'Obligations et communication', d: '', html: 'Selon la nature de l’incident : notification à la CNIL sous 72 heures en cas de violation de données personnelles présentant un risque, information des personnes concernées si le risque est élevé, dépôt de plainte, signalement au CERT compétent, et pour les entités concernées par la directive <b>NIS2</b>, notification de l’incident significatif à l’ANSSI dans les délais prévus. La communication interne et externe se prépare : un message clair et factuel évite les rumeurs.' }
],
synthese: ['IDS : signatures et anomalies ; Suricata et eve.json', 'SIEM : collecter, normaliser, corréler ; Wazuh', 'SOC : tri N1, investigation N2/N3, MITRE ATT&CK', 'Réponse : préparation, identification, confinement, éradication, restauration, retour d’expérience', 'Chronologie à partir de journaux et de fichiers, sur des copies avec empreintes', 'CNIL 72 h, plainte, CERT, NIS2'],
qcm: [
['Différence entre IDS et IPS ?', ['Aucune', 'L’IPS bloque, l’IDS alerte', 'L’IDS chiffre', 'L’IPS est un antivirus'], 1, 'Prévention.'],
['Rôle d’un SIEM ?', ['Chiffrer', 'Collecter et corréler les journaux', 'Remplacer le pare-feu', 'Sauvegarder'], 1, 'Vision d’ensemble.'],
['Quelle phase vient juste après l’identification ?', ['Éradication', 'Confinement', 'Restauration', 'Retour d’expérience'], 1, 'Arrêter la propagation.'],
['Pourquoi travailler sur des copies lors de l’analyse ?', ['Plus rapide', 'Préserver les preuves', 'Obligatoire pour Linux', 'Économiser le disque'], 1, 'Valeur probante.'],
['Que décrit MITRE ATT&CK ?', ['Des pare-feux', 'Les tactiques et techniques des attaquants', 'Des lois', 'Des protocoles'], 1, 'Référentiel commun.'],
['Condition préalable à une bonne corrélation ?', ['Des écrans larges', 'Des horloges synchronisées et des journaux complets', 'Un IPS', 'Un VPN'], 1, 'NTP et collecte.']
],
td: [
{ n: 'Application', c: 'Écrire trois règles de corrélation utiles pour un établissement scolaire.', r: 'Compte d’administration utilisé hors du réseau d’administration · plus de 20 échecs puis une réussite pour un même compte en 10 min · création d’une règle de transfert externe dans une boîte de personnel.' },
{ n: 'Application', c: 'À partir d’extraits de journaux fournis, reconstituer la chronologie d’une intrusion SSH.', r: 'Échecs répétés depuis une IP · connexion réussie · ajout d’une clé dans authorized_keys · téléchargement d’un binaire · tâche cron ajoutée · connexions sortantes vers un serveur externe.' },
{ n: 'Approfondissement', c: 'Rédiger le plan de réponse d’un serveur web compromis, phase par phase.', r: 'Préparation (contacts, sauvegardes) · identification (périmètre) · confinement (isoler, bloquer les comptes, conserver une image) · éradication (réinstallation propre, correction de la faille) · restauration (données saines, surveillance) · retour d’expérience.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Suricata, Wazuh et un serveur ciblé.', etapes: ['Déployer Suricata sur un port miroir', 'Déclencher une alerte connue', 'Remonter les journaux dans Wazuh', 'Écrire une règle de corrélation', 'Mener l’analyse d’un serveur compromis simulé', 'Rédiger le rapport d’incident'] },
plusLoin: [['Documentation Suricata', 'https://docs.suricata.io/'], ['Documentation Wazuh', 'https://documentation.wazuh.com/'], ['MITRE ATT&CK', 'https://attack.mitre.org/'], ['ANSSI · Crise d’origine cyber, les clés d’une gestion opérationnelle et stratégique', 'https://cyber.gouv.fr/publications/crise-dorigine-cyber-les-cles-dune-gestion-operationnelle-et-strategique']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Wazuh signale à 22 h 40 la création d’un compte utilisateur « support2 » sur le serveur web public, en dehors de toute demande.', etapes: [
['Identifier', 'journalctl --since "22:00" | grep -E "useradd|sshd.*Accepted"\n22:31:07 sshd: Accepted password for www-admin from 45.143.220.9\n22:39:52 useradd: new user: name=support2, UID=1004\n→ Connexion par mot de passe avec un compte d’administration du site, puis création d’un compte : compromission confirmée.'],
['Confiner', '→ Blocage de l’adresse au pare-feu, verrouillage des comptes www-admin et support2, retrait du serveur de la répartition de charge, copie des journaux et image du disque avant toute modification.'],
['Analyser', 'find / -xdev -newer /tmp/repere -type f 2>/dev/null | grep -v proc\n/home/support2/.ssh/authorized_keys\n/etc/cron.d/maj-sys\n→ Persistance par clé SSH et tâche cron qui télécharge un script. Le mot de passe de www-admin était faible et l’authentification par mot de passe était encore autorisée.'],
['Éradiquer et restaurer', '→ Réinstallation du serveur à partir d’une image propre, restauration du site depuis la sauvegarde de la veille (vérifiée), authentification SSH par clé uniquement, fail2ban, rotation de tous les secrets du site.'],
['Retour d’expérience', '→ Cause racine : exception « temporaire » autorisant les mots de passe, jamais retirée. Actions : revue des exceptions, règle SIEM sur les connexions SSH par mot de passe, audit des autres serveurs. Pas de données personnelles concernées : pas de notification CNIL, décision documentée.']
] },
erreurs: ['Éteindre ou réinstaller avant de collecter les traces', 'Nettoyer au lieu de réinstaller une machine compromise', 'Oublier de changer les secrets', 'Trop de règles non ajustées', 'Pas de journal de bord', 'Pas de retour d’expérience'],
memo: [['Commande / outil', 'Usage'], ['suricata -T -c /etc/suricata/suricata.yaml', 'valider la configuration'], ['jq \'select(.event_type=="alert")\' eve.json', 'filtrer les alertes'], ['last -F · lastb', 'connexions réussies et échouées'], ['journalctl --since … | grep sshd', 'connexions SSH'], ['find / -xdev -newer repère', 'fichiers modifiés récemment'], ['ss -tnp', 'connexions en cours et processus'], ['ls -la /etc/cron.* ; systemctl list-timers', 'persistance planifiée'], ['sha256sum preuve', 'empreinte des éléments collectés'], ['Wazuh · tableau de bord', 'corrélation et alertes']],
examen: { titre: 'Sujet type · 20 points', q: ['Comparer IDS, IPS et SIEM. (3 pts)', 'Écrire trois règles de corrélation. (4 pts)', 'Décrire les six phases de la réponse à incident. (6 pts)', 'Reconstituer une chronologie à partir de journaux. (5 pts)', 'Obligations légales. (2 pts)'], r: ['alerte, blocage, corrélation', 'voir TD', 'voir cours', 'étapes horodatées', 'CNIL, plainte, NIS2'] },
prof: { deroule: [['Ouverture', 'Combien de temps pour détecter une intrusion ?'], ['Cours', 'IDS et SIEM'], ['Pratique', 'Suricata et Wazuh'], ['Cours', 'Réponse à incident'], ['Exercice de crise', 'Serveur compromis'], ['Clôture', 'Retour d’expérience collectif']], diff: 'Élèves en difficulté : chronologie guidée. Élèves avancés : règles Sigma, analyse mémoire.', eval: 'Rapport d’incident, sujet type.' } },
exemples: [
['Filtrer les alertes Suricata', 'Point du matin.', 'jq -r \'select(.event_type=="alert") | [.timestamp, .src_ip, .alert.signature] | @tsv\' /var/log/suricata/eve.json | tail -2', '2026-10-01T02:14:09 10.62.20.57 ET POLICY curl User-Agent Outbound\n2026-10-01T03:40:51 10.62.40.20 ET MALWARE Possible Cobalt Strike Beacon', 'La seconde alerte, sur un serveur, est prioritaire : investigation immédiate.'],
['Connexions échouées', 'Suspicion de force brute.', 'lastb -F | head -3', 'admin ssh:notty 45.143.220.9 Thu Oct 1 22:30:58 2026\nadmin ssh:notty 45.143.220.9 Thu Oct 1 22:30:55 2026', 'Tentatives répétées sur le compte admin depuis la même adresse.'],
['Fichiers récents', 'Analyse d’un serveur.', 'find /etc /home /var/www -xdev -newermt "2026-10-01 22:00" -type f', '/etc/cron.d/maj-sys\n/home/support2/.ssh/authorized_keys\n/var/www/html/.cache.php', 'Trois fichiers créés pendant l’intrusion : persistance cron, clé SSH et porte dérobée web.'],
['Connexions actives', 'Activité en cours.', 'ss -tnp state established | grep -v :22', 'ESTAB 0 0 10.62.40.20:48822 185.220.101.4:443 users:(("kworker-upd",pid=3107,fd=3))', 'Un processus au nom trompeur communique avec l’extérieur : isoler et collecter la mémoire du processus.'],
['Empreinte d’une preuve', 'Collecte.', 'sha256sum /mnt/preuves/srv-web.img', '8f3a…c21e /mnt/preuves/srv-web.img', 'L’empreinte, consignée dans le journal de bord, prouve que l’image n’a pas été modifiée.'],
['Tester une règle Suricata', 'Nouvelle règle.', 'suricata -T -c /etc/suricata/suricata.yaml -S local.rules', 'Configuration provided was successfully loaded. Exiting.', 'La règle se charge sans erreur ; on la valide ensuite sur une capture de test.']
],
termes: ['IDS', 'IPS', 'Suricata', 'signature', 'eve.json', 'SIEM', 'corrélation', 'Wazuh', 'SOC', 'faux positif', 'MITRE ATT&CK', 'réponse à incident', 'confinement', 'éradication', 'restauration', 'retour d’expérience', 'chronologie', 'persistance', 'journal de bord', 'NIS2']
});
reg({
cours: { id: 'gouvernance', match: /Gouvernance|ISO 27001|EBIOS|PSSI|Droit|RGPD|loi Godfrain|NIS2|Gouvernance et RGPD|licences libres|Droit du numérique/i,
titre: 'Gouvernance et droit du numérique : RGPD, PSSI, ISO 27001, EBIOS RM, NIS2', duree: '',
niveau: 'BTS SIO (CEJM) · BUT R&T / Informatique · Master · RSSI et DPO',
prerequis: ['Menaces et hygiène'],
competences: ['BTS SIO · CEJM et Bloc 3 · Protéger les données personnelles', 'Master · Piloter la sécurité', 'BUT · Respecter le cadre légal'],
intro: 'La sécurité et la protection des données ne sont pas qu’une affaire de technique : elles reposent sur des obligations légales, une organisation, des responsabilités et une analyse des risques. Un technicien qui installe un outil de suivi des élèves doit savoir quelles données il a le droit de collecter, combien de temps les garder et qui doit être informé. Ce module présente le RGPD, le cadre pénal, la politique de sécurité, les normes et méthodes de gestion des risques, et les nouvelles obligations européennes.',
cm: [
{ t: 'Le RGPD : principes', d: '', html: 'Le <b>Règlement général sur la protection des données</b> (applicable depuis mai 2018) encadre tout traitement de données personnelles. Ses principes : <b>licéité</b> (une base légale : mission d’intérêt public pour un établissement, obligation légale, consentement…), <b>finalité</b> déterminée, <b>minimisation</b> (seulement les données nécessaires), <b>exactitude</b>, <b>durée de conservation</b> limitée, <b>sécurité</b> et <b>responsabilité</b> (accountability : pouvoir démontrer sa conformité). Les mineurs bénéficient d’une protection renforcée.' },
{ t: 'Droits, registre et AIPD', d: '', html: 'Les personnes disposent de droits : information, accès, rectification, effacement, limitation, opposition, portabilité. Le responsable de traitement tient un <b>registre des traitements</b>, désigne un <b>délégué à la protection des données</b> (DPO, obligatoire pour les organismes publics), encadre ses <b>sous-traitants</b> par contrat (article 28) et réalise une <b>analyse d’impact</b> (AIPD) pour les traitements à risque élevé, par exemple le suivi à grande échelle de mineurs ou l’usage d’algorithmes d’aide à la décision. Les transferts de données hors de l’Union européenne sont strictement encadrés : l’hébergement en France ou dans l’UE simplifie la conformité.' },
{ t: 'Le cadre pénal', d: '', html: 'La <b>loi Godfrain</b> (1988), intégrée aux articles 323-1 à 323-8 du Code pénal, punit l’accès ou le maintien frauduleux dans un système, l’entrave à son fonctionnement, l’introduction ou la modification frauduleuse de données, et la détention d’outils conçus pour ces infractions sans motif légitime. Le manquement à l’obligation de sécurité des données personnelles est aussi sanctionnable. Les chartes informatiques d’établissement rappellent ces règles aux utilisateurs.' },
{ t: 'PSSI et ISO 27001', d: '', html: 'La <b>politique de sécurité des systèmes d’information</b> (PSSI) fixe les objectifs, les règles et les responsabilités de l’organisation : gestion des accès, des mots de passe, des sauvegardes, des postes nomades, des incidents. La norme <b>ISO/IEC 27001</b> décrit un <b>système de management de la sécurité</b> (SMSI) fondé sur l’amélioration continue (planifier, faire, vérifier, agir) et sur l’analyse des risques ; l’annexe A et la norme ISO 27002 proposent un catalogue de mesures. La certification est un argument fort pour un hébergeur ou un éditeur de logiciels scolaires.' },
{ t: 'EBIOS Risk Manager', d: '', html: 'La méthode <b>EBIOS RM</b>, publiée par l’ANSSI, structure l’analyse des risques en ateliers : définir le <b>cadre et le socle de sécurité</b> ; identifier les <b>sources de risque</b> et leurs objectifs ; construire des <b>scénarios stratégiques</b> (par quelles parties prenantes l’attaquant passe) puis <b>opérationnels</b> (quelles techniques) ; enfin décider du <b>traitement du risque</b> : réduire, transférer, accepter, avec des mesures et un risque résiduel assumé par la direction. Elle permet de justifier les investissements de sécurité par les risques réels.' },
{ t: 'NIS2, licences et contrats', d: '', html: 'La directive européenne <b>NIS2</b>, transposée en droit français, étend fortement le nombre d’entités soumises à des obligations de cybersécurité (entités essentielles et importantes, dont de nombreuses administrations et collectivités) : mesures de gestion des risques, notification des incidents significatifs à l’ANSSI, responsabilité de la direction. Le <b>règlement sur l’IA</b> (AI Act) et le <b>règlement sur la cyber-résilience</b> complètent ce cadre.<br><br>Côté logiciels, une <b>licence</b> fixe les droits d’usage : licences libres permissives (MIT, BSD, Apache) ou à réciprocité (GPL, AGPL, qui impose de publier les modifications d’un logiciel utilisé en service en ligne). Un contrat d’hébergement ou de service précise niveaux de service, localisation des données, réversibilité et clauses de sous-traitance RGPD.' }
],
synthese: ['RGPD : licéité, finalité, minimisation, conservation limitée, sécurité, responsabilité', 'Droits des personnes, registre, DPO, contrats de sous-traitance, AIPD', 'Loi Godfrain : accès, maintien, entrave, modification frauduleux', 'PSSI et ISO 27001 : SMSI et amélioration continue', 'EBIOS RM : sources de risque, scénarios, traitement', 'NIS2, AI Act, licences libres et contrats'],
qcm: [
['Quel principe impose de ne collecter que les données nécessaires ?', ['Licéité', 'Minimisation', 'Portabilité', 'Exactitude'], 1, 'Article 5.'],
['Quand une AIPD est-elle obligatoire ?', ['Toujours', 'Pour les traitements susceptibles d’un risque élevé', 'Jamais', 'Pour les sites web'], 1, 'Par exemple suivi de mineurs à grande échelle.'],
['Qu’impose la licence AGPL ?', ['Rien', 'Publier les modifications d’un logiciel utilisé en service en ligne', 'Payer', 'Interdire l’usage commercial'], 1, 'Réciprocité étendue au réseau.'],
['Quelle méthode française d’analyse des risques ?', ['ITIL', 'EBIOS RM', 'Scrum', 'CVSS'], 1, 'Publiée par l’ANSSI.'],
['Que décrit ISO 27001 ?', ['Un pare-feu', 'Un système de management de la sécurité', 'Une loi', 'Un protocole'], 1, 'SMSI.'],
['Qui notifie un incident significatif sous NIS2 ?', ['La CNIL', 'L’entité concernée, à l’ANSSI', 'Le fournisseur', 'Personne'], 1, 'Obligation de notification.']
],
td: [
{ n: 'Application', c: 'Remplir la fiche du registre des traitements pour la fonction de suivi des élèves de Syllabus.', r: 'Finalité : suivi pédagogique · base légale : mission d’intérêt public · personnes : élèves, enseignants · données : identité, résultats, activité dans les labs · destinataires : équipe pédagogique, vie scolaire · durée : année scolaire + 1 an · hébergement : France · mesures : chiffrement, contrôle d’accès, journalisation.' },
{ n: 'Application', c: 'Identifier trois données dont la collecte serait excessive pour ce traitement et justifier.', r: 'Adresse postale des familles, données de santé, géolocalisation : non nécessaires au suivi pédagogique (minimisation).' },
{ n: 'Approfondissement', c: 'Mener un atelier EBIOS RM simplifié sur le risque « fuite des notes des élèves ».', r: 'Source : élève curieux ou groupe criminel · objectif : revente ou divulgation · scénario : compte enseignant hameçonné → accès aux exports · mesures : MFA, journalisation des exports, alertes, sensibilisation · risque résiduel accepté par la direction.' }
],
tp: { type: 'Étude de cas', url: 'apercu-legal.html', desc: 'Mise en conformité d’un nouvel outil numérique.', etapes: ['Lire les mentions légales et la politique de confidentialité', 'Remplir la fiche du registre', 'Décider si une AIPD est nécessaire', 'Vérifier le contrat de sous-traitance', 'Rédiger l’information aux familles'] },
plusLoin: [['CNIL · Guide RGPD pour les établissements d’enseignement', 'https://www.cnil.fr/fr/education'], ['ANSSI · Méthode EBIOS Risk Manager', 'https://cyber.gouv.fr/la-methode-ebios-risk-manager'], ['ANSSI · NIS2', 'https://cyber.gouv.fr/la-directive-nis-2'], ['Légifrance · Code pénal, art. 323-1', 'https://www.legifrance.gouv.fr/']] },
plus: { cas: { titre: '', texte: '' },
resolu: { enonce: 'Un enseignant souhaite utiliser en classe une application gratuite de quiz, hébergée hors de l’Union européenne, qui demande nom, prénom et adresse électronique des élèves.', etapes: [
['Qualifier le traitement', '→ Données personnelles de mineurs, finalité pédagogique, responsable de traitement : le chef d’établissement. Le DPO académique doit être consulté.'],
['Analyser l’outil', '→ Politique de confidentialité : données utilisées pour la publicité, hébergement aux États-Unis, pas de contrat de sous-traitance proposé.'],
['Conclure', '→ L’outil ne peut pas être utilisé en l’état : pas de base contractuelle, finalité publicitaire incompatible, transfert hors UE non encadré.'],
['Proposer une alternative', '→ Utilisation d’un outil référencé par l’académie ou hébergé en France, avec comptes pseudonymes et sans adresse électronique des élèves.'],
['Documenter', '→ Inscription au registre de l’outil retenu, information des familles, et note interne rappelant la procédure de validation des outils numériques.']
] },
erreurs: ['Collecter « au cas où »', 'Pas de durée de conservation', 'Utiliser un outil sans contrat de sous-traitance', 'Oublier le DPO', 'Confondre anonymisation et pseudonymisation', 'Ignorer la licence d’un logiciel réutilisé'],
memo: [['Notion', 'À retenir'], ['Données personnelles', 'toute information sur une personne identifiable'], ['Responsable de traitement', 'qui décide des finalités et moyens'], ['Sous-traitant', 'traite pour le compte du responsable, contrat art. 28'], ['Registre', 'liste des traitements'], ['AIPD', 'analyse d’impact si risque élevé'], ['Violation de données', 'notification CNIL sous 72 h si risque'], ['Loi Godfrain', 'Code pénal 323-1 et suivants'], ['PSSI', 'règles de sécurité de l’organisation'], ['ISO 27001', 'système de management de la sécurité'], ['EBIOS RM', 'analyse des risques ANSSI'], ['NIS2', 'obligations cyber étendues, notification ANSSI']],
examen: { titre: 'Sujet type · 20 points', q: ['Énoncer les principes du RGPD et les appliquer à un cas. (6 pts)', 'Remplir une fiche de registre. (4 pts)', 'Qualifier juridiquement trois situations (accès non autorisé, outil sans contrat, fuite). (4 pts)', 'Présenter EBIOS RM. (3 pts)', 'Comparer licences MIT et GPL. (3 pts)'], r: ['voir cours', 'finalité, base, données, durée', 'Godfrain, art. 28, art. 33', 'ateliers', 'permissive / réciprocité'] },
prof: { deroule: [['Ouverture', 'Quelles données l’établissement a-t-il sur vous ?'], ['Cours', 'RGPD'], ['Étude de cas', 'Outil de quiz'], ['Cours', 'PSSI, ISO 27001, EBIOS, NIS2'], ['Clôture', 'Quiz flash']], diff: 'Tous niveaux : cas concrets. Élèves avancés : atelier EBIOS complet.', eval: 'Fiche registre, sujet type.' } },
exemples: [
['Fiche registre', 'Nouveau traitement.', 'Traitement : suivi des labs · Finalité : aide pédagogique · Base : mission d’intérêt public · Durée : année + 1 an', 'Fiche complète', 'Chaque champ se justifie ; la durée évite de conserver indéfiniment l’historique des élèves.'],
['Pseudonymiser un export', 'Statistiques pour le conseil pédagogique.', 'python3 -c "import hashlib; print(hashlib.sha256(b\'sel-secret:070123456AB\').hexdigest()[:12])"', 'a3f9c1e07b42', 'L’identifiant est remplacé par une valeur dérivée ; sans le sel, il est difficile de remonter à l’élève. Ce sont encore des données personnelles (pseudonymisées), pas anonymes.'],
['Vérifier la localisation', 'Choix d’un hébergeur.', 'dig +short app.fournisseur.example ; whois 51.15.0.10 | grep -i country', '51.15.0.10\ncountry: FR', 'Serveur situé en France. On vérifie aussi contractuellement la localisation des sauvegardes et du support.'],
['Licence d’une dépendance', 'Réutilisation d’un composant.', 'grep -m1 -i license node_modules/xterm/package.json', '"license": "MIT"', 'Licence permissive : réutilisation possible en conservant la mention de copyright.'],
['Durée de conservation', 'Purge annuelle.', 'SELECT COUNT(*) FROM activites WHERE date < now() - interval \'2 years\';', '184 233', 'Ces lignes dépassent la durée prévue au registre : elles doivent être supprimées ou anonymisées.'],
['Journal des accès', 'Démontrer la conformité.', 'SELECT utilisateur, action, date FROM journal_acces WHERE action = \'export_notes\' ORDER BY date DESC LIMIT 2;', 'm.belkacemi | export_notes | 2026-09-30 16:02\nd.roche | export_notes | 2026-09-28 11:47', 'La traçabilité des exports permet de répondre à une demande de la CNIL ou d’une famille.']
],
termes: ['RGPD', 'donnée personnelle', 'responsable de traitement', 'sous-traitant', 'base légale', 'finalité', 'minimisation', 'durée de conservation', 'registre', 'DPO', 'AIPD', 'violation de données', 'CNIL', 'loi Godfrain', 'PSSI', 'ISO 27001', 'SMSI', 'EBIOS RM', 'NIS2', 'AI Act', 'licence MIT', 'GPL', 'AGPL', 'pseudonymisation']
});
})();

View file

@ -0,0 +1,27 @@
// Approfondissements premium (partie 1) : fusionnés par identifiant dans le cours
window.CONTENUS_PLUS = Object.assign(window.CONTENUS_PLUS || {}, {
binaire: {
cm: [{ t: 'Entiers signés : le complément à deux', d: '20 min', html: 'Pour coder un entier négatif sur n bits, on inverse tous les bits de sa valeur absolue puis on ajoute 1. Le bit de poids fort indique le signe. Sur 8 bits, on code de −128 à +127.', exemple: '+5 = 0000 0101\ninverser → 1111 1010\n+1 → 1111 1011 = −5\nVérification : 1111 1011 + 0000 0101 = 1 0000 0000 (la retenue sort) → 0' , attention: 'Un compteur sur 8 bits signé qui dépasse 127 repasse à −128 : c’est un débordement, à l’origine de bugs célèbres (vol 501 d’Ariane 5).' },
{ t: 'Opérations bit à bit', d: '15 min', html: 'ET (&amp;), OU (|), OU exclusif (^) et décalages (&lt;&lt;, &gt;&gt;) travaillent bit par bit. Le ET logique entre une adresse IP et son masque donne l’adresse du réseau : c’est exactement ce que fait un routeur.', exemple: '192.168.10.65 = 11000000.10101000.00001010.01000001\n255.255.255.192 = 11111111.11111111.11111111.11000000\nET = 11000000.10101000.00001010.01000000 = 192.168.10.64' }],
cas: { titre: 'Mise en situation · Service informatique d’un lycée', texte: 'Le technicien doit vérifier qu’un nouveau poste, configuré en 10.20.37.200/22, appartient bien au réseau administratif 10.20.36.0/22. Il convertit les deux adresses en binaire, applique le masque et compare les parties réseau. Sa note d’intervention indique le calcul et la conclusion.' },
erreurs: ['Lire les restes des divisions dans le mauvais sens', 'Oublier de compléter à 8 bits avec des 0 à gauche', 'Confondre ko (1 000) et Kio (1 024)', 'Découper le binaire par 4 depuis la gauche pour l’hexadécimal'],
qcm: [['Que vaut 1111 1111 sur 8 bits en complément à deux ?', ['255', '−1', '−127', '−128'], 1, 'Inverser donne 0000 0000, +1 = 1 : c’est −1.'], ['Quel est le résultat de 1100 & 1010 ?', ['1110', '1000', '0110', '0100'], 1, 'ET bit à bit : seuls les bits à 1 des deux côtés restent.'], ['10.20.37.200/22 appartient au réseau :', ['10.20.37.0', '10.20.36.0', '10.20.32.0', '10.20.0.0'], 1, '37 = 0010 0101 ; avec 6 bits de masque sur le 3e octet → 0010 0100 = 36.']],
examen: { titre: 'Sujet type · 30 min · 10 points', q: ['Convertir 213 en binaire et en hexadécimal. (2 pts)', 'Donner le masque /21 en décimal pointé. (2 pts)', 'Calculer l’adresse réseau de 172.16.45.130/20. (3 pts)', 'Coder −20 sur 8 bits en complément à deux. (3 pts)'], r: ['1101 0101 · 0xD5', '255.255.248.0', '172.16.32.0', '+20 = 0001 0100 → 1110 1011 → 1110 1100'] },
prof: { deroule: [['0-10 min', 'Accroche : pourquoi un disque de 1 To affiche 931 Go'], ['10-40 min', 'CM parties 1 à 4, exemples au tableau'], ['40-55 min', 'QCM flash de vérification'], ['55-90 min', 'TD échauffement et application en binômes'], ['Séance 2', 'Hexadécimal, complément à deux, TP générateur']], diff: 'Élèves en difficulté : tableau des puissances de 2 affiché en permanence et exercices d’échauffement du générateur. Élèves avancés : opérations bit à bit et IEEE 754.', eval: 'Évaluation formative par le générateur (objectif 18/20 sur 20 conversions), sommative par le sujet type.' } },
terminal: {
cm: [{ t: 'Rechercher : find et locate', d: '15 min', html: '<code>find</code> parcourt l’arborescence en direct avec de nombreux critères (nom, taille, date, propriétaire). <code>locate</code> interroge une base indexée, beaucoup plus rapide mais pas toujours à jour (<code>updatedb</code>).', exemple: 'find /var/log -name "*.log" -mtime -1 # modifiés depuis 24 h\nfind /home -user alice -size +100M # gros fichiers d’alice\nfind . -name "*.tmp" -delete # nettoyage' },
{ t: 'Variables d’environnement et PATH', d: '10 min', html: '<code>echo $HOME</code>, <code>echo $USER</code>, <code>echo $PATH</code>. Le PATH liste les dossiers où le shell cherche les commandes. Une commande « introuvable » est souvent un problème de PATH.' }],
cas: { titre: 'Mise en situation · Serveur plein', texte: 'Le serveur de fichiers du lycée signale un disque plein à 98 %. Le technicien se connecte en SSH, repère les dossiers les plus lourds avec du -sh /* | sort -h, identifie des journaux non tournés dans /var/log et des fichiers temporaires, puis nettoie sans toucher aux données des utilisateurs.' },
erreurs: ['Confondre chemin absolu et relatif après un cd', 'Oublier -r pour copier un dossier', 'Lancer rm -rf avec une variable vide (rm -rf $DOSSIER/)', 'Taper une commande avec sudo par réflexe'],
qcm: [['Que fait find . -name "*.tmp" -delete ?', ['Liste les .tmp', 'Supprime les .tmp du dossier courant et des sous-dossiers', 'Crée des .tmp', 'Rien sans sudo'], 1, '-delete supprime chaque fichier trouvé.'], ['Pourquoi locate peut-il ne pas trouver un fichier créé il y a 5 minutes ?', ['Il est cassé', 'Sa base n’est pas encore mise à jour', 'Il ne cherche que dans /etc', 'Il faut être root'], 1, 'updatedb tourne en général une fois par jour.'], ['Quelle variable contient la liste des dossiers de commandes ?', ['$HOME', '$PATH', '$SHELL', '$PWD'], 1, 'PATH.']],
examen: { titre: 'Sujet type · 20 min · 10 points', q: ['Donner les commandes pour créer ~/tp/j1 et y copier /etc/hostname. (3 pts)', 'Trouver les fichiers de plus de 50 Mo dans /home. (3 pts)', 'Expliquer la différence entre /etc/hosts et etc/hosts. (2 pts)', 'Citer 2 précautions avant un rm -r. (2 pts)'], r: ['mkdir -p ~/tp/j1 && cp /etc/hostname ~/tp/j1/', 'find /home -size +50M', 'Absolu depuis la racine / relatif au dossier courant', 'Vérifier avec ls le chemin exact · ne pas utiliser sudo sans raison'] },
prof: { deroule: [['0-10 min', 'Démonstration : la même tâche à la souris et en 3 commandes'], ['10-40 min', 'CM : arborescence, chemins, commandes'], ['40-110 min', 'Lab VM en autonomie, indices débloqués après 3 essais'], ['110-120 min', 'Quiz flash de sortie']], diff: 'Difficulté : fiche mémo imprimée des 15 commandes. Avancés : OverTheWire Bandit niveaux 0 à 10.', eval: 'Validation automatique des étapes du lab et QCM de lecture du CM.' } },
droits: {
cm: [{ t: 'sudo et le principe du moindre privilège', d: '15 min', html: 'On ne travaille pas en root. <code>sudo</code> donne temporairement des droits d’administration, tracés dans <code>/var/log/auth.log</code>. Le fichier <code>/etc/sudoers</code> (édité avec <code>visudo</code>) précise qui peut lancer quoi.', exemple: '# /etc/sudoers.d/techniciens\n%techs ALL=(root) /usr/bin/systemctl restart apache2' },
{ t: 'Auditer les droits d’un serveur', d: '15 min', html: 'Un audit rapide cherche : les fichiers modifiables par tous (<code>find / -perm -o+w -type f</code>), les binaires SUID, les dossiers personnels trop ouverts, et les clés SSH lisibles par d’autres.' }],
cas: { titre: 'Mise en situation · Partage pédagogique', texte: 'Les enseignants de SIO veulent un dossier commun où chacun dépose ses sujets, où les collègues peuvent modifier, mais où les élèves ne voient rien. Le technicien crée le groupe profsio, le dossier /srv/sio en 2770 avec umask 007, vérifie avec su, puis documente la procédure.' },
erreurs: ['Mettre 777 « pour que ça marche »', 'Oublier x sur un dossier', 'usermod -G sans -a qui retire les autres groupes', 'Oublier que le changement de groupe nécessite une reconnexion'],
qcm: [['Pourquoi éviter chmod 777 ?', ['C’est lent', 'Tout le monde peut modifier ou remplacer le fichier', 'Ça ne marche pas', 'Ça efface le fichier'], 1, 'N’importe quel compte, y compris un service compromis, peut écrire.'], ['Quel outil édite sudoers sans risque de le casser ?', ['nano', 'visudo', 'vim', 'sudoedit'], 1, 'visudo vérifie la syntaxe avant d’enregistrer.'], ['Après usermod -aG devs bob, bob ne voit pas le dossier. Pourquoi ?', ['Erreur de commande', 'Il doit se reconnecter', 'Il faut redémarrer', 'Il faut chmod 777'], 1, 'Les groupes sont lus à l’ouverture de session.']],
examen: { titre: 'Sujet type · 40 min · 20 points', q: ['Traduire 2750 et expliquer chaque chiffre. (4 pts)', 'Écrire les commandes pour créer le partage /srv/compta (groupe compta, 2770). (6 pts)', 'Un fichier est en 640, propriétaire alice, groupe rh. Bob (groupe rh) peut-il le modifier ? Justifier. (4 pts)', 'Proposer 3 vérifications d’audit et leurs commandes. (6 pts)'], r: ['SGID + rwx r-x ---', 'addgroup compta ; mkdir /srv/compta ; chown root:compta /srv/compta ; chmod 2770 /srv/compta', 'Non : le groupe n’a que r (4)', 'find / -perm -4000 · find / -perm -o+w -type f · ls -ld /home/*'] },
prof: { deroule: [['0-15 min', 'Scénario : fuite d’un dossier de notes mal protégé'], ['15-60 min', 'CM et démonstration en direct'], ['60-90 min', 'TD : conversions et diagnostics'], ['Séance 2', 'Lab VM complet, puis sujet type en fin de séance']], diff: 'Difficulté : tableau r/w/x × fichier/dossier imprimé. Avancés : ACL et audit ANSSI.', eval: 'Étapes du lab corrigées dans la VM, sujet type sur 20.' } }
});

View file

@ -0,0 +1,28 @@
// Approfondissements premium (partie 2)
window.CONTENUS_PLUS = Object.assign(window.CONTENUS_PLUS || {}, {
tubes: {
cm: [{ t: 'Expressions régulières avec grep -E', d: '20 min', html: 'Une expression régulière décrit un motif : <code>^</code> début de ligne, <code>$</code> fin, <code>.</code> n’importe quel caractère, <code>[0-9]</code> un chiffre, <code>+</code> une ou plusieurs fois, <code>{3}</code> exactement 3 fois.', exemple: "grep -E '^[0-9]{1,3}(\\.[0-9]{1,3}){3}' acces.log # lignes qui commencent par une IP\ngrep -Ei 'error|fail|denied' /var/log/syslog # plusieurs motifs, sans casse" },
{ t: 'Scripter la chaîne', d: '15 min', html: 'Une chaîne utile se range dans un script avec un en-tête, des variables et un code de retour, puis se planifie avec cron ou un timer systemd.', exemple: '#!/bin/bash\nLOG=/var/log/auth.log\nSEUIL=100\nNB=$(grep -c "Failed password" "$LOG")\nif [ "$NB" -gt "$SEUIL" ]; then\n echo "Alerte : $NB échecs SSH" | mail -s "SSH" admin@lycee.fr\nfi' }],
cas: { titre: 'Mise en situation · Attaque par force brute', texte: 'Un lundi matin, le serveur SSH du lycée est lent. Le technicien analyse auth.log avec une chaîne grep | awk | sort | uniq -c, identifie 3 adresses responsables de 95 % des échecs, les bloque au pare-feu, puis installe fail2ban et écrit un script d’alerte quotidien.' },
erreurs: ['Oublier sort avant uniq', 'Utiliser > au lieu de >> et écraser un rapport', 'Mettre des guillemets simples autour d’une variable ($LOG ne s’étend pas)', 'Lancer cat f | grep au lieu de grep motif f (inutile mais courant)'],
qcm: [['Que trouve grep -E "^#" fichier ?', ['Les lignes contenant #', 'Les lignes qui commencent par #', 'Les lignes vides', 'Rien'], 1, '^ ancre en début de ligne.'], ['Que fait grep -c ?', ['Colorise', 'Compte les lignes correspondantes', 'Coupe les colonnes', 'Ignore la casse'], 1, '-c = count.'], ['Dans un script, $(commande) sert à :', ['Commenter', 'Récupérer la sortie de la commande', 'Lancer en arrière-plan', 'Créer une fonction'], 1, 'Substitution de commande.']],
examen: { titre: 'Sujet type · 40 min · 20 points', q: ['Compter les utilisateurs dont le shell est /bin/bash. (4 pts)', 'Afficher les 5 IP qui génèrent le plus d’erreurs 404 dans acces.log. (6 pts)', 'Écrire un script qui alerte si plus de 50 échecs SSH. (6 pts)', 'Expliquer la différence entre > et >>, 2> et &>. (4 pts)'], r: ['grep -c "/bin/bash$" /etc/passwd', "awk '$9==404 {print $1}' acces.log | sort | uniq -c | sort -rn | head -5", 'Voir le script de la partie 7, SEUIL=50', '> écrase, >> ajoute, 2> erreurs seules, &> les deux flux'] },
prof: { deroule: [['0-10 min', 'Accroche : un vrai journal de 20 000 lignes projeté'], ['10-50 min', 'CM et construction d’une chaîne pas à pas au tableau'], ['50-120 min', 'TP IDE Bash avec tests'], ['Séance 2', 'Expressions régulières, script d’alerte, sujet type']], diff: 'Difficulté : construire la chaîne commande par commande en vérifiant chaque sortie. Avancés : awk avancé, OverTheWire.', eval: 'Tests automatiques de l’IDE et sujet type.' } },
osi: {
cm: [{ t: 'TCP et UDP en détail', d: '20 min', html: '<b>TCP</b> établit une connexion (poignée de main SYN, SYN-ACK, ACK), numérote les segments, les acquitte et les retransmet : fiable mais plus lent. <b>UDP</b> envoie sans connexion ni garantie : rapide, adapté au DNS, à la voix sur IP, au streaming.', table: [['Service', 'Protocole', 'Port'], ['HTTP / HTTPS', 'TCP', '80 / 443'], ['SSH', 'TCP', '22'], ['DNS', 'UDP (TCP si grosse réponse)', '53'], ['DHCP', 'UDP', '67 / 68'], ['SMTP', 'TCP', '25 / 587']] },
{ t: 'ARP : de l’IP à la MAC', d: '15 min', html: 'Pour envoyer une trame à 192.168.10.1, le poste doit connaître sa MAC. Il diffuse une requête ARP « qui a 192.168.10.1 ? » ; la passerelle répond. La table est visible avec <code>ip neigh</code>.', attention: 'ARP n’est pas authentifié : l’usurpation ARP (ARP spoofing) permet une attaque de l’homme du milieu sur un réseau local.' }],
cas: { titre: 'Mise en situation · « Internet ne marche plus »', texte: 'Une salle entière perd l’accès au web. Le technicien teste couche par couche : les liens sont actifs, les postes ont une IP, la passerelle répond au ping, mais la résolution de noms échoue. Il identifie un serveur DNS en panne, bascule sur le DNS secondaire et rédige un compte rendu d’incident.' },
erreurs: ['Confondre switch et routeur', 'Penser que l’adresse MAC de destination est celle du serveur distant', 'Conclure qu’une machine est éteinte parce que le ping échoue', 'Confondre port et protocole'],
qcm: [['Quels messages composent la poignée de main TCP ?', ['GET, POST, PUT', 'SYN, SYN-ACK, ACK', 'ARP, RARP', 'HELLO, BYE'], 1, 'Trois messages établissent la connexion.'], ['Quel protocole traduit une adresse IP en MAC ?', ['DNS', 'DHCP', 'ARP', 'ICMP'], 2, 'ARP, sur le réseau local.'], ['Pourquoi la voix sur IP utilise-t-elle UDP ?', ['Plus sûr', 'Une perte vaut mieux qu’un retard', 'Obligatoire', 'Plus de ports'], 1, 'Retransmettre un son en retard ne sert à rien.']],
examen: { titre: 'Sujet type · 45 min · 20 points', q: ['Compléter le tableau des 7 couches OSI avec rôle et exemple. (7 pts)', 'Décrire l’encapsulation d’une requête HTTPS. (5 pts)', 'Comparer TCP et UDP, avec deux usages chacun. (4 pts)', 'Proposer une démarche de diagnostic pour « le site ne s’affiche pas ». (4 pts)'], r: ['Voir tableau du CM', 'HTTP → TLS → TCP 443 → IP → Ethernet', 'TCP fiable (web, SSH) · UDP rapide (DNS, VoIP)', 'ip link → ip addr → ping passerelle → nslookup → curl'] },
prof: { deroule: [['0-15 min', 'Analogie de la lettre postale et de ses enveloppes'], ['15-60 min', 'CM, tableau des couches construit avec la classe'], ['60-90 min', 'TD encapsulation et diagnostic'], ['Séance 2', 'TP capture tcpdump, sujet type']], diff: 'Difficulté : moyen mnémotechnique et schéma à compléter. Avancés : analyse Wireshark d’une poignée de main TLS.', eval: 'QCM flash de début d’heure, sujet type.' } },
sql: {
cm: [{ t: 'Concevoir : du MCD au schéma relationnel', d: '25 min', html: 'Avant d’écrire des requêtes, on modélise : les <b>entités</b> (Élève, Classe, Devoir), leurs <b>attributs</b> et les <b>associations</b> avec leurs cardinalités. Une association 1,n devient une clé étrangère ; une association n,n devient une table de liaison.', exemple: 'ELEVE(id, nom, prenom, #classe_id)\nCLASSE(id, libelle, annee)\nDEVOIR(id, titre, date_rendu)\nRENDU(#eleve_id, #devoir_id, note, date_depot)' },
{ t: 'Index et performances', d: '15 min', html: 'Un <b>index</b> accélère les recherches sur une colonne, comme l’index d’un livre. On indexe les clés étrangères et les colonnes souvent filtrées. <code>EXPLAIN</code> montre comment la base exécute une requête.' },
{ t: 'Transactions', d: '15 min', html: 'Une transaction regroupe plusieurs opérations qui réussissent ou échouent ensemble (propriétés ACID). <code>BEGIN; … COMMIT;</code> valide, <code>ROLLBACK;</code> annule tout.', exemple: 'BEGIN;\nUPDATE comptes SET solde = solde - 50 WHERE id = 1;\nUPDATE comptes SET solde = solde + 50 WHERE id = 2;\nCOMMIT;' }],
cas: { titre: 'Mise en situation · Base de l’ENT', texte: 'La direction veut un tableau de bord : taux de rendu des devoirs par classe, élèves sans aucun rendu depuis 3 semaines, moyenne par matière. Le développeur modélise les tables, écrit les requêtes avec jointures et agrégats, crée les index utiles et vérifie les performances avec EXPLAIN.' },
erreurs: ['Oublier une colonne non agrégée dans le GROUP BY', 'Utiliser WHERE à la place de HAVING', 'Faire un UPDATE sans WHERE', 'Concaténer des saisies utilisateur dans une requête'],
qcm: [['Une association n,n entre Élève et Devoir devient :', ['une colonne', 'une table de liaison', 'un index', 'une vue'], 1, 'RENDU(#eleve_id, #devoir_id, …).'], ['Que garantit une transaction ?', ['La vitesse', 'Tout ou rien', 'Le chiffrement', 'Le tri'], 1, 'Atomicité : toutes les opérations ou aucune.'], ['À quoi sert EXPLAIN ?', ['Commenter', 'Montrer le plan d’exécution', 'Supprimer un index', 'Exporter'], 1, 'Pour repérer les requêtes lentes.']],
examen: { titre: 'Sujet type · 1 h · 20 points', q: ['Écrire le schéma relationnel à partir d’un énoncé donné. (5 pts)', 'Liste des élèves de SIO1 triés par moyenne décroissante. (3 pts)', 'Taux de rendu du devoir 7 par classe. (6 pts)', 'Élèves sans aucun rendu. (3 pts)', 'Sécuriser une requête de connexion. (3 pts)'], r: ['Voir partie 8', "SELECT … WHERE c.libelle = 'SIO1' ORDER BY moyenne DESC", 'COUNT(r.eleve_id) * 100.0 / COUNT(e.id) … LEFT JOIN … GROUP BY classe', 'LEFT JOIN rendus … WHERE r.eleve_id IS NULL', 'Requête préparée avec paramètres'] },
prof: { deroule: [['0-10 min', 'Accroche : « combien d’élèves n’ont pas rendu le DM ? » en une ligne'], ['10-60 min', 'CM SELECT, WHERE, ORDER BY'], ['60-120 min', 'TP IDE SQL'], ['Séance 2', 'Agrégats et jointures'], ['Séance 3', 'Modélisation, transactions, sujet type']], diff: 'Difficulté : requêtes à trous dans l’IDE. Avancés : SQLBolt, HackerRank SQL niveau Medium.', eval: 'Tests automatiques IDE, sujet type sur 20.' } }
});

View file

@ -0,0 +1,30 @@
// Approfondissements premium (partie 3)
window.CONTENUS_PLUS = Object.assign(window.CONTENUS_PLUS || {}, {
python: {
cm: [{ t: 'Gérer les erreurs : try / except', d: '15 min', html: 'Une saisie invalide ne doit pas faire planter le programme. On capture l’exception attendue et on réagit.', exemple: 'while True:\n try:\n note = float(input("Note : "))\n break\n except ValueError:\n print("Entrez un nombre, par exemple 12.5")' },
{ t: 'Lire et écrire des fichiers', d: '15 min', html: 'Le mot-clé <code>with</code> ouvre un fichier et le referme automatiquement. Le module <code>csv</code> lit un tableur exporté.', exemple: 'import csv\nwith open("notes.csv", encoding="utf-8") as f:\n for ligne in csv.DictReader(f, delimiter=";"):\n print(ligne["nom"], ligne["note"])' },
{ t: 'Bonnes pratiques professionnelles', d: '10 min', html: 'Noms explicites, fonctions courtes qui font une seule chose, docstrings, tests avec <code>assert</code> ou pytest, respect de la PEP 8, versionnement avec Git.' }],
cas: { titre: 'Mise en situation · Automatiser un bilan de notes', texte: 'Un enseignant exporte chaque trimestre ses notes en CSV. Le script Python lit le fichier, calcule moyenne, médiane et écart-type par élève, repère ceux sous 8 et génère un rapport texte prêt pour le conseil de classe. 20 minutes de travail manuel deviennent 2 secondes.' },
erreurs: ['Mélanger tabulations et espaces', 'Oublier de convertir input() en nombre', 'Modifier une liste pendant qu’on la parcourt', 'Utiliser = au lieu de == dans une condition'],
qcm: [['Quelle exception lève float("abc") ?', ['TypeError', 'ValueError', 'KeyError', 'IndexError'], 1, 'La valeur ne peut pas être convertie.'], ['Pourquoi utiliser with open(...) ?', ['C’est plus rapide', 'Le fichier est refermé automatiquement', 'Obligatoire en Python 3', 'Pour chiffrer'], 1, 'Même en cas d’erreur.'], ['Quel est le rôle d’une docstring ?', ['Accélérer', 'Documenter la fonction', 'Tester', 'Commenter une ligne'], 1, 'Elle s’affiche avec help().']],
examen: { titre: 'Sujet type · 1 h · 20 points', q: ['Écrire saisir_note() qui redemande tant que la saisie n’est pas un nombre entre 0 et 20. (5 pts)', 'Écrire moyenne(l) et ses 2 tests. (4 pts)', 'Lire notes.csv et afficher les élèves sous 8. (6 pts)', 'Trouver et corriger 3 erreurs dans un programme fourni. (5 pts)'], r: ['while True + try/except + test 0 ≤ n ≤ 20', 'return sum(l)/len(l) ; assert …', 'csv.DictReader + float(ligne["note"]) < 8', 'Selon le programme fourni'] },
prof: { deroule: [['0-10 min', 'Démonstration : un script qui fait le bilan de la classe'], ['10-50 min', 'CM variables, conditions'], ['50-120 min', 'TP IDE avec tests automatiques'], ['Séance 2', 'Boucles, fonctions'], ['Séance 3', 'Fichiers, erreurs, sujet type']], diff: 'Difficulté : exercices à trous et indices progressifs. Avancés : HackerRank Python, projet de bilan CSV.', eval: 'Tests IDE, projet noté, sujet type.' } },
web: {
cm: [{ t: 'Rendre une page dynamique avec JavaScript', d: '20 min', html: 'JavaScript s’exécute dans le navigateur et modifie la page (le DOM) en réaction aux actions de l’utilisateur.', exemple: '<button id="b">Lever la main</button>\n<script>\n document.getElementById("b").addEventListener("click", () => {\n alert("Le prof est prévenu");\n });\n</script>' },
{ t: 'Sécurité web : les bases', d: '20 min', html: 'Les failles les plus courantes (OWASP Top 10) : injection SQL, <b>XSS</b> (script injecté dans une page), contrôle d’accès défaillant, configurations par défaut. Réponses : valider les entrées, échapper les sorties, HTTPS partout, mises à jour, en-têtes de sécurité.', attention: 'Afficher tel quel un commentaire saisi par un visiteur permet d’exécuter son script chez tous les autres visiteurs : c’est une XSS.' },
{ t: 'Virtual hosts et HTTPS sous Apache', d: '15 min', html: 'Un même serveur héberge plusieurs sites grâce aux <i>virtual hosts</i>. Certbot obtient et renouvelle un certificat Let’s Encrypt.', exemple: 'sudo a2ensite intranet.conf\nsudo systemctl reload apache2\nsudo certbot --apache -d intranet.lycee.fr' }],
cas: { titre: 'Mise en situation · Intranet du BTS', texte: 'La section veut un intranet pour publier les plannings. Le technicien installe Apache sur une VM Debian, crée un virtual host intranet.lycee.fr, publie une page HTML/CSS, active HTTPS avec Let’s Encrypt, puis vérifie les journaux et teste depuis plusieurs navigateurs.' },
erreurs: ['Oublier la balise meta charset (accents cassés)', 'Confondre <title> et <h1>', 'Envoyer un mot de passe en GET', 'Modifier la configuration sans apachectl configtest'],
qcm: [['Qu’est-ce qu’une XSS ?', ['Un virus', 'L’injection d’un script dans une page vue par d’autres', 'Une panne DNS', 'Un certificat expiré'], 1, 'Cross-Site Scripting.'], ['Quelle commande vérifie la configuration d’Apache ?', ['apache2 --check', 'apachectl configtest', 'systemctl test apache2', 'a2ensite -t'], 1, 'À lancer avant chaque reload.'], ['Que fait certbot --apache ?', ['Installe Apache', 'Obtient et installe un certificat HTTPS', 'Crée un site', 'Chiffre les fichiers'], 1, 'Avec renouvellement automatique.']],
examen: { titre: 'Sujet type · 1 h · 20 points', q: ['Décrire une requête et une réponse HTTP complètes. (4 pts)', 'Écrire une page HTML valide avec titre, liste et formulaire. (6 pts)', 'Écrire le CSS d’une grille de 3 cartes. (4 pts)', 'Citer 3 mesures de sécurité pour un site. (6 pts)'], r: ['GET / Host … → 200 OK Content-Type …', 'doctype, lang, meta charset, h1, ul, form method=post', 'display: grid; grid-template-columns: repeat(3, 1fr)', 'HTTPS · échapper les sorties · requêtes préparées · mises à jour'] },
prof: { deroule: [['0-10 min', 'Ouvrir les outils de développement sur un vrai site'], ['10-50 min', 'CM HTTP et HTML'], ['50-120 min', 'Lab web : Apache + navigateur relié'], ['Séance 2', 'CSS, JavaScript, sécurité, sujet type']], diff: 'Difficulté : squelette HTML fourni. Avancés : freeCodeCamp, virtual hosts et HTTPS.', eval: 'Lab validé automatiquement, mini-site noté, sujet type.' } },
crypto: {
cm: [{ t: 'Certificats et PKI', d: '20 min', html: 'Un certificat lie une clé publique à une identité (un nom de domaine), signé par une <b>autorité de certification</b>. Le navigateur vérifie la chaîne jusqu’à une autorité racine de confiance, la date de validité et le nom.', exemple: 'openssl s_client -connect www.lycee.fr:443 -servername www.lycee.fr </dev/null \\\n | openssl x509 -noout -subject -issuer -dates' },
{ t: 'Attaques courantes', d: '15 min', html: '<b>Force brute</b> et <b>dictionnaire</b> sur les mots de passe, <b>bourrage d’identifiants</b> (réutilisation de mots de passe fuités), <b>hameçonnage</b>, <b>homme du milieu</b>. Parades : mots de passe longs et uniques, gestionnaire de mots de passe, authentification à deux facteurs.' },
{ t: 'Le cadre français', d: '10 min', html: 'L’<b>ANSSI</b> publie les recommandations techniques (tailles de clés, algorithmes). La <b>CNIL</b> encadre les mots de passe dans les services en ligne. Le <b>RGPD</b> impose de protéger les données personnelles, y compris par chiffrement.' }],
cas: { titre: 'Mise en situation · Audit des accès', texte: 'Après une tentative d’intrusion, la direction demande un audit. Le technicien vérifie que les mots de passe de l’application sont stockés en Argon2id, remplace l’accès SSH par mot de passe par des clés Ed25519, contrôle les certificats HTTPS et rédige des recommandations en citant l’ANSSI et la CNIL.' },
erreurs: ['Confondre chiffrer et hacher', 'Partager sa clé privée', 'Stocker un mot de passe en SHA-256 sans sel', 'Ignorer un avertissement de certificat'],
qcm: [['Qui signe un certificat HTTPS ?', ['Le navigateur', 'Une autorité de certification', 'Le FAI', 'La CNIL'], 1, 'L’AC atteste l’identité du domaine.'], ['Quelle parade contre le bourrage d’identifiants ?', ['Changer d’adresse IP', 'Un mot de passe unique par service et la double authentification', 'Désactiver HTTPS', 'Un antivirus'], 1, 'Un mot de passe fuité ne sert alors qu’une fois.'], ['Quel organisme publie les recommandations cryptographiques en France ?', ['CNIL', 'ANSSI', 'ARCEP', 'INRIA'], 1, 'L’ANSSI.']],
examen: { titre: 'Sujet type · 1 h · 20 points', q: ['Distinguer hachage, chiffrement symétrique et asymétrique, avec un usage chacun. (6 pts)', 'Expliquer comment stocker des mots de passe. (4 pts)', 'Décrire l’établissement d’une session HTTPS. (5 pts)', 'Proposer la configuration SSH d’un serveur exposé. (5 pts)'], r: ['intégrité · disques et sessions · échange et signature', 'Argon2id + sel unique + coût adapté', 'certificat → échange de clés → clé de session AES', 'clés Ed25519, PasswordAuthentication no, PermitRootLogin no, fail2ban'] },
prof: { deroule: [['0-15 min', 'Accroche : vérifier si son adresse a fuité (haveibeenpwned)'], ['15-60 min', 'CM hachage et mots de passe'], ['60-90 min', 'TD et démonstration openssl'], ['Séance 2', 'Asymétrique, HTTPS, lab SSH, sujet type']], diff: 'Difficulté : analogies (cadenas et clé). Avancés : Root-Me, analyse d’un certificat, attaque par dictionnaire avec hashcat sur un jeu d’essai.', eval: 'Lab SSH validé, sujet type sur 20.' } }
});

View file

@ -0,0 +1,69 @@
// Niveau 4 : parties de CM supplémentaires, exercice résolu, fiche mémo, vrai/faux
(function () {
var X = window.CONTENUS_PLUS = window.CONTENUS_PLUS || {};
var add = function (id, o) { var c = X[id] = X[id] || {}; c.cm = (c.cm || []).concat(o.cm || []); c.qcm = (c.qcm || []).concat(o.qcm || []); c.memo = o.memo; c.resolu = o.resolu; c.vf = o.vf; };
add('binaire', {
cm: [{ t: 'Nombres à virgule : IEEE 754 en bref', d: '15 min', html: 'Un <b>float</b> est stocké en trois parties : un bit de signe, un exposant et une mantisse, comme une notation scientifique en base 2. La plupart des décimaux n’ont pas d’écriture binaire finie : 0,1 en binaire est une suite infinie, arrondie.', exemple: '>>> 0.1 + 0.2\n0.30000000000000004\n>>> round(0.1 + 0.2, 2) == 0.3\nTrue', attention: 'Ne jamais comparer deux float avec == : on compare leur écart à une petite tolérance, ou on travaille en entiers (centimes plutôt qu’euros).' },
{ t: 'Le texte aussi est un nombre : ASCII et UTF-8', d: '15 min', html: 'Chaque caractère a un numéro. <b>ASCII</b> code 128 caractères sur 7 bits (« A » = 65). <b>UTF-8</b> code tous les caractères du monde sur 1 à 4 octets et reste compatible avec ASCII. Les accents cassés (« é » au lieu de « é ») viennent d’un fichier UTF-8 lu comme du Latin-1.', exemple: '>>> ord("A"), ord("é")\n(65, 233)\n>>> "é".encode("utf-8")\nb\'\\xc3\\xa9\' # 2 octets' }],
qcm: [['Pourquoi 0.1 + 0.2 ne vaut pas exactement 0.3 en Python ?', ['Bug de Python', '0,1 n’a pas d’écriture binaire finie', 'Le processeur est trop lent', 'Il faut importer math'], 1, 'La valeur est arrondie à la représentation la plus proche.'], ['Combien d’octets pour « é » en UTF-8 ?', ['1', '2', '3', '4'], 1, '0xC3 0xA9.']],
memo: [['Conversion', 'Méthode'], ['binaire → décimal', 'additionner les poids des bits à 1'], ['décimal → binaire', 'divisions par 2, restes de bas en haut'], ['binaire ↔ hexa', 'groupes de 4 bits depuis la droite'], ['négatif sur n bits', 'inverser puis +1'], ['n bits', '2ⁿ valeurs, de 0 à 2ⁿ − 1'], ['1 octet', '8 bits, 0 à 255, 0x00 à 0xFF']],
resolu: { enonce: 'Le poste 192.168.1.77/27 et le poste 192.168.1.100/27 sont-ils dans le même sous-réseau ?', etapes: [['Identifier l’octet utile', '/27 = 24 + 3 : seul le 4e octet est partagé, avec 3 bits de réseau.'], ['Écrire le masque', '3 bits à 1 sur le dernier octet : 1110 0000 = 224.'], ['Convertir les deux octets', '77 = 0100 1101 · 100 = 0110 0100'], ['Appliquer le ET', '0100 1101 & 1110 0000 = 0100 0000 = 64\n0110 0100 & 1110 0000 = 0110 0000 = 96'], ['Conclure', 'Réseaux .64 et .96 : ils ne sont pas dans le même sous-réseau. Il faut passer par un routeur.']] },
vf: [['Un octet peut valoir 256.', 0, 'Maximum 255.'], ['0xFF = 255.', 1, '15 × 16 + 15.'], ['Sur 8 bits signés, 1000 0000 vaut −128.', 1, 'C’est la plus petite valeur.'], ['1 Kio = 1 000 octets.', 0, '1 Kio = 1 024 octets.']] });
add('terminal', {
cm: [{ t: 'Lire et éditer un fichier', d: '15 min', html: '<code>cat</code> affiche un fichier court, <code>less</code> un fichier long (flèches, / pour chercher, q pour quitter), <code>head -n 20</code> et <code>tail -n 20</code> le début et la fin. <code>nano</code> est l’éditeur le plus simple : Ctrl+O enregistre, Ctrl+X quitte.', exemple: 'less /var/log/syslog\ntail -n 50 /var/log/auth.log\nnano ~/notes.txt' },
{ t: 'Processus et services', d: '20 min', html: 'Chaque programme lancé est un <b>processus</b> avec un numéro (PID). <code>ps aux</code> les liste, <code>top</code> ou <code>htop</code> les montre en direct, <code>kill PID</code> en arrête un. Les <b>services</b> (serveur web, SSH) sont gérés par systemd avec <code>systemctl</code>.', exemple: 'ps aux | grep apache2\nsystemctl status ssh\nsudo systemctl restart apache2\njournalctl -u ssh --since "1 hour ago"' },
{ t: 'Espace disque et archives', d: '10 min', html: '<code>df -h</code> montre l’occupation des disques, <code>du -sh dossier</code> la taille d’un dossier. <code>tar czf archive.tar.gz dossier/</code> crée une archive compressée, <code>tar xzf</code> l’extrait.' }],
qcm: [['Quelle commande montre les processus en temps réel ?', ['ps', 'top', 'ls', 'df'], 1, 'top (ou htop) se rafraîchit en continu.'], ['Comment redémarrer le serveur web ?', ['restart apache2', 'sudo systemctl restart apache2', 'kill apache2', 'apache2 --restart'], 1, 'systemctl gère les services.'], ['Quelle commande donne l’espace libre des disques ?', ['du -sh', 'df -h', 'free', 'ls -h'], 1, 'df = disk free.']],
memo: [['Besoin', 'Commande'], ['Où suis-je ?', 'pwd'], ['Que contient ce dossier ?', 'ls -la'], ['Aller dans un dossier', 'cd chemin'], ['Créer', 'mkdir -p · touch'], ['Copier / déplacer', 'cp -r · mv'], ['Supprimer', 'rm · rm -r'], ['Lire un fichier', 'cat · less · tail -f'], ['Chercher', 'find · grep'], ['Aide', 'man · --help'], ['Services', 'systemctl status|restart'], ['Disque', 'df -h · du -sh']],
resolu: { enonce: 'Le disque du serveur est plein. Trouver ce qui prend de la place et libérer de l’espace sans rien casser.', etapes: [['Mesurer', 'df -h → / est à 98 %.'], ['Localiser', 'sudo du -sh /* 2>/dev/null | sort -h → /var est le plus gros.'], ['Descendre', 'sudo du -sh /var/* | sort -h → /var/log fait 18 Go.'], ['Identifier', 'ls -lhS /var/log | head → un fichier debug.log de 17 Go.'], ['Agir proprement', 'sudo truncate -s 0 /var/log/debug.log puis corriger la configuration qui l’a produit.'], ['Vérifier', 'df -h → / redescend à 41 %. Noter l’intervention.']] },
vf: [['Linux a une corbeille pour rm.', 0, 'rm supprime définitivement.'], ['~ désigne /home/utilisateur.', 1, 'Le dossier personnel.'], ['cd sans argument ramène dans le dossier personnel.', 1, 'Comme cd ~.'], ['Les fichiers cachés commencent par un tiret.', 0, 'Par un point.']] });
add('droits', {
cm: [{ t: 'Droits et services web', d: '15 min', html: 'Le serveur Apache tourne sous l’utilisateur <b>www-data</b>. Pour qu’il lise un site, les fichiers doivent être lisibles par lui ; pour qu’il écrive (dossier d’envoi de fichiers), seul ce dossier doit lui appartenir.', exemple: 'sudo chown -R root:www-data /var/www/site\nsudo find /var/www/site -type d -exec chmod 750 {} \\;\nsudo find /var/www/site -type f -exec chmod 640 {} \\;\nsudo chown www-data /var/www/site/uploads' },
{ t: 'Sécuriser les clés et les secrets', d: '10 min', html: 'SSH refuse une clé privée trop ouverte. Le dossier <code>~/.ssh</code> doit être en 700, la clé privée en 600, <code>authorized_keys</code> en 600. Même règle pour un fichier <code>.env</code> contenant des mots de passe de base de données.' }],
qcm: [['Sous quel compte tourne Apache sous Debian ?', ['root', 'apache', 'www-data', 'nobody'], 2, 'www-data.'], ['Droits attendus pour ~/.ssh/id_ed25519 ?', ['644', '600', '755', '777'], 1, 'Lisible par le seul propriétaire.']],
memo: [['Octal', 'Symbolique', 'Usage typique'], ['600', 'rw-------', 'clé privée, .env'], ['644', 'rw-r--r--', 'fichier public'], ['640', 'rw-r-----', 'fichier de site web'], ['700', 'rwx------', '~/.ssh, dossier privé'], ['750', 'rwxr-x---', 'dossier de site, script'], ['755', 'rwxr-xr-x', 'programme public'], ['2770', 'rwxrws---', 'dossier partagé de groupe'], ['1777', 'rwxrwxrwt', '/tmp']],
resolu: { enonce: 'Le site affiche « 403 Forbidden » après une mise en ligne. Diagnostiquer et corriger.', etapes: [['Lire le journal', 'sudo tail /var/log/apache2/error.log → « Permission denied: /var/www/site/index.html ».'], ['Vérifier le fichier', 'ls -l → -rw------- alice alice : www-data ne peut pas lire.'], ['Vérifier les dossiers', 'namei -l /var/www/site/index.html → tous les dossiers sont traversables.'], ['Corriger', 'sudo chown -R root:www-data /var/www/site && sudo chmod 640 index.html'], ['Tester', 'curl -I localhost → 200 OK.']] },
vf: [['chmod 777 est une bonne solution temporaire.', 0, 'Jamais sur un serveur.'], ['Seul root peut utiliser chown.', 1, 'Pour changer de propriétaire.'], ['Sans x, on ne peut pas entrer dans un dossier.', 1, 'x = traverser.'], ['/etc/shadow est lisible par tous.', 0, 'root uniquement.']] });
add('tubes', {
cm: [{ t: 'xargs et tee', d: '10 min', html: '<code>xargs</code> transforme des lignes en arguments pour une autre commande. <code>tee</code> écrit dans un fichier tout en laissant passer le flux, pratique pour garder une trace.', exemple: 'find . -name "*.log" -mtime +30 | xargs rm\n./diagnostic.sh | tee rapport-$(date +%F).txt' },
{ t: 'Planifier avec cron', d: '15 min', html: '<code>crontab -e</code> ouvre la liste des tâches planifiées. Cinq champs : minute, heure, jour du mois, mois, jour de la semaine.', exemple: '# Tous les jours à 7 h 30\n30 7 * * * /home/admin/alerte-ssh.sh\n# Chaque lundi à minuit\n0 0 * * 1 /home/admin/sauvegarde.sh', attention: 'cron n’a pas le même PATH que vous : utilisez des chemins absolus dans vos scripts.' }],
qcm: [['Que signifie 0 2 * * * dans une crontab ?', ['Toutes les 2 minutes', 'Tous les jours à 2 h', 'Le 2 de chaque mois', 'Toutes les 2 heures'], 1, 'minute 0, heure 2.'], ['À quoi sert tee ?', ['Trier', 'Écrire dans un fichier et à l’écran', 'Compter', 'Compresser'], 1, 'Comme un T de plomberie.']],
memo: [['Syntaxe', 'Effet'], ['cmd > f', 'sortie dans f (écrase)'], ['cmd >> f', 'sortie ajoutée à f'], ['cmd 2> f', 'erreurs dans f'], ['cmd &> f', 'tout dans f'], ['a | b', 'sortie de a → entrée de b'], ['sort | uniq -c | sort -rn', 'classement par fréquence'], ['tail -f f', 'suivre f en direct'], ['$(cmd)', 'insérer la sortie de cmd']],
resolu: { enonce: 'Trouver les 3 utilisateurs qui se sont le plus connectés en SSH cette semaine.', etapes: [['Filtrer', 'grep "Accepted" /var/log/auth.log'], ['Repérer la colonne', 'Ligne type : … Accepted publickey for alice from … → le nom est le 9e champ.'], ['Extraire', "… | awk '{print $9}'"], ['Compter', '… | sort | uniq -c'], ['Classer', '… | sort -rn | head -3'], ['Résultat', ' 42 alice\n 17 bob\n 3 admin']] },
vf: [['> ajoute à la fin du fichier.', 0, 'Il écrase ; >> ajoute.'], ['uniq -c compte les doublons même non consécutifs.', 0, 'Il faut trier avant.'], ['2>/dev/null masque les erreurs.', 1, 'Elles sont jetées.'], ['grep -i ignore la casse.', 1, 'i = insensitive.']] });
add('osi', {
cm: [{ t: 'Le DNS : l’annuaire d’Internet', d: '15 min', html: 'Le <b>DNS</b> traduit un nom (www.lycee.fr) en adresse IP. Le poste interroge un résolveur, qui remonte si besoin la hiérarchie : racine, puis .fr, puis lycee.fr. Les réponses sont gardées en cache.', exemple: '$ dig +short www.lycee.fr\n93.184.216.34\n$ nslookup www.lycee.fr' },
{ t: 'Le DHCP : obtenir une adresse automatiquement', d: '10 min', html: 'À la connexion, le poste diffuse une demande. Le serveur <b>DHCP</b> lui attribue une IP, un masque, une passerelle et des serveurs DNS pour une durée donnée (le bail). Quatre messages : Discover, Offer, Request, Ack.' }],
qcm: [['Quel service traduit un nom en adresse IP ?', ['DHCP', 'DNS', 'ARP', 'NAT'], 1, 'DNS.'], ['Dans quel ordre se déroule un échange DHCP ?', ['Offer, Discover, Ack, Request', 'Discover, Offer, Request, Ack', 'Request, Ack, Discover, Offer', 'Ack, Request, Offer, Discover'], 1, 'DORA.']],
memo: [['Couche', 'Adresse', 'Équipement', 'Commande de test'], ['1 Physique', '—', 'câble, hub', 'ip link'], ['2 Liaison', 'MAC', 'switch', 'ip neigh'], ['3 Réseau', 'IP', 'routeur', 'ping, traceroute'], ['4 Transport', 'port', 'pare-feu', 'ss -tlnp'], ['7 Application', 'nom, URL', 'serveur', 'curl, dig']],
resolu: { enonce: 'Un poste obtient l’adresse 169.254.12.7. Que se passe-t-il et comment corriger ?', etapes: [['Reconnaître l’adresse', '169.254.x.x est une adresse auto-attribuée : le poste n’a reçu aucune réponse DHCP.'], ['Couche 1-2', 'ip link → lien UP ; le câble est bon.'], ['Tester le DHCP', 'sudo dhclient -v eth0 → DHCPDISCOVER sans réponse.'], ['Localiser', 'Un autre poste de la même prise obtient une IP : le problème vient du port du switch (VLAN mal affecté).'], ['Corriger et vérifier', 'Remettre le port dans le bon VLAN, relancer dhclient → IP 192.168.10.42.']] },
vf: [['Un routeur travaille en couche 3.', 1, 'Il route selon l’IP.'], ['UDP garantit la livraison.', 0, 'C’est TCP.'], ['L’adresse MAC change à chaque routeur.', 1, 'Nouvelle trame à chaque saut.'], ['Le DNS utilise le port 80.', 0, 'Port 53.']] });
add('sql', {
cm: [{ t: 'Sous-requêtes', d: '15 min', html: 'Une requête peut en contenir une autre, dans le WHERE ou le FROM. Utile pour comparer à une moyenne ou filtrer selon une liste calculée.', exemple: '-- Élèves au-dessus de la moyenne générale\nSELECT nom, moyenne FROM eleves\nWHERE moyenne > (SELECT AVG(moyenne) FROM eleves);\n\n-- Élèves des classes de BTS\nSELECT nom FROM eleves\nWHERE classe_id IN (SELECT id FROM classes WHERE libelle LIKE \'BTS%\');' },
{ t: 'Vues et droits', d: '10 min', html: 'Une <b>vue</b> est une requête enregistrée qu’on interroge comme une table. On donne aux utilisateurs l’accès à la vue plutôt qu’aux tables complètes, pour ne montrer que ce qui est nécessaire.', exemple: 'CREATE VIEW v_moyennes AS\nSELECT c.libelle, AVG(e.moyenne) AS moy FROM eleves e JOIN classes c ON c.id = e.classe_id GROUP BY c.libelle;\nGRANT SELECT ON v_moyennes TO direction;' }],
qcm: [['Qu’est-ce qu’une vue ?', ['Une copie de table', 'Une requête enregistrée interrogeable comme une table', 'Un index', 'Une sauvegarde'], 1, 'Elle ne stocke pas les données.'], ['Que renvoie IN (SELECT …) ?', ['Une erreur', 'Les lignes dont la valeur est dans la liste calculée', 'Un nombre', 'Une jointure'], 1, 'Filtre par appartenance.']],
memo: [['Clause', 'Rôle', 'Exemple'], ['SELECT', 'colonnes', 'SELECT nom, prenom'], ['FROM / JOIN', 'tables', 'FROM eleves e JOIN classes c ON …'], ['WHERE', 'filtrer les lignes', "WHERE ville = 'Lyon'"], ['GROUP BY', 'regrouper', 'GROUP BY classe_id'], ['HAVING', 'filtrer les groupes', 'HAVING COUNT(*) > 20'], ['ORDER BY', 'trier', 'ORDER BY nom DESC'], ['LIMIT', 'limiter', 'LIMIT 10']],
resolu: { enonce: 'Afficher, pour chaque classe, le nombre d’élèves ayant rendu le DM 3 et le taux de rendu.', etapes: [['Identifier les tables', 'classes, eleves, rendus(eleve_id, devoir_id).'], ['Partir des élèves', 'FROM eleves e JOIN classes c ON c.id = e.classe_id'], ['Garder les non-rendus', "LEFT JOIN rendus r ON r.eleve_id = e.id AND r.devoir_id = 3 (condition dans le ON, sinon le LEFT JOIN est annulé)"], ['Regrouper', 'GROUP BY c.libelle'], ['Calculer', 'SELECT c.libelle, COUNT(r.eleve_id) AS rendus, ROUND(100.0 * COUNT(r.eleve_id) / COUNT(e.id)) AS taux']] },
vf: [['WHERE peut contenir COUNT(*).', 0, 'On utilise HAVING.'], ['Une clé primaire peut être NULL.', 0, 'Jamais.'], ['LEFT JOIN garde toutes les lignes de gauche.', 1, 'Avec NULL à droite.'], ['DELETE sans WHERE supprime la table.', 0, 'Il vide la table, qui reste.']] });
add('python', {
cm: [{ t: 'Parcourir listes et dictionnaires', d: '15 min', html: '<code>enumerate</code> donne l’indice et la valeur, <code>.items()</code> les paires clé/valeur d’un dictionnaire. Les <b>listes en compréhension</b> construisent une liste en une ligne.', exemple: 'notes = {"Léa": 12, "Inès": 17, "Tom": 8}\nfor nom, n in notes.items():\n print(nom, n)\n\nadmis = [nom for nom, n in notes.items() if n >= 10]\n# [\'Léa\', \'Inès\']' },
{ t: 'Modules et bibliothèques', d: '10 min', html: '<code>import</code> charge un module. La bibliothèque standard couvre déjà beaucoup : <code>math</code>, <code>random</code>, <code>statistics</code>, <code>datetime</code>, <code>csv</code>, <code>json</code>. Pour la data, on installe pandas et matplotlib.', exemple: 'import statistics as st\nst.mean([12, 17, 8]) # 12.33…\nst.median([12, 17, 8]) # 12' }],
qcm: [['Que produit [x * 2 for x in range(3)] ?', ['[0, 2, 4]', '[2, 4, 6]', '[0, 1, 2]', 'Erreur'], 0, 'range(3) = 0, 1, 2.'], ['Quel module calcule une médiane ?', ['math', 'statistics', 'random', 'csv'], 1, 'statistics.median.']],
memo: [['Besoin', 'Syntaxe'], ['Afficher', 'print(f"{x} ans")'], ['Lire un nombre', 'int(input())'], ['Condition', 'if … : / elif … : / else :'], ['Répéter n fois', 'for i in range(n):'], ['Tant que', 'while condition:'], ['Fonction', 'def f(x): return …'], ['Liste', 'l.append(x) · len(l) · l[-1]'], ['Dictionnaire', 'd[k] · d.get(k, 0) · d.items()'], ['Erreur', 'try: … except ValueError:'], ['Test', 'assert f(2) == 4']],
resolu: { enonce: 'Écrire une fonction qui renvoie la mention d’une liste de notes (moyenne), en refusant une liste vide.', etapes: [['Définir le contrat', 'Entrée : liste de nombres. Sortie : texte. Erreur si liste vide.'], ['Écrire les tests d’abord', 'assert mention([16, 18]) == "TB"\nassert mention([9, 11]) == "Passable"'], ['Calculer la moyenne', 'm = sum(notes) / len(notes)'], ['Gérer le cas limite', 'if not notes: raise ValueError("liste vide")'], ['Choisir la mention', 'if m >= 16: return "TB" … return "Insuffisant"'], ['Lancer les tests', 'Tous les assert passent : la fonction est prête.']] },
vf: [['Python compte les indices à partir de 1.', 0, 'À partir de 0.'], ['input() renvoie un texte.', 1, 'Toujours.'], ['range(5) contient 5.', 0, '0 à 4.'], ['L’indentation est obligatoire.', 1, 'Elle délimite les blocs.']] });
add('web', {
cm: [{ t: 'Formulaires et traitement côté serveur', d: '15 min', html: 'Le formulaire envoie des paires nom=valeur. Côté serveur (PHP, Python), on récupère ces valeurs, on les <b>valide</b>, puis on les enregistre. Toute donnée venant du navigateur est suspecte, même si un champ est marqué required en HTML.', exemple: '<form method="post" action="/inscription">\n <label>Email <input type="email" name="email" required></label>\n <button>Envoyer</button>\n</form>' },
{ t: 'Accessibilité et responsive', d: '15 min', html: 'Un site accessible se lit au clavier et au lecteur d’écran : images avec <code>alt</code>, champs avec <code>label</code>, contrastes suffisants, titres hiérarchisés. En France, le <b>RGAA</b> fixe ces règles pour les services publics. Le <b>responsive</b> adapte la mise en page à l’écran avec les media queries.', exemple: '@media (max-width: 700px) {\n .grille { grid-template-columns: 1fr; }\n}' }],
qcm: [['Pourquoi valider côté serveur si le HTML a required ?', ['Inutile', 'Le navigateur peut être contourné', 'Pour la vitesse', 'Pour le SEO'], 1, 'N’importe qui peut envoyer une requête à la main.'], ['Quel référentiel encadre l’accessibilité des sites publics en France ?', ['RGPD', 'RGAA', 'ANSSI', 'W3C-FR'], 1, 'Référentiel général d’amélioration de l’accessibilité.']],
memo: [['Élément', 'Rôle'], ['<h1>…<h6>', 'titres hiérarchisés'], ['<p>', 'paragraphe'], ['<a href>', 'lien'], ['<img alt>', 'image + texte alternatif'], ['<ul><li>', 'liste'], ['<form method>', 'formulaire GET ou POST'], ['.classe {}', 'règle CSS'], ['display: grid', 'grille'], ['@media', 'adaptation à l’écran'], ['200 · 301 · 403 · 404 · 500', 'codes HTTP clés']],
resolu: { enonce: 'Publier une page « Planning BTS » sur un serveur Debian neuf et la rendre accessible en HTTPS.', etapes: [['Installer', 'sudo apt update && sudo apt install apache2'], ['Publier', 'sudo nano /var/www/html/index.html → page HTML avec h1 et tableau'], ['Vérifier', 'curl -I localhost → 200 OK'], ['Nommer le site', 'Enregistrer planning.lycee.fr dans le DNS vers l’IP du serveur'], ['Passer en HTTPS', 'sudo apt install certbot python3-certbot-apache && sudo certbot --apache -d planning.lycee.fr'], ['Contrôler', 'Ouvrir https://planning.lycee.fr, cadenas présent, puis tail /var/log/apache2/access.log']] },
vf: [['<title> s’affiche dans la page.', 0, 'Dans l’onglet.'], ['POST cache les données dans l’URL.', 0, 'Il les met dans le corps.'], ['Le code 404 signifie « introuvable ».', 1, 'Not Found.'], ['HTTPS utilise le port 443.', 1, 'Par défaut.']] });
add('crypto', {
cm: [{ t: 'Authentification à deux facteurs', d: '10 min', html: 'Un facteur est quelque chose que l’on <b>sait</b> (mot de passe), que l’on <b>possède</b> (téléphone, clé de sécurité) ou que l’on <b>est</b> (empreinte). Combiner deux facteurs rend inutile un mot de passe volé. Les codes TOTP à 6 chiffres changent toutes les 30 secondes.' },
{ t: 'Chiffrer un fichier en pratique', d: '15 min', html: 'Pour transmettre un document sensible, on le chiffre avec un outil éprouvé plutôt qu’avec un mot de passe de fichier ZIP.', exemple: '# Symétrique (mot de passe partagé)\ngpg -c bulletin.pdf # → bulletin.pdf.gpg\ngpg -d bulletin.pdf.gpg > bulletin.pdf\n\n# Vérifier une signature de téléchargement\ngpg --verify debian.iso.sign debian.iso' }],
qcm: [['Un code reçu par application sur son téléphone est un facteur :', ['que l’on sait', 'que l’on possède', 'que l’on est', 'aucun'], 1, 'Le téléphone est possédé.'], ['Que fait gpg -c fichier ?', ['Le compresse', 'Le chiffre avec un mot de passe', 'Le signe', 'Le hache'], 1, '-c = chiffrement symétrique.']],
memo: [['Besoin', 'Outil recommandé'], ['Vérifier un fichier', 'SHA-256 (sha256sum)'], ['Stocker un mot de passe', 'Argon2id ou bcrypt, avec sel'], ['Chiffrer un disque', 'AES-256 (LUKS, BitLocker)'], ['Chiffrer un fichier', 'gpg, age'], ['Se connecter à un serveur', 'SSH + clé Ed25519'], ['Site web', 'TLS 1.2 ou 1.3, certificat valide'], ['Comptes utilisateurs', 'double authentification'], ['À bannir', 'MD5, SHA-1, DES, mots de passe en clair']],
resolu: { enonce: 'Mettre en place une connexion SSH par clé vers le serveur 192.168.10.10 et interdire les mots de passe.', etapes: [['Générer la paire', 'ssh-keygen -t ed25519 -C "prof@lycee" (phrase de passe conseillée)'], ['Déposer la clé publique', 'ssh-copy-id prof@192.168.10.10'], ['Tester avant de fermer', 'ssh prof@192.168.10.10 → connexion sans mot de passe. Garder cette session ouverte.'], ['Durcir sshd', 'sudo nano /etc/ssh/sshd_config → PasswordAuthentication no, PermitRootLogin no'], ['Appliquer', 'sudo sshd -t && sudo systemctl restart ssh'], ['Vérifier', 'Depuis un autre terminal : ssh -o PubkeyAuthentication=no prof@… → refusé.']] },
vf: [['Un hachage se déchiffre avec la bonne clé.', 0, 'Il est à sens unique.'], ['La clé publique peut être diffusée.', 1, 'C’est son rôle.'], ['MD5 convient pour les mots de passe.', 0, 'Cassé et trop rapide.'], ['HTTPS chiffre le trafic avec une clé de session.', 1, 'Symétrique, négociée au départ.']] });
})();

View file

@ -0,0 +1,206 @@
// Cours complets · Réseaux lot 1 : IPv4 et sous-réseaux, VLAN 802.1Q, DHCP
(function () {
var K = 'stroke="#000" stroke-width="3"', F = 'font-family="Archivo,sans-serif" font-weight="800"', M = 'font-family="IBM Plex Mono,monospace" font-weight="600"';
var bx = function (x, y, w, h, c, t, fs) { return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" fill="' + c + '" ' + K + '/><text x="' + (x + w / 2) + '" y="' + (y + h / 2 + 5) + '" text-anchor="middle" ' + F + ' font-size="' + (fs || 15) + '">' + t + '</text>'; };
var tx = function (x, y, s, fs, mono) { return '<text x="' + x + '" y="' + y + '" ' + (mono ? M : F) + ' font-size="' + (fs || 14) + '">' + s + '</text>'; };
var svg = function (w, h, b) { return '<svg viewBox="0 0 ' + w + ' ' + h + '" class="h-auto w-full" role="img">' + b + '</svg>'; };
var Y = '#FFD23F', G = '#7CF2C0', P = '#FF6FB5', L = '#C4B5FD', N = '#F6F5F1', W = '#fff';
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_LONG = window.CONTENUS_LONG || {})[c.cours.id] = c.long;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = c.schemas;
};
// ============ IPv4, CIDR, masques, sous-réseaux ============
reg({
cours: { id: 'ipv4', match: /IPv4 : classes|CIDR|masques|sous-réseaux|VLSM/i,
titre: 'Adressage IPv4 : masques, CIDR et découpage en sous-réseaux', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T S1-S2 · TSSR',
prerequis: ['Binaire : poids des bits d’un octet', 'Modèle OSI et TCP/IP'],
competences: ['BTS SIO · Bloc 1 · Exploiter les éléments d’un réseau', 'BTS SIO SISR · Bloc 2 · Concevoir une solution d’infrastructure réseau', 'BTS CIEL · C03 · Concevoir un réseau', 'BUT R&T · Compétence « Connecter les entreprises et les usagers »'],
intro: 'Chaque équipement qui communique sur un réseau IP possède une adresse, et chaque adresse appartient à un réseau. Savoir lire une adresse et son masque, c’est savoir si deux machines peuvent se parler directement ou doivent passer par un routeur, combien de postes un réseau peut accueillir et comment découper un espace d’adresses pour séparer les services d’un établissement. C’est la compétence la plus évaluée à l’épreuve pratique du BTS SIO SISR, et celle qu’un technicien utilise tous les jours.',
cm: [
{ t: 'Anatomie d’une adresse IPv4', d: '', html: 'Une adresse IPv4 est un nombre de <b>32 bits</b>. Pour la rendre lisible, on la découpe en quatre octets écrits en décimal et séparés par des points : c’est la notation <b>décimale pointée</b>. 192.168.10.65 correspond en binaire à 11000000.10101000.00001010.01000001.<br><br>Une adresse se divise toujours en deux parties. La <b>partie réseau</b>, à gauche, est commune à toutes les machines du même réseau. La <b>partie hôte</b>, à droite, distingue chaque machine au sein de ce réseau. C’est le <b>masque</b> qui indique où passe la frontière entre les deux.<br><br>Deux adresses de chaque réseau sont réservées et ne peuvent pas être attribuées à une machine : l’<b>adresse de réseau</b>, où tous les bits d’hôte valent 0, qui désigne le réseau lui-même, et l’<b>adresse de diffusion</b> (broadcast), où tous les bits d’hôte valent 1, qui permet d’envoyer un paquet à toutes les machines du réseau à la fois.' },
{ t: 'Le masque de sous-réseau et la notation CIDR', d: '', html: 'Le masque est lui aussi un nombre de 32 bits, composé d’une suite continue de 1 (la partie réseau) suivie d’une suite de 0 (la partie hôte). 255.255.255.0 s’écrit 11111111.11111111.11111111.00000000 : 24 bits de réseau, 8 bits d’hôte.<br><br>La notation <b>CIDR</b> (Classless Inter-Domain Routing, RFC 4632) écrit simplement le nombre de bits à 1 après une barre oblique : 192.168.10.65/24. Elle est plus courte, sans ambiguïté, et c’est celle qu’utilisent Linux, les équipements réseau et les fournisseurs cloud.<br><br>Les valeurs possibles pour un octet de masque ne sont pas quelconques : comme les 1 sont contigus, un octet ne peut valoir que 0, 128, 192, 224, 240, 248, 252, 254 ou 255. Un masque comme 255.255.253.0 n’existe pas.', table: [['CIDR', 'Masque', 'Bits d’hôte', 'Adresses', 'Hôtes utilisables'], ['/8', '255.0.0.0', '24', '16 777 216', '16 777 214'], ['/16', '255.255.0.0', '16', '65 536', '65 534'], ['/22', '255.255.252.0', '10', '1 024', '1 022'], ['/24', '255.255.255.0', '8', '256', '254'], ['/25', '255.255.255.128', '7', '128', '126'], ['/26', '255.255.255.192', '6', '64', '62'], ['/27', '255.255.255.224', '5', '32', '30'], ['/28', '255.255.255.240', '4', '16', '14'], ['/30', '255.255.255.252', '2', '4', '2'], ['/32', '255.255.255.255', '0', '1', 'une seule machine']] },
{ t: 'Calculer adresse de réseau, diffusion et plage', d: '', html: 'Le calcul se fait toujours de la même manière. On applique un <b>ET logique</b> bit à bit entre l’adresse et le masque : on obtient l’adresse de réseau. On met ensuite tous les bits d’hôte à 1 : on obtient l’adresse de diffusion. Les adresses utilisables sont toutes celles comprises entre les deux.<br><br>En pratique, on ne convertit que l’<b>octet significatif</b>, celui où le masque n’est ni 255 ni 0. Les octets à 255 se recopient, les octets à 0 deviennent 0 pour le réseau et 255 pour la diffusion.<br><br>Il existe un raccourci très efficace : le <b>nombre magique</b>. On calcule 256 moins la valeur de l’octet significatif du masque. Pour un /26, l’octet vaut 192, donc 256 − 192 = 64 : les réseaux commencent tous à un multiple de 64 (0, 64, 128, 192). L’adresse 192.168.10.100 tombe entre 64 et 127 : réseau .64, diffusion .127, hôtes de .65 à .126.', exemple: 'Adresse : 192.168.10.100/26\nMasque : 255.255.255.192 (octet significatif : le 4e)\nMagique : 256 − 192 = 64 → blocs 0, 64, 128, 192\n100 est dans le bloc 64 à 127\n\nRéseau : 192.168.10.64\nDiffusion : 192.168.10.127\nHôtes : 192.168.10.65 → 192.168.10.126 (62 adresses)', attention: 'Le nombre d’hôtes utilisables est 2ⁿ − 2, n étant le nombre de bits d’hôte. On retire toujours l’adresse de réseau et l’adresse de diffusion. Seule exception : le /31, réservé aux liaisons point à point entre deux routeurs (RFC 3021).' },
{ t: 'Adresses privées, publiques et spéciales', d: '', html: 'Il n’y a qu’environ 4,3 milliards d’adresses IPv4, bien moins que le nombre d’appareils connectés. Pour ne pas les épuiser, la <b>RFC 1918</b> réserve trois plages <b>privées</b>, utilisables librement à l’intérieur d’un réseau mais jamais routées sur Internet. Les postes d’un lycée sont presque toujours en adresses privées et sortent sur Internet grâce au <b>NAT</b> du routeur, qui les traduit en une adresse publique.<br><br>D’autres plages ont un rôle particulier et doivent être reconnues au premier coup d’œil, car elles orientent immédiatement un diagnostic.', table: [['Plage', 'Rôle'], ['10.0.0.0/8', 'privée, grands réseaux (académies, entreprises)'], ['172.16.0.0/12', 'privée, de 172.16 à 172.31'], ['192.168.0.0/16', 'privée, box et petits réseaux'], ['127.0.0.0/8', 'boucle locale, la machine elle-même'], ['169.254.0.0/16', 'APIPA : aucun serveur DHCP n’a répondu'], ['100.64.0.0/10', 'CGNAT des opérateurs'], ['224.0.0.0/4', 'multidiffusion (multicast)'], ['0.0.0.0', 'toutes les interfaces / route par défaut']] },
{ t: 'Les classes : un héritage à connaître', d: '', html: 'Avant 1993, les adresses étaient réparties en <b>classes</b> déterminées par les premiers bits : classe A (1 à 126, masque /8), classe B (128 à 191, /16), classe C (192 à 223, /24), classe D (multicast) et E (expérimentale). Ce système gaspillait énormément d’adresses : une entreprise qui avait besoin de 300 adresses recevait une classe B de 65 534 adresses.<br><br>Le CIDR a supprimé cette rigidité : le masque est désormais libre. Les classes n’ont plus d’existence technique, mais le vocabulaire reste, et certains sujets d’examen ou vieux équipements les mentionnent encore. Retenez surtout qu’aujourd’hui, <b>seul le masque compte</b>.' },
{ t: 'Découper un réseau : le subnetting', d: '', html: 'Découper un réseau en sous-réseaux consiste à <b>emprunter des bits</b> à la partie hôte pour allonger la partie réseau. Chaque bit emprunté double le nombre de sous-réseaux et divise par deux leur taille.<br><br>Pourquoi découper ? Pour séparer les usages : administration, salles pédagogiques, Wi-Fi invités, serveurs, caméras. Chaque sous-réseau est un domaine de diffusion distinct, ce qui limite le trafic inutile, et le passage de l’un à l’autre se fait par un routeur ou un pare-feu où l’on peut filtrer. Un élève sur le Wi-Fi n’a alors aucun accès direct au serveur de notes.<br><br>La méthode : déterminer le nombre de sous-réseaux nécessaires, trouver la plus petite puissance de 2 qui le couvre, en déduire le nombre de bits à emprunter, puis le nouveau masque et le pas entre chaque sous-réseau.', exemple: 'Réseau de départ : 192.168.10.0/24, besoin de 4 sous-réseaux\n4 = 2² → on emprunte 2 bits → nouveau masque /26\nPas : 256 − 192 = 64\n\nSR1 : 192.168.10.0/26 hôtes .1 à .62 diffusion .63\nSR2 : 192.168.10.64/26 hôtes .65 à .126 diffusion .127\nSR3 : 192.168.10.128/26 hôtes .129 à .190 diffusion .191\nSR4 : 192.168.10.192/26 hôtes .193 à .254 diffusion .255' },
{ t: 'VLSM : des sous-réseaux de tailles différentes', d: '', html: 'Dans la réalité, les besoins sont inégaux : 120 postes en salles pédagogiques, 50 à l’administration, 20 serveurs, et des liaisons entre routeurs qui n’ont besoin que de 2 adresses. Découper en parts égales gaspillerait des adresses. Le <b>VLSM</b> (Variable Length Subnet Mask) attribue à chaque besoin le masque juste suffisant.<br><br>La règle d’or est de <b>traiter les besoins du plus grand au plus petit</b>. On place le plus gros sous-réseau en premier, au début de la plage, puis le suivant immédiatement après, et ainsi de suite. En procédant dans cet ordre, chaque sous-réseau commence naturellement sur une frontière valide et aucun ne chevauche un autre.', exemple: 'Plage : 172.16.0.0/24 Besoins : 120, 50, 20 hôtes + 2 liaisons de 2\n\n120 hôtes → 2⁷ − 2 = 126 → /25 → 172.16.0.0/25 (.1 à .126)\n 50 hôtes → 2⁶ − 2 = 62 → /26 → 172.16.0.128/26 (.129 à .190)\n 20 hôtes → 2⁵ − 2 = 30 → /27 → 172.16.0.192/27 (.193 à .222)\n 2 hôtes → 2² − 2 = 2 → /30 → 172.16.0.224/30 (.225 et .226)\n 2 hôtes → /30 → 172.16.0.228/30 (.229 et .230)\nReste libre : 172.16.0.232 à 172.16.0.255', attention: 'Prévoyez toujours une marge de croissance. Un sous-réseau dimensionné à l’hôte près sera saturé à la rentrée suivante. En entreprise, on vise en général 30 % d’adresses libres.' },
{ t: 'Agrégation de routes (supernetting)', d: '', html: 'L’opération inverse du découpage consiste à <b>résumer</b> plusieurs réseaux contigus en un seul préfixe plus court. Un routeur de site qui connaît 192.168.0.0/24, 192.168.1.0/24, 192.168.2.0/24 et 192.168.3.0/24 peut annoncer une seule route 192.168.0.0/22 au routeur central. Les tables de routage restent petites et les modifications internes au site ne perturbent pas le reste du réseau.<br><br>Pour trouver le préfixe résumé, on écrit les réseaux en binaire et on garde la plus longue suite de bits communs à tous.' }
],
synthese: ['Adresse IPv4 = 32 bits = partie réseau + partie hôte, séparées par le masque', 'Réseau = adresse ET masque ; diffusion = bits d’hôte à 1', 'Hôtes utilisables = 2ⁿ − 2', 'Nombre magique = 256 − octet significatif du masque', 'Subnetting : chaque bit emprunté double les sous-réseaux', 'VLSM : du plus grand besoin au plus petit, avec une marge', '10/8, 172.16/12, 192.168/16 sont privées ; 169.254 signale un échec DHCP'],
qcm: [
['Combien d’hôtes utilisables dans un /27 ?', ['32', '30', '27', '62'], 1, '2⁵ − 2 = 30.'],
['Quelle est l’adresse de réseau de 10.4.77.200/20 ?', ['10.4.64.0', '10.4.77.0', '10.4.72.0', '10.4.0.0'], 0, 'Magique 256 − 240 = 16 sur le 3e octet : 77 est dans le bloc 64 à 79.'],
['Quelle est l’adresse de diffusion de 192.168.5.130/25 ?', ['192.168.5.127', '192.168.5.255', '192.168.5.254', '192.168.5.128'], 1, 'Bloc de 128 : réseau .128, diffusion .255.'],
['Quel masque correspond à /21 ?', ['255.255.248.0', '255.255.240.0', '255.255.252.0', '255.255.255.248'], 0, '21 = 16 + 5 bits : 11111000 = 248.'],
['Un poste a l’adresse 169.254.33.7. Que s’est-il passé ?', ['Il est sur Internet', 'Il n’a reçu aucune réponse DHCP', 'Il est en IPv6', 'Il est en boucle locale'], 1, 'APIPA : adresse auto-attribuée.'],
['Laquelle n’est PAS une adresse privée ?', ['10.200.1.1', '172.20.0.5', '172.40.0.5', '192.168.99.1'], 2, '172.16/12 va de 172.16 à 172.31.'],
['On doit découper un /24 en 6 sous-réseaux égaux. Quel masque ?', ['/26', '/27', '/28', '/25'], 1, '6 → 8 = 2³, on emprunte 3 bits.'],
['192.168.1.70/26 et 192.168.1.130/26 peuvent-ils communiquer sans routeur ?', ['Oui', 'Non, ils sont dans deux sous-réseaux différents', 'Seulement en IPv6', 'Seulement avec un switch'], 1, 'Réseaux .64 et .128.'],
['En VLSM, dans quel ordre traite-t-on les besoins ?', ['Du plus petit au plus grand', 'Du plus grand au plus petit', 'Par ordre alphabétique', 'Peu importe'], 1, 'Pour que chaque bloc commence sur une frontière valide.'],
['Quel préfixe résume 10.1.4.0/24 à 10.1.7.0/24 ?', ['10.1.4.0/22', '10.1.0.0/16', '10.1.4.0/23', '10.1.4.0/21'], 0, '4 = 000001 00 à 7 = 000001 11 : 22 bits communs.'],
['Pourquoi le /31 est-il une exception ?', ['Il n’existe pas', 'Il est utilisé pour des liaisons point à point sans réseau ni diffusion', 'Il est réservé au multicast', 'Il est public'], 1, 'RFC 3021.'],
['Combien de /26 dans un /22 ?', ['4', '8', '16', '32'], 2, '26 − 22 = 4 bits : 2⁴ = 16.']
],
td: [
{ n: 'Échauffement', c: 'Pour chaque adresse, donner le masque en décimal pointé, l’adresse de réseau, la diffusion et le nombre d’hôtes : 192.168.1.37/24 · 10.0.0.200/25 · 172.16.8.50/28.', r: '255.255.255.0 · .0 · .255 · 254\n255.255.255.128 · 10.0.0.128 · 10.0.0.255 · 126\n255.255.255.240 · 172.16.8.48 · 172.16.8.63 · 14' },
{ n: 'Échauffement', c: 'Classer en privée, publique ou spéciale : 8.8.8.8, 172.31.255.1, 172.32.0.1, 127.0.0.53, 169.254.1.1, 100.70.3.2.', r: 'publique · privée · publique · boucle locale · APIPA · CGNAT' },
{ n: 'Application', c: 'Découper 192.168.20.0/24 en 8 sous-réseaux égaux. Donner pour chacun le réseau, la première et la dernière adresse utilisables et la diffusion.', r: '/27, pas de 32 : .0 (.1-.30, .31) · .32 (.33-.62, .63) · .64 · .96 · .128 · .160 · .192 · .224 (.225-.254, .255)' },
{ n: 'Application', c: 'Un lycée dispose de 10.10.0.0/22. Besoins : pédagogie 400 postes, administration 100, Wi-Fi invités 200, serveurs 25, liaison vers le rectorat 2. Proposer un plan VLSM.', r: 'Pédagogie 400 → /23 : 10.10.0.0/23 (.0.1 à .1.254)\nInvités 200 → /24 : 10.10.2.0/24\nAdministration 100 → /25 : 10.10.3.0/25\nServeurs 25 → /27 : 10.10.3.128/27\nLiaison → /30 : 10.10.3.160/30\nReste : 10.10.3.164 à 10.10.3.255' },
{ n: 'Application', c: 'Un technicien a configuré un poste en 192.168.10.140/24 alors que le réseau des salles est 192.168.10.128/26. Décrire les symptômes et la correction.', r: 'Le poste croit que tout 192.168.10.x est local : il n’enverra jamais à la passerelle les paquets destinés aux autres sous-réseaux (.0/26, .64/26, .192/26) et fera des requêtes ARP sans réponse. Correction : masque /26.' },
{ n: 'Approfondissement', c: 'Résumer en un seul préfixe : 172.16.32.0/24, 172.16.33.0/24, 172.16.34.0/24, 172.16.35.0/24. Peut-on ajouter 172.16.36.0/24 au même résumé sans englober d’autres réseaux ?', r: '172.16.32.0/22. Avec .36, il faut un /21 (172.16.32.0/21) qui englobe aussi .37 à .39 : acceptable seulement si ces réseaux sont aussi derrière ce routeur.' },
{ n: 'Approfondissement', c: 'Écrire en Python une fonction infos(cidr) qui renvoie réseau, diffusion et nombre d’hôtes, à l’aide du module ipaddress, puis la tester sur 10.4.77.200/20.', r: 'import ipaddress\ndef infos(c):\n n = ipaddress.ip_interface(c).network\n return n.network_address, n.broadcast_address, n.num_addresses - 2\n\nprint(infos("10.4.77.200/20"))\n# (IPv4Address(\'10.4.64.0\'), IPv4Address(\'10.4.79.255\'), 4094)' }
],
tp: { type: 'Générateur + Lab VM', url: 'apercu-generateur.html', desc: 'Exercices de sous-réseaux générés à l’infini, puis application du plan sur deux VM et un routeur.', etapes: ['20 calculs réseau / diffusion corrigés au fil de l’eau', 'Découpage en parts égales, puis en VLSM', 'Appliquer le plan : ip addr add sur deux VM', 'Vérifier que deux sous-réseaux ne se voient pas sans routeur', 'Ajouter la route et tester avec ping'] },
plusLoin: [['RFC 1918 · adresses privées', 'https://www.rfc-editor.org/rfc/rfc1918'], ['RFC 4632 · CIDR', 'https://www.rfc-editor.org/rfc/rfc4632'], ['Module Python ipaddress', 'https://docs.python.org/fr/3/library/ipaddress.html'], ['Cisco Networking Academy · CCNA ITN', 'https://www.netacad.com/']] },
plus: {
cas: { titre: 'Mise en situation · Le plan d’adressage du nouveau bâtiment', texte: 'Le lycée ouvre un bâtiment de BTS avec trois salles de TP de 30 postes, un laboratoire réseau, une salle des professeurs et des bornes Wi-Fi. Le rectorat attribue la plage 10.62.48.0/22. Le technicien recense les besoins, ajoute 30 % de marge, construit un plan VLSM, le documente dans un tableur partagé avec la passerelle de chaque sous-réseau (toujours la première adresse, par convention), puis configure les étendues DHCP correspondantes. Six mois plus tard, l’ajout d’une quatrième salle se fait en dix minutes grâce à la réserve prévue.' },
erreurs: ['Oublier de retirer 2 au nombre d’hôtes', 'Confondre adresse de réseau et première adresse utilisable', 'Écrire un masque impossible comme 255.255.250.0', 'Traiter les besoins VLSM du plus petit au plus grand et créer des chevauchements', 'Attribuer l’adresse de diffusion à une imprimante', 'Vérifier l’adresse d’un poste sans vérifier son masque', 'Oublier que 172.16/12 s’arrête à 172.31'],
memo: [['Préfixe', 'Masque', 'Pas', 'Hôtes'], ['/24', '255.255.255.0', '256 (3e octet +1)', '254'], ['/25', '.128', '128', '126'], ['/26', '.192', '64', '62'], ['/27', '.224', '32', '30'], ['/28', '.240', '16', '14'], ['/29', '.248', '8', '6'], ['/30', '.252', '4', '2'], ['/23', '255.255.254.0', '2 sur le 3e octet', '510'], ['/22', '255.255.252.0', '4 sur le 3e octet', '1 022'], ['/20', '255.255.240.0', '16 sur le 3e octet', '4 094']],
examen: { titre: 'Sujet type · 20 points', q: ['Pour 172.18.93.14/19 : masque, réseau, diffusion, première et dernière adresse, nombre d’hôtes. (5 pts)', 'Découper 192.168.100.0/24 en 5 sous-réseaux de taille égale et donner les 5 premiers. (4 pts)', 'Plan VLSM pour 10.0.0.0/23 : 200, 100, 60, 10 hôtes et deux liaisons point à point. (7 pts)', 'Expliquer pourquoi un poste en 169.254.x.x ne peut pas accéder au serveur de fichiers. (2 pts)', 'Donner le préfixe résumé de 10.8.8.0/24 à 10.8.11.0/24. (2 pts)'], r: ['255.255.224.0 · 172.18.64.0 · 172.18.95.255 · .64.1 à .95.254 · 8 190', '/27 : .0, .32, .64, .96, .128', '/24 10.0.0.0 · /25 10.0.1.0 · /26 10.0.1.128 · /28 10.0.1.192 · /30 10.0.1.208 et .212', 'Pas de DHCP : adresse hors du réseau, pas de passerelle', '10.8.8.0/22'] },
prof: { deroule: [['Ouverture', 'Défi : « ces deux postes peuvent-ils se parler ? », vote à main levée puis explication'], ['Cours', 'Structure d’une adresse, masque, CIDR, calcul réseau et diffusion'], ['Entraînement', 'Générateur : 20 calculs, objectif 18/20'], ['Cours', 'Subnetting puis VLSM au tableau à partir d’un cas réel'], ['Pratique', 'TD VLSM en binômes, puis application sur les VM'], ['Clôture', 'Quiz flash de 5 questions']], diff: 'Élèves en difficulté : tableau des préfixes imprimé, méthode du nombre magique, exercices du générateur au niveau 1. Élèves avancés : agrégation de routes, module ipaddress en Python, défis de sous-réseaux chronométrés.', eval: 'Générateur (formatif), TD VLSM relevé, sujet type sur 20 en conditions d’examen.' }
},
exemples: [
['Lire l’adresse d’un serveur', 'Première connexion à une VM.', 'ip -br addr show ens18', 'ens18 UP 192.168.10.20/26', 'Le /26 donne un pas de 64 : le serveur est dans 192.168.10.0/26, avec des hôtes de .1 à .62 et une diffusion en .63. Un poste en .70 est sur un autre sous-réseau et passera par la passerelle.'],
['Calculer avec ipcalc', 'Vérifier un calcul avant de configurer un switch.', 'ipcalc 10.4.77.200/20', 'Network: 10.4.64.0/20\nNetmask: 255.255.240.0 = 20\nBroadcast: 10.4.79.255\nHostMin: 10.4.64.1\nHostMax: 10.4.79.254\nHosts/Net: 4094', 'ipcalc (paquet ipcalc) fait le calcul complet. Utile pour vérifier, mais à l’examen il faut savoir le faire à la main avec le nombre magique : 256 − 240 = 16 sur le 3e octet.'],
['Découper en Python', 'Générer automatiquement le plan pour un script d’inventaire.', 'import ipaddress\nfor sr in ipaddress.ip_network("192.168.10.0/24").subnets(new_prefix=26):\n print(sr, sr[1], sr[-2])', '192.168.10.0/26 192.168.10.1 192.168.10.62\n192.168.10.64/26 192.168.10.65 192.168.10.126\n192.168.10.128/26 192.168.10.129 192.168.10.190\n192.168.10.192/26 192.168.10.193 192.168.10.254', 'subnets découpe le réseau ; sr[1] est la première adresse utilisable, sr[-2] la dernière (sr[-1] est la diffusion).'],
['Repérer un mauvais masque', 'Un poste accède à Internet mais pas à l’imprimante 192.168.10.70.', 'ip -br addr ; ip route', 'ens3 UP 192.168.10.40/24\ndefault via 192.168.10.1\n192.168.10.0/24 dev ens3 scope link', 'Le réseau réel est en /26. Avec son /24, le poste croit l’imprimante locale et la cherche par ARP, sans réponse car elle est dans un autre sous-réseau. Correction : masque /26.'],
['Tester qu’une adresse est dans un réseau', 'Script de contrôle des baux DHCP.', 'python3 -c "import ipaddress as i; print(i.ip_address(\'10.62.49.7\') in i.ip_network(\'10.62.48.0/23\'))"', 'True', '10.62.48.0/23 couvre 10.62.48.0 à 10.62.49.255 : l’adresse est bien dans la plage.'],
['Ajouter une adresse temporaire', 'Tester un nouveau sous-réseau sur un serveur sans redémarrage.', 'sudo ip addr add 192.168.10.65/26 dev ens18\nip -br addr show ens18', 'ens18 UP 192.168.10.20/26 192.168.10.65/26', 'Une interface peut porter plusieurs adresses. Cette modification disparaît au redémarrage ; pour la rendre permanente, on l’ajoute dans /etc/network/interfaces.']
],
long: { figure: svg(720, 190, bx(20, 20, 510, 50, Y, '192.168.10 · partie réseau (26 bits)', 15) + bx(530, 20, 170, 50, G, 'hôte (6 bits)', 15) + tx(20, 110, '11000000.10101000.00001010.01', 16, 1) + tx(345, 110, '000001', 16, 1) + tx(20, 150, 'masque /26 = 255.255.255.192', 16, 1) + tx(20, 180, 'réseau .0 · hôtes .1 à .62 · diffusion .63', 15)), legende: 'Le masque /26 sépare 26 bits de réseau et 6 bits d’hôte.', photo: 'Photo : tableau blanc avec un plan d’adressage dessiné, ou armoire réseau d’un établissement',
lecture: ['Un plan d’adressage, c’est la carte d’un réseau. Sans lui, personne ne sait quelle adresse est libre, quelle machine se cache derrière 10.62.48.37 ni pourquoi l’imprimante de la salle des professeurs n’est pas joignable depuis le CDI. Avec lui, chaque adresse raconte quelque chose : le bâtiment, l’usage, le type d’équipement.', 'Le masque est la notion qui pose le plus de difficultés, parce qu’elle est invisible pour l’utilisateur. Deux postes avec des adresses qui se ressemblent peuvent être dans deux réseaux différents, et deux adresses très différentes peuvent être dans le même réseau. Seul le calcul binaire tranche. Une fois ce calcul automatisé grâce au nombre magique, on lit un plan d’adressage comme on lit un plan de ville.', 'Le découpage en sous-réseaux n’est pas qu’un exercice de calcul. C’est un outil de sécurité et d’organisation. Isoler le Wi-Fi des invités, séparer les caméras de surveillance, mettre les serveurs à part, c’est limiter ce qu’un appareil compromis peut atteindre. Les recommandations de l’ANSSI sur le cloisonnement des réseaux reposent entièrement sur ce principe.', 'Enfin, IPv4 a ses limites : il n’y a plus d’adresses publiques disponibles, et le NAT est devenu la norme. IPv6, avec ses 128 bits, supprime ce problème et fait l’objet d’un chapitre à part. Mais IPv4 restera présent dans les réseaux d’établissement pendant encore de nombreuses années, et ce que vous apprenez ici sur les préfixes et le découpage se transpose directement en IPv6.'] },
schemas: { termes: ['CIDR', 'VLSM', 'NAT', 'APIPA', 'RFC 1918', 'masque', 'nombre magique', 'adresse de réseau', 'adresse de diffusion', 'broadcast', 'ET logique', '/24', '/25', '/26', '/27', '/28', '/30', '/22', '255.255.255.192', '169.254', 'ipcalc', 'ipaddress', 'ip addr'],
figs: [[2, 'Le nombre magique : 256 − 192 = 64', svg(720, 140, [['.0', Y], ['.64', G], ['.128', L], ['.192', P]].map(function (c, i) { return bx(20 + i * 172, 30, 162, 50, c[1], c[0] + ' → ' + [63, 127, 191, 255][i], 16); }).join('') + tx(20, 115, '192.168.10.100 tombe dans le bloc .64 → réseau .64, diffusion .127', 15))],
[6, 'VLSM : du plus grand besoin au plus petit', svg(720, 110, bx(10, 30, 350, 50, Y, '/25 · 120 hôtes', 15) + bx(360, 30, 175, 50, G, '/26 · 50', 15) + bx(535, 30, 88, 50, L, '/27 · 20', 13) + bx(623, 30, 22, 50, P, '', 11) + bx(645, 30, 22, 50, P, '', 11) + bx(667, 30, 43, 50, N, 'libre', 11) + tx(623, 100, '/30 ×2', 12))]] }
});
// ============ VLAN 802.1Q ============
reg({
cours: { id: 'vlan', match: /VLAN|802\.1Q|trunk/i,
titre: 'VLAN 802.1Q : segmenter le réseau, trunk et routage inter-VLAN', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T S2-S3 · TSSR',
prerequis: ['Ethernet, adresses MAC, commutation', 'Adressage IPv4 et sous-réseaux'],
competences: ['BTS SIO SISR · Bloc 2 · Concevoir une solution d’infrastructure réseau', 'BTS SIO · Bloc 3 · Sécuriser les équipements', 'BTS CIEL · C03 · Concevoir et configurer un réseau', 'BUT R&T · Compétence « Administrer les réseaux »'],
intro: 'Dans un établissement, les postes des élèves, ceux de l’administration, les imprimantes, les caméras, la téléphonie et le Wi-Fi invités sont souvent branchés sur les mêmes switchs. Sans précaution, ils partagent un seul réseau : un élève peut voir le trafic de diffusion de l’administration et tenter d’atteindre le serveur de notes. Les VLAN permettent de créer plusieurs réseaux logiques étanches sur une même infrastructure physique, sans recâbler. C’est l’un des gestes les plus fréquents d’un technicien réseau, et un attendu central de l’option SISR.',
cm: [
{ t: 'Le problème : un seul domaine de diffusion', d: '', html: 'Un switch transmet les trames de <b>diffusion</b> (ARP, DHCP, certaines annonces) sur tous ses ports. Tous les équipements branchés sur des switchs reliés entre eux forment un seul <b>domaine de diffusion</b>. Plus il est grand, plus chaque machine reçoit de trafic inutile, et plus un incident (tempête de diffusion, faux serveur DHCP) touche de monde.<br><br>Le second problème est la sécurité. Sur un même domaine de diffusion, rien n’empêche une machine de parler directement à une autre. Le filtrage entre services est impossible, car le trafic ne passe par aucun routeur ni pare-feu.<br><br>La solution historique était d’acheter un switch par service. Coûteux, rigide, et impossible à faire évoluer quand un bureau change d’affectation. Le <b>VLAN</b> (Virtual LAN) règle le problème par la configuration.' },
{ t: 'Principe du VLAN', d: '', html: 'Un VLAN est un domaine de diffusion logique identifié par un numéro, le <b>VLAN ID</b>, de 1 à 4094. Chaque port du switch est affecté à un VLAN. Deux ports dans des VLAN différents se comportent exactement comme s’ils étaient sur deux switchs séparés : aucune trame ne passe de l’un à l’autre.<br><br>Chaque VLAN correspond en pratique à un <b>sous-réseau IP</b> distinct. VLAN 10 pour l’administration en 10.62.10.0/24, VLAN 20 pour la pédagogie en 10.62.20.0/24, VLAN 30 pour le Wi-Fi invités, VLAN 99 pour l’administration des équipements. Cette correspondance un VLAN = un sous-réseau est la règle en entreprise ; la documenter évite bien des erreurs.<br><br>Le <b>VLAN 1</b> est le VLAN par défaut de tous les ports. Pour des raisons de sécurité, on ne l’utilise pas pour le trafic de production, et l’administration des switchs se fait dans un VLAN dédié.', table: [['VLAN', 'Nom', 'Sous-réseau', 'Passerelle'], ['10', 'ADMIN', '10.62.10.0/24', '10.62.10.1'], ['20', 'PEDAGO', '10.62.20.0/23', '10.62.20.1'], ['30', 'WIFI-INVITES', '10.62.30.0/24', '10.62.30.1'], ['40', 'SERVEURS', '10.62.40.0/27', '10.62.40.1'], ['99', 'MGMT', '10.62.99.0/28', '10.62.99.1']] },
{ t: 'Ports d’accès et ports trunk', d: '', html: 'Un <b>port d’accès</b> (access) appartient à un seul VLAN. C’est le port sur lequel on branche un poste, une imprimante, une caméra. L’équipement branché ne sait pas qu’il est dans un VLAN : il envoie des trames Ethernet ordinaires, le switch s’occupe de tout.<br><br>Un <b>port trunk</b> (appelé tagged chez certains constructeurs) transporte plusieurs VLAN à la fois. On l’utilise entre deux switchs, entre un switch et un routeur, entre un switch et un hyperviseur, ou vers une borne Wi-Fi qui diffuse plusieurs réseaux. Pour que le switch d’en face sache à quel VLAN appartient chaque trame, on y ajoute une étiquette : c’est le rôle de la norme <b>IEEE 802.1Q</b>.' },
{ t: 'La trame 802.1Q', d: '', html: 'La norme 802.1Q insère un <b>en-tête de 4 octets</b> dans la trame Ethernet, entre l’adresse MAC source et le champ EtherType. Ce <b>tag</b> contient l’identifiant de protocole 0x8100, 3 bits de priorité (PCP, utilisés pour la qualité de service de la téléphonie), 1 bit DEI et les 12 bits du VLAN ID. 12 bits donnent 4 096 valeurs, dont 0 et 4095 sont réservées : d’où la plage 1 à 4094.<br><br>Le switch ajoute le tag quand une trame sort par un trunk et le retire quand elle sort par un port d’accès. La trame s’allonge de 4 octets, ce qui explique que la taille maximale d’une trame étiquetée soit de 1 522 octets au lieu de 1 518.<br><br>Le <b>VLAN natif</b> est le VLAN dont les trames circulent sans tag sur un trunk. Les deux extrémités d’un trunk doivent avoir le même VLAN natif, sinon les trames non étiquetées changent de VLAN en traversant le lien.', exemple: '| MAC dest | MAC src | 0x8100 | PCP DEI VID=20 | EtherType 0x0800 | Données IP ... | FCS |\n <------- tag 802.1Q ------->', attention: 'Le VLAN natif est la cible de l’attaque par double étiquetage (VLAN hopping). Bonne pratique : choisir un VLAN natif inutilisé (par exemple 999), ne l’affecter à aucun port d’accès, et désactiver la négociation automatique des trunks (DTP chez Cisco).' },
{ t: 'Configurer un switch', d: '', html: 'La syntaxe varie selon les constructeurs, mais la logique est identique : créer les VLAN, affecter les ports d’accès, configurer les trunks en listant explicitement les VLAN autorisés. Voici l’exemple d’un switch Cisco, très présent dans les établissements et dans les laboratoires de BTS.', exemple: 'Switch(config)# vlan 10\nSwitch(config-vlan)# name ADMIN\nSwitch(config)# vlan 20\nSwitch(config-vlan)# name PEDAGO\n\nSwitch(config)# interface range fa0/1 - 12\nSwitch(config-if-range)# switchport mode access\nSwitch(config-if-range)# switchport access vlan 20\n\nSwitch(config)# interface gi0/1\nSwitch(config-if)# switchport mode trunk\nSwitch(config-if)# switchport trunk allowed vlan 10,20,30,99\nSwitch(config-if)# switchport trunk native vlan 999\nSwitch(config-if)# switchport nonegotiate\n\nSwitch# show vlan brief\nSwitch# show interfaces trunk' },
{ t: 'VLAN sous Linux', d: '', html: 'Un serveur Linux peut lui aussi recevoir un trunk et créer une <b>sous-interface</b> par VLAN. C’est le cas d’un routeur Linux, d’un pare-feu ou d’un hyperviseur qui héberge des VM dans plusieurs réseaux. Le module noyau 8021q gère l’étiquetage.', exemple: 'sudo ip link add link ens18 name ens18.10 type vlan id 10\nsudo ip addr add 10.62.10.1/24 dev ens18.10\nsudo ip link set ens18.10 up\n\nip -d link show ens18.10\n5: ens18.10@ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500\n vlan protocol 802.1Q id 10 <REORDER_HDR>' },
{ t: 'Le routage inter-VLAN', d: '', html: 'Puisque deux VLAN sont deux réseaux distincts, il faut un <b>routeur</b> pour les faire communiquer. C’est une bonne nouvelle : ce point de passage obligé est l’endroit où l’on filtre. On autorise la pédagogie à joindre le serveur de fichiers mais pas le serveur de vie scolaire, on autorise le Wi-Fi invités à sortir sur Internet mais à rien d’autre.<br><br>Trois architectures existent. Le <b>routeur sur un bâton</b> (router-on-a-stick) : un seul lien trunk vers un routeur ou un pare-feu qui possède une sous-interface par VLAN ; simple et économique, c’est la solution des petits sites et des laboratoires. Le <b>switch de niveau 3</b> : le switch route lui-même entre ses VLAN grâce à des interfaces virtuelles (SVI), à la vitesse du matériel ; c’est la solution des réseaux de campus. Le <b>pare-feu</b> en cœur de réseau, qui route et filtre à la fois ; c’est ce que recommandent les guides de cloisonnement.', exemple: '! Routeur sur un bâton (Cisco)\ninterface gi0/0.10\n encapsulation dot1Q 10\n ip address 10.62.10.1 255.255.255.0\ninterface gi0/0.20\n encapsulation dot1Q 20\n ip address 10.62.20.1 255.255.254.0\n\n! Switch de niveau 3\nip routing\ninterface vlan 10\n ip address 10.62.10.1 255.255.255.0' },
{ t: 'Dépanner des VLAN', d: '', html: 'Les pannes de VLAN ont une signature typique : un poste obtient une adresse d’un autre réseau, ou n’en obtient aucune (169.254.x.x), ou voit certains équipements et pas d’autres. La démarche consiste à vérifier, dans l’ordre, le VLAN du port d’accès, la présence du VLAN sur tous les trunks traversés, la cohérence du VLAN natif aux deux bouts, puis la sous-interface ou la SVI de la passerelle. <code>show vlan brief</code> et <code>show interfaces trunk</code> répondent aux trois premières questions ; <code>show mac address-table</code> montre dans quel VLAN le switch a vu passer la MAC du poste.' }
],
synthese: ['Un VLAN = un domaine de diffusion = un sous-réseau IP', 'Port d’accès : un VLAN, trames sans tag ; trunk : plusieurs VLAN, trames étiquetées 802.1Q', 'Tag de 4 octets, VLAN ID sur 12 bits : 1 à 4094', 'Même VLAN natif aux deux bouts d’un trunk, et un VLAN natif inutilisé', 'Lister explicitement les VLAN autorisés sur un trunk', 'Routage inter-VLAN = point de filtrage : router-on-a-stick, switch L3 ou pare-feu'],
qcm: [
['Combien de VLAN utilisables permet 802.1Q ?', ['1 024', '4 094', '4 096', '65 535'], 1, '12 bits, 0 et 4095 réservés.'],
['Sur quel type de port branche-t-on un poste élève ?', ['Trunk', 'Accès', 'Natif', 'Console'], 1, 'Un seul VLAN, trames non étiquetées.'],
['Quelle est la taille du tag 802.1Q ?', ['2 octets', '4 octets', '8 octets', '12 bits'], 1, '4 octets, dont 12 bits de VLAN ID.'],
['Deux postes dans deux VLAN différents sur le même switch peuvent communiquer :', ['directement', 'seulement via un routeur ou un switch L3', 'jamais', 'seulement en IPv6'], 1, 'Deux réseaux distincts.'],
['Qu’est-ce que le VLAN natif ?', ['Le VLAN 1 obligatoirement', 'Le VLAN dont les trames circulent sans tag sur un trunk', 'Le VLAN d’administration', 'Le VLAN le plus utilisé'], 1, 'Il doit être identique des deux côtés.'],
['Quelle commande Cisco affiche les trunks et les VLAN qu’ils transportent ?', ['show vlan brief', 'show interfaces trunk', 'show ip route', 'show running-config vlan'], 1, 'Elle liste aussi le VLAN natif.'],
['Un poste d’une salle obtient une adresse du Wi-Fi invités. Cause probable ?', ['Câble abîmé', 'Port d’accès dans le mauvais VLAN', 'DNS en panne', 'Carte réseau défectueuse'], 1, 'Le DHCP qui répond est celui du VLAN du port.'],
['Quel est l’avantage sécurité du routage inter-VLAN ?', ['Aucun', 'Un point de passage où l’on peut filtrer', 'Il chiffre le trafic', 'Il supprime ARP'], 1, 'On y place les règles de pare-feu.'],
['Sous Linux, quelle commande crée la sous-interface du VLAN 20 sur ens18 ?', ['vlan add 20 ens18', 'ip link add link ens18 name ens18.20 type vlan id 20', 'ip addr add vlan 20', 'ifconfig ens18:20'], 1, 'Module 8021q.'],
['Pourquoi éviter d’utiliser le VLAN 1 ?', ['Il est lent', 'Tous les ports y sont par défaut et il est visé par des attaques', 'Il est payant', 'Il ne supporte pas IP'], 1, 'Bonne pratique de sécurité.']
],
td: [
{ n: 'Échauffement', c: 'Pour chaque équipement, dire s’il se branche sur un port d’accès ou un trunk : poste élève, lien entre deux switchs, imprimante, borne Wi-Fi diffusant 3 réseaux, hyperviseur hébergeant des VM de plusieurs VLAN, caméra.', r: 'accès · trunk · accès · trunk · trunk · accès' },
{ n: 'Application', c: 'À partir du plan de VLAN du cours, écrire la configuration Cisco d’un switch de 24 ports : ports 1 à 16 en PEDAGO, 17 à 20 en ADMIN, 21 et 22 en SERVEURS, port Gi0/1 en trunk vers le cœur avec VLAN natif 999.', r: 'vlan 10/20/40/99/999 + name\ninterface range fa0/1-16 → switchport mode access, switchport access vlan 20\ninterface range fa0/17-20 → vlan 10\ninterface range fa0/21-22 → vlan 40\ninterface gi0/1 → switchport mode trunk, allowed vlan 10,20,30,40,99, native vlan 999, nonegotiate' },
{ n: 'Application', c: 'Configurer sur un routeur Linux le routage entre les VLAN 10 et 20 reçus en trunk sur ens19, avec les passerelles du plan.', r: 'ip link add link ens19 name ens19.10 type vlan id 10\nip addr add 10.62.10.1/24 dev ens19.10\nip link add link ens19 name ens19.20 type vlan id 20\nip addr add 10.62.20.1/23 dev ens19.20\nip link set ens19.10 up ; ip link set ens19.20 up\nsysctl -w net.ipv4.ip_forward=1' },
{ n: 'Application', c: 'Un poste du VLAN 20 branché sur le switch B n’obtient pas d’adresse, alors que les postes du VLAN 20 du switch A fonctionnent. Proposer trois vérifications et leurs commandes.', r: 'show vlan brief sur B (le port est-il en VLAN 20 ? le VLAN 20 existe-t-il ?)\nshow interfaces trunk sur A et B (le VLAN 20 est-il autorisé sur le trunk ?)\nnatif identique des deux côtés ; show mac address-table interface <port>' },
{ n: 'Approfondissement', c: 'Expliquer le principe de l’attaque par double étiquetage et les deux configurations qui l’empêchent.', r: 'L’attaquant, dans le VLAN natif, envoie une trame avec deux tags : le premier (natif) est retiré par le premier switch, le second l’envoie dans le VLAN cible sur le trunk. Parades : VLAN natif inutilisé et différent de tous les VLAN d’accès, et étiquetage du VLAN natif (vlan dot1q tag native) ; désactiver DTP.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un bridge Linux avec filtrage VLAN, trois VM clientes et un routeur Linux sur un bâton.', etapes: ['Créer les VLAN 10 et 20 sur le bridge', 'Placer chaque VM sur son port d’accès', 'Vérifier que les deux VLAN ne se voient pas', 'Configurer le trunk et les sous-interfaces du routeur', 'Activer le routage et tester le ping inter-VLAN', 'Ajouter une règle nftables qui bloque VLAN 20 → VLAN 10'] },
plusLoin: [['IEEE 802.1Q (présentation de la norme)', 'https://standards.ieee.org/ieee/802.1Q/10323/'], ['ANSSI · Recommandations relatives à l’interconnexion d’un système d’information à Internet', 'https://cyber.gouv.fr/publications/definition-dune-architecture-de-passerelle-dinterconnexion-securisee'], ['Documentation iproute2 · ip-link (type vlan)', 'https://man7.org/linux/man-pages/man8/ip-link.8.html']] },
plus: {
cas: { titre: 'Mise en situation · Isoler le Wi-Fi des invités', texte: 'Lors des journées portes ouvertes, les visiteurs se connectent au Wi-Fi du lycée, qui est aujourd’hui sur le même réseau que les postes pédagogiques. Le proviseur demande de séparer les deux. Le technicien crée le VLAN 30 WIFI-INVITES, le transporte en trunk jusqu’aux bornes, associe le SSID « Lycee-Invites » à ce VLAN, crée une sous-interface sur le pare-feu avec une étendue DHCP dédiée, et n’autorise depuis ce VLAN que DNS, HTTP et HTTPS vers Internet. Il vérifie depuis un smartphone qu’aucun poste interne n’est joignable.' },
erreurs: ['Oublier d’autoriser un nouveau VLAN sur un trunk intermédiaire', 'Deux VLAN natifs différents aux deux bouts d’un trunk', 'Laisser tous les postes dans le VLAN 1', 'Créer le VLAN sur un seul switch', 'Confondre VLAN et sous-réseau dans la documentation', 'Oublier d’activer ip_forward sur un routeur Linux', 'Oublier la sous-interface ou la SVI de passerelle du nouveau VLAN'],
memo: [['Tâche', 'Cisco', 'Linux'], ['Créer un VLAN', 'vlan 20 / name PEDAGO', 'bridge vlan add vid 20 dev <port>'], ['Port d’accès', 'switchport mode access / access vlan 20', 'bridge vlan add vid 20 dev tap0 pvid untagged'], ['Trunk', 'switchport mode trunk / trunk allowed vlan …', 'ip link add link ens18 name ens18.20 type vlan id 20'], ['VLAN natif', 'switchport trunk native vlan 999', 'trames non étiquetées sur ens18'], ['Voir les VLAN', 'show vlan brief', 'bridge vlan show'], ['Voir les trunks', 'show interfaces trunk', 'ip -d link show'], ['Passerelle inter-VLAN', 'interface vlan 20 / ip address …', 'ip addr add … dev ens18.20'], ['Activer le routage', 'ip routing', 'sysctl -w net.ipv4.ip_forward=1'], ['Table MAC', 'show mac address-table', 'bridge fdb show']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer la différence entre un port d’accès et un port trunk, avec un exemple d’usage chacun. (3 pts)', 'Décrire le contenu du tag 802.1Q et justifier la plage 1 à 4094. (3 pts)', 'Écrire la configuration du switch et du routeur pour 3 VLAN donnés, en router-on-a-stick. (8 pts)', 'Diagnostiquer un cas de poste sans adresse à partir de sorties show fournies. (4 pts)', 'Citer deux bonnes pratiques de sécurité liées aux VLAN. (2 pts)'], r: ['Accès : un VLAN, poste ; trunk : plusieurs VLAN étiquetés, lien inter-switch', 'TPID 0x8100, PCP, DEI, VID 12 bits ; 0 et 4095 réservés', 'vlan / access / trunk allowed / sous-interfaces dot1Q + IP', 'Selon les sorties : VLAN absent du trunk, mauvais VLAN d’accès, natif incohérent', 'VLAN natif inutilisé, pas de VLAN 1, trunks explicites, DTP désactivé'] },
prof: { deroule: [['Ouverture', 'Démonstration : capture d’un DHCP visible par toute la salle, puis la même chose après séparation en VLAN'], ['Cours', 'Domaine de diffusion, VLAN, accès et trunk, tag 802.1Q'], ['Démonstration', 'Configuration d’un switch en direct, show vlan brief'], ['Pratique', 'Lab VM : bridge VLAN, routeur sur un bâton, filtrage'], ['Clôture', 'Chaque binôme présente son plan de VLAN et ses règles de filtrage']], diff: 'Élèves en difficulté : plan de VLAN fourni, configuration à trous. Élèves avancés : switch de niveau 3, VLAN hopping en lab isolé, VLAN et hyperviseur Proxmox.', eval: 'Étapes du lab validées automatiquement, relevé de la configuration, sujet type.' }
},
exemples: [
['Vérifier les VLAN d’un switch', 'Avant d’ajouter une salle.', 'Switch# show vlan brief', 'VLAN Name Status Ports\n1 default active Fa0/23, Fa0/24\n10 ADMIN active Fa0/17, Fa0/18, Fa0/19, Fa0/20\n20 PEDAGO active Fa0/1 - Fa0/16\n40 SERVEURS active Fa0/21, Fa0/22', 'On lit l’affectation de chaque port. Les ports 23 et 24 sont restés en VLAN 1 : à affecter ou à désactiver (shutdown) pour qu’aucun appareil branché dessus ne se retrouve dans le VLAN par défaut.'],
['Contrôler un trunk', 'Un VLAN ne passe pas entre deux bâtiments.', 'Switch# show interfaces trunk', 'Port Mode Encapsulation Status Native vlan\nGi0/1 on 802.1q trunking 999\n\nPort Vlans allowed on trunk\nGi0/1 10,20,99', 'Le VLAN 30 (Wi-Fi invités) n’est pas dans la liste autorisée : les bornes de l’autre bâtiment ne le reçoivent pas. Correction : switchport trunk allowed vlan add 30.'],
['Voir un tag 802.1Q', 'Vérifier qu’un trunk arrive bien étiqueté sur le routeur Linux.', 'sudo tcpdump -eni ens19 -c 2 vlan', '10:02:11 bc:24:11:5e:2a:10 > ff:ff:ff:ff:ff:ff, ethertype 802.1Q (0x8100), length 346: vlan 20, p 0, ethertype IPv4, 0.0.0.0.68 > 255.255.255.255.67: DHCP Discover', '-e affiche l’en-tête Ethernet. On voit le tag 0x8100, le VLAN 20 et la priorité 0 : une demande DHCP d’un poste du VLAN 20 arrive bien sur le trunk.'],
['Créer une sous-interface Linux', 'Routeur Linux pour le laboratoire.', 'sudo ip link add link ens19 name ens19.30 type vlan id 30\nsudo ip addr add 10.62.30.1/24 dev ens19.30\nsudo ip link set ens19.30 up\nip -br addr show ens19.30', 'ens19.30@ens19 UP 10.62.30.1/24', 'Le routeur devient la passerelle du VLAN 30. Pour la rendre permanente, on déclare l’interface dans /etc/network/interfaces (iface ens19.30 inet static).'],
['Retrouver le port d’un poste', 'Un poste suspect doit être localisé physiquement.', 'Switch# show mac address-table address 00:15:5d:0a:11:07', 'Vlan Mac Address Type Ports\n20 0015.5d0a.1107 DYNAMIC Fa0/9', 'Le poste est en VLAN 20 sur le port 9. Avec le plan de brassage, on retrouve la prise murale de la salle.'],
['Filtrer entre VLAN', 'Interdire à la pédagogie d’accéder au VLAN administration.', 'sudo nft add rule inet filter forward iifname "ens19.20" oifname "ens19.10" drop\nsudo nft list chain inet filter forward', 'chain forward {\n iifname "ens19.20" oifname "ens19.10" drop\n}', 'Le routeur inter-VLAN est l’endroit idéal pour filtrer : tout le trafic entre VLAN passe par lui. On ajoute ensuite les exceptions nécessaires (serveur d’impression, par exemple) avant la règle de blocage.']
],
long: { figure: svg(720, 240, bx(250, 10, 220, 50, N, 'Routeur / pare-feu', 15) + '<line x1="360" y1="60" x2="360" y2="110" ' + K + ' stroke-dasharray="8 5"/>' + tx(372, 92, 'trunk 802.1Q : VLAN 10, 20, 30', 13) + bx(200, 110, 320, 50, W, 'Switch', 16) + [['VLAN 10 · ADMIN', Y], ['VLAN 20 · PEDAGO', G], ['VLAN 30 · INVITÉS', P]].map(function (v, i) { return '<line x1="' + (260 + i * 100) + '" y1="160" x2="' + (110 + i * 250) + '" y2="190" ' + K + '/>' + bx(20 + i * 250, 190, 180, 44, v[1], v[0], 14); }).join('')), legende: 'Un seul switch, trois réseaux étanches. Le passage de l’un à l’autre se fait par le routeur, où l’on filtre.', photo: 'Photo : switch administrable avec câbles de couleur par VLAN',
lecture: ['Un VLAN, c’est une cloison posée dans un switch. Physiquement, tous les câbles arrivent au même endroit ; logiquement, chaque groupe de ports forme son propre réseau. On peut ainsi réorganiser un établissement entier depuis un clavier : déplacer un bureau, créer une salle d’examen isolée, ouvrir un réseau pour un événement, sans jamais toucher à un câble.', 'Les couleurs de câble dans les armoires ne sont pas décoratives : beaucoup d’établissements attribuent une couleur à chaque VLAN pour repérer d’un coup d’œil les usages. Ce n’est qu’une aide visuelle, la vérité est dans la configuration, mais elle évite des erreurs de brassage.', 'Le trunk est la notion clé. Un seul câble entre deux switchs transporte tous les VLAN, chaque trame portant son étiquette. Si un VLAN n’est pas autorisé sur un seul des trunks du chemin, il s’arrête là. La majorité des pannes de VLAN viennent de cet oubli.', 'Enfin, les VLAN sont un outil de sécurité, pas une sécurité en soi. Ils séparent, mais c’est le routeur ou le pare-feu entre les VLAN qui décide de ce qui passe. Un plan de VLAN sans règles de filtrage entre eux n’apporte qu’une partie du bénéfice.'] },
schemas: { termes: ['VLAN', '802.1Q', 'trunk', 'VLAN natif', 'port d’accès', 'tag', 'VLAN ID', 'SVI', 'switchport', 'show vlan brief', 'show interfaces trunk', 'show mac address-table', 'dot1Q', 'ip_forward', 'router-on-a-stick', 'DTP', 'domaine de diffusion', 'nft'],
figs: [[3, 'La trame étiquetée 802.1Q', svg(720, 110, bx(10, 30, 110, 50, N, 'MAC dest', 13) + bx(120, 30, 110, 50, N, 'MAC src', 13) + bx(230, 30, 150, 50, Y, 'tag 0x8100 · VID', 13) + bx(380, 30, 110, 50, N, 'EtherType', 13) + bx(490, 30, 150, 50, W, 'Données', 13) + bx(640, 30, 70, 50, N, 'FCS', 13) + tx(230, 100, '4 octets ajoutés sur le trunk', 13))]] }
});
// ============ DHCP ============
reg({
cours: { id: 'dhcp', match: /DHCP/i,
titre: 'DHCP : attribuer automatiquement les adresses, serveur, relais et baux', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T S2 · TSSR',
prerequis: ['Adressage IPv4 et sous-réseaux', 'VLAN 802.1Q (pour le relais)'],
competences: ['BTS SIO SISR · Bloc 2 · Installer, configurer et administrer un service réseau', 'BTS SIO · Bloc 1 · Mettre à disposition des utilisateurs un service informatique', 'BTS CIEL · C04 · Installer un service réseau', 'BUT R&T · Compétence « Administrer »'],
intro: 'Configurer à la main l’adresse, le masque, la passerelle et les serveurs DNS de 800 postes, 200 imprimantes et des centaines de téléphones qui arrivent chaque matin sur le Wi-Fi est impossible. Le protocole DHCP le fait automatiquement, en quelques millisecondes, à chaque connexion. Quand il tombe en panne, plus personne n’a de réseau. Savoir l’installer, le dimensionner, le sécuriser et le dépanner est donc une compétence de base du technicien réseau et un grand classique de l’épreuve SISR.',
cm: [
{ t: 'Ce que DHCP distribue', d: '', html: 'Le protocole <b>DHCP</b> (Dynamic Host Configuration Protocol, RFC 2131) fournit à un client toute sa configuration réseau : une <b>adresse IP</b> et son <b>masque</b>, la <b>passerelle</b> par défaut (option 3), les <b>serveurs DNS</b> (option 6), le <b>nom de domaine</b> (option 15), et selon les besoins le serveur NTP, le serveur de démarrage réseau PXE (options 66 et 67) ou le contrôleur des bornes Wi-Fi.<br><br>Cette configuration n’est pas donnée définitivement mais <b>prêtée</b> pour une durée limitée : le <b>bail</b>. À mi-bail, le client demande à le renouveler. S’il disparaît, l’adresse redevient libre à l’expiration. C’est ce qui permet de servir plus d’appareils qu’il n’y a d’adresses, tant qu’ils ne sont pas tous connectés en même temps.' },
{ t: 'L’échange DORA', d: '', html: 'L’obtention d’un bail se fait en quatre messages, que l’on retient par l’acronyme <b>DORA</b>. Au départ, le client n’a pas d’adresse et ne connaît pas le serveur : il communique en <b>diffusion</b>, depuis l’adresse 0.0.0.0 vers 255.255.255.255, en UDP du port <b>68</b> (client) vers le port <b>67</b> (serveur).<br><br><b>Discover</b> : le client crie « y a-t-il un serveur DHCP ? ». <b>Offer</b> : chaque serveur qui l’entend propose une adresse. <b>Request</b> : le client choisit une offre et la demande officiellement, toujours en diffusion, pour que les autres serveurs sachent que leur offre est refusée. <b>Ack</b> : le serveur choisi confirme et le bail commence.<br><br>Avant d’utiliser l’adresse, un client bien conçu vérifie par une requête ARP que personne ne l’utilise déjà. Si c’est le cas, il envoie un <b>Decline</b> et recommence.', table: [['Message', 'Émetteur', 'Destination', 'Rôle'], ['DHCPDISCOVER', 'client 0.0.0.0:68', '255.255.255.255:67', 'chercher un serveur'], ['DHCPOFFER', 'serveur :67', 'client :68', 'proposer une adresse'], ['DHCPREQUEST', 'client 0.0.0.0:68', '255.255.255.255:67', 'accepter une offre'], ['DHCPACK', 'serveur :67', 'client :68', 'confirmer le bail'], ['DHCPNAK', 'serveur', 'client', 'refuser (adresse invalide)'], ['DHCPRELEASE', 'client', 'serveur', 'rendre l’adresse']] },
{ t: 'Le cycle de vie d’un bail', d: '', html: 'Un bail ne se renouvelle pas au dernier moment. À <b>50 %</b> de sa durée (temps T1), le client envoie un Request directement à son serveur. Si le serveur répond, le bail repart pour une durée complète. Sans réponse, le client réessaie ; à <b>87,5 %</b> (temps T2), il passe en diffusion et accepte la réponse de n’importe quel serveur. À l’expiration, il perd son adresse et recommence un Discover.<br><br>Le choix de la durée est un compromis. Un bail long (8 jours) réduit le trafic et rend le réseau plus tolérant à une panne du serveur, mais garde les adresses des appareils partis. Un bail court (1 heure) libère vite les adresses, indispensable sur un Wi-Fi invités très fréquenté, mais multiplie les échanges. Dans un établissement, on choisit typiquement 8 heures à 1 jour pour les postes fixes et 1 à 2 heures pour le Wi-Fi.' },
{ t: 'Étendues, exclusions et réservations', d: '', html: 'Une <b>étendue</b> (scope, ou subnet dans ISC) est la plage d’adresses que le serveur distribue sur un sous-réseau, avec ses options. On n’y met jamais tout le sous-réseau : on <b>exclut</b> les adresses fixes des équipements d’infrastructure (passerelle, serveurs, switchs, imprimantes réseau).<br><br>Une <b>réservation</b> associe une adresse précise à une adresse MAC. L’imprimante de la salle des professeurs reçoit ainsi toujours la même adresse tout en restant gérée par DHCP, ce qui est plus simple à maintenir qu’une adresse fixe configurée sur l’appareil : un changement de DNS, par exemple, se fait en un seul endroit.', table: [['Plage', 'Usage'], ['10.62.20.1', 'passerelle (exclue)'], ['10.62.20.2 à 10.62.20.19', 'serveurs et équipements fixes (exclus)'], ['10.62.20.20 à 10.62.21.200', 'étendue distribuée aux postes'], ['10.62.21.201 à 10.62.21.254', 'réservations (imprimantes, bornes)']] },
{ t: 'Installer un serveur DHCP sous Debian', d: '', html: 'Le serveur historique <b>isc-dhcp-server</b> est encore très répandu dans les établissements et au programme des référentiels. Son successeur, <b>Kea</b>, développé par le même organisme (ISC), est recommandé pour les nouvelles installations ; sa configuration est en JSON mais les notions sont identiques. On configure d’abord l’interface d’écoute, puis les étendues.', exemple: '# /etc/default/isc-dhcp-server\nINTERFACESv4="ens18"\n\n# /etc/dhcp/dhcpd.conf\noption domain-name "lycee.local";\noption domain-name-servers 10.62.40.10, 10.62.40.11;\ndefault-lease-time 28800; # 8 h\nmax-lease-time 86400; # 24 h\nauthoritative;\n\nsubnet 10.62.20.0 netmask 255.255.254.0 {\n range 10.62.20.20 10.62.21.200;\n option routers 10.62.20.1;\n}\n\nhost imprimante-sdp {\n hardware ethernet 00:1b:a9:4c:22:10;\n fixed-address 10.62.21.210;\n}\n\n# Vérifier puis démarrer\nsudo dhcpd -t -cf /etc/dhcp/dhcpd.conf\nsudo systemctl restart isc-dhcp-server', attention: 'Le serveur doit posséder une adresse fixe dans chaque sous-réseau qu’il sert directement, et ne doit pas être client DHCP lui-même. Validez toujours la syntaxe avec dhcpd -t avant de redémarrer : une erreur de point-virgule arrête le service pour tout l’établissement.' },
{ t: 'Le relais DHCP', d: '', html: 'Le Discover est une <b>diffusion</b> : il ne traverse pas les routeurs. Or dans un réseau segmenté en VLAN, on ne veut pas un serveur DHCP par VLAN. La solution est le <b>relais DHCP</b> (DHCP relay agent, commande ip helper-address chez Cisco) : le routeur ou le switch de niveau 3 qui sert de passerelle au VLAN intercepte le Discover et le retransmet en <b>unicast</b> au serveur central.<br><br>Le relais remplit le champ <b>giaddr</b> (gateway IP address) avec son adresse dans le VLAN du client. C’est ce champ qui permet au serveur de savoir de quel sous-réseau vient la demande, et donc dans quelle étendue choisir l’adresse. Un serveur qui reçoit une demande relayée sans étendue correspondant au giaddr ne répond pas.', exemple: '! Cisco, sur la passerelle du VLAN 20\ninterface vlan 20\n ip address 10.62.20.1 255.255.254.0\n ip helper-address 10.62.40.5\n\n# Linux, paquet isc-dhcp-relay\n# /etc/default/isc-dhcp-relay\nSERVERS="10.62.40.5"\nINTERFACES="ens19.20 ens19.30 ens19.40"' },
{ t: 'Sécurité : faux serveurs et épuisement', d: '', html: 'DHCP n’a aucune authentification. Le premier serveur qui répond l’emporte. Un élève qui branche une box ou lance un serveur DHCP sur son ordinateur peut distribuer une fausse passerelle et intercepter tout le trafic de la salle : c’est le <b>faux serveur DHCP</b> (rogue DHCP). Une variante consiste à envoyer des milliers de Discover avec des MAC aléatoires pour vider l’étendue : c’est l’<b>épuisement</b> (DHCP starvation), qui prive tout le monde d’adresse.<br><br>La parade principale est le <b>DHCP snooping</b> des switchs : seuls les ports déclarés de confiance (trunk vers le serveur) ont le droit d’émettre des Offer et des Ack ; sur les ports d’accès, ces messages sont jetés. Couplé à une limite de débit par port et à la sécurité de port (nombre maximal de MAC), il neutralise les deux attaques.', exemple: 'Switch(config)# ip dhcp snooping\nSwitch(config)# ip dhcp snooping vlan 20,30\nSwitch(config)# interface gi0/1\nSwitch(config-if)# ip dhcp snooping trust\nSwitch(config)# interface range fa0/1 - 24\nSwitch(config-if-range)# ip dhcp snooping limit rate 10' },
{ t: 'Dépanner DHCP', d: '', html: 'Le symptôme le plus courant est un poste en <b>169.254.x.x</b> : il n’a reçu aucune réponse. On vérifie dans l’ordre : le lien et le VLAN du port, la présence du relais sur la passerelle du VLAN, l’état du service et ses journaux sur le serveur, puis l’étendue elle-même (pleine ?). Le second symptôme est un poste qui reçoit une adresse d’un mauvais réseau : faux serveur, ou port dans le mauvais VLAN.<br><br>Côté client Linux, <code>sudo dhclient -v ens18</code> rejoue l’échange en affichant chaque message. Côté serveur, <code>journalctl -u isc-dhcp-server</code> montre les DHCPDISCOVER reçus et les réponses, et le fichier <code>/var/lib/dhcp/dhcpd.leases</code> liste tous les baux en cours. Une capture <code>tcpdump -ni ens18 port 67 or port 68</code> tranche en cas de doute.' }
],
synthese: ['DHCP distribue adresse, masque, passerelle, DNS et options pour la durée d’un bail', 'DORA : Discover, Offer, Request, Ack, en UDP 67/68 et en diffusion', 'Renouvellement à 50 % (T1) puis 87,5 % (T2)', 'Étendue + exclusions + réservations par MAC', 'Relais DHCP (ip helper-address) : la diffusion traverse le routeur grâce au champ giaddr', 'DHCP snooping contre les faux serveurs et l’épuisement', '169.254.x.x = aucun serveur n’a répondu'],
qcm: [
['Quels ports utilise DHCP ?', ['TCP 67/68', 'UDP 67 (serveur) et 68 (client)', 'UDP 53', 'TCP 80'], 1, 'UDP, serveur 67, client 68.'],
['Dans quel ordre se déroule l’échange ?', ['Request, Offer, Discover, Ack', 'Discover, Offer, Request, Ack', 'Offer, Discover, Ack, Request', 'Discover, Request, Offer, Ack'], 1, 'DORA.'],
['Pourquoi le Request est-il envoyé en diffusion ?', ['Par erreur', 'Pour informer les autres serveurs que leur offre est refusée', 'Pour aller plus vite', 'Parce que le client a déjà une IP'], 1, 'Tous les serveurs qui ont fait une offre l’entendent.'],
['À quel moment un client tente-t-il de renouveler son bail ?', ['À l’expiration', 'À 50 % de la durée', 'Au redémarrage seulement', 'Toutes les heures'], 1, 'T1 = 50 %.'],
['Pourquoi un serveur DHCP central ne reçoit-il pas les demandes d’un autre VLAN sans configuration ?', ['Le pare-feu bloque', 'Le Discover est une diffusion qui ne traverse pas les routeurs', 'DHCP ne supporte pas les VLAN', 'Il faut IPv6'], 1, 'Il faut un relais.'],
['Quel champ permet au serveur de choisir la bonne étendue pour une demande relayée ?', ['chaddr', 'giaddr', 'yiaddr', 'ciaddr'], 1, 'L’adresse du relais dans le VLAN du client.'],
['Quelle fonction de switch bloque les faux serveurs DHCP ?', ['Spanning tree', 'DHCP snooping', 'Port mirroring', 'LLDP'], 1, 'Seuls les ports de confiance peuvent envoyer Offer et Ack.'],
['Un poste obtient 169.254.12.4. Première hypothèse ?', ['Il est sur Internet', 'Aucun serveur DHCP n’a répondu', 'Le DNS est en panne', 'Il a une adresse IPv6'], 1, 'APIPA.'],
['Pour que l’imprimante ait toujours la même adresse tout en restant gérée par DHCP, on utilise :', ['une exclusion', 'une réservation par adresse MAC', 'un bail infini', 'une adresse APIPA'], 1, 'host … hardware ethernet … fixed-address.'],
['Quelle commande valide la configuration d’isc-dhcp-server ?', ['dhcpd -t -cf /etc/dhcp/dhcpd.conf', 'systemctl check dhcp', 'dhcp --test', 'dhclient -t'], 0, 'À lancer avant chaque redémarrage.'],
['Quelle durée de bail choisir pour un Wi-Fi invités très fréquenté ?', ['30 jours', '8 jours', '1 à 2 heures', 'Infinie'], 2, 'Pour libérer rapidement les adresses.']
],
td: [
{ n: 'Échauffement', c: 'Compléter le tableau DORA : pour chaque message, adresse et port source, adresse et port destination.', r: 'Discover : 0.0.0.0:68 → 255.255.255.255:67\nOffer : serveur:67 → client ou diffusion:68\nRequest : 0.0.0.0:68 → 255.255.255.255:67\nAck : serveur:67 → client ou diffusion:68' },
{ n: 'Échauffement', c: 'Un bail dure 8 heures et commence à 8 h. À quelle heure le client tente-t-il de le renouveler auprès de son serveur, puis auprès de n’importe quel serveur ?', r: 'T1 = 50 % → 12 h ; T2 = 87,5 % → 15 h' },
{ n: 'Application', c: 'Écrire la configuration dhcpd.conf pour le VLAN 30 invités (10.62.30.0/24) : passerelle .1, DNS 10.62.40.10, étendue de .20 à .250, bail de 1 heure.', r: 'subnet 10.62.30.0 netmask 255.255.255.0 {\n range 10.62.30.20 10.62.30.250;\n option routers 10.62.30.1;\n option domain-name-servers 10.62.40.10;\n default-lease-time 3600;\n max-lease-time 7200;\n}' },
{ n: 'Application', c: 'Le serveur DHCP est dans le VLAN 40 (10.62.40.5). Configurer la passerelle Cisco des VLAN 20 et 30 pour que leurs postes obtiennent une adresse.', r: 'interface vlan 20\n ip helper-address 10.62.40.5\ninterface vlan 30\n ip helper-address 10.62.40.5\n(et une étendue par sous-réseau sur le serveur)' },
{ n: 'Application', c: 'Tous les postes de la salle B204 reçoivent une adresse en 192.168.1.x alors que la salle est en 10.62.20.0/23. Diagnostiquer et corriger.', r: 'Un faux serveur (box, VM en mode pont) répond plus vite que le vrai. Le trouver : tcpdump -ni ens18 port 67 et relever la MAC source de l’Offer, puis show mac address-table pour le port. Prévention : DHCP snooping.' },
{ n: 'Approfondissement', c: 'L’étendue des invités (231 adresses, bail de 8 h) est pleine chaque matin de journée portes ouvertes. Proposer deux solutions et les chiffrer.', r: 'Réduire le bail à 1 h : les adresses des visiteurs partis sont libérées 7 h plus tôt. Agrandir en /23 (≈ 500 adresses). Idéalement les deux.' },
{ n: 'Approfondissement', c: 'Expliquer le principe de l’attaque par épuisement DHCP et les deux protections à configurer sur un switch.', r: 'Des milliers de Discover avec des MAC aléatoires réservent toutes les adresses. Parades : ip dhcp snooping limit rate sur les ports d’accès, et port-security avec un nombre maximal de MAC par port.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un serveur isc-dhcp-server, un relais Linux et des clients dans deux VLAN.', etapes: ['Installer isc-dhcp-server et fixer l’adresse du serveur', 'Créer l’étendue du VLAN serveurs et tester avec dhclient -v', 'Ajouter une réservation par MAC', 'Configurer le relais pour le VLAN 20', 'Observer DORA avec tcpdump', 'Consulter les baux dans dhcpd.leases'] },
plusLoin: [['RFC 2131 · DHCP', 'https://www.rfc-editor.org/rfc/rfc2131'], ['RFC 2132 · options DHCP', 'https://www.rfc-editor.org/rfc/rfc2132'], ['ISC Kea · documentation', 'https://kea.readthedocs.io/'], ['Debian Wiki · DHCP Server', 'https://wiki.debian.org/DHCP_Server']] },
plus: {
cas: { titre: 'Mise en situation · La rentrée et ses 600 nouveaux appareils', texte: 'Le jour de la rentrée, les élèves de BTS connectent leurs ordinateurs personnels au Wi-Fi pédagogique. À 10 h, les nouveaux arrivants n’obtiennent plus d’adresse. Le technicien consulte les journaux du serveur : « no free leases » sur l’étendue Wi-Fi. L’étendue compte 200 adresses avec un bail de 7 jours, dimensionnée pour l’année précédente. Il passe le bail à 4 heures, étend le sous-réseau en /23 après accord de l’équipe réseau du rectorat, met à jour l’étendue, puis ajoute une supervision qui alerte à 80 % d’occupation.' },
erreurs: ['Inclure l’adresse de la passerelle dans l’étendue', 'Oublier le relais sur un nouveau VLAN', 'Laisser le serveur DHCP en client DHCP', 'Redémarrer le service sans dhcpd -t : une faute de syntaxe coupe tout le monde', 'Choisir un bail de plusieurs jours sur un Wi-Fi public', 'Oublier authoritative et laisser les clients garder des adresses d’un ancien réseau', 'Ne pas activer le DHCP snooping'],
memo: [['Besoin', 'Commande ou fichier'], ['Configurer le serveur', '/etc/dhcp/dhcpd.conf'], ['Choisir l’interface', '/etc/default/isc-dhcp-server'], ['Valider la syntaxe', 'dhcpd -t -cf /etc/dhcp/dhcpd.conf'], ['Redémarrer', 'systemctl restart isc-dhcp-server'], ['Voir les journaux', 'journalctl -u isc-dhcp-server -f'], ['Voir les baux', '/var/lib/dhcp/dhcpd.leases'], ['Tester côté client', 'dhclient -v ens18 · dhclient -r'], ['Capturer l’échange', 'tcpdump -ni ens18 port 67 or port 68'], ['Relais Cisco', 'ip helper-address <serveur>'], ['Protéger', 'ip dhcp snooping · trust sur le trunk']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire l’échange DORA avec adresses et ports. (4 pts)', 'Écrire la configuration d’un serveur servant deux sous-réseaux, avec une réservation. (6 pts)', 'Expliquer le rôle du relais et du champ giaddr, et configurer le relais sur la passerelle. (4 pts)', 'Diagnostiquer un poste en 169.254.x.x à partir des sorties fournies. (4 pts)', 'Présenter le DHCP snooping. (2 pts)'], r: ['Voir tableau DORA du cours', 'deux blocs subnet + host … fixed-address', 'Relaie la diffusion en unicast ; giaddr = adresse du relais qui désigne l’étendue ; ip helper-address', 'VLAN, relais, service, étendue pleine', 'Ports de confiance, Offer/Ack jetés ailleurs'] },
prof: { deroule: [['Ouverture', 'Chaque élève tape ip addr : d’où vient cette adresse ?'], ['Cours', 'DORA, bail, étendues, réservations'], ['Démonstration', 'tcpdump pendant un dhclient -v projeté au tableau'], ['Pratique', 'Lab VM : serveur, réservation, relais'], ['Cours', 'Sécurité : faux serveur et snooping'], ['Clôture', 'Quiz flash de 5 questions']], diff: 'Élèves en difficulté : configuration à compléter, schéma DORA imprimé. Élèves avancés : migration vers Kea, basculement entre deux serveurs, options PXE.', eval: 'Étapes du lab validées, analyse d’une capture DORA, sujet type.' }
},
exemples: [
['Rejouer DORA côté client', 'Un poste Linux n’a pas d’adresse.', 'sudo dhclient -r ens18 && sudo dhclient -v ens18', 'DHCPDISCOVER on ens18 to 255.255.255.255 port 67 interval 3\nDHCPOFFER of 10.62.20.57 from 10.62.20.1\nDHCPREQUEST for 10.62.20.57 on ens18 to 255.255.255.255 port 67\nDHCPACK of 10.62.20.57 from 10.62.20.1\nbound to 10.62.20.57 -- renewal in 13906 seconds.', '-r rend le bail, -v affiche chaque message. L’offre vient de 10.62.20.1 : c’est le relais sur la passerelle. Renouvellement dans 13 906 s, soit environ la moitié d’un bail de 8 h.'],
['Lire les journaux du serveur', 'Des postes n’obtiennent plus d’adresse.', 'journalctl -u isc-dhcp-server --since "10 min ago" | tail -3', 'DHCPDISCOVER from 3c:22:fb:10:7a:01 via 10.62.30.1: network 10.62.30.0/24: no free leases\nDHCPDISCOVER from 88:66:5a:0e:21:9c via 10.62.30.1: network 10.62.30.0/24: no free leases', '« via 10.62.30.1 » montre que le relais du VLAN 30 fonctionne ; « no free leases » que l’étendue est pleine. Il faut raccourcir le bail ou agrandir le sous-réseau.'],
['Consulter un bail', 'Qui utilise l’adresse 10.62.20.57 ?', 'grep -A6 "lease 10.62.20.57" /var/lib/dhcp/dhcpd.leases', 'lease 10.62.20.57 {\n starts 3 2026/09/30 08:02:11;\n ends 3 2026/09/30 16:02:11;\n binding state active;\n hardware ethernet bc:24:11:5e:2a:10;\n client-hostname "PC-B204-07";\n}', 'Le fichier des baux indique la MAC et le nom de la machine. Avec show mac address-table sur le switch, on retrouve la prise physique.'],
['Capturer un faux serveur', 'La salle reçoit des adresses en 192.168.1.x.', 'sudo tcpdump -eni ens18 -c 4 "udp port 67 or udp port 68"', '… 00:15:5d:0a:11:07 > ff:ff:ff:ff:ff:ff … 0.0.0.0.68 > 255.255.255.255.67: DHCP Discover\n… f4:ca:e5:31:88:02 > ff:ff:ff:ff:ff:ff … 192.168.1.1.67 > 255.255.255.255.68: DHCP Offer\n… 00:1b:a9:4c:22:01 > … 10.62.20.1.67 > 255.255.255.255.68: DHCP Offer', 'Deux offres arrivent : celle du vrai relais (10.62.20.1) et celle d’un intrus en 192.168.1.1, MAC f4:ca:e5:31:88:02. On la cherche sur le switch pour trouver le port, souvent une box ou un routeur Wi-Fi branché par un élève.'],
['Tester la configuration avant redémarrage', 'Ajout d’une nouvelle étendue.', 'sudo dhcpd -t -cf /etc/dhcp/dhcpd.conf', '/etc/dhcp/dhcpd.conf line 24: semicolon expected.\n option routers 10.62.30.1\n ^\nConfiguration file errors encountered -- exiting', 'Il manque un point-virgule ligne 24. Sans ce test, le redémarrage aurait arrêté le service DHCP pour tout l’établissement.'],
['Configurer le relais sur Linux', 'Le routeur Linux sert trois VLAN, le serveur est dans le VLAN 40.', 'sudo apt install isc-dhcp-relay\n# SERVERS="10.62.40.5" INTERFACES="ens19.20 ens19.30 ens19.40"\nsudo systemctl restart isc-dhcp-relay\njournalctl -u isc-dhcp-relay | tail -2', 'Forwarded BOOTREQUEST for bc:24:11:5e:2a:10 to 10.62.40.5\nForwarded BOOTREPLY for bc:24:11:5e:2a:10 to 10.62.20.57', 'Le relais reçoit la diffusion du poste et la transmet au serveur en unicast, puis relaie la réponse. L’interface du VLAN du serveur doit aussi figurer dans INTERFACES pour recevoir les réponses.']
],
long: { figure: svg(720, 250, bx(20, 20, 160, 50, Y, 'Client', 16) + bx(540, 20, 160, 50, G, 'Serveur DHCP', 16) + '<line x1="100" y1="70" x2="100" y2="240" ' + K + ' stroke-dasharray="6 6"/><line x1="620" y1="70" x2="620" y2="240" ' + K + ' stroke-dasharray="6 6"/>' + [['Discover', 1, 95], ['Offer', 0, 135], ['Request', 1, 175], ['Ack', 0, 215]].map(function (m) { var y = m[2]; return (m[1] ? '<line x1="100" y1="' + y + '" x2="615" y2="' + (y + 10) + '" ' + K + '/>' : '<line x1="620" y1="' + y + '" x2="105" y2="' + (y + 10) + '" ' + K + '/>') + tx(320, y - 4, m[0], 15); }).join('')), legende: 'DORA : quatre messages pour obtenir un bail.', photo: 'Photo : salle informatique avec des postes au démarrage',
lecture: ['Quand un élève ouvre son ordinateur et que le Wi-Fi se connecte, il ne se passe en apparence rien. En réalité, en quelques millisecondes, son appareil a crié sur le réseau qu’il cherchait une adresse, un serveur lui en a proposé une, il l’a acceptée, et le serveur lui a confirmé son bail avec l’adresse de la passerelle et des DNS. Sans ce dialogue, il faudrait configurer chaque appareil à la main.', 'DHCP est l’un des rares services dont tout le monde dépend et que personne ne voit. C’est pour cela que sa panne est si spectaculaire : les appareils déjà connectés continuent quelques heures grâce à leur bail, puis tombent les uns après les autres. Un technicien averti sait qu’une panne DHCP se manifeste souvent avec un décalage, et regarde les journaux du serveur dès les premiers appels.', 'Dans un réseau segmenté en VLAN, un seul serveur central sert tous les sous-réseaux grâce aux relais. C’est plus simple à administrer, mais cela fait du serveur et des relais des points critiques. Les établissements importants installent deux serveurs en basculement (failover), qui se partagent les étendues et se remplacent mutuellement.', 'Enfin, DHCP illustre parfaitement une réalité de la sécurité réseau : les protocoles de base ont été conçus à une époque où tous les participants étaient de confiance. Il n’y a aucune authentification, et c’est au réseau lui-même, par le DHCP snooping des switchs, de faire respecter les règles.'] },
schemas: { termes: ['DHCP', 'DORA', 'Discover', 'Offer', 'Request', 'Ack', 'NAK', 'bail', 'T1', 'T2', 'étendue', 'réservation', 'exclusion', 'relais', 'giaddr', 'ip helper-address', 'DHCP snooping', 'isc-dhcp-server', 'Kea', 'dhcpd.conf', 'dhcpd.leases', 'dhclient', 'APIPA', '169.254', 'UDP 67', 'UDP 68'],
figs: [[5, 'Le relais transforme la diffusion en unicast', svg(720, 140, bx(10, 40, 140, 50, Y, 'Poste VLAN 20', 13) + '<line x1="150" y1="65" x2="258" y2="65" ' + K + '/>' + tx(160, 55, 'diffusion', 12) + bx(260, 30, 200, 70, L, 'Relais .20.1', 14) + '<line x1="460" y1="65" x2="568" y2="65" ' + K + '/>' + tx(470, 55, 'unicast', 12) + bx(570, 40, 140, 50, G, 'Serveur .40.5', 13) + tx(260, 125, 'giaddr = 10.62.20.1 → étendue du VLAN 20', 13))]] }
});
})();

View file

@ -0,0 +1,230 @@
// Cours complets · Réseaux lot 2 : DNS, NAT/PAT, routage statique
(function () {
var K = 'stroke="#000" stroke-width="3"', F = 'font-family="Archivo,sans-serif" font-weight="800"', M = 'font-family="IBM Plex Mono,monospace" font-weight="600"';
var bx = function (x, y, w, h, c, t, fs) { return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" fill="' + c + '" ' + K + '/><text x="' + (x + w / 2) + '" y="' + (y + h / 2 + 5) + '" text-anchor="middle" ' + F + ' font-size="' + (fs || 15) + '">' + t + '</text>'; };
var ln = function (x1, y1, x2, y2) { return '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" ' + K + '/>'; };
var tx = function (x, y, s, fs, mono) { return '<text x="' + x + '" y="' + y + '" ' + (mono ? M : F) + ' font-size="' + (fs || 14) + '">' + s + '</text>'; };
var svg = function (w, h, b) { return '<svg viewBox="0 0 ' + w + ' ' + h + '" class="h-auto w-full" role="img">' + b + '</svg>'; };
var Y = '#FFD23F', G = '#7CF2C0', P = '#FF6FB5', L = '#C4B5FD', N = '#F6F5F1', W = '#fff';
var reg = function (c) {
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
(window.CONTENUS_LONG = window.CONTENUS_LONG || {})[c.cours.id] = c.long;
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = c.schemas;
};
// ================= DNS =================
reg({
cours: { id: 'dns', match: /DNS : résolution|zones, enregistrements|Bind/i,
titre: 'DNS : résolution de noms, zones, enregistrements et serveur Bind9', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T S2-S3 · TSSR / AIS',
prerequis: ['Modèle TCP/IP, UDP et TCP', 'Adressage IPv4', 'Diagnostic réseau en ligne de commande'],
competences: ['BTS SIO SISR · Bloc 2 · Installer, configurer et administrer un service d’infrastructure', 'BTS SIO · Bloc 1 · Répondre aux incidents', 'BTS CIEL · C04 · Installer un service réseau', 'BUT R&T · Compétence « Administrer les réseaux et l’Internet »'],
intro: '« Ce n’est jamais le DNS. C’était le DNS. » Cette plaisanterie d’administrateurs résume une réalité de production : une part considérable des incidents qualifiés de « panne réseau » est en fait une panne de résolution de noms. L’annuaire Active Directory, la messagerie, les certificats HTTPS, les mises à jour, l’ENT : tout repose sur la capacité à traduire un nom en adresse. Un DNS lent rend tout lent, un DNS faux rend tout faux, un DNS absent arrête un établissement. Ce module décrit le fonctionnement interne du protocole, la structure des zones et des enregistrements, la mise en œuvre d’un serveur Bind9 faisant autorité et d’un résolveur, et la méthode de diagnostic avec dig.',
cm: [
{ t: 'Un annuaire distribué et hiérarchique', d: '', html: 'Le DNS (Domain Name System, RFC 1034 et 1035) est une base de données distribuée à l’échelle mondiale. Aucun serveur ne connaît tous les noms : chacun fait <b>autorité</b> sur une portion de l’arbre, appelée <b>zone</b>, et sait à qui déléguer le reste.<br><br>L’arbre part de la <b>racine</b>, notée par un point final implicite. Sous la racine se trouvent les <b>domaines de premier niveau</b> (TLD) : génériques comme .com et .org, nationaux comme .fr, géré par l’Afnic. Sous .fr, chaque organisation enregistre son domaine, par exemple lycee-exemple.fr, puis y crée librement des sous-domaines et des noms d’hôtes : www, ent, mail, intranet.<br><br>Un nom complet, de l’hôte jusqu’à la racine, s’appelle un <b>FQDN</b> (Fully Qualified Domain Name) : <code>ent.lycee-exemple.fr.</code> avec le point final. Chaque étiquette est limitée à 63 caractères et le nom complet à 253.' },
{ t: 'Résolveur récursif et serveur faisant autorité', d: '', html: 'Il faut distinguer deux rôles, souvent confondus. Le <b>serveur faisant autorité</b> (authoritative) détient les données officielles d’une zone et répond uniquement sur cette zone ; il pose le drapeau <b>aa</b> dans ses réponses. Le <b>résolveur récursif</b> (recursive resolver) reçoit les questions des clients, part à la recherche de la réponse en interrogeant les serveurs faisant autorité, puis la garde en <b>cache</b> pendant la durée indiquée par le <b>TTL</b>.<br><br>Sur un poste, un troisième acteur intervient : le <b>stub resolver</b> de la bibliothèque système, qui se contente de transmettre la question au résolveur configuré. Sous Debian récent, systemd-resolved sert souvent de résolveur local à l’adresse 127.0.0.53 ; le fichier <code>/etc/resolv.conf</code> pointe alors vers lui, et <code>resolvectl status</code> montre les vrais serveurs utilisés.<br><br>Mélanger les deux rôles sur un même serveur exposé à Internet est une faute : un résolveur ouvert à tous est exploité pour des <b>attaques par amplification</b>, où de petites requêtes à l’adresse source usurpée provoquent de grosses réponses vers la victime.' },
{ t: 'Le parcours d’une résolution', d: '', html: 'Quand un élève tape ent.lycee-exemple.fr et que rien n’est en cache, le résolveur procède par <b>requêtes itératives</b>. Il interroge un des 13 ensembles de <b>serveurs racine</b> (a.root-servers.net à m.root-servers.net, répliqués en anycast dans le monde entier), dont il connaît les adresses grâce au fichier d’amorçage (root hints). La racine ne connaît pas la réponse mais renvoie une <b>délégation</b> : la liste des serveurs de .fr. Le résolveur interroge alors un serveur de .fr, qui délègue à son tour vers les serveurs de lycee-exemple.fr. Ceux-ci, faisant autorité, donnent enfin l’adresse.<br><br>Le client, lui, n’a posé qu’une seule question, dite <b>récursive</b> (drapeau RD, recursion desired), et reçoit une réponse complète. Toutes les réponses intermédiaires sont mises en cache : la question suivante sur un autre nom en .fr évitera l’interrogation de la racine.<br><br>Le transport se fait en <b>UDP port 53</b> pour la plupart des requêtes. Si la réponse dépasse la taille autorisée (512 octets historiquement, environ 1 232 octets avec EDNS0), le serveur renvoie une réponse tronquée (drapeau TC) et le client recommence en <b>TCP port 53</b>. Les transferts de zone utilisent toujours TCP.', exemple: '$ dig +trace ent.lycee-exemple.fr\n. 518400 IN NS a.root-servers.net.\n;; Received 811 bytes from 127.0.0.53#53\nfr. 172800 IN NS d.nic.fr.\n;; Received 1217 bytes from 198.41.0.4#53(a.root-servers.net)\nlycee-exemple.fr. 3600 IN NS ns1.lycee-exemple.fr.\n;; Received 402 bytes from 194.0.9.1#53(d.nic.fr)\nent.lycee-exemple.fr. 300 IN A 203.0.113.40\n;; Received 65 bytes from 203.0.113.2#53(ns1.lycee-exemple.fr)' },
{ t: 'Les types d’enregistrements', d: '', html: 'Une zone est une liste d’<b>enregistrements de ressources</b> (RR), chacun composé d’un nom, d’un TTL, d’une classe (IN, pour Internet) d’un type et d’une valeur. Il faut connaître les types suivants par cœur, et surtout leurs pièges.<br><br>Un <b>CNAME</b> est un alias : il redirige un nom vers un autre nom. Un nom qui porte un CNAME ne peut porter aucun autre enregistrement, ce qui interdit un CNAME à l’apex de la zone (le nom du domaine lui-même). Un <b>MX</b> désigne un serveur de messagerie avec une priorité : la plus petite valeur est essayée en premier. Il doit pointer vers un nom qui a un enregistrement A, jamais vers un CNAME ni une adresse IP. Les <b>TXT</b> portent des informations textuelles, aujourd’hui essentiellement la politique anti-usurpation de la messagerie (SPF, DKIM, DMARC) et les preuves de propriété de domaine.', table: [['Type', 'Rôle', 'Exemple de valeur'], ['A', 'nom → adresse IPv4', '203.0.113.40'], ['AAAA', 'nom → adresse IPv6', '2001:db8:40::10'], ['CNAME', 'alias vers un autre nom', 'ent.lycee-exemple.fr.'], ['MX', 'serveur de messagerie + priorité', '10 mx1.lycee-exemple.fr.'], ['NS', 'serveurs faisant autorité de la zone', 'ns1.lycee-exemple.fr.'], ['SOA', 'paramètres de la zone', 'ns1… hostmaster… 2026093001 …'], ['PTR', 'adresse → nom (zone inverse)', 'ent.lycee-exemple.fr.'], ['TXT', 'texte libre, SPF, DKIM, DMARC', '"v=spf1 mx -all"'], ['SRV', 'localiser un service (AD, SIP)', '0 100 389 dc1.lycee.local.'], ['CAA', 'autorités autorisées à émettre un certificat', '0 issue "letsencrypt.org"']] },
{ t: 'Anatomie d’un fichier de zone', d: '', html: 'Le fichier de zone commence par la directive <code>$TTL</code>, puis l’enregistrement <b>SOA</b> (Start of Authority), obligatoire et unique. Il contient le serveur primaire, l’adresse de l’administrateur (le premier point remplace le @), puis cinq valeurs qui pilotent la réplication.<br><br>Le <b>serial</b> est un numéro de version. Les serveurs secondaires ne récupèrent la zone que s’il a augmenté ; la convention AAAAMMJJnn (date plus numéro de modification du jour) évite les erreurs. <b>Refresh</b> fixe l’intervalle de vérification des secondaires, <b>retry</b> le délai avant une nouvelle tentative en cas d’échec, <b>expire</b> la durée au bout de laquelle un secondaire sans nouvelles cesse de répondre, et le dernier champ le <b>TTL négatif</b>, durée de mise en cache d’une réponse « ce nom n’existe pas ».<br><br>Dans le fichier, <code>@</code> désigne le nom de la zone, et tout nom qui ne se termine pas par un point est complété par ce nom. C’est la source de l’erreur la plus fréquente : écrire <code>mx1.lycee-exemple.fr</code> sans point final produit <code>mx1.lycee-exemple.fr.lycee-exemple.fr.</code>', exemple: '$TTL 3600\n@ IN SOA ns1.lycee-exemple.fr. hostmaster.lycee-exemple.fr. (\n 2026093001 ; serial AAAAMMJJnn\n 7200 ; refresh\n 900 ; retry\n 1209600 ; expire (14 jours)\n 300 ) ; TTL négatif\n IN NS ns1.lycee-exemple.fr.\n IN NS ns2.lycee-exemple.fr.\n IN MX 10 mx1.lycee-exemple.fr.\n IN TXT "v=spf1 mx -all"\n@ IN A 203.0.113.40\nns1 IN A 203.0.113.2\nns2 IN A 198.51.100.2\nmx1 IN A 203.0.113.25\nent IN A 203.0.113.40\nwww IN CNAME ent', attention: 'Oublier d’incrémenter le serial après une modification est l’erreur classique : le primaire répond avec les nouvelles données, les secondaires avec les anciennes, et le comportement devient aléatoire selon le serveur interrogé.' },
{ t: 'Zones inverses', d: '', html: 'La résolution inverse traduit une adresse en nom grâce aux enregistrements <b>PTR</b>. Elle est placée sous le domaine spécial <b>in-addr.arpa</b>, avec les octets de l’adresse écrits à l’envers, car le DNS lit les noms de droite à gauche du plus général au plus précis, alors qu’une adresse IP se lit de gauche à droite. Le réseau 10.62.40.0/24 correspond à la zone <code>40.62.10.in-addr.arpa</code>.<br><br>Les zones inverses ne sont pas un luxe. Les serveurs de messagerie refusent souvent le courrier d’une adresse sans PTR cohérent, les journaux deviennent lisibles, SSH et certains outils d’administration l’utilisent, et Active Directory en dépend. Pour les adresses publiques, la zone inverse est gérée par l’opérateur ou l’hébergeur, à qui l’on demande la création du PTR.', exemple: '; /etc/bind/db.10.62.40\n$TTL 3600\n@ IN SOA ns1.lycee.local. hostmaster.lycee.local. ( 2026093001 7200 900 1209600 300 )\n IN NS ns1.lycee.local.\n10 IN PTR ns1.lycee.local.\n20 IN PTR ent.lycee.local.' },
{ t: 'Mettre en œuvre Bind9', d: '', html: '<b>Bind9</b>, maintenu par l’ISC, reste la référence au programme des BTS et dans de nombreuses infrastructures. On le configure dans <code>/etc/bind/named.conf.options</code> pour le comportement général (récursivité, redirecteurs, listes d’accès) et dans <code>named.conf.local</code> pour déclarer les zones.<br><br>La récursivité doit être <b>limitée aux réseaux internes</b> par une ACL. Les <b>forwarders</b> permettent de faire suivre les requêtes externes vers les résolveurs de l’opérateur ou du rectorat plutôt que de partir de la racine. Pour la redondance, on déclare un serveur <b>secondaire</b> (type secondary, anciennement slave) qui récupère la zone par <b>transfert AXFR/IXFR</b>, le primaire l’autorisant explicitement avec allow-transfer et le prévenant par un NOTIFY à chaque changement de serial.<br><br>Deux outils valident la configuration avant tout rechargement : <code>named-checkconf</code> pour la syntaxe générale et <code>named-checkzone</code> pour chaque fichier de zone. On recharge ensuite sans coupure avec <code>rndc reload</code>.', exemple: '// named.conf.options\nacl interne { 10.62.0.0/16; 127.0.0.1; };\noptions {\n directory "/var/cache/bind";\n recursion yes;\n allow-recursion { interne; };\n allow-query { any; };\n forwarders { 1.1.1.1; 9.9.9.9; };\n dnssec-validation auto;\n listen-on { 10.62.40.10; 127.0.0.1; };\n};\n\n// named.conf.local\nzone "lycee.local" {\n type primary;\n file "/etc/bind/db.lycee.local";\n allow-transfer { 10.62.40.11; };\n also-notify { 10.62.40.11; };\n};\nzone "40.62.10.in-addr.arpa" {\n type primary;\n file "/etc/bind/db.10.62.40";\n};' },
{ t: 'Cache, TTL et propagation', d: '', html: 'Chaque réponse est mise en cache par le résolveur pendant la durée de son <b>TTL</b>. C’est ce qui rend le DNS rapide et résistant, mais c’est aussi ce qui explique qu’une modification ne soit pas visible partout immédiatement. On parle de « propagation », alors qu’il ne se propage rien : les caches expirent simplement les uns après les autres.<br><br>La conséquence opérationnelle est capitale : avant une migration de serveur, on <b>abaisse le TTL</b> de l’enregistrement concerné (par exemple de 86 400 à 300 secondes) au moins une durée de l’ancien TTL à l’avance. Le jour J, le changement d’adresse est pris en compte en cinq minutes. Une fois la migration validée, on remonte le TTL.<br><br>Les réponses négatives sont aussi mises en cache : si un nom a été demandé avant sa création, les clients continueront à recevoir NXDOMAIN pendant la durée du TTL négatif du SOA. On vide le cache d’un résolveur Bind avec <code>rndc flushname nom</code>, celui de systemd-resolved avec <code>resolvectl flush-caches</code>.' },
{ t: 'Sécurité du DNS', d: '', html: 'Le DNS d’origine n’authentifie rien. Un attaquant capable d’injecter une fausse réponse dans un cache (<b>empoisonnement de cache</b>, rendu célèbre par l’attaque Kaminsky en 2008) redirige tous les utilisateurs vers le serveur de son choix. Les parades sont la randomisation du port source et de l’identifiant de requête, désormais systématique, et surtout <b>DNSSEC</b>, qui signe cryptographiquement les enregistrements : le résolveur vérifie une chaîne de confiance depuis la racine et rejette toute réponse falsifiée (drapeau <b>ad</b> dans la réponse validée). L’Afnic signe la zone .fr depuis 2010.<br><br>La confidentialité est un autre sujet : les requêtes DNS classiques circulent en clair et révèlent tous les sites consultés. <b>DoT</b> (DNS over TLS, port 853) et <b>DoH</b> (DNS over HTTPS, port 443) les chiffrent. Sur un réseau d’établissement, DoH configuré directement dans les navigateurs contourne le filtrage imposé par le cadre réglementaire ; il se maîtrise par une politique de configuration des navigateurs.<br><br>Enfin, on restreint les <b>transferts de zone</b> aux seuls secondaires : un AXFR ouvert livre la cartographie complète du réseau à n’importe qui.' }
],
synthese: ['Serveur faisant autorité (drapeau aa) ≠ résolveur récursif avec cache', 'Résolution itérative : racine → TLD → domaine, réponses mises en cache selon le TTL', 'UDP 53, TCP 53 pour les grosses réponses et les transferts', 'A, AAAA, CNAME, MX, NS, SOA, PTR, TXT, SRV : connaître rôle et pièges', 'Point final obligatoire dans les zones, serial incrémenté à chaque modification', 'Abaisser le TTL avant une migration', 'Récursivité limitée par ACL, transferts restreints, DNSSEC validé'],
qcm: [
['Quel drapeau indique qu’une réponse vient d’un serveur faisant autorité ?', ['rd', 'ra', 'aa', 'ad'], 2, 'aa = authoritative answer.'],
['Dans quel cas le DNS utilise-t-il TCP ?', ['Jamais', 'Réponse tronquée et transferts de zone', 'Pour les requêtes MX', 'Toujours en IPv6'], 1, 'TC = 1 ou AXFR/IXFR.'],
['Pourquoi ne peut-on pas mettre un CNAME sur lycee-exemple.fr lui-même ?', ['Trop long', 'Un nom avec CNAME ne peut porter aucun autre enregistrement, or l’apex porte SOA et NS', 'Interdit par l’Afnic', 'Les CNAME sont obsolètes'], 1, 'Règle de la RFC 1034.'],
['Vers quoi doit pointer un enregistrement MX ?', ['Une adresse IP', 'Un nom qui possède un enregistrement A ou AAAA', 'Un CNAME', 'Un TXT'], 1, 'Jamais vers une IP ni un alias.'],
['La zone inverse de 192.168.5.0/24 s’appelle :', ['192.168.5.in-addr.arpa', '5.168.192.in-addr.arpa', '192.168.5.rev', 'arpa.5.168.192'], 1, 'Octets inversés.'],
['Un secondaire ne récupère pas la nouvelle version de la zone. Cause la plus probable ?', ['Le TTL est trop long', 'Le serial n’a pas été incrémenté', 'Le MX est faux', 'DNSSEC est actif'], 1, 'Le secondaire compare les serials.'],
['Dans un fichier de zone, « mail IN A 10.0.0.5 » dans la zone lycee.local crée :', ['mail.', 'mail.lycee.local.', 'mail.local.', 'une erreur'], 1, 'Le nom est complété par l’origine.'],
['Pourquoi abaisser le TTL avant une migration ?', ['Pour accélérer la résolution', 'Pour que les caches expirent vite quand l’adresse change', 'Pour DNSSEC', 'Pour réduire la charge'], 1, 'Sinon les anciens clients gardent l’ancienne adresse jusqu’à l’expiration.'],
['Quel est le risque d’un résolveur ouvert à tout Internet ?', ['Aucun', 'Utilisation pour des attaques par amplification', 'Perte des zones', 'Il devient faisant autorité'], 1, 'Réponses volumineuses vers une IP usurpée.'],
['Que garantit DNSSEC ?', ['La confidentialité des requêtes', 'L’authenticité et l’intégrité des réponses', 'La rapidité', 'La disponibilité'], 1, 'Signature, pas chiffrement.'],
['Quelle commande vérifie un fichier de zone avant rechargement ?', ['named-checkconf', 'named-checkzone lycee.local /etc/bind/db.lycee.local', 'rndc check', 'dig +check'], 1, 'named-checkconf vérifie la configuration générale.'],
['Un poste Debian a nameserver 127.0.0.53 dans resolv.conf. Comment voir le vrai serveur DNS ?', ['cat /etc/hosts', 'resolvectl status', 'ip route', 'dig 127.0.0.53'], 1, 'systemd-resolved fait office de relais local.']
],
td: [
{ n: 'Échauffement', c: 'Pour chaque besoin, donner le type d’enregistrement : (a) faire pointer intranet vers 10.62.40.20, (b) faire de www un alias de ent, (c) déclarer le serveur de messagerie, (d) prouver la propriété du domaine à un fournisseur, (e) retrouver le nom de 10.62.40.20.', r: '(a) A · (b) CNAME · (c) MX (+ A du serveur) · (d) TXT · (e) PTR dans la zone inverse' },
{ n: 'Échauffement', c: 'Repérer les trois erreurs de ce fragment de zone : « @ IN MX 10 203.0.113.25 » ; « mx1 IN CNAME serveur » ; « ent IN A 203.0.113.40 » suivi de « ent IN CNAME web ».', r: 'MX vers une IP (il faut un nom) · MX qui mènerait à un CNAME · ent porte à la fois un A et un CNAME' },
{ n: 'Application', c: 'Rédiger le fichier de zone complet de lycee.local : deux serveurs de noms (10.62.40.10 et .11), un serveur de messagerie en priorité 10, l’ENT en 10.62.40.20, un alias www vers l’ENT, et un enregistrement SPF qui n’autorise que le MX.', r: '$TTL 3600\n@ IN SOA ns1.lycee.local. hostmaster.lycee.local. ( 2026093001 7200 900 1209600 300 )\n IN NS ns1.lycee.local.\n IN NS ns2.lycee.local.\n IN MX 10 mx1.lycee.local.\n IN TXT "v=spf1 mx -all"\nns1 IN A 10.62.40.10\nns2 IN A 10.62.40.11\nmx1 IN A 10.62.40.25\nent IN A 10.62.40.20\nwww IN CNAME ent' },
{ n: 'Application', c: 'Configurer Bind9 pour qu’il fasse autorité sur lycee.local, qu’il ne serve la récursivité qu’au réseau 10.62.0.0/16, qu’il fasse suivre les requêtes externes vers 1.1.1.1, et que seul 10.62.40.11 puisse transférer la zone.', r: 'acl interne { 10.62.0.0/16; 127.0.0.1; };\noptions { recursion yes; allow-recursion { interne; }; forwarders { 1.1.1.1; }; };\nzone "lycee.local" { type primary; file "/etc/bind/db.lycee.local"; allow-transfer { 10.62.40.11; }; also-notify { 10.62.40.11; }; };' },
{ n: 'Application', c: 'L’ENT doit migrer de 203.0.113.40 vers 203.0.113.80 samedi à 8 h. Le TTL actuel est de 86 400 s. Établir le rétroplanning des opérations DNS.', r: 'Au plus tard vendredi 8 h : TTL de ent à 300, serial +1, rndc reload.\nSamedi 8 h : A de ent → 203.0.113.80, serial +1, reload ; vérifier avec dig @ns1 et @ns2.\nAprès validation (lundi) : TTL remonté à 3600 ou 86400.' },
{ n: 'Approfondissement', c: 'Expliquer pourquoi un nom créé à 10 h, mais demandé par erreur à 9 h 58, reste introuvable pour certains postes jusqu’à 10 h 03. Comment l’éviter ?', r: 'La réponse NXDOMAIN de 9 h 58 a été mise en cache pour la durée du TTL négatif du SOA (300 s). Éviter de tester avant publication, ou vider le cache du résolveur : rndc flushname nom.' },
{ n: 'Approfondissement', c: 'Montrer avec dig que la zone d’un domaine public est signée par DNSSEC et que votre résolveur valide les signatures.', r: 'dig +dnssec lycee-exemple.fr SOA → présence d’un enregistrement RRSIG\nflags de la réponse contenant ad (authenticated data) → le résolveur a validé la chaîne\ndig +cd … permet de comparer sans validation' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Deux VM Bind9 (primaire et secondaire) et un client, vérifications par dig dans la VM.', etapes: ['Installer bind9 et bind9-dnsutils', 'Créer la zone directe lycee.local et la zone inverse', 'Valider avec named-checkconf et named-checkzone', 'Configurer le secondaire et vérifier le transfert', 'Restreindre la récursivité et tester depuis un réseau non autorisé', 'Provoquer puis corriger un oubli de serial'] },
plusLoin: [['RFC 1034 · DNS concepts', 'https://www.rfc-editor.org/rfc/rfc1034'], ['RFC 1035 · DNS implementation', 'https://www.rfc-editor.org/rfc/rfc1035'], ['Documentation Bind9 (ISC)', 'https://bind9.readthedocs.io/'], ['Afnic · DNSSEC et la zone .fr', 'https://www.afnic.fr/'], ['ANSSI · Bonnes pratiques pour l’acquisition et l’exploitation de noms de domaine', 'https://cyber.gouv.fr/publications/bonnes-pratiques-pour-lacquisition-et-lexploitation-de-noms-de-domaine']] },
plus: {
cas: { titre: 'Incident · « Plus personne ne se connecte à l’ENT »', texte: '' },
resolu: { enonce: 'Mardi 8 h 10 : depuis tous les postes du lycée, ent.lycee.local ne s’ouvre plus (« Impossible de trouver l’adresse du serveur »). Le serveur de l’ENT fonctionne. La veille au soir, un collègue a ajouté un enregistrement dans la zone. Diagnostiquer et rétablir.', etapes: [
['1 · Isoler la couche', 'ping -c 2 10.62.40.20\n2 packets transmitted, 2 received, 0% packet loss\ndig ent.lycee.local\n;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL\n→ Le serveur de l’ENT répond par son adresse : le réseau fonctionne. La résolution échoue avec SERVFAIL, et non NXDOMAIN : le résolveur n’a pas pu obtenir de réponse valide. Le problème est dans le DNS.'],
['2 · Interroger chaque serveur séparément', 'dig @10.62.40.10 ent.lycee.local +short\n(aucune réponse, SERVFAIL)\ndig @10.62.40.11 ent.lycee.local +short\n10.62.40.20\n→ Le secondaire répond correctement, le primaire non. Les postes qui interrogent d’abord le primaire échouent. On se concentre sur ns1.'],
['3 · Lire les journaux du primaire', 'sudo journalctl -u named --since "yesterday 18:00" | grep -i lycee\nzone lycee.local/IN: loading from master file /etc/bind/db.lycee.local failed: bad dotted quad\nzone lycee.local/IN: not loaded due to errors.\n→ Bind a refusé de charger la zone après le rechargement d’hier soir. Il ne fait donc plus autorité sur lycee.local et renvoie SERVFAIL.'],
['4 · Localiser l’erreur', 'sudo named-checkzone lycee.local /etc/bind/db.lycee.local\n/etc/bind/db.lycee.local:21: bad dotted quad\nzone lycee.local/IN: loading from master file failed: bad dotted quad\nsed -n 21p /etc/bind/db.lycee.local\nimprimante-cdi IN A 10.62.40.2O\n→ Ligne 21, l’adresse se termine par la lettre O au lieu du chiffre 0. Une faute de frappe a mis hors service la zone entière.'],
['5 · Corriger, incrémenter le serial, valider, recharger', 'sudo sed -i "21s/10.62.40.2O/10.62.40.200/" /etc/bind/db.lycee.local\n(serial 2026092901 → 2026093001)\nsudo named-checkzone lycee.local /etc/bind/db.lycee.local\nzone lycee.local/IN: loaded serial 2026093001\nOK\nsudo rndc reload lycee.local\nzone reload queued\n→ La zone se charge. Le nouveau serial déclenche un NOTIFY vers le secondaire, qui récupérera la version corrigée.'],
['6 · Vérifier et prévenir', 'dig @10.62.40.10 ent.lycee.local +short\n10.62.40.20\ndig @10.62.40.11 lycee.local SOA +short\nns1.lycee.local. hostmaster.lycee.local. 2026093001 7200 900 1209600 300\n→ Les deux serveurs répondent et le secondaire est au nouveau serial. Compte rendu : cause (faute de frappe non validée), impact (ENT inaccessible de 18 h 30 à 8 h 25 pour la moitié des postes), correction, et action préventive : named-checkzone obligatoire avant rndc reload, dans un script de publication.']
] },
erreurs: ['Oublier le point final d’un nom dans un fichier de zone', 'Modifier une zone sans incrémenter le serial', 'Mettre un CNAME à l’apex ou à côté d’un autre enregistrement', 'Faire pointer un MX vers une adresse IP ou un CNAME', 'Laisser la récursivité ouverte à Internet', 'Autoriser les transferts de zone à tous', 'Changer une adresse sans avoir abaissé le TTL à l’avance', 'Recharger Bind sans named-checkzone'],
memo: [['Commande', 'Usage en production'], ['dig nom', 'réponse complète : status, flags, TTL, serveur interrogé'], ['dig +short nom', 'la valeur seule, pour un script'], ['dig @serveur nom', 'interroger un serveur précis (primaire, secondaire, public)'], ['dig nom MX / NS / SOA / TXT', 'interroger un type précis'], ['dig -x 10.62.40.20', 'résolution inverse (PTR)'], ['dig +trace nom', 'suivre la délégation depuis la racine'], ['dig +dnssec nom', 'voir les signatures RRSIG et le drapeau ad'], ['resolvectl status', 'serveurs DNS réellement utilisés par le poste'], ['resolvectl flush-caches', 'vider le cache local systemd-resolved'], ['named-checkconf', 'valider la configuration Bind'], ['named-checkzone zone fichier', 'valider un fichier de zone'], ['rndc reload [zone]', 'recharger sans coupure'], ['rndc flushname nom', 'retirer un nom du cache du résolveur'], ['journalctl -u named', 'journaux de Bind']],
examen: { titre: 'Sujet type · 20 points', q: ['Décrire le parcours complet d’une résolution de ent.lycee-exemple.fr sans cache, en distinguant requêtes récursive et itératives. (4 pts)', 'Rédiger la zone directe et la zone inverse d’un petit réseau fourni. (6 pts)', 'Configurer un serveur Bind9 primaire et un secondaire, avec récursivité restreinte. (5 pts)', 'Analyser une sortie dig fournie : status, flags, TTL, section AUTHORITY. (3 pts)', 'Expliquer le rôle de DNSSEC et la différence avec DoH. (2 pts)'], r: ['Client → résolveur (RD) ; résolveur → racine → .fr → domaine (itératif) ; mise en cache', 'SOA, NS, A, MX, CNAME ; PTR dans x.y.z.in-addr.arpa', 'type primary / secondary, allow-transfer, also-notify, allow-recursion', 'Selon la sortie', 'DNSSEC : authenticité ; DoH : confidentialité'] },
prof: { deroule: [['Ouverture', 'Démonstration : couper le DNS d’un poste et constater que « plus rien ne marche » alors que le ping par IP fonctionne'], ['Cours', 'Hiérarchie, rôles, parcours d’une résolution avec dig +trace projeté'], ['Cours', 'Enregistrements et fichier de zone, lecture d’une zone réelle'], ['Pratique', 'Lab Bind9 primaire, secondaire, zone inverse'], ['Incident', 'Panne injectée (serial, point final, faute de frappe) à diagnostiquer'], ['Clôture', 'Quiz flash, puis rédaction du compte rendu d’incident']], diff: 'Élèves en difficulté : fichier de zone à compléter, fiche mémo dig. Élèves avancés : DNSSEC avec signature automatique dans Bind, vues (split horizon), comparaison avec Unbound et PowerDNS.', eval: 'Étapes du lab vérifiées par dig dans la VM, compte rendu d’incident, sujet type sur 20.' }
},
exemples: [
['Lire une réponse dig complète', 'Un utilisateur signale que le site met du temps à s’ouvrir.', 'dig ent.lycee-exemple.fr', ';; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41872\n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n\n;; ANSWER SECTION:\nent.lycee-exemple.fr. 247 IN A 203.0.113.40\n\n;; Query time: 1 msec\n;; SERVER: 10.62.40.10#53(10.62.40.10) (UDP)', 'status NOERROR, une réponse, drapeaux rd ra (récursivité demandée et disponible) sans aa : c’est le résolveur interne qui répond depuis son cache, comme le confirment le TTL décroissant (247 s restantes) et le temps de 1 ms. La résolution n’est pas en cause ; on cherche ailleurs.'],
['Distinguer NXDOMAIN et SERVFAIL', 'Deux utilisateurs ne joignent pas deux sites différents.', 'dig +short nexistepas.lycee.local ; dig nexistepas.lycee.local | grep status\ndig panne.exemple | grep status', 'status: NXDOMAIN\nstatus: SERVFAIL', 'NXDOMAIN est une réponse valide : le serveur faisant autorité affirme que le nom n’existe pas (faute de frappe, enregistrement oublié). SERVFAIL signifie que le résolveur n’a pas réussi à obtenir de réponse fiable : serveurs faisant autorité injoignables, zone qui ne se charge pas, ou signature DNSSEC invalide. Les deux ne se diagnostiquent pas du tout de la même façon.'],
['Comparer primaire et secondaire', 'Le comportement change selon les postes après une modification.', 'for s in 10.62.40.10 10.62.40.11; do dig @$s lycee.local SOA +short; done', 'ns1.lycee.local. hostmaster.lycee.local. 2026093002 7200 900 1209600 300\nns1.lycee.local. hostmaster.lycee.local. 2026093001 7200 900 1209600 300', 'Les serials diffèrent : le secondaire n’a pas récupéré la dernière version. Vérifier sur le primaire allow-transfer et also-notify, puis dans les journaux du secondaire les messages « transfer of lycee.local … failed ». On force le transfert avec rndc retransfer lycee.local sur le secondaire.'],
['Vérifier la configuration de messagerie', 'Des courriels du lycée arrivent en spam chez les parents.', 'dig +short lycee-exemple.fr MX\ndig +short lycee-exemple.fr TXT\ndig +short -x 203.0.113.25', '10 mx1.lycee-exemple.fr.\n"v=spf1 mx -all"\n(aucune réponse)', 'Le MX et la politique SPF sont corrects, mais l’adresse du serveur d’envoi n’a pas d’enregistrement PTR. Beaucoup de fournisseurs pénalisent un expéditeur sans résolution inverse cohérente. On demande à l’hébergeur la création du PTR 203.0.113.25 → mx1.lycee-exemple.fr, et on ajoute DKIM et DMARC.'],
['Tester la récursivité depuis l’extérieur', 'Audit de sécurité du serveur DNS exposé.', 'dig @203.0.113.2 www.wikipedia.org', ';; ->>HEADER<<- opcode: QUERY, status: REFUSED\n;; flags: qr rd; QUERY: 1, ANSWER: 0\n;; WARNING: recursion requested but not available', 'Depuis Internet, le serveur refuse de résoudre un domaine qui n’est pas le sien (REFUSED, pas de drapeau ra) : il n’est pas un résolveur ouvert. Il doit en revanche répondre avec aa pour dig @203.0.113.2 lycee-exemple.fr.'],
['Trouver le vrai serveur DNS d’un poste', 'Un poste Debian résout différemment des autres.', 'cat /etc/resolv.conf | grep nameserver ; resolvectl status ens18 | grep -A2 "DNS Servers"', 'nameserver 127.0.0.53\nCurrent DNS Server: 192.168.1.1\n DNS Servers: 192.168.1.1', 'resolv.conf ne montre que le relais local systemd-resolved. resolvectl révèle que le poste utilise 192.168.1.1, une box et non le DNS du lycée : il a probablement été configuré à la main ou reçoit un bail d’un faux serveur DHCP. On corrige la configuration réseau du poste.']
],
long: { figure: svg(720, 250, bx(20, 100, 120, 50, Y, 'Poste', 15) + ln(140, 125, 200, 125) + bx(200, 95, 150, 60, L, 'Résolveur', 15) + [['Racine .', N, 20], ['.fr', G, 95], ['lycee-exemple.fr', P, 170]].map(function (r) { return ln(350, 125, 460, r[2] + 25) + bx(460, r[2], 240, 50, r[1], r[0], 14); }).join('') + tx(20, 200, '1 requête récursive', 13) + tx(360, 245, '3 requêtes itératives, réponses en cache', 13)), legende: 'Le poste pose une seule question ; le résolveur descend l’arbre et garde les réponses en cache.', photo: 'Photo : écran d’administration montrant une sortie dig ou une zone Bind',
lecture: ['Personne ne tape d’adresse IP pour aller sur l’ENT : on tape un nom. Entre le moment où l’on appuie sur Entrée et celui où la page commence à se charger, le navigateur a d’abord demandé à un résolveur quelle adresse correspondait à ce nom. Tant que cette réponse n’est pas arrivée, rien d’autre ne peut commencer. C’est pourquoi un DNS lent ralentit tout, et un DNS en panne donne l’impression que tout le réseau est coupé, alors que les câbles, les switchs et les serveurs fonctionnent parfaitement.', 'La force du DNS est sa décentralisation. Aucun organisme ne gère tous les noms : la racine délègue .fr à l’Afnic, qui délègue lycee-exemple.fr au lycée, qui gère ses propres noms. Chaque niveau n’a besoin de connaître que le suivant. Cette architecture a permis au système de passer de quelques milliers de noms dans les années 1980 à des centaines de millions aujourd’hui, avec le même protocole.', 'Sa faiblesse est symétrique : puisque chacun gère sa part, une erreur locale est immédiatement visible par tout le monde. Une faute de frappe dans un fichier de zone, un serial oublié, un TTL mal choisi avant une migration, et des milliers d’utilisateurs sont touchés. Un bon administrateur DNS est avant tout quelqu’un de méthodique : il valide chaque fichier, versionne ses zones, et teste chaque serveur séparément.', 'Enfin, le DNS est devenu un enjeu de sécurité majeur. Il sert à détourner des utilisateurs vers de faux sites, à exfiltrer des données par des requêtes camouflées, à amplifier des attaques par déni de service. DNSSEC, la restriction de la récursivité et la surveillance des requêtes font désormais partie du socle de sécurité recommandé par l’ANSSI pour tout système d’information.'] },
schemas: { termes: ['DNS', 'FQDN', 'TTL', 'SOA', 'serial', 'NS', 'MX', 'CNAME', 'PTR', 'TXT', 'SRV', 'AAAA', 'NXDOMAIN', 'SERVFAIL', 'NOERROR', 'REFUSED', 'dig', 'dig +trace', 'dig +short', 'resolvectl', 'named-checkzone', 'named-checkconf', 'rndc reload', 'Bind9', 'forwarders', 'allow-recursion', 'allow-transfer', 'AXFR', 'IXFR', 'NOTIFY', 'DNSSEC', 'RRSIG', 'DoH', 'DoT', 'in-addr.arpa', 'SPF', 'DKIM', 'DMARC', 'UDP 53'],
figs: [[4, 'Les cinq valeurs du SOA', svg(720, 110, [['serial', Y], ['refresh', G], ['retry', G], ['expire', L], ['TTL négatif', P]].map(function (v, i) { return bx(10 + i * 142, 30, 132, 50, v[1], v[0], 14); }).join('') + tx(10, 105, 'le secondaire ne transfère la zone que si le serial augmente', 13))]] }
});
// ================= NAT / PAT =================
reg({
cours: { id: 'nat', match: /NAT et PAT|masquerade/i,
titre: 'NAT et PAT : traduction d’adresses, masquerade et redirection de ports avec nftables', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T S2-S3 · TSSR',
prerequis: ['Adressage IPv4, adresses privées', 'TCP et UDP, ports', 'Routage statique (conseillé)'],
competences: ['BTS SIO SISR · Bloc 2 · Concevoir une solution d’infrastructure réseau', 'BTS SIO · Bloc 3 · Cybersécurité des services informatiques', 'BTS CIEL · C08 · Sécuriser un réseau', 'BUT R&T · Compétence « Connecter les entreprises »'],
intro: 'Les 1 500 appareils d’un lycée utilisent des adresses privées en 10.x.x.x, qui ne sont pas routées sur Internet. Pourtant, chacun consulte des sites, télécharge des mises à jour et joint des services en ligne. C’est la traduction d’adresses, le NAT, qui rend cela possible : à la sortie, le routeur ou le pare-feu remplace l’adresse privée par une adresse publique et garde la trace de chaque connexion pour renvoyer les réponses au bon poste. Dans l’autre sens, c’est encore le NAT qui publie un serveur interne sur Internet. Comprendre son fonctionnement dans le noyau, savoir le configurer avec nftables et le dépanner avec conntrack est indispensable à quiconque administre une passerelle.',
cm: [
{ t: 'Pourquoi traduire les adresses', d: '', html: 'L’espace IPv4 compte environ 4,3 milliards d’adresses, et les registres régionaux ont épuisé leurs réserves (le RIPE NCC, qui couvre l’Europe, a distribué ses derniers blocs /22 en 2019). Les adresses privées de la RFC 1918 ont permis de continuer à connecter des milliards d’appareils, à condition de <b>traduire</b> leurs adresses à la frontière avec Internet.<br><br>Le NAT (Network Address Translation, RFC 3022) modifie les champs d’adresse, et éventuellement de port, des en-têtes IP, TCP et UDP des paquets qui traversent un équipement, puis recalcule les sommes de contrôle. Il ne s’agit pas d’un mécanisme de sécurité en soi, mais il a un effet de bord apprécié : sans règle explicite, aucune connexion initiée depuis Internet ne peut atteindre un poste interne, puisque le routeur ne saurait pas à qui la transmettre.' },
{ t: 'Les formes de NAT', d: '', html: 'On distingue les traductions selon le champ modifié et le sens. Le <b>SNAT</b> (source NAT) modifie l’adresse source des paquets sortants : c’est lui qui permet aux postes de naviguer. Le <b>DNAT</b> (destination NAT) modifie l’adresse de destination des paquets entrants : c’est lui qui publie un serveur interne (redirection de port, port forwarding).<br><br>Le NAT <b>statique</b> associe une adresse privée à une adresse publique, un pour un, dans les deux sens. Le <b>PAT</b> (Port Address Translation), appelé aussi NAT surchargé ou NAPT, fait partager une seule adresse publique à des milliers de postes en traduisant aussi le port source : chaque connexion sortante reçoit un port source unique sur l’adresse publique, ce qui permet de distinguer les réponses. C’est ce que fait toute box domestique et la passerelle de presque tous les établissements.<br><br>Le <b>masquerade</b> est un SNAT dont l’adresse de remplacement est prise automatiquement sur l’interface de sortie. On l’utilise quand cette adresse est attribuée dynamiquement par l’opérateur.', table: [['Forme', 'Champ modifié', 'Sens', 'Usage'], ['SNAT', 'adresse source', 'sortant', 'accès Internet des postes'], ['Masquerade', 'adresse source (auto)', 'sortant', 'IP publique dynamique'], ['PAT / NAPT', 'adresse et port source', 'sortant', 'des milliers de postes derrière une IP'], ['DNAT', 'adresse (et port) destination', 'entrant', 'publier un serveur interne'], ['NAT statique 1:1', 'adresse, dans les deux sens', 'les deux', 'serveur avec IP publique dédiée']] },
{ t: 'Le suivi de connexions : conntrack', d: '', html: 'Sous Linux, le NAT repose sur le <b>suivi de connexions</b> du noyau (netfilter conntrack). Au premier paquet d’une connexion, le noyau crée une entrée qui mémorise le quintuplet d’origine (protocole, adresse et port source, adresse et port destination) et le quintuplet traduit. Les règles de NAT ne sont évaluées que pour ce <b>premier paquet</b> ; tous les paquets suivants, dans les deux sens, sont traduits directement grâce à l’entrée conntrack.<br><br>Chaque entrée a un état (NEW, ESTABLISHED, RELATED) et une durée de vie qui dépend du protocole : environ cinq jours pour une connexion TCP établie, 30 secondes pour un flux UDP sans réponse. La table a une taille maximale (<code>net.netfilter.nf_conntrack_max</code>) ; lorsqu’elle est pleine, le noyau écrit « nf_conntrack: table full, dropping packet » dans les journaux et rejette les nouvelles connexions. C’est un incident classique sur les passerelles d’établissement lors d’une journée d’examens en ligne.', exemple: '$ sudo conntrack -L -p tcp --dport 443 | head -2\ntcp 6 431987 ESTABLISHED src=10.62.20.57 dst=142.250.75.227 sport=51544 dport=443 src=142.250.75.227 dst=203.0.113.10 sport=443 dport=51544 [ASSURED] mark=0 use=1\n\n$ sudo conntrack -C\n18342\n$ sysctl net.netfilter.nf_conntrack_max\nnet.netfilter.nf_conntrack_max = 262144' },
{ t: 'Où agit le NAT : les crochets netfilter', d: '', html: 'Netfilter définit des points d’accroche (hooks) dans le parcours d’un paquet. Le <b>DNAT</b> intervient au crochet <b>prerouting</b>, avant la décision de routage : il faut changer la destination avant que le noyau ne choisisse où envoyer le paquet. Le <b>SNAT</b> intervient au crochet <b>postrouting</b>, juste avant la sortie, une fois l’interface de sortie connue. Entre les deux, le paquet traverse le crochet <b>forward</b>, où le pare-feu doit l’autoriser.<br><br>Cet ordre a une conséquence pratique qui piège souvent : dans la chaîne forward, on voit un paquet redirigé par DNAT avec sa <b>nouvelle</b> adresse de destination (le serveur interne), mais un paquet sortant avec son adresse source <b>d’origine</b> (le poste interne), puisque le SNAT n’a pas encore eu lieu. Les règles de filtrage s’écrivent donc avec les adresses privées.', exemple: 'entrée ─► prerouting (DNAT) ─► routage ─► forward (filtrage) ─► postrouting (SNAT) ─► sortie\n └─► input (vers la machine elle-même)' },
{ t: 'Configurer une passerelle avec nftables', d: '', html: '<b>nftables</b> est le cadre de filtrage par défaut de Debian depuis la version 10 ; il remplace iptables, dont les commandes sont encore présentes sous forme de couche de compatibilité. Une table de type <b>ip nat</b> contient les chaînes prerouting et postrouting. Il faut aussi activer le routage IP, sans quoi le noyau ne transmettra aucun paquet entre les interfaces.<br><br>La configuration permanente se place dans <code>/etc/nftables.conf</code>, chargée au démarrage par le service nftables. On la valide avec <code>nft -c -f</code> avant de l’appliquer : une erreur de syntaxe laisse sinon la passerelle sans règle.', exemple: '# Activer le routage de façon permanente\necho "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-routage.conf\nsudo sysctl --system\n\n# /etc/nftables.conf (extrait)\ntable ip nat {\n chain prerouting {\n type nat hook prerouting priority dstnat;\n iifname "wan0" tcp dport 443 dnat to 10.62.40.20:443\n }\n chain postrouting {\n type nat hook postrouting priority srcnat;\n oifname "wan0" ip saddr 10.62.0.0/16 masquerade\n }\n}\ntable inet filtre {\n chain forward {\n type filter hook forward priority filter; policy drop;\n ct state established,related accept\n iifname "lan0" oifname "wan0" accept\n iifname "wan0" ip daddr 10.62.40.20 tcp dport 443 ct state new accept\n }\n}\n\nsudo nft -c -f /etc/nftables.conf && sudo systemctl reload nftables' },
{ t: 'Publier un serveur : DNAT et hairpin', d: '', html: 'Pour rendre l’ENT interne accessible depuis Internet, on crée un DNAT de l’adresse publique, port 443, vers l’adresse privée du serveur, et une règle de forward qui autorise ce flux. Le serveur interne doit avoir la passerelle comme route par défaut, sinon ses réponses partiraient ailleurs et ne seraient pas re-traduites.<br><br>Un cas particulier surprend souvent : un poste <b>interne</b> qui tente de joindre le serveur par son adresse <b>publique</b>. Le DNAT s’applique, le paquet est envoyé au serveur, mais celui-ci répond directement au poste, qui est sur le même réseau, sans repasser par la passerelle. Le poste reçoit une réponse venant d’une adresse qu’il n’a pas contactée et l’ignore. C’est le problème du <b>hairpin NAT</b> (NAT loopback). Deux solutions : ajouter un SNAT pour ce flux afin de forcer le retour par la passerelle, ou, mieux, utiliser un <b>DNS interne</b> (split horizon) qui renvoie l’adresse privée aux clients internes.' },
{ t: 'Limites et effets de bord du NAT', d: '', html: 'Le NAT casse le principe de bout en bout d’IP. Les protocoles qui transportent des adresses IP dans leurs données (FTP en mode actif, SIP pour la téléphonie, certains jeux) ne fonctionnent qu’avec des modules d’aide (conntrack helpers) ou des mécanismes de traversée (STUN, TURN). IPsec en mode transport s’accommode mal de la modification des en-têtes, d’où l’encapsulation NAT-T en UDP 4500.<br><br>Côté opérateurs, le manque d’adresses a conduit au <b>CGNAT</b> (Carrier-Grade NAT, plage 100.64.0.0/10) : l’abonné lui-même reçoit une adresse privée, traduite une seconde fois chez l’opérateur. Il devient impossible d’héberger un service chez soi, et la traçabilité impose aux opérateurs de journaliser les ports attribués. La vraie solution est <b>IPv6</b>, où chaque appareil dispose d’adresses globales et où la sécurité repose sur le pare-feu, pas sur la traduction.<br><br>Enfin, le NAT a des obligations légales : en France, l’organisme qui fournit un accès à Internet doit pouvoir identifier l’utilisateur à l’origine d’une connexion. Sur une passerelle NAT, cela suppose de conserver les journaux de connexions (adresse interne, port source traduit, horodatage) pendant la durée prévue par la réglementation.', attention: 'Le NAT n’est pas un pare-feu. Une règle DNAT publiée sans filtrage associé, ou un pare-feu en policy accept, expose le serveur interne à tout Internet. Chaque redirection doit s’accompagner d’une règle forward explicite et d’un serveur à jour.' }
],
synthese: ['SNAT/masquerade en postrouting pour sortir, DNAT en prerouting pour publier', 'PAT : des milliers de postes derrière une IP grâce à la traduction du port source', 'Le NAT repose sur conntrack : les règles ne s’appliquent qu’au premier paquet', 'Activer net.ipv4.ip_forward et filtrer dans forward avec les adresses privées', 'Hairpin NAT : préférer un DNS interne', 'Le NAT n’est pas un pare-feu ; IPv6 rend la traduction inutile'],
qcm: [
['À quel crochet netfilter s’applique le DNAT ?', ['postrouting', 'prerouting', 'forward', 'output uniquement'], 1, 'Avant la décision de routage.'],
['Quelle forme de NAT utilise une passerelle dont l’IP publique change à chaque reconnexion ?', ['DNAT', 'NAT statique', 'Masquerade', 'Hairpin'], 2, 'L’adresse est lue sur l’interface de sortie.'],
['Comment le PAT distingue-t-il les réponses destinées à deux postes qui joignent le même site ?', ['Par l’adresse MAC', 'Par le port source traduit', 'Par le TTL', 'Il ne peut pas'], 1, 'Chaque connexion reçoit un port unique sur l’IP publique.'],
['Sans net.ipv4.ip_forward=1, que se passe-t-il ?', ['Rien', 'Le noyau ne transmet pas les paquets entre les interfaces', 'Le NAT devient statique', 'Le DNS s’arrête'], 1, 'La machine n’agit pas comme routeur.'],
['Dans la chaîne forward, un paquet sortant d’un poste interne a pour adresse source :', ['l’IP publique', 'l’IP privée du poste', '0.0.0.0', 'l’IP de la passerelle'], 1, 'Le SNAT intervient après, en postrouting.'],
['Que signifie « nf_conntrack: table full, dropping packet » ?', ['Le disque est plein', 'La table de suivi de connexions est saturée', 'Le pare-feu est désactivé', 'Le DNS est lent'], 1, 'Augmenter nf_conntrack_max et chercher la cause.'],
['Un poste interne ne joint pas le serveur publié par l’adresse publique, alors que l’extérieur y parvient. Problème ?', ['DNS externe', 'Hairpin NAT', 'MTU', 'Serial DNS'], 1, 'La réponse revient sans être re-traduite.'],
['Quelle plage est utilisée par le CGNAT des opérateurs ?', ['10.0.0.0/8', '100.64.0.0/10', '169.254.0.0/16', '192.0.2.0/24'], 1, 'RFC 6598.'],
['Quelle commande liste les connexions suivies ?', ['nft list ruleset', 'conntrack -L', 'ss -nat', 'ip route'], 1, 'Paquet conntrack.'],
['Pourquoi le NAT n’est-il pas une mesure de sécurité suffisante ?', ['Il chiffre mal', 'Il ne filtre rien : une redirection sans règle forward expose le serveur', 'Il est lent', 'Il est interdit'], 1, 'Le filtrage relève du pare-feu.']
],
td: [
{ n: 'Échauffement', c: 'Pour chaque besoin, indiquer SNAT, masquerade, DNAT ou NAT statique : (a) les postes naviguent derrière une IP fixe, (b) la box obtient son IP par DHCP de l’opérateur, (c) publier le serveur web interne sur le port 443, (d) donner une IP publique dédiée à un serveur de visioconférence.', r: '(a) SNAT · (b) masquerade · (c) DNAT · (d) NAT statique 1:1' },
{ n: 'Échauffement', c: 'Remplir la table de traduction PAT : deux postes 10.62.20.5:50001 et 10.62.20.9:50001 joignent 93.184.216.34:443 via l’IP publique 203.0.113.10.', r: '10.62.20.5:50001 ↔ 203.0.113.10:50001 (ou autre port libre)\n10.62.20.9:50001 ↔ 203.0.113.10:61234 (port réécrit pour éviter la collision)' },
{ n: 'Application', c: 'Écrire la configuration nftables complète d’une passerelle : LAN 10.62.0.0/16 sur lan0, WAN sur wan0 en IP dynamique, sortie Internet pour tout le LAN, filtrage forward en politique drop.', r: 'table ip nat { chain postrouting { type nat hook postrouting priority srcnat; oifname "wan0" ip saddr 10.62.0.0/16 masquerade } }\ntable inet filtre { chain forward { type filter hook forward priority filter; policy drop; ct state established,related accept; iifname "lan0" oifname "wan0" accept } }\n+ net.ipv4.ip_forward=1' },
{ n: 'Application', c: 'Publier le serveur SSH interne 10.62.40.30 sur le port externe 2222, en limitant l’accès à l’adresse du rectorat 198.51.100.50.', r: 'prerouting : iifname "wan0" ip saddr 198.51.100.50 tcp dport 2222 dnat to 10.62.40.30:22\nforward : iifname "wan0" ip saddr 198.51.100.50 ip daddr 10.62.40.30 tcp dport 22 ct state new accept' },
{ n: 'Application', c: 'Le serveur publié répond aux tests depuis la passerelle mais pas depuis Internet. conntrack montre des entrées [UNREPLIED]. Proposer deux causes et leurs vérifications.', r: 'Route par défaut du serveur qui ne passe pas par la passerelle (ip route sur le serveur).\nRègle forward manquante ou pare-feu local du serveur (nft list ruleset, tcpdump sur l’interface LAN de la passerelle).' },
{ n: 'Approfondissement', c: 'Expliquer le problème du hairpin NAT et comparer les deux solutions (SNAT de retour et DNS split horizon).', r: 'Le serveur répond directement au poste interne, qui rejette la réponse. SNAT de retour : fonctionne mais le serveur voit toutes les connexions internes venir de la passerelle (journaux moins utiles). Split horizon : le DNS interne renvoie l’IP privée, le trafic ne passe plus par la passerelle ; solution recommandée.' },
{ n: 'Approfondissement', c: 'Une passerelle d’établissement de 3 000 utilisateurs rejette des connexions le jour du bac blanc en ligne. Chiffrer la taille nécessaire de la table conntrack et proposer les réglages.', r: 'Environ 100 à 200 connexions simultanées par appareil actif en navigation moderne : 3 000 × 150 ≈ 450 000 entrées. nf_conntrack_max à 524288 (mémoire ≈ 300 octets par entrée ≈ 150 Mo), réduire nf_conntrack_tcp_timeout_established de 5 jours à quelques heures, superviser conntrack -C.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Une passerelle Debian à deux interfaces, un client LAN et un serveur web interne.', etapes: ['Activer le routage IP', 'Écrire la table nat avec masquerade', 'Filtrer forward en politique drop', 'Publier le serveur web par DNAT', 'Observer les traductions avec conntrack -E', 'Résoudre le hairpin NAT par un DNS interne'] },
plusLoin: [['RFC 3022 · Traditional NAT', 'https://www.rfc-editor.org/rfc/rfc3022'], ['RFC 6598 · plage 100.64.0.0/10', 'https://www.rfc-editor.org/rfc/rfc6598'], ['Wiki nftables · Performing NAT', 'https://wiki.nftables.org/wiki-nftables/index.php/Performing_Network_Address_Translation_(NAT)'], ['Debian Wiki · nftables', 'https://wiki.debian.org/nftables']] },
plus: {
cas: { titre: '', texte: '' },
resolu: { enonce: 'Lundi 9 h : le serveur de visioconférence interne (10.62.40.50), publié la semaine dernière sur l’adresse publique 203.0.113.10 port 443, fonctionne pour les élèves à la maison mais pas pour les professeurs dans le lycée, qui utilisent le même lien https://visio.lycee-exemple.fr. Diagnostiquer et corriger.', etapes: [
['1 · Reproduire depuis l’intérieur et l’extérieur', 'Depuis un poste interne :\ncurl -sI --max-time 5 https://visio.lycee-exemple.fr\ncurl: (28) Connection timed out after 5001 milliseconds\nDepuis un smartphone en 4G : la page s’ouvre.\n→ Le service fonctionne depuis Internet. Seul le trafic interne vers l’adresse publique échoue : ce n’est ni le serveur ni la redirection de base.'],
['2 · Vérifier la résolution', 'dig +short visio.lycee-exemple.fr\n203.0.113.10\n→ Le poste interne reçoit l’adresse publique : il envoie donc ses paquets à la passerelle, qui applique le DNAT vers 10.62.40.50.'],
['3 · Observer la passerelle', 'sudo conntrack -L -d 203.0.113.10 | grep 10.62.20\ntcp 6 118 SYN_SENT src=10.62.20.57 dst=203.0.113.10 sport=52011 dport=443 [UNREPLIED] src=10.62.40.50 dst=10.62.20.57 sport=443 dport=52011\n→ La traduction est bien faite (réponse attendue de 10.62.40.50), mais la connexion reste en SYN_SENT et [UNREPLIED] : la passerelle ne voit jamais la réponse.'],
['4 · Observer le serveur', 'Sur 10.62.40.50 :\nsudo tcpdump -ni ens18 -c 2 host 10.62.20.57 and port 443\nIP 10.62.20.57.52011 > 10.62.40.50.443: Flags [S]\nIP 10.62.40.50.443 > 10.62.20.57.52011: Flags [S.]\n→ Le serveur reçoit le SYN venant du poste (source non traduite) et lui répond directement, sans passer par la passerelle. Le poste reçoit un SYN-ACK de 10.62.40.50 alors qu’il attendait 203.0.113.10 : il l’ignore. C’est un problème de hairpin NAT.'],
['5 · Corriger par un DNS interne (split horizon)', 'Sur le résolveur interne Bind9, ajout d’une vue ou d’une zone locale :\nzone "visio.lycee-exemple.fr" { type primary; file "/etc/bind/db.visio-interne"; };\n; db.visio-interne : @ IN A 10.62.40.50\nsudo named-checkzone visio.lycee-exemple.fr /etc/bind/db.visio-interne && sudo rndc reload\ndig +short visio.lycee-exemple.fr (depuis un poste interne)\n10.62.40.50\n→ Les clients internes reçoivent l’adresse privée et joignent le serveur directement ; l’extérieur continue de recevoir l’adresse publique.'],
['6 · Vérifier et documenter', 'curl -sI https://visio.lycee-exemple.fr | head -1\nHTTP/2 200\n→ Service rétabli pour l’interne. Compte rendu : symptôme (timeout interne uniquement), preuve (conntrack UNREPLIED + tcpdump sur le serveur), cause (hairpin NAT), correction (split horizon), à noter : le certificat HTTPS reste valide car le nom est identique. Alternative écartée : SNAT de retour, qui masquerait les adresses des postes dans les journaux du serveur.']
] },
erreurs: ['Oublier d’activer net.ipv4.ip_forward', 'Écrire le filtrage forward avec l’adresse publique au lieu de l’adresse privée', 'Publier un DNAT sans règle forward correspondante, ou avec une politique accept', 'Oublier que le serveur publié doit avoir la passerelle comme route par défaut', 'Tester la publication depuis l’intérieur et conclure à une panne (hairpin)', 'Laisser la table conntrack à sa taille par défaut sur une grosse passerelle', 'Appliquer /etc/nftables.conf sans nft -c et se retrouver sans règles', 'Considérer le NAT comme une protection suffisante'],
memo: [['Commande', 'Usage en production'], ['sysctl net.ipv4.ip_forward', 'vérifier que la machine route'], ['nft list ruleset', 'voir toutes les règles, NAT et filtrage'], ['nft list table ip nat', 'voir uniquement les traductions'], ['nft -c -f /etc/nftables.conf', 'valider la configuration sans l’appliquer'], ['systemctl reload nftables', 'appliquer la configuration permanente'], ['conntrack -L', 'lister les connexions suivies et leurs traductions'], ['conntrack -E', 'suivre en direct la création des traductions'], ['conntrack -C', 'nombre d’entrées dans la table'], ['sysctl net.netfilter.nf_conntrack_max', 'taille maximale de la table'], ['conntrack -D -s <ip>', 'supprimer les entrées d’un poste après un changement de règle'], ['tcpdump -ni wan0 port 443', 'voir les paquets après traduction côté WAN'], ['curl ifconfig.me', 'connaître l’IP publique de sortie']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer le fonctionnement du PAT à l’aide d’une table de traduction pour trois connexions. (4 pts)', 'Situer SNAT, DNAT et le filtrage dans le parcours netfilter et en déduire les adresses visibles en forward. (4 pts)', 'Écrire la configuration nftables d’une passerelle avec sortie Internet et publication d’un serveur web. (7 pts)', 'Diagnostiquer un cas de hairpin NAT à partir de sorties conntrack et tcpdump fournies. (3 pts)', 'Expliquer pourquoi IPv6 rend le NAT inutile et ce qui assure alors la sécurité. (2 pts)'], r: ['Quintuplets d’origine et traduits, ports réécrits', 'prerouting DNAT → forward (adresses privées) → postrouting SNAT', 'table ip nat (prerouting dnat, postrouting masquerade) + table inet filtre forward policy drop', 'UNREPLIED côté passerelle, réponse directe côté serveur', 'Adresses globales pour tous ; pare-feu à état'] },
prof: { deroule: [['Ouverture', 'curl ifconfig.me sur tous les postes : tout le monde a la même adresse, pourquoi ?'], ['Cours', 'Pénurie IPv4, formes de NAT, PAT et table de traduction'], ['Cours', 'Netfilter, conntrack, ordre des crochets'], ['Pratique', 'Lab passerelle : masquerade, filtrage, DNAT'], ['Incident', 'Hairpin NAT injecté à diagnostiquer'], ['Clôture', 'Quiz flash et discussion IPv6']], diff: 'Élèves en difficulté : configuration nftables à compléter, schéma du parcours netfilter imprimé. Élèves avancés : NAT et IPv6 (NPTv6), dimensionnement conntrack, journalisation légale des traductions.', eval: 'Étapes du lab validées, analyse de l’incident, sujet type.' }
},
exemples: [
['Vérifier que la passerelle route', 'Aucun poste n’accède à Internet après le redémarrage de la passerelle.', 'sysctl net.ipv4.ip_forward', 'net.ipv4.ip_forward = 0', 'Le routage a été activé à la main avec sysctl -w mais jamais rendu permanent : il a disparu au redémarrage. Correction : fichier /etc/sysctl.d/99-routage.conf contenant net.ipv4.ip_forward=1, puis sysctl --system.'],
['Lire les traductions en cours', 'Qui, dans le lycée, se connecte à ce serveur externe signalé par le CERT ?', 'sudo conntrack -L -d 185.220.101.4', 'tcp 6 431998 ESTABLISHED src=10.62.20.114 dst=185.220.101.4 sport=49822 dport=9001 src=185.220.101.4 dst=203.0.113.10 sport=9001 dport=49822 [ASSURED]', 'La première moitié de la ligne est le flux d’origine, la seconde le flux traduit vu depuis Internet. Le poste interne 10.62.20.114 est à l’origine de la connexion. Sans la table conntrack ou ses journaux, l’adresse publique 203.0.113.10 seule ne permettrait pas d’identifier le poste.'],
['Suivre les traductions en direct', 'Vérifier qu’une nouvelle redirection de port est bien utilisée.', 'sudo conntrack -E -p tcp --dport 443 -d 203.0.113.10', '[NEW] tcp 6 120 SYN_SENT src=198.51.100.7 dst=203.0.113.10 sport=60412 dport=443 [UNREPLIED] src=10.62.40.20 dst=198.51.100.7 sport=443 dport=60412\n[UPDATE] tcp 6 60 SYN_RECV …\n[UPDATE] tcp 6 432000 ESTABLISHED … [ASSURED]', 'Le DNAT est appliqué dès le premier paquet (réponse attendue de 10.62.40.20). La connexion passe de SYN_SENT à ESTABLISHED : le serveur répond et le filtrage laisse passer. Une connexion bloquée en [UNREPLIED] aurait orienté vers le filtrage ou la route du serveur.'],
['Détecter une table conntrack saturée', 'Le jour d’un examen en ligne, des connexions échouent au hasard.', 'sudo dmesg | grep conntrack | tail -1 ; sudo conntrack -C ; sysctl -n net.netfilter.nf_conntrack_max', 'nf_conntrack: nf_conntrack: table full, dropping packet\n65536\n65536', 'La table est pleine : chaque nouvelle connexion est rejetée. On augmente immédiatement la limite (sysctl -w net.netfilter.nf_conntrack_max=524288), puis on la rend permanente et on réduit la durée de vie des connexions établies inactives.'],
['Afficher les règles de NAT', 'Audit de la passerelle avant la rentrée.', 'sudo nft list table ip nat', 'table ip nat {\n chain prerouting {\n type nat hook prerouting priority dstnat; policy accept;\n iifname "wan0" tcp dport 3389 dnat to 10.62.10.15:3389\n iifname "wan0" tcp dport 443 dnat to 10.62.40.20:443\n }\n chain postrouting {\n type nat hook postrouting priority srcnat; policy accept;\n oifname "wan0" ip saddr 10.62.0.0/16 masquerade\n }\n}', 'Une redirection du bureau à distance (RDP, port 3389) vers un poste de l’administration a été oubliée depuis un dépannage. C’est une exposition directe à Internet d’un service très attaqué. On la supprime et on passe par un VPN pour les accès à distance.'],
['Connaître son adresse de sortie', 'Le prestataire demande l’IP à autoriser sur son service.', 'curl -s https://ifconfig.me ; echo ; ip -br addr show wan0', '203.0.113.10\nwan0 UP 203.0.113.10/29', 'Les deux valeurs concordent : les postes sortent bien par l’adresse de l’interface WAN. Si curl avait renvoyé une adresse en 100.64.x.x ou différente de celle de wan0, on serait derrière un CGNAT ou un autre équipement de traduction.']
],
long: { figure: svg(720, 220, bx(20, 30, 180, 44, Y, '10.62.20.5:50001', 14) + bx(20, 90, 180, 44, Y, '10.62.20.9:50001', 14) + ln(200, 52, 280, 90) + ln(200, 112, 280, 112) + bx(280, 70, 160, 80, L, 'Passerelle PAT', 15) + ln(440, 110, 510, 110) + bx(510, 30, 190, 44, G, '203.0.113.10:50001', 13) + bx(510, 90, 190, 44, G, '203.0.113.10:61234', 13) + tx(20, 190, 'Une seule adresse publique, un port source unique par connexion', 14)), legende: 'Le PAT fait partager une adresse publique en réécrivant le port source.', photo: 'Photo : pare-feu ou routeur de bordure dans une baie',
lecture: ['Le NAT est l’un de ces mécanismes que tout le monde utilise sans le voir. Chaque smartphone, chaque ordinateur d’élève, chaque imprimante d’un établissement a une adresse privée ; tous sortent sur Internet par la même adresse publique. Pour les sites consultés, un lycée de 1 500 personnes n’est qu’une seule machine qui ouvre beaucoup de connexions.', 'Techniquement, c’est un tour de passe-passe réalisé par le noyau du système de la passerelle. Il réécrit l’adresse, souvent le port, recalcule les sommes de contrôle, et note tout dans une table pour faire l’opération inverse au retour. Cette table est la clé du fonctionnement, et donc du dépannage : quand un flux ne passe pas, c’est en lisant les entrées conntrack que l’on voit si la traduction a eu lieu et si la réponse est revenue.', 'Il faut garder en tête que le NAT a été inventé comme un pis-aller, en attendant IPv6. Il complique les applications, empêche d’héberger simplement des services, et oblige à conserver des journaux pour pouvoir identifier un utilisateur. Sa réputation de protection vient uniquement du fait qu’il bloque par défaut les connexions entrantes ; c’est le pare-feu qui doit assurer ce rôle de façon explicite.', 'Enfin, chaque redirection de port est une porte ouverte vers l’intérieur du réseau. Les audits de sécurité trouvent régulièrement des redirections oubliées vers des bureaux à distance, des caméras ou des interfaces d’administration. Une bonne pratique consiste à documenter chaque règle DNAT avec son demandeur et sa date, et à les revoir à chaque rentrée.'] },
schemas: { termes: ['NAT', 'PAT', 'SNAT', 'DNAT', 'NAPT', 'masquerade', 'conntrack', 'netfilter', 'nftables', 'nft', 'prerouting', 'postrouting', 'forward', 'ip_forward', 'nf_conntrack_max', 'ESTABLISHED', 'UNREPLIED', 'SYN_SENT', 'hairpin NAT', 'split horizon', 'CGNAT', '100.64.0.0/10', 'RFC 1918', 'IPv6', 'port forwarding'],
figs: [[3, 'Le parcours d’un paquet dans netfilter', svg(720, 110, [['prerouting · DNAT', Y], ['routage', N], ['forward · filtrage', L], ['postrouting · SNAT', G]].map(function (c, i) { return bx(10 + i * 178, 30, 168, 50, c[1], c[0], 13) + (i < 3 ? ln(178 + i * 178, 55, 188 + i * 178, 55) : ''); }).join(''))]] }
});
// ================= Routage statique =================
reg({
cours: { id: 'routage', match: /Routage statique|table de routage|route par défaut|Deux LAN et un routeur/i,
titre: 'Routage statique : table de routage, route par défaut et interconnexion de réseaux', duree: '',
niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T S2 · TSSR',
prerequis: ['Adressage IPv4 et sous-réseaux', 'Diagnostic réseau en ligne de commande'],
competences: ['BTS SIO SISR · Bloc 2 · Concevoir et mettre en œuvre une solution d’infrastructure réseau', 'BTS CIEL · C03 · Configurer un réseau', 'BUT R&T · Compétence « Connecter » · niveau 1'],
intro: 'Dès qu’un établissement compte plus d’un réseau, il faut des routeurs, et chaque routeur doit savoir où envoyer les paquets. Le routage statique consiste à écrire soi-même ces chemins. C’est la forme de routage la plus simple, la plus prévisible, et celle qu’on trouve partout : route par défaut de chaque poste, liaison entre deux sites, accès à un réseau de laboratoire derrière un routeur Linux. C’est aussi la base indispensable pour comprendre les protocoles dynamiques comme OSPF : un routeur, dynamique ou non, ne fait qu’une chose, chercher dans sa table la route la plus précise vers la destination.',
cm: [
{ t: 'Ce que fait un routeur', d: '', html: 'Un routeur reçoit un paquet sur une interface, lit l’<b>adresse IP de destination</b>, cherche dans sa <b>table de routage</b> la meilleure route vers cette destination, décrémente le TTL, puis réémet le paquet dans une nouvelle trame vers le <b>prochain saut</b> (next hop). Il ne connaît jamais le chemin complet : il sait seulement à quel voisin confier le paquet. Chaque routeur prend sa décision indépendamment, saut par saut.<br><br>Toute machine IP, y compris un simple poste, possède une table de routage et fait ce raisonnement pour chaque paquet qu’elle émet. La différence est qu’un poste ne transmet pas les paquets qui ne lui sont pas destinés, alors qu’un routeur le fait : sous Linux, c’est le paramètre <code>net.ipv4.ip_forward</code> qui fait basculer une machine d’un rôle à l’autre.' },
{ t: 'La table de routage', d: '', html: 'Chaque entrée associe un <b>préfixe de destination</b> à une <b>interface de sortie</b> et, si la destination n’est pas directement connectée, à une <b>passerelle</b> (next hop). On y trouve trois sortes de routes. Les routes <b>connectées</b> sont créées automatiquement quand une adresse est configurée sur une interface : le réseau de l’interface est joignable directement. Les routes <b>statiques</b> sont ajoutées par l’administrateur. Les routes <b>dynamiques</b> sont apprises par un protocole de routage.<br><br>Sous Linux, <code>ip route</code> affiche la table principale. Le champ <code>proto kernel</code> désigne une route connectée, <code>proto static</code> une route ajoutée à la main ou par la configuration, <code>scope link</code> une destination joignable sans passerelle.', exemple: '$ ip route\ndefault via 10.62.0.1 dev ens18 proto static\n10.62.0.0/24 dev ens18 proto kernel scope link src 10.62.0.254\n10.62.40.0/24 dev ens19 proto kernel scope link src 10.62.40.1\n172.20.0.0/16 via 10.62.40.2 dev ens19 proto static' },
{ t: 'La règle de la correspondance la plus longue', d: '', html: 'Une destination peut correspondre à plusieurs routes : une route par défaut 0.0.0.0/0 correspond à tout, une route 10.62.0.0/16 à tout le réseau de l’établissement, une route 10.62.40.0/24 au seul réseau des serveurs. Le routeur choisit toujours la route dont le préfixe est le <b>plus long</b> (longest prefix match), c’est-à-dire la plus précise.<br><br>À longueur égale, il départage par la <b>distance administrative</b> (la confiance accordée à la source de la route : 0 pour une route connectée, 1 pour une statique chez Cisco, 110 pour OSPF) puis par la <b>métrique</b>. Sous Linux, c’est la métrique qui départage directement : la plus faible l’emporte.<br><br>Cette règle permet des configurations élégantes : une route par défaut vers Internet, et quelques routes plus précises vers les réseaux internes, sans conflit.', exemple: 'Destination 10.62.40.25\n 0.0.0.0/0 via 10.62.0.1 correspond (0 bit)\n 10.62.0.0/16 via 10.62.0.2 correspond (16 bits)\n 10.62.40.0/24 dev ens19 correspond (24 bits) ← choisie\n\n$ ip route get 10.62.40.25\n10.62.40.25 dev ens19 src 10.62.40.1' },
{ t: 'La route par défaut', d: '', html: 'La route <b>par défaut</b>, notée <code>default</code> ou 0.0.0.0/0, est utilisée quand aucune autre route ne correspond. Sur un poste, c’est la seule route utile en plus du réseau local : tout ce qui n’est pas local part vers la passerelle. Sur un routeur de bordure, elle pointe vers l’opérateur.<br><br>Une machine sans route par défaut ne joint que les réseaux qu’elle connaît explicitement. Le message d’erreur est alors immédiat et caractéristique : <code>connect: Network is unreachable</code>, émis localement sans qu’aucun paquet ne sorte. À ne pas confondre avec un « Destination Host Unreachable » renvoyé par un routeur ou par la machine elle-même quand une résolution ARP échoue.' },
{ t: 'Ajouter des routes sous Linux', d: '', html: 'La commande <code>ip route add</code> ajoute une route immédiatement, mais <b>de façon non persistante</b> : elle disparaît au redémarrage ou lors d’un redémarrage de l’interface. La persistance dépend du gestionnaire réseau : ifupdown (<code>/etc/network/interfaces</code>, directive post-up ou routes dans le bloc de l’interface), systemd-networkd (section [Route] d’un fichier .network) ou NetworkManager (<code>nmcli connection modify … +ipv4.routes</code>).<br><br>La passerelle indiquée doit être <b>directement joignable</b> par une route connectée ; sinon le noyau refuse la route avec « Nexthop has invalid gateway ».', exemple: '# Immédiat, non persistant\nsudo ip route add 172.20.0.0/16 via 10.62.40.2 dev ens19\nsudo ip route replace default via 10.62.0.1\nsudo ip route del 172.20.0.0/16\n\n# Persistant avec ifupdown : /etc/network/interfaces\nauto ens19\niface ens19 inet static\n address 10.62.40.1/24\n up ip route add 172.20.0.0/16 via 10.62.40.2\n down ip route del 172.20.0.0/16 via 10.62.40.2\n\n# Persistant avec systemd-networkd : /etc/systemd/network/ens19.network\n[Route]\nDestination=172.20.0.0/16\nGateway=10.62.40.2' },
{ t: 'Routes statiques sur un routeur Cisco', d: '', html: 'Sur les routeurs des laboratoires de BTS, la syntaxe est <code>ip route réseau masque prochain-saut</code>. On peut aussi indiquer l’interface de sortie, utile sur les liaisons point à point, et une distance administrative en fin de commande pour créer une <b>route flottante</b> : une route de secours qui ne s’installe dans la table que si la route principale disparaît.', exemple: 'R1(config)# ip route 172.20.0.0 255.255.0.0 10.62.40.2\nR1(config)# ip route 0.0.0.0 0.0.0.0 203.0.113.1\nR1(config)# ip route 0.0.0.0 0.0.0.0 198.51.100.1 250 ! route flottante de secours\nR1# show ip route static\nS* 0.0.0.0/0 [1/0] via 203.0.113.1\nS 172.20.0.0/16 [1/0] via 10.62.40.2' },
{ t: 'Le chemin retour', d: '', html: 'L’erreur la plus fréquente en routage statique n’est pas l’aller mais le <b>retour</b>. Une communication fonctionne seulement si chaque routeur du chemin connaît aussi le réseau d’origine. Ajouter sur R1 une route vers le réseau derrière R2 ne suffit pas : R2 doit avoir une route vers le réseau derrière R1, sinon les réponses partent vers sa route par défaut et se perdent.<br><br>Le symptôme typique : un tcpdump sur la machine de destination montre les requêtes qui arrivent et les réponses qui partent, mais l’émetteur ne reçoit rien. On vérifie alors la table de chaque routeur <b>dans les deux sens</b>.<br><br>Un piège voisin est le <b>routage asymétrique</b> : l’aller et le retour empruntent des chemins différents. Cela fonctionne pour le routage pur, mais un pare-feu à état sur un seul des deux chemins ne voit qu’une moitié de la connexion et la rejette.', attention: 'Linux applique par défaut un filtrage anti-usurpation, rp_filter : il rejette un paquet dont l’adresse source ne serait pas routée par l’interface d’arrivée. En cas de routage asymétrique légitime, les paquets disparaissent sans message. On le vérifie avec sysctl net.ipv4.conf.all.rp_filter.' },
{ t: 'Statique ou dynamique ?', d: '', html: 'Le routage statique est sûr, lisible, sans consommation de ressources et sans risque qu’un voisin mal configuré annonce de fausses routes. Il convient parfaitement aux petites topologies, aux sites terminaux qui n’ont qu’une sortie (stub networks) et aux routes par défaut.<br><br>Il devient ingérable dès que le réseau grandit : chaque ajout de réseau impose de modifier tous les routeurs, et une panne de lien n’est pas contournée automatiquement, sauf route flottante. Au-delà de quelques routeurs ou dès qu’il y a des chemins redondants, on passe à un protocole de routage dynamique comme <b>OSPF</b>, qui recalcule les chemins en quelques secondes après une panne. Dans la pratique, les deux coexistent : protocole dynamique en cœur de réseau, routes statiques en bordure.' }
],
synthese: ['Un routeur choisit le prochain saut à partir de l’adresse de destination, saut par saut', 'Routes connectées, statiques, dynamiques', 'Correspondance la plus longue, puis distance administrative, puis métrique', 'Route par défaut 0.0.0.0/0 ; « Network is unreachable » sans elle', 'ip route add est volatile : rendre persistant dans la configuration réseau', 'Toujours vérifier le chemin retour', 'Statique pour les petites topologies et les bordures, dynamique au-delà'],
qcm: [
['Quelle information du paquet le routeur utilise-t-il pour décider ?', ['Adresse MAC source', 'Adresse IP de destination', 'Port TCP', 'TTL'], 1, 'Il cherche la route vers la destination.'],
['Pour 10.62.40.25, quelle route est choisie entre 0.0.0.0/0, 10.62.0.0/16 et 10.62.40.0/24 ?', ['0.0.0.0/0', '10.62.0.0/16', '10.62.40.0/24', 'Aucune'], 2, 'Préfixe le plus long.'],
['Que signifie « connect: Network is unreachable » ?', ['Le serveur a refusé', 'Aucune route, pas même par défaut, ne correspond', 'Le DNS est en panne', 'Le câble est coupé'], 1, 'Erreur locale, aucun paquet envoyé.'],
['Une route ajoutée avec ip route add :', ['est permanente', 'disparaît au redémarrage', 'est propagée aux voisins', 'remplace la route par défaut'], 1, 'Il faut l’écrire dans la configuration réseau.'],
['Que marque proto kernel dans ip route ?', ['Une route statique', 'Une route connectée créée avec l’adresse de l’interface', 'Une route OSPF', 'Une route par défaut'], 1, 'Générée automatiquement.'],
['R1 a une route vers le LAN de R2, mais les pings échouent. Hypothèse prioritaire ?', ['DNS', 'R2 n’a pas de route retour vers le LAN de R1', 'MTU', 'Câble'], 1, 'Toujours vérifier le retour.'],
['Qu’est-ce qu’une route flottante ?', ['Une route qui change de passerelle', 'Une route de secours avec une distance administrative plus élevée', 'Une route IPv6', 'Une route temporaire'], 1, 'Elle ne s’installe qu’en cas de perte de la principale.'],
['Pourquoi le noyau refuse-t-il « ip route add 172.20.0.0/16 via 192.0.2.9 » avec « Nexthop has invalid gateway » ?', ['Le réseau n’existe pas', 'La passerelle n’est pas sur un réseau directement connecté', 'Il manque sudo', 'La route existe déjà'], 1, 'Le prochain saut doit être joignable directement.'],
['Quel paramètre fait d’une machine Linux un routeur ?', ['net.ipv4.rp_filter', 'net.ipv4.ip_forward', 'net.core.somaxconn', 'net.ipv4.tcp_syncookies'], 1, 'Transmission entre interfaces.'],
['Quand préférer OSPF au routage statique ?', ['Pour une seule route par défaut', 'Dès qu’il y a plusieurs routeurs et des chemins redondants', 'Jamais', 'Pour un poste client'], 1, 'Reconvergence automatique.']
],
td: [
{ n: 'Échauffement', c: 'Pour la table suivante — default via 10.0.0.1 ; 10.0.0.0/24 dev eth0 ; 192.168.5.0/24 via 10.0.0.2 ; 192.168.5.128/25 via 10.0.0.3 — donner le prochain saut pour 192.168.5.10, 192.168.5.200, 10.0.0.77 et 8.8.8.8.', r: '10.0.0.2 · 10.0.0.3 (préfixe /25 plus long) · direct sur eth0 · 10.0.0.1' },
{ n: 'Application', c: 'Topologie : LAN A 10.1.0.0/24 — R1 (.1 / 10.255.0.1) — lien 10.255.0.0/30 — R2 (10.255.0.2 / .1) — LAN B 10.2.0.0/24. Écrire toutes les routes nécessaires sur R1, R2 et un poste de chaque LAN pour que A et B communiquent.', r: 'R1 : ip route add 10.2.0.0/24 via 10.255.0.2\nR2 : ip route add 10.1.0.0/24 via 10.255.0.1\nPoste A : default via 10.1.0.1 · Poste B : default via 10.2.0.1\nip_forward=1 sur R1 et R2' },
{ n: 'Application', c: 'Rendre persistante sur R1 (Debian, ifupdown) la route vers 10.2.0.0/24, puis la vérifier après redémarrage de l’interface.', r: 'Dans le bloc iface de l’interface du lien : up ip route add 10.2.0.0/24 via 10.255.0.2\nsudo ifdown ens19 && sudo ifup ens19 ; ip route | grep 10.2.0.0' },
{ n: 'Application', c: 'Un traceroute de A vers B affiche 10.1.0.1 puis 10.255.0.2, puis des étoiles, et la machine B voit arriver les paquets. Diagnostiquer.', r: 'L’aller fonctionne jusqu’à B. B répond mais la réponse ne revient pas : route par défaut absente ou erronée sur B, ou route retour manquante sur R2. Vérifier ip route sur B et R2.' },
{ n: 'Approfondissement', c: 'Un site a deux accès Internet : fibre principale (203.0.113.1) et ADSL de secours (198.51.100.1). Proposer la configuration Cisco et Linux d’une bascule automatique avec des routes statiques.', r: 'Cisco : ip route 0.0.0.0 0.0.0.0 203.0.113.1 et ip route 0.0.0.0 0.0.0.0 198.51.100.1 250 (flottante), idéalement avec un suivi IP SLA.\nLinux : deux routes par défaut avec métriques 100 et 200 ; la seconde n’est utilisée que si la première disparaît (lien down).' },
{ n: 'Approfondissement', c: 'Résumer les routes vers 172.20.0.0/24, 172.20.1.0/24, 172.20.2.0/24 et 172.20.3.0/24, toutes derrière 10.62.40.2, en une seule route. Quel est le risque si 172.20.3.0/24 est déplacé derrière un autre routeur ?', r: 'ip route add 172.20.0.0/22 via 10.62.40.2. Si 172.20.3.0/24 part ailleurs, ajouter une route plus précise 172.20.3.0/24 via le nouveau routeur : la correspondance la plus longue la fera primer sur le résumé.' }
],
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Deux LAN, deux routeurs Linux et un lien de transit.', etapes: ['Adresser les quatre machines', 'Activer ip_forward sur les routeurs', 'Ajouter les routes aller', 'Constater l’échec, ajouter les routes retour', 'Valider avec traceroute dans les deux sens', 'Rendre la configuration persistante et redémarrer'] },
plusLoin: [['man ip-route', 'https://man7.org/linux/man-pages/man8/ip-route.8.html'], ['RFC 1812 · Requirements for IPv4 Routers', 'https://www.rfc-editor.org/rfc/rfc1812'], ['systemd.network · section [Route]', 'https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html']] },
plus: {
cas: { titre: '', texte: '' },
resolu: { enonce: 'Mercredi 14 h : après l’installation d’un routeur Linux pour le nouveau laboratoire de BTS (réseau 172.20.0.0/24), les élèves du labo accèdent à Internet mais pas au serveur de fichiers de l’établissement (10.62.40.10), alors que les postes du labo et le serveur se « pingent » depuis le routeur du labo. Diagnostiquer et corriger.', etapes: [
['1 · Reproduire et qualifier', 'Depuis un poste du labo :\nping -c 2 10.62.40.10\n2 packets transmitted, 0 received, 100% packet loss\ntraceroute -n 10.62.40.10\n 1 172.20.0.1 0.4 ms\n 2 10.62.0.1 1.1 ms\n 3 * * *\n→ Le paquet sort par le routeur du labo (172.20.0.1), puis part vers la passerelle principale 10.62.0.1. Il faut comprendre pourquoi il ne va pas plus loin.'],
['2 · Regarder côté serveur', 'Sur 10.62.40.10 :\nsudo tcpdump -ni ens18 -c 4 icmp\nIP 172.20.0.23 > 10.62.40.10: ICMP echo request, id 7, seq 1\nIP 10.62.40.10 > 172.20.0.23: ICMP echo reply, id 7, seq 1\n→ Les requêtes arrivent et le serveur répond. L’aller fonctionne ; c’est le retour qui se perd.'],
['3 · Suivre le chemin retour', 'Sur le serveur :\nip route get 172.20.0.23\n172.20.0.23 via 10.62.40.1 dev ens18 src 10.62.40.10\nSur la passerelle du réseau serveurs (10.62.40.1) :\nip route get 172.20.0.23\n172.20.0.23 via 203.0.113.1 dev wan0 src 203.0.113.10\n→ La passerelle ne connaît pas 172.20.0.0/24 : elle envoie la réponse vers sa route par défaut, c’est-à-dire vers Internet, où elle est perdue.'],
['4 · Identifier le prochain saut correct', 'Sur le routeur du labo :\nip -br addr\nens18 UP 10.62.0.20/24\nens19 UP 172.20.0.1/24\n→ Le routeur du labo est joignable sur le réseau d’infrastructure en 10.62.0.20. C’est ce prochain saut que la passerelle doit utiliser pour 172.20.0.0/24.'],
['5 · Ajouter la route retour, puis la rendre persistante', 'Sur la passerelle :\nsudo ip route add 172.20.0.0/24 via 10.62.0.20\nping -c 2 172.20.0.23\n2 packets transmitted, 2 received, 0% packet loss\nPersistance dans /etc/network/interfaces, bloc de l’interface 10.62.0.1 :\n up ip route add 172.20.0.0/24 via 10.62.0.20\n→ La passerelle sait désormais renvoyer vers le labo. On l’écrit dans la configuration pour survivre au prochain redémarrage.'],
['6 · Valider dans les deux sens et documenter', 'Depuis un poste du labo :\ntraceroute -n 10.62.40.10\n 1 172.20.0.1 0.4 ms\n 2 10.62.0.1 0.9 ms\n 3 10.62.40.10 1.2 ms\n→ Accès rétabli. On note que l’aller passe encore par la passerelle principale : il serait plus direct d’ajouter sur le routeur du labo une route 10.62.0.0/16 via 10.62.0.1, ce qui est déjà le cas par la route par défaut. Compte rendu : cause (route retour absente lors de l’ajout d’un réseau), correction, mise à jour du schéma réseau et de la liste de contrôle « nouveau réseau » : ajouter la route sur tous les routeurs concernés, dans les deux sens.']
] },
erreurs: ['Oublier la route retour', 'Oublier ip_forward sur le routeur Linux', 'Ajouter des routes avec ip route add sans les rendre persistantes', 'Indiquer un prochain saut qui n’est pas sur un réseau directement connecté', 'Oublier la route par défaut d’un poste', 'Confondre « Network is unreachable » (pas de route locale) et « Host Unreachable » (échec ARP ou message d’un routeur)', 'Créer un routage asymétrique à travers un pare-feu à état', 'Laisser rp_filter bloquer un chemin asymétrique légitime'],
memo: [['Commande', 'Usage en production'], ['ip route', 'afficher la table principale'], ['ip route get <ip>', 'route, interface et source réellement utilisées'], ['ip route add <réseau> via <ip>', 'ajouter une route (volatile)'], ['ip route replace default via <ip>', 'changer la route par défaut'], ['ip route del <réseau>', 'supprimer une route'], ['ip route add … metric 200', 'route de secours de moindre priorité'], ['sysctl net.ipv4.ip_forward', 'vérifier le rôle de routeur'], ['sysctl net.ipv4.conf.all.rp_filter', 'vérifier le filtrage anti-usurpation'], ['traceroute -n <ip>', 'suivre le chemin aller'], ['tcpdump -ni <if> icmp', 'voir si les réponses repartent'], ['show ip route (Cisco)', 'table d’un routeur Cisco'], ['ip route 0.0.0.0 0.0.0.0 <ip> 250 (Cisco)', 'route flottante de secours']],
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer la règle de la correspondance la plus longue sur trois routes fournies. (3 pts)', 'Écrire les routes de quatre routeurs pour une topologie donnée, dans les deux sens. (8 pts)', 'Rendre ces routes persistantes sous Debian. (3 pts)', 'Diagnostiquer un problème de route retour à partir de sorties traceroute et tcpdump. (4 pts)', 'Comparer routage statique et dynamique. (2 pts)'], r: ['Préfixe le plus long, puis distance, puis métrique', 'Chaque routeur connaît tous les réseaux distants', 'post-up / up dans /etc/network/interfaces ou [Route]', 'Réponse partant vers la route par défaut', 'Simplicité et contrôle contre reconvergence et passage à l’échelle'] },
prof: { deroule: [['Ouverture', 'Jeu de rôle : chaque élève est un routeur et ne connaît que ses voisins'], ['Cours', 'Table de routage, correspondance la plus longue, route par défaut'], ['Démonstration', 'ip route get sur plusieurs destinations'], ['Pratique', 'Lab deux LAN et deux routeurs, routes aller puis retour'], ['Incident', 'Route retour manquante à diagnostiquer'], ['Clôture', 'Quiz flash, ouverture vers OSPF']], diff: 'Élèves en difficulté : schéma de topologie annoté, routes aller données. Élèves avancés : route flottante, routage par politique (ip rule), résumé de routes.', eval: 'Étapes du lab validées automatiquement, analyse de l’incident, sujet type.' }
},
exemples: [
['Savoir par où part un paquet', 'Un serveur a deux interfaces et il faut savoir laquelle est utilisée pour joindre la sauvegarde.', 'ip route get 172.20.0.15', '172.20.0.15 via 10.62.40.2 dev ens19 src 10.62.40.1 uid 0\n cache', 'La commande ne fait qu’interroger la table : elle indique la route retenue, l’interface et l’adresse source. C’est le moyen le plus fiable de vérifier une hypothèse de routage sans générer de trafic.'],
['Diagnostiquer l’absence de route par défaut', 'Une VM fraîchement installée ne joint pas Internet.', 'ping -c 1 1.1.1.1 ; ip route', 'ping: connect: Network is unreachable\n10.62.40.0/24 dev ens18 proto kernel scope link src 10.62.40.33', 'Erreur immédiate et locale : aucune route ne correspond à 1.1.1.1, faute de route par défaut. Correction : sudo ip route add default via 10.62.40.1, puis gateway 10.62.40.1 dans /etc/network/interfaces.'],
['Ajouter une route vers un réseau de laboratoire', 'Le réseau 172.20.0.0/24 est derrière le routeur 10.62.0.20.', 'sudo ip route add 172.20.0.0/24 via 10.62.0.20\nip route | grep 172.20', '172.20.0.0/24 via 10.62.0.20 dev ens18', 'La route apparaît sans proto static explicite car ajoutée à la main. Elle sera perdue au redémarrage tant qu’elle n’est pas écrite dans la configuration réseau.'],
['Refus d’une passerelle non joignable', 'Faute de frappe dans l’adresse du prochain saut.', 'sudo ip route add 172.20.0.0/24 via 10.62.1.20', 'Error: Nexthop has invalid gateway.', 'Le prochain saut 10.62.1.20 n’appartient à aucun réseau directement connecté ; le noyau ne saurait pas l’atteindre. Le prochain saut doit toujours être sur un réseau d’une interface locale.'],
['Préparer une route de secours', 'Le site a une fibre et une 4G de secours.', 'sudo ip route add default via 203.0.113.1 metric 100\nsudo ip route add default via 192.168.8.1 metric 200\nip route show default', 'default via 203.0.113.1 dev wan0 metric 100\ndefault via 192.168.8.1 dev lte0 metric 200', 'Deux routes par défaut coexistent ; la métrique la plus faible l’emporte. Si l’interface wan0 tombe, sa route disparaît et la 4G prend le relais. Attention : si la fibre reste « up » mais que l’opérateur est en panne, la bascule n’a pas lieu ; il faut alors une supervision active.'],
['Détecter un rejet par rp_filter', 'Des paquets arrivent sur une interface mais ne sont jamais traités.', 'sysctl net.ipv4.conf.ens19.rp_filter ; sudo sysctl -w net.ipv4.conf.all.log_martians=1 ; sudo dmesg | tail -1', 'net.ipv4.conf.ens19.rp_filter = 1\nIPv4: martian source 10.62.40.10 from 172.20.0.23, on dev ens19', 'Le filtre anti-usurpation strict rejette un paquet dont la source ne serait pas routée par ens19 : c’est la signature d’un routage asymétrique. On corrige le routage si possible, sinon on passe rp_filter en mode souple (2) sur cette interface.']
],
long: { figure: svg(720, 200, bx(10, 70, 130, 50, Y, 'LAN A 10.1.0.0/24', 12) + ln(140, 95, 200, 95) + bx(200, 70, 90, 50, L, 'R1', 16) + ln(290, 95, 430, 95) + tx(300, 85, '10.255.0.0/30', 12, 1) + bx(430, 70, 90, 50, L, 'R2', 16) + ln(520, 95, 580, 95) + bx(580, 70, 130, 50, G, 'LAN B 10.2.0.0/24', 12) + tx(160, 160, 'R1 : 10.2.0.0/24 via 10.255.0.2', 13, 1) + tx(400, 185, 'R2 : 10.1.0.0/24 via 10.255.0.1', 13, 1)), legende: 'Chaque routeur doit connaître le réseau distant : une route à l’aller, une au retour.', photo: 'Photo : routeurs de laboratoire reliés par des câbles',
lecture: ['Le routage, c’est la capacité d’Internet à acheminer un paquet d’un bout à l’autre de la planète sans qu’aucun équipement ne connaisse le chemin complet. Chaque routeur ne prend qu’une décision locale : à quel voisin confier ce paquet. La somme de ces décisions construit le chemin.', 'Le routage statique est la forme la plus directe de cette décision : l’administrateur écrit lui-même les règles. Il ne se passe rien d’autre, aucune négociation, aucune surprise. C’est ce qui en fait l’outil idéal pour les situations simples, et un excellent terrain d’apprentissage : quand une route statique ne marche pas, la cause est toujours dans ce que l’on a écrit, ou oublié d’écrire.', 'Ce qu’on oublie le plus souvent, c’est le retour. Un paquet qui atteint sa destination n’est que la moitié du travail ; la réponse doit refaire le chemin inverse, et chaque routeur sur ce chemin doit savoir où renvoyer. Prendre l’habitude de raisonner systématiquement dans les deux sens évite l’immense majorité des pannes de routage.', 'Enfin, le routage statique a ses limites. Dans un réseau de campus avec des liens redondants, écrire et maintenir les routes à la main devient source d’erreurs, et aucune panne n’est contournée automatiquement. C’est la raison d’être des protocoles dynamiques comme OSPF, étudiés ensuite, qui reposent exactement sur les mêmes principes de table et de correspondance la plus longue.'] },
schemas: { termes: ['table de routage', 'route par défaut', 'prochain saut', 'next hop', 'longest prefix match', 'correspondance la plus longue', 'distance administrative', 'métrique', 'route flottante', 'ip route', 'ip route get', 'ip route add', 'ip_forward', 'rp_filter', 'proto kernel', 'scope link', 'Network is unreachable', 'traceroute', 'OSPF', 'route retour'],
figs: [[2, 'Correspondance la plus longue', svg(720, 130, bx(10, 20, 220, 44, N, '0.0.0.0/0 · 0 bit', 14) + bx(245, 20, 220, 44, W, '10.62.0.0/16 · 16 bits', 14) + bx(480, 20, 230, 44, G, '10.62.40.0/24 · 24 bits ✓', 14) + tx(10, 100, 'destination 10.62.40.25 : la route la plus précise l’emporte', 14))]] }
});
})();

View file

@ -0,0 +1,42 @@
// Schémas supplémentaires insérés dans le CM + termes à surligner
(function () {
var K = 'stroke="#000" stroke-width="3"', F = 'font-family="Archivo,sans-serif" font-weight="800"', M = 'font-family="IBM Plex Mono,monospace" font-weight="600"';
var b = function (x, y, w, h, fill, t, fs, col) { return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h + '" fill="' + fill + '" ' + K + '/><text x="' + (x + w / 2) + '" y="' + (y + h / 2 + (fs || 15) / 3) + '" text-anchor="middle" ' + F + ' font-size="' + (fs || 15) + '"' + (col ? ' fill="' + col + '"' : '') + '>' + t + '</text>'; };
var a = function (x1, y1, x2, y2) { return '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" ' + K + ' marker-end="url(#fl2)"/>'; };
var t = function (x, y, s, fs, mono) { return '<text x="' + x + '" y="' + y + '" ' + (mono ? M : F) + ' font-size="' + (fs || 14) + '">' + s + '</text>'; };
var S = function (w, h, body) { return '<svg viewBox="0 0 ' + w + ' ' + h + '" class="h-auto w-full" role="img"><defs><marker id="fl2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z"/></marker></defs>' + body + '</svg>'; };
var Y = '#FFD23F', G = '#7CF2C0', P = '#FF6FB5', L = '#C4B5FD', N = '#F6F5F1', W = '#fff', D = '#13233A';
window.CONTENUS_SCHEMAS = Object.assign(window.CONTENUS_SCHEMAS || {}, {
binaire: { termes: ['bit', 'octet', 'quartet', 'poids fort', 'complément à deux', 'hexadécimal', 'base 2', 'base 16', '0x', 'Kio', 'ko'], figs: [
[3, 'Divisions successives : 45 en binaire', S(720, 250, [45, 22, 11, 5, 2, 1].map(function (n, i) { return b(20, 10 + i * 38, 140, 32, N, n + ' ÷ 2', 14) + b(180, 10 + i * 38, 60, 32, Y, n % 2, 16); }).join('') + a(280, 210, 280, 20) + t(300, 120, 'lecture de bas en haut', 16) + b(300, 140, 220, 50, G, '101101₂', 22))],
[4, 'Regrouper par 4 bits pour passer en hexadécimal', S(720, 170, b(20, 20, 150, 50, Y, '1011', 22) + b(180, 20, 150, 50, L, '0010', 22) + a(95, 70, 95, 105) + a(255, 70, 255, 105) + b(20, 110, 150, 45, W, 'B', 22) + b(180, 110, 150, 45, W, '2', 22) + t(380, 90, '1011 0010₂ = 0xB2 = 178', 20, 1))],
[6, 'Complément à deux : coder −5 sur 8 bits', S(720, 200, b(20, 20, 300, 44, W, '+5 = 0000 0101', 17) + a(170, 64, 170, 84) + b(20, 86, 300, 44, Y, 'inverser : 1111 1010', 17) + a(170, 130, 170, 150) + b(20, 152, 300, 44, P, '+1 : 1111 1011 = −5', 17) + t(360, 60, 'Plage sur 8 bits signés :', 16) + b(360, 80, 340, 50, G, '−128 … +127', 22))] ] },
terminal: { termes: ['pwd', 'ls -la', 'ls -l', 'cd ..', 'cd', 'mkdir -p', 'touch', 'cp -r', 'mv', 'rm -rf', 'rm', 'man', 'find', 'locate', 'echo $HOME', 'echo $PATH', '$HOME', '$PATH', '$USER', 'history', 'Ctrl+C', 'Ctrl+R', 'Tab', 'sudo', '/etc', '/home', '/var/log', '/tmp', '~'], figs: [
[0, 'Lire l’invite du shell', S(720, 170, b(20, 50, 110, 50, Y, 'eleve', 18) + b(130, 50, 40, 50, W, '@', 18) + b(170, 50, 120, 50, G, 'debian', 18) + b(290, 50, 30, 50, W, ':', 18) + b(320, 50, 60, 50, L, '~', 20) + b(380, 50, 50, 50, P, '$', 20) + t(20, 35, 'utilisateur', 13) + t(170, 35, 'machine', 13) + t(320, 35, 'dossier', 13) + t(380, 130, '$ = utilisateur · # = root', 15))],
[2, 'Chemin absolu et chemin relatif', S(720, 180, b(20, 20, 330, 44, Y, '/home/eleve/projets/web', 15) + t(20, 90, 'absolu : part toujours de /', 15) + b(380, 20, 320, 44, G, 'projets/web', 15) + t(380, 90, 'relatif : part du dossier courant', 15) + b(20, 120, 200, 40, N, '. = ici', 15) + b(240, 120, 200, 40, N, '.. = parent', 15) + b(460, 120, 240, 40, N, '~ = /home/eleve', 15))],
[4, 'Les 4 gestes sur les fichiers', S(720, 130, [['mkdir', Y, 'créer'], ['cp', G, 'copier'], ['mv', L, 'déplacer'], ['rm', P, 'supprimer']].map(function (c, i) { return b(20 + i * 175, 20, 160, 50, c[1], c[0], 20) + t(40 + i * 175, 100, c[2], 15); }).join(''))] ] },
droits: { termes: ['chmod', 'chown', 'umask', 'usermod -aG', 'adduser', 'sudo', 'visudo', 'SUID', 'SGID', 'sticky bit', 'setfacl', 'getfacl', '/etc/passwd', '/etc/shadow', '/etc/group', 'rwx', '777', '750', '644', 'moindre privilège'], figs: [
[2, 'Calculer un droit octal : r = 4, w = 2, x = 1', S(720, 170, b(20, 20, 80, 50, Y, 'r 4', 18) + t(110, 52, '+', 22) + b(130, 20, 80, 50, Y, 'w 2', 18) + t(220, 52, '+', 22) + b(240, 20, 80, 50, Y, 'x 1', 18) + t(330, 52, '=', 22) + b(360, 20, 80, 50, G, '7', 24) + b(20, 100, 80, 50, Y, 'r 4', 18) + t(110, 132, '+', 22) + b(130, 100, 80, 50, N, '– 0', 18) + t(220, 132, '+', 22) + b(240, 100, 80, 50, Y, 'x 1', 18) + t(330, 132, '=', 22) + b(360, 100, 80, 50, G, '5', 24) + t(480, 95, 'rwxr-x--- = 750', 20, 1))],
[4, 'Les trois droits spéciaux', S(720, 150, [['SUID 4', Y, 'droits du propriétaire'], ['SGID 2', G, 'hérite du groupe'], ['Sticky 1', P, 'chacun ses fichiers']].map(function (c, i) { return b(20 + i * 235, 20, 215, 55, c[1], c[0], 20) + t(30 + i * 235, 110, c[2], 15); }).join(''))] ] },
tubes: { termes: ['stdin', 'stdout', 'stderr', '2>', '>>', '&>', '/dev/null', 'grep', 'grep -E', 'grep -c', 'cut', 'sort', 'uniq -c', 'wc -l', 'head', 'tail -f', 'awk', 'sed', '|', '$( )', 'cron'], figs: [
[0, 'Les trois flux d’un processus', S(720, 180, b(260, 60, 200, 60, D, 'processus', 18, '#fff') + b(20, 70, 160, 40, Y, '0 · stdin', 15) + a(180, 90, 258, 90) + a(460, 75, 538, 45) + b(540, 20, 160, 40, G, '1 · stdout', 15) + a(460, 105, 538, 135) + b(540, 120, 160, 40, P, '2 · stderr', 15))],
[1, 'Écraser ou ajouter', S(720, 150, b(20, 20, 200, 50, Y, 'cmd > f', 18) + t(240, 52, 'f est vidé puis rempli', 15) + b(20, 85, 200, 50, G, 'cmd >> f', 18) + t(240, 117, 'la sortie s’ajoute à la fin de f', 15))] ] },
osi: { termes: ['TCP', 'UDP', 'IP', 'ICMP', 'ARP', 'MAC', 'Ethernet', 'HTTP', 'HTTPS', 'DNS', 'TLS', 'port', 'segment', 'paquet', 'trame', 'switch', 'routeur', 'SYN', 'ACK', 'ping', 'ip addr', 'ip link', 'ip neigh', 'ss -tlnp', 'tcpdump'], figs: [
[3, 'Encapsulation : chaque couche ajoute son en-tête', S(720, 230, b(470, 10, 230, 40, P, 'Données HTTP', 15) + b(360, 60, 110, 40, L, 'TCP', 15) + b(470, 60, 230, 40, P, 'Données', 15) + b(250, 110, 110, 40, G, 'IP', 15) + b(360, 110, 340, 40, L, 'segment', 15) + b(140, 160, 110, 40, Y, 'Ethernet', 15) + b(250, 160, 450, 40, G, 'paquet', 15) + t(20, 35, 'Application', 13) + t(20, 85, 'Transport', 13) + t(20, 135, 'Réseau', 13) + t(20, 185, 'Liaison', 13))],
[6, 'La poignée de main TCP', S(720, 230, b(20, 20, 150, 44, Y, 'Client', 17) + b(550, 20, 150, 44, G, 'Serveur', 17) + '<line x1="95" y1="64" x2="95" y2="220" ' + K + ' stroke-dasharray="6 6"/><line x1="625" y1="64" x2="625" y2="220" ' + K + ' stroke-dasharray="6 6"/>' + a(95, 95, 622, 115) + t(330, 95, 'SYN', 16, 1) + a(625, 145, 98, 165) + t(310, 145, 'SYN-ACK', 16, 1) + a(95, 195, 622, 215) + t(330, 195, 'ACK', 16, 1))],
[7, 'ARP : qui a 192.168.10.1 ?', S(720, 170, b(20, 60, 160, 50, Y, 'Poste', 17) + a(180, 75, 538, 75) + t(220, 65, 'broadcast : qui a .10.1 ?', 14, 1) + b(540, 60, 160, 50, G, 'Passerelle', 17) + a(540, 100, 182, 100) + t(220, 130, 'réponse : 3c:52:82:1a:0f:9b', 14, 1))] ] },
sql: { termes: ['SELECT', 'FROM', 'WHERE', 'ORDER BY', 'GROUP BY', 'HAVING', 'JOIN', 'LEFT JOIN', 'INNER JOIN', 'INSERT INTO', 'UPDATE', 'DELETE', 'COUNT', 'AVG', 'LIMIT', 'DISTINCT', 'clé primaire', 'clé étrangère', 'BEGIN', 'COMMIT', 'ROLLBACK', 'EXPLAIN', 'index', 'requête préparée'], figs: [
[4, 'INNER JOIN et LEFT JOIN', S(720, 200, '<circle cx="140" cy="100" r="70" fill="' + Y + '" ' + K + '/><circle cx="220" cy="100" r="70" fill="' + Y + '" fill-opacity=".001" ' + K + '/><path d="M180 43 A70 70 0 0 1 180 157 A70 70 0 0 1 180 43" fill="' + G + '" ' + K + '/>' + t(60, 190, 'LEFT JOIN : toute la gauche', 14) + '<circle cx="480" cy="100" r="70" fill="' + W + '" ' + K + '/><circle cx="560" cy="100" r="70" fill="' + W + '" fill-opacity=".001" ' + K + '/><path d="M520 43 A70 70 0 0 1 520 157 A70 70 0 0 1 520 43" fill="' + G + '" ' + K + '/>' + t(420, 190, 'INNER JOIN : l’intersection', 14))],
[3, 'Ordre logique d’exécution d’une requête', S(720, 110, ['FROM', 'WHERE', 'GROUP BY', 'HAVING', 'SELECT', 'ORDER BY'].map(function (c, i) { return b(10 + i * 118, 30, 104, 44, [Y, Y, G, G, L, P][i], c, 14) + (i < 5 ? a(114 + i * 118, 52, 126 + i * 118, 52) : ''); }).join(''))] ] },
python: { termes: ['print', 'input', 'int', 'float', 'str', 'bool', 'list', 'dict', 'if', 'elif', 'else', 'for', 'while', 'range', 'def', 'return', 'assert', 'try', 'except', 'with open', 'import csv', 'indentation'], figs: [
[3, 'Boucle for : un tour par élément', S(720, 170, [12, 15, 9, 18].map(function (n, i) { return b(20 + i * 90, 20, 80, 50, Y, n, 20); }).join('') + a(200, 80, 200, 110) + b(20, 115, 360, 44, G, 'total += n → 54', 17) + t(420, 55, 'for n in notes:', 18, 1) + t(420, 85, ' total += n', 18, 1))],
[5, 'Une fonction : entrée → traitement → sortie', S(720, 140, b(20, 40, 180, 60, Y, '[10, 20]', 18) + a(200, 70, 258, 70) + b(260, 30, 200, 80, D, 'moyenne()', 18, '#fff') + a(460, 70, 518, 70) + b(520, 40, 180, 60, G, '15.0', 22))] ] },
web: { termes: ['GET', 'POST', 'HTTP', 'HTTPS', '200', '404', '403', '500', '<h1>', '<title>', '<a>', 'class', 'display: grid', 'display: flex', 'apt install apache2', '/var/www/html', '/var/log/apache2', 'apachectl configtest', 'certbot', 'XSS', 'DOM', 'F12'], figs: [
[0, 'Anatomie d’une URL', S(720, 130, b(10, 30, 90, 44, Y, 'https', 15) + b(100, 30, 180, 44, G, 'www.lycee.fr', 15) + b(280, 30, 70, 44, L, ':443', 15) + b(350, 30, 200, 44, P, '/cours/index.html', 14) + b(550, 30, 160, 44, N, '?id=3', 15) + t(10, 105, 'protocole', 13) + t(100, 105, 'hôte', 13) + t(280, 105, 'port', 13) + t(350, 105, 'chemin', 13) + t(550, 105, 'paramètres', 13))],
[2, 'Structure d’une page HTML', S(720, 230, b(20, 10, 680, 36, N, '<html lang="fr">', 15) + b(40, 56, 300, 160, Y, '<head>', 15) + t(60, 150, 'meta, title, css', 14) + b(360, 56, 320, 160, G, '<body>', 15) + b(380, 110, 280, 30, W, '<h1>', 14) + b(380, 146, 280, 30, W, '<p> <a>', 14) + b(380, 182, 280, 26, W, '<form>', 13))] ] },
crypto: { termes: ['hachage', 'SHA-256', 'MD5', 'SHA-1', 'sel', 'bcrypt', 'Argon2id', 'AES', 'RSA', 'Ed25519', 'clé publique', 'clé privée', 'certificat', 'TLS', 'ssh-keygen', 'ssh-copy-id', 'sha256sum', 'PasswordAuthentication no', 'PermitRootLogin no', 'ANSSI', 'CNIL'], figs: [
[1, 'Hachage : une empreinte à sens unique', S(720, 150, b(20, 30, 170, 50, Y, '"bonjour"', 17) + a(190, 55, 258, 55) + b(260, 20, 170, 70, D, 'SHA-256', 18, '#fff') + a(430, 55, 498, 55) + b(500, 30, 200, 50, G, '2cb4b1…3e18', 15) + t(260, 130, 'impossible de revenir en arrière', 15))],
[2, 'Stocker un mot de passe', S(720, 150, b(20, 40, 140, 50, Y, 'mot de passe', 14) + t(170, 72, '+', 22) + b(195, 40, 120, 50, L, 'sel', 17) + a(315, 65, 378, 65) + b(380, 30, 150, 70, D, 'Argon2id', 17, '#fff') + a(530, 65, 588, 65) + b(590, 40, 110, 50, G, 'hash', 17))],
[5, 'HTTPS en trois temps', S(720, 150, [['1 · certificat', Y], ['2 · échange de clé', L], ['3 · trafic en AES', G]].map(function (c, i) { return b(20 + i * 235, 40, 210, 60, c[1], c[0], 15) + (i < 2 ? a(230 + i * 235, 70, 253 + i * 235, 70) : ''); }).join(''))] ] }
});
})();

Some files were not shown because too many files have changed in this diff Show more