Paillasse/design/js/contenus-lot-7.js

226 lines
55 KiB
JavaScript
Raw Normal View History

// 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']
});
})();