Durcissement système : bases#

Pourquoi#

SSH durci (chapitre précédent) protège le point d’entrée, mais un système compromis par une autre voie (une vulnérabilité applicative future, une élévation de privilège locale) doit rester difficile à exploiter et traçable. Ce chapitre pose les fondations générales de durcissement du système d’exploitation, indépendantes des services applicatifs qui seront installés dans les chapitres suivants. Comme pour SSH, ces bases sont posées tôt, avant l’installation des services, pour qu’aucun service ne tourne jamais sur un socle non durci, même temporairement.

Ce guide ne vise pas l’exhaustivité en matière de sécurité. Les mesures présentées ici sont celles jugées pertinentes pour ce projet ; elles ne remplacent pas une politique de sécurité complète adaptée au contexte propre à chaque déploiement. Le chapitre Lynis, plus loin, propose une méthode pour auditer objectivement le système au-delà de cette liste.

Comment#

Durcissement noyau via sysctl#

cat > /etc/sysctl.d/99-hardening.conf <<'EOF'
# === Kernel ===
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.sysrq = 4
kernel.randomize_va_space = 2
kernel.yama.ptrace_scope = 2
net.core.bpf_jit_harden = 2

# === Filesystem ===
fs.protected_fifos = 2
dev.tty.ldisc_autoload = 0

# === Réseau IPv4 ===
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_rfc1337 = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1

# === Réseau IPv6 ===
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
EOF

sysctl --system 2>&1 | grep -E "99-hardening|error"

Ces réglages suivent pour partie le guide ANSSI BP-028 (Recommandations de configuration d’un système GNU/Linux), notamment kptr_restrict et dmesg_restrict. Décomposons les valeurs non triviales :

DirectiveSignification
kernel.kptr_restrict = 2Masque les adresses de pointeurs noyau dans /proc à tous les utilisateurs, y compris root sans capacité CAP_SYSLOG : empêche de s’en servir pour contourner l’ASLR côté noyau
kernel.dmesg_restrict = 1Réserve la lecture du tampon dmesg aux processus avec CAP_SYSLOG, évite qu’un utilisateur non privilégié y glane des adresses ou des informations matérielles
kernel.sysrq = 4Bitmask restreinte des fonctions Magic SysRq (pas 0 totalement désactivé, pas 1 tout autorisé) : conserve une capacité de récupération d’urgence minimale plutôt que de la supprimer entièrement, un choix de doctrine documenté qui s’écarte volontairement de la suggestion Lynis par défaut
kernel.randomize_va_space = 2ASLR complet : pile, tas, mmap et PIE tous randomisés (la valeur 1, plus faible, laisse le tas non randomisé)
kernel.yama.ptrace_scope = 2Restreint ptrace aux processus disposant de CAP_SYS_PTRACE (plus strict que 1, qui autorise seulement un parent à tracer son propre enfant)
net.core.bpf_jit_harden = 2Durcit le compilateur JIT eBPF contre les attaques de type JIT spraying pour tous les utilisateurs, y compris privilégiés (1 ne durcirait que pour les utilisateurs non privilégiés)
fs.protected_fifos = 2Refuse l’ouverture d’un FIFO n’appartenant pas à l’utilisateur courant dans un répertoire world-writable avec sticky bit, même pour un processus privilégié (1 ferait exception pour root)
dev.tty.ldisc_autoload = 0Empêche un utilisateur non privilégié de charger arbitrairement une line discipline tty (chacune est un module noyau), fermant un vecteur d’élévation de privilèges locale connu

kernel.yama.ptrace_scope reste sans effet sur ce noyau précis. Le module de sécurité Yama n’est pas compilé dans linux-image-rpi-v8-rt (CONFIG_SECURITY_YAMA absent) : la directive ci-dessus est silencieusement ignorée, sans erreur ni avertissement. Aucun paramètre de démarrage n’y changerait quoi que ce soit ; seul un noyau recompilé avec cette option l’activerait, un effort non justifié pour ce seul gain. La directive est conservée telle quelle : inoffensive aujourd’hui, elle prendrait effet d’elle-même si un futur noyau la compilait.

Côté réseau, rp_filter = 1 active le filtrage par chemin inverse (reverse path filtering) : un paquet est rejeté si la route retour vers son adresse source ne passerait pas par l’interface d’arrivée, une protection anti-usurpation classique. Les directives accept_redirects, secure_redirects, send_redirects et accept_source_route à 0 désactivent respectivement l’acceptation de redirections ICMP (y compris celles provenant de passerelles de confiance, plus strict que le comportement par défaut), l’émission de redirections (ce serveur n’est pas un routeur), et le routage par la source (une option IP historique détournée à des fins d’usurpation) : ce sont des vecteurs classiques d’altération de table de routage par un attaquant du même réseau local, sans utilité légitime sur ce serveur. log_martians = 1 journalise les paquets à adresse impossible (utile en complément d’auditd et fail2ban, déjà en place). tcp_syncookies = 1 protège contre le SYN flood. tcp_rfc1337 = 1 applique la RFC 1337 (ignore les RST reçus en état TIME-WAIT, contre l’attaque dite de TIME-WAIT assassination). icmp_echo_ignore_broadcasts = 1 empêche ce serveur de participer involontairement à une attaque Smurf par amplification ICMP broadcast. icmp_ignore_bogus_error_responses = 1 supprime une catégorie connue de faux positifs générés par des routeurs mal configurés (évite du bruit de journalisation superflu sur un système où les logs sont déjà activement surveillés). Le doublon .all./.default. sur chaque directive IPv4 n’est pas redondant : .all. s’applique aux interfaces déjà présentes, .default. sert de valeur pour toute interface créée après coup. Les directives IPv6 reprennent la même logique anti-redirection/anti-source-route que leur équivalent IPv4.

Reboot automatique après panic kernel#

Sans réglage particulier, un kernel panic laisse la machine plantée indéfiniment tant qu’aucune intervention physique n’a lieu. Sur un serveur sans surveillance continue, avec des logs en tmpfs (la trace du panic disparaît de toute façon au reboot suivant), l’auto-reboot est préférable à un blocage silencieux :

cat > /etc/sysctl.d/90-kernelpanic-reboot.conf <<'EOF'
# Reboot automatique après un kernel panic.
# Valeur : nombre de secondes d'attente avant reboot après un panic.
kernel.panic = 10
EOF

sysctl --system 2>&1 | grep -i panic

Ce fichier est volontairement séparé de 99-hardening.conf ci-dessus : il répond à un objectif de résilience, pas de durcissement contre une attaque, et les deux préoccupations gagnent à rester dans des fichiers distincts.

Permissions strictes#

chmod 600 /etc/crontab /etc/ssh/sshd_config
chmod 700 /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly

La bannière légale (/etc/issue.net) a déjà été créée au chapitre précédent, avec la directive Banner correspondante dans sshd_config.

login.defs : politique de mots de passe#

sed -i 's|^UMASK\s.*|UMASK           027|' /etc/login.defs
grep -q "^UMASK" /etc/login.defs || echo "UMASK           027" >> /etc/login.defs

grep -q "^SHA_CRYPT_MIN_ROUNDS" /etc/login.defs || cat >> /etc/login.defs <<'EOF'

SHA_CRYPT_MIN_ROUNDS 50000
SHA_CRYPT_MAX_ROUNDS 100000
EOF

UMASK 027 retire par défaut tout droit au groupe “autres” et le droit d’écriture au groupe propriétaire sur les fichiers créés par un utilisateur (la valeur Debian par défaut, 022, laisse les fichiers lisibles par n’importe quel compte local). SHA_CRYPT_MIN_ROUNDS/MAX_ROUNDS relèvent le nombre d’itérations de la fonction de hachage SHA-512 utilisée pour /etc/shadow : le défaut glibc (environ 5000 tours) est resté inchangé depuis longtemps, alors que la puissance de calcul disponible pour une attaque hors ligne a considérablement augmenté. Monter à 50 000-100 000 tours multiplie le coût de calcul par mot de passe testé, sans impact perceptible sur un login interactif normal.

Restreindre les compilateurs#

# Empêche un attaquant qui a compromis un compte non-root de compiler
# un exploit local directement sur la machine
for c in /usr/bin/gcc /usr/bin/g++ /usr/bin/cc /usr/bin/cpp /usr/bin/as /usr/bin/ld; do
    if [ -x "$c" ]; then
        chmod 750 "$c"
        chown root:root "$c"
    fi
done

Blacklist des protocoles réseau exotiques#

cat > /etc/modprobe.d/99-blacklist-exotic-net.conf <<'EOF'
install dccp /bin/true
install sctp /bin/true
install rds /bin/true
install tipc /bin/true
EOF

auditd : traçabilité des fichiers sensibles#

mkdir -p /var/log/audit
chown root:root /var/log/audit
chmod 0750 /var/log/audit

cat > /etc/audit/rules.d/99-stratum1.rules <<'EOF'
# Comptes locaux et privilèges
-w /etc/passwd       -p wa -k identity
-w /etc/shadow       -p wa -k identity
-w /etc/group        -p wa -k identity
-w /etc/gshadow      -p wa -k identity
-w /etc/sudoers      -p wa -k scope
-w /etc/sudoers.d/   -p wa -k scope

# SSH
-w /etc/ssh/sshd_config        -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/     -p wa -k sshd_config

# Élévation de privilèges par un utilisateur non-root
# auid = uid de connexion d'origine (immuable, distinct de l'uid courant) ;
# auid >= 1000 exclut les comptes système, euid = 0 signale une exécution
# effective en tant que root, auid != 4294967295 (soit -1 en non signé, la
# valeur "aucune session" du kernel) exclut les processus système sans
# auid attribué (services, cron) pour ne garder que les vraies sessions
# utilisateur ayant obtenu un privilège root
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k root_execve

# Connexions
-w /var/log/lastlog -p wa -k logins
-w /var/log/faillog -p wa -k logins

-e 2
EOF

systemctl enable --now auditd
augenrules --load

# Dimensionnement pour un /var/log en tmpfs (chapitre Système de fichiers)
sed -i 's|^max_log_file = .*|max_log_file = 8|'                       /etc/audit/auditd.conf
sed -i 's|^num_logs = .*|num_logs = 4|'                               /etc/audit/auditd.conf
sed -i 's|^space_left_action = .*|space_left_action = SYSLOG|'        /etc/audit/auditd.conf
sed -i 's|^admin_space_left_action = .*|admin_space_left_action = SUSPEND|' /etc/audit/auditd.conf
systemctl restart auditd   # auditd ne recharge pas ces paramètres à chaud

Les valeurs par défaut d’auditd (8 Mo × 5 fichiers = 40 Mo, seuils d’alerte à 75/50 Mo) sont calibrées pour un disque de plusieurs Go. Ici, /var/log est un tmpfs de 256 Mo partagé avec nginx, certbot et journald (chapitre Système de fichiers) : 8 × 4 = 32 Mo maximum pour les journaux d’audit laisse une marge confortable au reste du tmpfs. space_left_action = SYSLOG se contente d’alerter tant qu’il reste de l’espace, alors que admin_space_left_action = SUSPEND arrête l’audit (pas le système) en dernier recours si l’espace devient vraiment critique, pour éviter qu’un /var/log saturé par les journaux d’audit eux-mêmes ne bloque les autres services qui y écrivent.

Erreurs à éviter#

  • Ne pas ajouter audit.log à logrotate. auditd gère sa propre rotation interne via max_log_file/num_logs. Une double rotation peut corrompre l’état du démon d’audit, voir aussi le chapitre Logrotate.
  • /var/log/audit/ absent au boot. Comme /var/log est un tmpfs (chapitre Système de fichiers), ce répertoire doit être recréé à chaque démarrage via tmpfiles.d, sans cette règle, auditd échoue avec Could not open dir /var/log/audit.
  • Surveiller (-w) un fichier qui n’existe pas encore. auditd refuse une règle -w pointant vers un chemin absent au moment du chargement (Error sending add rule: No such file or directory). C’est pour cette raison précise que les règles ci-dessus ne couvrent que ce qui existe déjà à ce stade du guide (comptes système, SSH), pas encore /etc/chrony/, /etc/nftables.conf ni /etc/fail2ban/, qui n’existeront qu’après les chapitres correspondants. Le chapitre Lynis, plus loin, revient sur ces règles pour les compléter une fois tous les services installés.
  • Restreindre les compilateurs avant d’avoir fini l’installation. Certains paquets ou scripts d’installation peuvent avoir besoin de compiler quelque chose (modules DKMS, par exemple). Il faut s’assurer qu’aucune étape ultérieure de l’installation ne dépend d’un compilateur accessible à un utilisateur non-root avant d’appliquer cette restriction.

Vérifier#

sysctl net.ipv4.tcp_syncookies kernel.kptr_restrict
# Doivent refléter les valeurs du fichier 99-hardening.conf

auditctl -l | head -10
auditctl -s | grep -i enabled
# "enabled 2" = configuration immuable

stat -c "%a" /etc/crontab /etc/cron.d
# 600 et 700 respectivement

Ce socle sera complété plus loin dans ce guide par le pare-feu (chapitre nftables), fail2ban, et un audit global via Lynis une fois tous les services installés, voir le chapitre dédié Lynis, qui reprend ces réglages dans le cadre d’un score de durcissement global.