Lab : reprendre en main les serveurs de Signalements
Ce lab rassemble le cours Linux : administration système. Vous reprenez en main deux serveurs de Signalements comme si Camille venait de partir : sig-app-1 (Ubuntu 24.04, 172.16.8.11) sert une version réduite de l'API, sig-outils (Debian 13, 172.16.8.20) exécute l'export nocturne, reçoit les journaux et pousse les sauvegardes. Vous appliquez chaque leçon, puis un script vérifie sur chaque machine les propriétés attendues. Il valide le niveau 200 des compétences « Démarrage, noyau et configuration du système », « Configuration réseau et pare-feu d'un hôte Linux », « Système de fichiers et permissions », « Processus, services et systemd », « Identité, authentification et IAM », « Durcissement des systèmes », « Journaux centralisés » et « Sauvegarde et reprise d'activité ».
Comptez deux journées. Toutes les pièces sont dans les leçons ; essayez d'écrire chaque fichier avant d'ouvrir la leçon correspondante.
Caution
Ce lab se fait uniquement sur des machines jetables. Il casse volontairement le démarrage, ferme SSH, verrouille l'audit jusqu'au redémarrage et modifie PAM : jamais sur votre poste ni sur un serveur qui compte.
Note
Le script a été écrit à partir des pages de manuel et du format de sortie documenté des outils cités (systemctl show, findmnt, ss, nft, ufw, chronyc, auditctl, sshd -T, restic --json). Il ne fait que des lectures : aucune commande ne modifie les machines ni le dépôt de sauvegarde.
Ce qu'il faut
- Deux machines virtuelles jetables, locales ou chez Scaleway : une Ubuntu 24.04 (
sig-app-1), une Debian 13 (sig-outils), chacune avec un accès console qui ne dépend pas du réseau. - Un réseau privé
172.16.8.0/22qui les relie :pn-signalementschez Scaleway (adresses réservées dans l'IPAM), ou un réseau interne de l'hyperviseur. Votre poste ou votre bastion doit avoir une adresse dans ce réseau (par exemple172.16.8.1), puisque SSH n'y sera plus ouvert qu'à lui. - Les noms
sig-app-1.pn-signalements.internaletsig-outils.pn-signalements.internalrésolus sur les deux machines : par le DNS interne de Scaleway, ou, sur des machines locales, par une ligne dans/etc/hostsde chacune. - Un second disque de quelques gigaoctets attaché à
sig-outils. - Un bucket
sig-sauvegardesenfr-par(Object Storage Scaleway), versionné, et une application IAM dont la clé ne peut écrire que soussig-sauvegardes/sig-outils/. N'activez pas le verrouillage d'objets pour ce lab : il est irréversible et vous empêcherait de supprimer le bucket à la fin. Le coût se compte en centimes. - Les paquets
jqetcurlsursig-outils.
L'application
Sur sig-app-1, une API minimale écrite avec la seule bibliothèque standard de Python, dans /opt/signalements/app.py (0644 root:root) :
#!/usr/bin/env python3
"""Signalements, version de laboratoire : répond à GET /sante."""
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
class Gestionnaire(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/sante":
code, corps = 200, {"etat": "ok", "version": os.environ.get("APP_VERSION", "inconnue")}
else:
code, corps = 404, {"erreur": "introuvable"}
donnees = json.dumps(corps).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(donnees)))
self.end_headers()
self.wfile.write(donnees)
adresse = os.environ.get("SIGNALEMENTS_ADRESSE", "127.0.0.1")
HTTPServer((adresse, 8000), Gestionnaire).serve_forever()Son fichier /etc/signalements/env (0640 root:signalements) contient APP_VERSION=1.0.0 et SIGNALEMENTS_ADRESSE=172.16.8.11. L'unité signalements.service la lance sous le compte système signalements avec /usr/bin/python3 /opt/signalements/app.py.
Sur sig-outils, l'export nocturne est un module Python lancé par python3 -m signalements.export depuis /opt/signalements. Créez /opt/signalements/signalements/__init__.py (vide) et /opt/signalements/signalements/export.py :
"""Signalements, version de laboratoire : export CSV quotidien pour la mairie."""
import argparse
import csv
import datetime
import os
import tempfile
def main():
parametres = argparse.ArgumentParser()
parametres.add_argument("--sortie", default="/srv/donnees/exports")
sortie = parametres.parse_args().sortie
jour = datetime.date.today().isoformat()
final = os.path.join(sortie, f"signalements-{jour}.csv")
# Écrire à côté puis renommer : une relance remplace le fichier, la mairie ne lit jamais un fichier partiel.
fd, provisoire = tempfile.mkstemp(dir=sortie, prefix=".export-", suffix=".tmp")
with os.fdopen(fd, "w", newline="") as fichier:
ecrivain = csv.writer(fichier)
ecrivain.writerow(["id", "type", "commune", "date"])
ecrivain.writerow([1, "nid-de-poule", os.environ.get("EXPORT_COMMUNE", "inconnue"), jour])
os.chmod(provisoire, 0o640)
os.replace(provisoire, final)
print(f"export écrit : {final}")
if __name__ == "__main__":
main()Son fichier /etc/signalements/env (0640 root:signalements) contient par exemple EXPORT_COMMUNE=Exempleville. Faute de base de données, le lab n'exporte rien de sig-db et la sauvegarde ne comprend pas de pg_dump.
Les exigences
| Exigence | Machine | Leçon |
|---|---|---|
L'état des lieux est fait en lecture seule, session enregistrée sur votre poste ; une fiche de serveur et un journal des changements existent pour chaque machine, dans votre dépôt, pas sur le serveur ; etckeeper versionne /etc | les deux | 1 |
Le fuseau est UTC ; net.ipv4.ip_forward vaut 0 | les deux | 3, 9 |
/etc/sysctl.d/60-durcissement.conf applique les réglages de la leçon 14 : kptr_restrict = 2, yama.ptrace_scope = 2, unprivileged_bpf_disabled = 1, suid_dumpable = 0, protected_*, rp_filter = 1 par motif sur toutes les interfaces, redirections ICMP refusées | les deux | 3, 14 |
Un disque dédié, partitionné et formaté en ext4, est monté sur /srv/donnees par UUID= avec nofail et nodev,nosuid,noexec ; findmnt --verify ne signale aucune erreur ; /srv/donnees/exports et /srv/donnees/pieces-jointes sont en 750 signalements:signalements, /srv/donnees/journaux en 750 à root | sig-outils | 5, 6 |
L'interface du réseau privé porte l'adresse de la machine en /22, de façon persistante ; les fichiers netplan ne sont lisibles que par root ; l'autre machine répond et son nom interne se résout | les deux | 7 |
ufw est actif, refuse par défaut en entrée, limite SSH (limit) au réseau privé et n'ouvre le port 8000 qu'à 172.16.8.0/22 | sig-app-1 | 8 |
nftables.service est activé ; la table inet filtre jette par défaut en entrée et n'accepte SSH que depuis le réseau privé, le port 6514 et UDP 123 que depuis sig-app-1 | sig-outils | 8 |
Chaque modification du pare-feu a été faite sous un minuteur retour-pare-feu (systemd-run --on-active=), arrêté après le test d'une nouvelle connexion | les deux | 8 |
chrony remplace systemd-timesyncd et l'horloge est synchronisée ; sig-outils sert l'heure au seul réseau privé, sig-app-1 le suit en priorité | les deux | 9 |
signalements-export.timer est activé, Persistent=true, OnCalendar= à 5 h, heure de Paris ; le service est oneshot, sous signalements, avec TimeoutStartSec=, OnFailure=notifier-echec@%n.service et RequiresMountsFor=/srv/donnees ; il a réussi au moins une fois et un CSV est dans /srv/donnees/exports | sig-outils | 10 |
L'export est borné : MemoryHigh= sous MemoryMax=, CPUWeight= inférieur à 100, TasksMax= d'au plus 256 | sig-outils | 11 |
Le journal est persistant (drop-in avec Storage=persistent et MaxRetentionSec=) et contient au moins le démarrage précédent | les deux | 12 |
rsyslog envoie vers sig-outils:6514 en TCP et TLS, authentification x509/name, framing par comptage d'octets, file sur disque ; sig-outils n'écoute que sur 172.16.8.20:6514, n'accepte que les certificats nommés dans PermittedPeer, range les journaux reçus sous /srv/donnees/journaux/<hôte>/syslog.log et les fait tourner par /etc/logrotate.d/journaux-distants | les deux | 12 |
pam_faillock est actif par des profils pam-auth-update (preauth avant pam_unix, authfail, pile account), avec deny = 5, unlock_time = 900 et silent ; aucun compte n'a de mot de passe vide | les deux | 13 |
SSH : PermitRootLogin no, clés seules, AllowGroups acces-ssh, MaxAuthTries 3, bannière /etc/issue.net sans nom de distribution, d'après sshd -T | les deux | 14 |
Les modules dccp et sctp sont interdits par install ... /bin/false | les deux | 3, 14 |
Le service applicatif est confiné (signalements.service sur sig-app-1, signalements-export.service sur sig-outils) : NoNewPrivileges=yes, ProtectSystem=strict, aucune capacité, filtre @system-service, et une note de systemd-analyze security d'au plus 5,0 | les deux | 14 |
L'API répond 200 sur http://172.16.8.11:8000/sante, y compris depuis sig-outils, et n'écoute que sur 172.16.8.11 | sig-app-1 | 7, 8, 14 |
auditd tourne avec les règles de la leçon 15 (clés identite, sudoers, sshd, audit-conf, module-load, heure, root-cmd), --loginuid-immutable, aucun enregistrement perdu, configuration verrouillée par -e 2, et audit=1 sur la ligne de commande du noyau | les deux | 2, 15 |
AIDE est initialisé (/var/lib/aide/aide.db), avec le fragment 90_aide_signalements, et sa vérification quotidienne est activée | les deux | 15 |
restic sauvegarde sig-outils dans s3:https://s3.fr-par.scw.cloud/sig-sauvegardes/sig-outils par sig-sauvegarde.timer (Persistent=true, service oneshot avec OnFailure=) ; secrets en 0600 ; dernier instantané fichiers de moins de 26 heures ; /etc/signalements restauré sous /root/restauration et identique à l'original | sig-outils | 16 |
Pistes
- Commencez par l'état des lieux de la leçon 1, script d'inventaire compris, avant toute modification : c'est lui qui alimente la fiche. Installez etckeeper en premier changement, et notez chaque étape du lab dans le journal des changements (quoi, pourquoi, vérification, retour arrière).
- Avant de toucher au démarrage, vérifiez que la console affiche le menu de GRUB et qu'un compte de secours a un mot de passe (leçon 5). Entraînez-vous une fois : ligne
fstabavec un UUID inventé, redémarrage, récupération parsystemd.unit=emergency.target SYSTEMD_SULOGIN_FORCE=1. Prenez un instantané si vos machines le permettent. - Le disque :
lsblk --scsi, puis travaillez par le lien/dev/disk/by-id/(ouby-pathsur une machine locale),wipefsavantmkfs.ext4 -L donnees -m 1. Après chaque modification defstab:findmnt --verify,daemon-reload,mount -a(leçon 6). - Créez le compte d'administration nominatif et le groupe
acces-sshavant d'interdire root en SSH. Gardez une session root ouverte pendant toutes les modifications de SSH, de PAM et du pare-feu (leçons 8, 13 et 14). - Pour chrony, les serveurs de Scaleway ne sont joignables que depuis ses instances : sur des machines locales, donnez à
sig-outilsles serveurs du pool etntp.obspm.fren NTS (leçon 9). - L'unité
notifier-echec@.servicedu lab peut se contenter d'unExecStart=/usr/bin/logger -t notifier-echec "Échec de %i": le webhook de la leçon 10 n'est pas exigé. Testez l'export parsystemctl start signalements-export.service, qui attend la fin. - Les certificats TLS se fabriquent sur votre poste (leçon 12) ; sur Ubuntu, la clé va dans
/etc/rsyslog.d/tls/en0640 root:syslog, sinon AppArmor et le comptesyslogla refusent. Validez chaque configuration parrsyslogd -N1. - Posez
-e 2en dernier, une foisauditctl -lcorrect, puis redémarrez les deux machines : cela vérifie au passagefstab,audit=1, le pare-feu chargé au démarrage et le journal persistant. - Pour la sauvegarde, copiez le mot de passe du dépôt dans votre gestionnaire de secrets avant la première sauvegarde. Lancez à la main
restic check, puis restaurez :restic restore latest --tag fichiers --target /root/restauration --include /etc/signalements.
Vérifier
Enregistrez le script sous verifier-lab.sh sur les deux machines. Sur sig-app-1, lancez d'abord logger -t lab-verif "essai depuis sig-app-1", puis sudo bash verifier-lab.sh ; ensuite, sur sig-outils, sudo bash verifier-lab.sh. Le script reconnaît la machine par /etc/os-release et n'applique que les contrôles de son rôle.
#!/usr/bin/env bash
# Vérifie les propriétés attendues du lab « reprendre en main les serveurs de Signalements ».
# Usage : sudo bash verifier-lab.sh (sur sig-app-1, puis sur sig-outils, qui exige jq)
# Rôle déduit de /etc/os-release : Ubuntu = sig-app-1, Debian = sig-outils.
# Chaque contrôle affiche OK ou ÉCHEC ; le code de sortie est le nombre d'échecs.
set -uo pipefail
export LC_ALL=C.UTF-8
echecs=0
controle() { # controle "libellé" commande...
local libelle=$1; shift
if "$@" > /dev/null 2>&1; then
echo "OK $libelle"
else
echo "ÉCHEC $libelle"
echecs=$((echecs + 1))
fi
}
droits() { [ "$(stat -c '%a %U:%G' "$1")" = "$2" ]; } # chemin "mode proprio:groupe"
propriete() { [ "$(systemctl show -p "$2" --value "$1")" = "$3" ]; } # unité propriété valeur
noyau() { [ "$(sysctl -n "$1")" = "$2" ]; } # clé valeur
paquet() { dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q 'install ok installed'; }
sshd_vaut() { sshd -T 2>/dev/null | grep -qx "$1"; } # "mot-clé valeur" effectif
module_interdit() { modprobe -n -v "$1" 2>&1 | grep -q 'install /bin/false'; }
# --- Contrôles communs aux deux machines ---
preauth_avant_unix() {
awk '/pam_faillock\.so[[:space:]]+preauth/ {p = NR} /pam_unix\.so/ && !u {u = NR}
END {exit !(p && u && p < u)}' /etc/pam.d/common-auth
}
faillock_regle() { grep -Eq "^[[:space:]]*$1[[:space:]]*(=[[:space:]]*$2)?[[:space:]]*$" /etc/security/faillock.conf; }
aucun_mot_de_passe_vide() { [ -z "$(awk -F: '$2 == ""' /etc/shadow)" ]; }
rp_filter_strict() {
local f
for f in /proc/sys/net/ipv4/conf/*/rp_filter; do [ "$(cat "$f")" = 1 ] || return 1; done
}
journal_persistant() {
local conf
conf=$(systemd-analyze cat-config systemd/journald.conf) || return 1
[ -d "/var/log/journal/$(cat /etc/machine-id)" ] &&
grep -qx 'Storage=persistent' <<< "$conf" && grep -q '^MaxRetentionSec=' <<< "$conf"
}
retour_arriere_trace() { # un minuteur retour-pare-feu a existé et n'est plus actif
journalctl -q --no-pager -u retour-pare-feu.timer | grep -q . &&
! systemctl is-active --quiet retour-pare-feu.timer
}
cles_audit() {
local regles cle
regles=$(auditctl -l) || return 1
for cle in identite sudoers sshd audit-conf module-load heure root-cmd; do
grep -Eq -- "(-k |key=)$cle( |$)" <<< "$regles" || return 1
done
}
audit_aucune_perte() { auditctl -s | awk '$1 == "lost" {found = 1; ok = ($2 == 0)} END {exit !(found && ok)}'; }
exposition_ok() { systemd-analyze security --no-pager --threshold=50 "$1"; } # note <= 5,0
aucune_capacite() { [ -z "$(systemctl show -p CapabilityBoundingSet --value "$1")" ]; }
[ "$(id -u)" -eq 0 ] || { echo "Lancez ce script avec sudo." >&2; exit 1; }
. /etc/os-release
case "$ID" in
ubuntu) role=app; moi=172.16.8.11; pair=172.16.8.20; nom_pair=sig-outils.pn-signalements.internal; confine=signalements.service ;;
debian) role=outils; moi=172.16.8.20; pair=172.16.8.11; nom_pair=sig-app-1.pn-signalements.internal; confine=signalements-export.service ;;
*) echo "Distribution non prévue par le lab : $ID" >&2; exit 1 ;;
esac
echo "Machine : $PRETTY_NAME, rôle $role"
controle "etckeeper versionne /etc" bash -c "$(declare -f paquet); paquet etckeeper && test -d /etc/.git"
controle "le fuseau est UTC" bash -c 't=$(timedatectl show -p Timezone --value); [ "$t" = Etc/UTC ] || [ "$t" = UTC ]'
controle "la machine ne route pas (ip_forward = 0)" noyau net.ipv4.ip_forward 0
controle "adresse privée $moi/22 présente" bash -c 'ip -o -4 addr show | grep -q "inet $1/22 "' _ "$moi"
controle "la machine paire $pair répond" ping -c 2 -W 2 -q "$pair"
controle "$nom_pair se résout en $pair" bash -c '[ "$(getent ahostsv4 "$1" | awk "NR == 1 {print \$1}")" = "$2" ]' _ "$nom_pair" "$pair"
controle "sysctl : kptr_restrict = 2" noyau kernel.kptr_restrict 2
controle "sysctl : yama.ptrace_scope = 2" noyau kernel.yama.ptrace_scope 2
controle "sysctl : unprivileged_bpf_disabled = 1" noyau kernel.unprivileged_bpf_disabled 1
controle "sysctl : suid_dumpable = 0" noyau fs.suid_dumpable 0
controle "sysctl : protected_symlinks et protected_hardlinks = 1" bash -c '[ "$(sysctl -n fs.protected_symlinks fs.protected_hardlinks | sort -u)" = 1 ]'
controle "sysctl : protected_regular et protected_fifos = 2" bash -c '[ "$(sysctl -n fs.protected_regular fs.protected_fifos | sort -u)" = 2 ]'
controle "sysctl : rp_filter = 1 sur toutes les interfaces" rp_filter_strict
controle "sysctl : redirections ICMP refusées (all et default, IPv4 et IPv6)" bash -c '[ "$(sysctl -n net.ipv4.conf.all.accept_redirects net.ipv4.conf.default.accept_redirects net.ipv6.conf.all.accept_redirects net.ipv6.conf.default.accept_redirects | sort -u)" = 0 ]'
controle "60-durcissement.conf est lu au démarrage" bash -c 'systemd-analyze cat-config sysctl.d | grep -qx "# /etc/sysctl.d/60-durcissement.conf"'
controle "chrony actif, systemd-timesyncd arrêté" bash -c 'systemctl is-active --quiet chrony && ! systemctl is-active --quiet systemd-timesyncd'
controle "horloge synchronisée selon timedatectl" bash -c '[ "$(timedatectl show -p NTPSynchronized --value)" = yes ]'
controle "chrony : Leap status Normal" bash -c 'chronyc tracking | grep -Eq "^Leap status +: Normal$"'
controle "journal persistant et borné dans le temps" journal_persistant
controle "le journal contient le démarrage précédent" journalctl -b -1 -n 1 -q --no-pager
controle "pam_faillock preauth avant pam_unix" preauth_avant_unix
controle "pam_faillock authfail dans common-auth" grep -Eq 'pam_faillock\.so[[:space:]]+authfail' /etc/pam.d/common-auth
controle "pam_faillock dans la pile account" grep -Eq '^account[[:space:]]+required[[:space:]]+pam_faillock\.so' /etc/pam.d/common-account
controle "faillock.conf : deny = 5, unlock_time = 900, silent" bash -c "$(declare -f faillock_regle); faillock_regle deny 5 && faillock_regle unlock_time 900 && faillock_regle silent"
controle "aucun mot de passe vide dans /etc/shadow" aucun_mot_de_passe_vide
controle "SSH : root refusé" sshd_vaut 'permitrootlogin no'
controle "SSH : pas de mot de passe" sshd_vaut 'passwordauthentication no'
controle "SSH : pas de clavier interactif" sshd_vaut 'kbdinteractiveauthentication no'
controle "SSH : AllowGroups acces-ssh" sshd_vaut 'allowgroups acces-ssh'
controle "SSH : MaxAuthTries 3" sshd_vaut 'maxauthtries 3'
controle "SSH : bannière /etc/issue.net sans nom de distribution" bash -c 'sshd -T | grep -qx "banner /etc/issue.net" && ! grep -Eqi "ubuntu|debian" /etc/issue.net'
controle "modules dccp et sctp interdits" bash -c "$(declare -f module_interdit); module_interdit dccp && module_interdit sctp"
controle "$confine : NoNewPrivileges=yes" propriete "$confine" NoNewPrivileges yes
controle "$confine : ProtectSystem=strict" propriete "$confine" ProtectSystem strict
controle "$confine : aucune capacité" aucune_capacite "$confine"
controle "$confine : exposition <= 5,0 (systemd-analyze security)" exposition_ok "$confine"
controle "pare-feu : minuteur de retour arrière utilisé puis arrêté" retour_arriere_trace
controle "auditd actif" systemctl is-active --quiet auditd
controle "audit=1 sur la ligne de commande du noyau" grep -qw 'audit=1' /proc/cmdline
controle "audit : configuration verrouillée (enabled 2)" bash -c 'auditctl -s | grep -qx "enabled 2"'
controle "audit : loginuid immuable" bash -c 'auditctl -s | grep -q "^loginuid_immutable 1"'
controle "audit : aucun enregistrement perdu" audit_aucune_perte
controle "audit : règles de la leçon 15 chargées" cles_audit
controle "AIDE : base de référence présente" test -s /var/lib/aide/aide.db
controle "AIDE : fragment 90_aide_signalements" test -f /etc/aide/aide.conf.d/90_aide_signalements
controle "AIDE : configuration valide" aide --config /etc/aide/aide.conf --config-check
controle "AIDE : vérification quotidienne activée" systemctl is-enabled --quiet dailyaidecheck.timer
if [ "$role" = app ]; then
# --- sig-app-1 (Ubuntu 24.04) ---
ecoute_privee_seule() { [ "$(ss -Hltn 'sport = :8000' | awk '{print $4}' | sort -u)" = "172.16.8.11:8000" ]; }
processus_confine() {
local pid; pid=$(systemctl show -p MainPID --value signalements)
[ "$pid" -gt 0 ] && grep -Eq '^NoNewPrivs:[[:space:]]+1$' "/proc/$pid/status" &&
grep -Eq '^Seccomp:[[:space:]]+2$' "/proc/$pid/status"
}
chrony_suit_outils() { chronyc -c -n sources | awk -F, '$3 == "172.16.8.20" && $2 ~ /[*+]/ {ok = 1} END {exit !ok}'; }
rsyslog_conf() { grep -rqsF -- "$1" /etc/rsyslog.d/; }
etat_ufw=$(ufw status verbose 2>/dev/null)
controle "signalements.service actif et activé" bash -c 'systemctl is-active --quiet signalements && systemctl is-enabled --quiet signalements'
controle "signalements.service tourne sous signalements" propriete signalements.service User signalements
controle "le processus a no_new_privs et un filtre seccomp" processus_confine
controle "GET /sante répond 200" curl -fsS -m 5 -o /dev/null http://172.16.8.11:8000/sante
controle "le port 8000 n'écoute que sur 172.16.8.11" ecoute_privee_seule
controle "fichiers netplan lisibles par root seul" bash -c '[ -z "$(find /etc/netplan -name "*.yaml" -perm /077)" ]'
controle "ufw actif et activé au démarrage" bash -c 'grep -qx "Status: active" <<< "$1" && grep -q "^ENABLED=yes" /etc/ufw/ufw.conf' _ "$etat_ufw"
controle "ufw : refus par défaut en entrée" bash -c 'grep -q "^Default: deny (incoming)" <<< "$1"' _ "$etat_ufw"
controle "ufw : SSH limité au réseau privé" bash -c 'grep -Eq "^22/tcp +LIMIT IN +172\.16\.8\.0/22" <<< "$1" && ! grep -Eq "^22(/tcp)? .*Anywhere" <<< "$1"' _ "$etat_ufw"
controle "ufw : port 8000 ouvert au seul réseau privé" bash -c 'grep -Eq "^8000/tcp +ALLOW IN +172\.16\.8\.0/22" <<< "$1" && ! grep -Eq "^8000(/tcp)? .*Anywhere" <<< "$1"' _ "$etat_ufw"
controle "chrony suit sig-outils (source retenue ou combinée)" chrony_suit_outils
controle "rsyslog actif, rsyslog-gnutls installé" bash -c "$(declare -f paquet); systemctl is-active --quiet rsyslog && paquet rsyslog-gnutls"
controle "rsyslog : configuration valide" rsyslogd -N1
controle "rsyslog : TLS imposé (StreamDriverMode=\"1\")" rsyslog_conf 'StreamDriverMode="1"'
controle "rsyslog : authentification x509/name" rsyslog_conf 'StreamDriverAuthMode="x509/name"'
controle "rsyslog : framing par comptage d'octets" rsyslog_conf 'TCP_Framing="octet-counted"'
controle "rsyslog : file d'attente sur disque" rsyslog_conf 'queue.filename='
controle "rsyslog : clé privée non lisible par tous" bash -c '[ -z "$(find /etc/rsyslog.d/tls -name "*.key" -perm /o+r)" ] && ls /etc/rsyslog.d/tls/*.key'
controle "connexion établie vers 172.16.8.20:6514" bash -c 'ss -Htn state established "( dport = :6514 )" | grep -q "172\.16\.8\.20:6514"'
else
# --- sig-outils (Debian 13) ---
command -v jq > /dev/null || { echo "Installez jq sur sig-outils." >&2; exit 1; }
fstab_uuid_nofail() {
findmnt --fstab -n -o SOURCE /srv/donnees | grep -q '^UUID=' &&
findmnt --fstab -n -o OPTIONS /srv/donnees | tr ',' '\n' | grep -qx nofail
}
options_montage() {
local o x; o=$(findmnt -n -o OPTIONS /srv/donnees) || return 1
for x in nosuid nodev noexec; do tr ',' '\n' <<< "$o" | grep -qx "$x" || return 1; done
}
volume_distinct() { mountpoint -q /srv/donnees && [ "$(findmnt -n -o SOURCE /srv/donnees)" != "$(findmnt -n -o SOURCE /)" ]; }
limites_memoire() {
local u=signalements-export.service high max
high=$(systemctl show -p MemoryHigh --value $u); max=$(systemctl show -p MemoryMax --value $u)
[[ $high =~ ^[0-9]+$ && $max =~ ^[0-9]+$ ]] && [ "$high" -lt "$max" ]
}
limites_cpu_taches() {
local u=signalements-export.service cpu tasks
cpu=$(systemctl show -p CPUWeight --value $u); tasks=$(systemctl show -p TasksMax --value $u)
[[ $cpu =~ ^[0-9]+$ && $tasks =~ ^[0-9]+$ ]] && [ "$cpu" -lt 100 ] && [ "$tasks" -le 256 ]
}
regle_restreinte() { # port motif : chaque règle de la chaîne entree sur ce port contient le motif
local lignes
lignes=$(nft list chain inet filtre entree | grep -E "dport $1( |$)") || return 1
! grep -vqF -- "$2" <<< "$lignes"
}
client_ntp_vu() { chronyc -n clients | awk '$1 == "172.16.8.11" {ok = 1} END {exit !ok}'; }
ecoute_6514() { [ "$(ss -Hltn 'sport = :6514' | awk '{print $4}' | sort -u)" = "172.16.8.20:6514" ]; }
rsyslog_conf() { grep -rqsF -- "$1" /etc/rsyslog.d/; }
derniere_sauvegarde_recente() {
local t
t=$( (set -a; . /etc/restic/sig-outils.env; set +a
restic --no-lock snapshots --tag fichiers --latest 1 --json) | jq -r 'map(.time) | max // empty') || return 1
[ -n "$t" ] && [ $(( $(date +%s) - $(date -d "$t" +%s) )) -lt $((26 * 3600)) ]
}
restauration_testee() { [ -d /root/restauration/etc/signalements ] && diff -rq /root/restauration/etc/signalements /etc/signalements; }
controle "/srv/donnees est un volume distinct de la racine" volume_distinct
controle "fstab : /srv/donnees par UUID, avec nofail" fstab_uuid_nofail
controle "/srv/donnees monté en nosuid, nodev, noexec" options_montage
controle "findmnt --verify sans erreur" findmnt --verify
controle "/srv/donnees/exports : 750 signalements:signalements" droits /srv/donnees/exports "750 signalements:signalements"
controle "/srv/donnees/pieces-jointes : 750 signalements:signalements" droits /srv/donnees/pieces-jointes "750 signalements:signalements"
controle "/srv/donnees/journaux : 750, propriétaire root" bash -c '[ "$(stat -c "%a %U" /srv/donnees/journaux)" = "750 root" ]'
controle "nftables activé au démarrage" systemctl is-enabled --quiet nftables
controle "nft : chaîne entree en politique drop" bash -c 'nft list chain inet filtre entree | grep -q "policy drop;"'
controle "nft : SSH seulement depuis des sources nommées" regle_restreinte 22 saddr
controle "nft : port 6514 réservé à sig-app-1" regle_restreinte 6514 172.16.8.11
controle "nft : NTP (UDP 123) réservé à sig-app-1" regle_restreinte 123 172.16.8.11
controle "l'API de sig-app-1 répond depuis sig-outils" curl -fsS -m 5 -o /dev/null http://172.16.8.11:8000/sante
controle "chrony sert le réseau privé (allow 172.16.8.0/22)" grep -rqsx 'allow 172.16.8.0/22' /etc/chrony/
controle "chrony : sig-app-1 figure parmi les clients" client_ntp_vu
controle "signalements-export.timer actif et activé" bash -c 'systemctl is-active --quiet signalements-export.timer && systemctl is-enabled --quiet signalements-export.timer'
controle "le minuteur rattrape les échéances manquées (Persistent)" propriete signalements-export.timer Persistent yes
controle "le minuteur a un déclencheur calendaire" bash -c 'systemctl show -p TimersCalendar --value signalements-export.timer | grep -q "OnCalendar="'
controle "le service d'export est oneshot" propriete signalements-export.service Type oneshot
controle "le service d'export tourne sous signalements" propriete signalements-export.service User signalements
controle "le service d'export a une durée bornée" bash -c '[ "$(systemctl show -p TimeoutStartUSec --value signalements-export.service)" != infinity ]'
controle "le service d'export prévient en cas d'échec" bash -c 'systemctl show -p OnFailure --value signalements-export.service | grep -q "notifier-echec@"'
controle "le service d'export exige /srv/donnees" bash -c 'systemctl show -p RequiresMountsFor --value signalements-export.service | grep -qw /srv/donnees'
controle "la dernière exécution de l'export a réussi" propriete signalements-export.service Result success
controle "un CSV est présent dans /srv/donnees/exports" bash -c 'find /srv/donnees/exports -maxdepth 1 -type f -name "signalements-*.csv" | grep -q .'
controle "export : MemoryHigh sous MemoryMax" limites_memoire
controle "export : CPUWeight < 100 et TasksMax <= 256" limites_cpu_taches
controle "rsyslog actif, rsyslog-gnutls installé" bash -c "$(declare -f paquet); systemctl is-active --quiet rsyslog && paquet rsyslog-gnutls"
controle "rsyslog : configuration valide" rsyslogd -N1
controle "rsyslog : écoute 6514 sur 172.16.8.20 seulement" ecoute_6514
controle "rsyslog : émetteurs authentifiés (x509/name)" rsyslog_conf 'StreamDriver.AuthMode="x509/name"'
controle "rsyslog : sig-app-1 dans PermittedPeer" rsyslog_conf '"sig-app-1.pn-signalements.internal"'
controle "rsyslog : clé privée non lisible par le groupe ni par tous" bash -c '[ -z "$(find /etc/rsyslog.d/tls -name "*.key" -perm /077)" ] && ls /etc/rsyslog.d/tls/*.key'
controle "le message lab-verif de sig-app-1 est arrivé" bash -c 'grep -qsF lab-verif /srv/donnees/journaux/*/syslog.log'
controle "logrotate : règle journaux-distants valide" logrotate -d /etc/logrotate.d/journaux-distants
controle "restic : secrets en 0600 root:root" bash -c "$(declare -f droits); droits /etc/restic/sig-outils.env '600 root:root' && droits /etc/restic/sig-outils.motdepasse '600 root:root'"
controle "restic : dépôt dans sig-sauvegardes/sig-outils" grep -q '^RESTIC_REPOSITORY=s3:https://s3.fr-par.scw.cloud/sig-sauvegardes/sig-outils' /etc/restic/sig-outils.env
controle "restic : instantané « fichiers » de moins de 26 h" derniere_sauvegarde_recente
controle "sig-sauvegarde.timer activé, Persistent" bash -c 'systemctl is-enabled --quiet sig-sauvegarde.timer && [ "$(systemctl show -p Persistent --value sig-sauvegarde.timer)" = yes ]'
controle "sig-sauvegarde.service oneshot, avec OnFailure" bash -c '[ "$(systemctl show -p Type --value sig-sauvegarde.service)" = oneshot ] && systemctl show -p OnFailure --value sig-sauvegarde.service | grep -q .'
controle "restauration testée : /etc/signalements identique" restauration_testee
fi
echo
echo "$echecs échec(s)"
exit "$echecs"Quelques contrôles demandent une explication :
- Les fonctions passées à
bash -c. Un contrôle qui combine plusieurs tests est lancé dans un shell enfant ;$(declare -f nom)y recopie la définition de la fonction utilisée, que l'enfant ne connaîtrait pas sinon. - La configuration effective plutôt que les fichiers.
sshd -Taffiche ce que le démon appliquera, inclusions comprises ;systemd-analyze cat-configmontre ce que lirontsystemd-sysctlet journald au démarrage ;systemctl showdonne les propriétés des unités après fusion des drop-ins. Un fichier correct mais contredit par un autre est donc signalé. findmnt --fstabaffiche la source telle qu'elle est écrite dansfstab(UUID=...), sans la traduire en chemin de périphérique, ce qui permet de vérifier l'emploi de l'UUID.systemd-analyze security --threshold=50échoue si la note d'exposition dépasse 50 sur l'échelle interne de 0 à 100, soit 5,0 sur l'échelle affichée.- Les règles d'audit sont reconnues par leur clé, sous la forme
key=ou-kselon la façon dontauditctl -lles réaffiche.enabled 2prouve le verrouillage,lost 0l'absence de perte depuis le démarrage. chronyc -cproduit la même information quechronyc sourcesau format CSV : le deuxième champ est l'état de sélection, le troisième l'adresse (affichée en chiffres grâce à-n).- Le minuteur de retour arrière est une unité transitoire : elle disparaît une fois arrêtée, mais systemd a consigné son démarrage dans le journal persistant, sous son nom.
- restic est interrogé avec
--no-lock, qui évite même la création d'un verrou dans le dépôt ;restic check, qui en pose un, reste un geste manuel. Le contrôle d'âge suit l'indicateur de la leçon 16 : 26 heures, pour une sauvegarde nocturne. - AIDE n'est pas relancé en vérification complète, qui prendrait plusieurs minutes et solliciterait le disque : le script contrôle la base, la configuration et l'activation du minuteur quotidien.
- La fiche de serveur et le journal des changements ne sont pas vérifiés par le script, puisqu'ils vivent dans votre dépôt et non sur les machines : ils se relisent en revue.
Démonter
Supprimez les deux machines et le disque supplémentaire. Chez Scaleway, supprimez aussi l'application IAM et sa clé, puis videz le bucket sig-sauvegardes avec toutes ses versions et marqueurs de suppression avant de le supprimer : un bucket versionné vidé par une simple suppression garde ses versions, et leur stockage reste facturé. Vérifiez enfin qu'il ne reste ni instantané, ni adresse IP, ni réseau privé (voir Les coûts, le contrat et la sortie). Effacez de votre gestionnaire de secrets le mot de passe du dépôt et la clé de l'autorité des journaux.
Plan du cours
Validation sur l'honneur, enregistrée dans ce navigateur. Elle valide le niveau 200 de Démarrage, noyau et configuration du système , Configuration réseau et pare-feu d'un hôte Linux , Système de fichiers et permissions , Processus, services et systemd , Identité, authentification et IAM , Durcissement des systèmes , Journaux centralisés , Sauvegarde et reprise d'activité et fait monter en rareté l'équipement de votre cosmonaute.