Aller au contenu

Lab : reprendre en main les serveurs de Signalements

Prérequis

Testé avec aide 0.18.6 / 0.19.1 auditd 3.1.2 / 4.0.2 chrony 4.5 / 4.6.1 debian 13 nftables 1.1.3 restic 0.18.0 rsyslog 8.2312.0 / 8.2504.0 systemd 255 / 257 ubuntu 24.04 ufw 0.36.2 , vérifié le 8 octobre 2026

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/22 qui les relie : pn-signalements chez 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 exemple 172.16.8.1), puisque SSH n'y sera plus ouvert qu'à lui.
  • Les noms sig-app-1.pn-signalements.internal et sig-outils.pn-signalements.internal résolus sur les deux machines : par le DNS interne de Scaleway, ou, sur des machines locales, par une ligne dans /etc/hosts de chacune.
  • Un second disque de quelques gigaoctets attaché à sig-outils.
  • Un bucket sig-sauvegardes en fr-par (Object Storage Scaleway), versionné, et une application IAM dont la clé ne peut écrire que sous sig-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 jq et curl sur sig-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

ExigenceMachineLeç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 /etcles deux1
Le fuseau est UTC ; net.ipv4.ip_forward vaut 0les deux3, 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éesles deux3, 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 à rootsig-outils5, 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ésoutles deux7
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/22sig-app-18
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-1sig-outils8
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 connexionles deux8
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 deux9
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/exportssig-outils10
L'export est borné : MemoryHigh= sous MemoryMax=, CPUWeight= inférieur à 100, TasksMax= d'au plus 256sig-outils11
Le journal est persistant (drop-in avec Storage=persistent et MaxRetentionSec=) et contient au moins le démarrage précédentles deux12
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-distantsles deux12
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 videles deux13
SSH : PermitRootLogin no, clés seules, AllowGroups acces-ssh, MaxAuthTries 3, bannière /etc/issue.net sans nom de distribution, d'après sshd -Tles deux14
Les modules dccp et sctp sont interdits par install ... /bin/falseles deux3, 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,0les deux14
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.11sig-app-17, 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 noyaules deux2, 15
AIDE est initialisé (/var/lib/aide/aide.db), avec le fragment 90_aide_signalements, et sa vérification quotidienne est activéeles deux15
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'originalsig-outils16

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 fstab avec un UUID inventé, redémarrage, récupération par systemd.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/ (ou by-path sur une machine locale), wipefs avant mkfs.ext4 -L donnees -m 1. Après chaque modification de fstab : findmnt --verify, daemon-reload, mount -a (leçon 6).
  • Créez le compte d'administration nominatif et le groupe acces-ssh avant 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-outils les serveurs du pool et ntp.obspm.fr en NTS (leçon 9).
  • L'unité notifier-echec@.service du lab peut se contenter d'un ExecStart=/usr/bin/logger -t notifier-echec "Échec de %i" : le webhook de la leçon 10 n'est pas exigé. Testez l'export par systemctl 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/ en 0640 root:syslog, sinon AppArmor et le compte syslog la refusent. Validez chaque configuration par rsyslogd -N1.
  • Posez -e 2 en dernier, une fois auditctl -l correct, puis redémarrez les deux machines : cela vérifie au passage fstab, 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 -T affiche ce que le démon appliquera, inclusions comprises ; systemd-analyze cat-config montre ce que liront systemd-sysctl et journald au démarrage ; systemctl show donne 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 --fstab affiche la source telle qu'elle est écrite dans fstab (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 -k selon la façon dont auditctl -l les réaffiche. enabled 2 prouve le verrouillage, lost 0 l'absence de perte depuis le démarrage.
  • chronyc -c produit la même information que chronyc sources au 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

    +200 XP 200 Compagnon

    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.