14. Durcissement du système#

Le durcissement (hardening) consiste à réduire la surface d’attaque d’un système en supprimant ce qui n’est pas nécessaire, en confinant ce qui reste, et en surveillant ce qui se passe. Ce chapitre présente les techniques et outils essentiels pour sécuriser un serveur Linux en production.


Surface d’attaque et principes fondamentaux#

La surface d’attaque est l’ensemble des points d’entrée qu’un attaquant peut exploiter : ports ouverts, services en cours d’exécution, fichiers accessibles, interfaces réseau, utilisateurs avec privilèges excessifs, logiciels installés.

Principe du moindre privilège#

Chaque composant (utilisateur, service, processus) ne doit disposer que des droits strictement nécessaires à sa fonction. Ce principe limite l’impact d’une compromission : un attaquant qui prend le contrôle d’un service confiné ne peut pas se propager librement.

Défense en profondeur#

La défense en profondeur (defense in depth) empile plusieurs couches de sécurité indépendantes. La compromission d’une couche ne doit pas suffire à compromettre le système entier :

Internet → Pare-feu réseau → Pare-feu hôte (nftables)
        → AppArmor/SELinux → Permissions POSIX
        → Monitoring/auditd → Alertes SIEM

Aucune mesure prise isolément n’est suffisante. La combinaison de plusieurs mécanismes complémentaires est ce qui rend un système résillient.

Important

Le durcissement n’est pas un état final mais un processus continu. Les nouvelles CVE, les changements d’architecture et les mises à jour d’application nécessitent des réévaluations régulières.


Réduction de la surface d’attaque#

Désactiver les services inutiles#

# Lister tous les services actifs
systemctl list-units --type=service --state=running

# Désactiver et stopper un service inutile
systemctl stop bluetooth
systemctl disable bluetooth
systemctl mask bluetooth  # Empêche toute réactivation accidentelle

# Vérifier les services activés au démarrage
systemctl list-unit-files --type=service --state=enabled

Services fréquemment inutiles sur un serveur : avahi-daemon (mDNS), cups (impression), bluetooth, ModemManager, wpa_supplicant (si pas de WiFi).

Supprimer les paquets superflus#

# Debian/Ubuntu : lister les paquets installés manuellement
apt-mark showmanual | sort

# Supprimer un paquet et ses dépendances orphelines
apt purge telnet rsh-client ftp
apt autoremove --purge

# Vérifier les paquets sans dépendant
deborphan

Inspecter les ports ouverts#

# Tous les ports en écoute avec le processus associé
ss -tlnp
State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
LISTEN  0       128     0.0.0.0:22          0.0.0.0:*          users:(("sshd",pid=1234))
LISTEN  0       511     0.0.0.0:80          0.0.0.0:*          users:(("nginx",pid=5678))
LISTEN  0       128     127.0.0.1:5432      0.0.0.0:*          users:(("postgres",pid=9012))
# Ports UDP
ss -ulnp

# Connexions établies
ss -tnp state established

Tip

PostgreSQL écoute sur 127.0.0.1:5432 (liaison locale uniquement). Si un service écoute sur 0.0.0.0 alors qu’il n’a pas besoin d’être accessible depuis l’extérieur, c’est une surface d’attaque inutile à fermer.


AppArmor#

AppArmor (Application Armor) est un système de contrôle d’accès obligatoire (MAC) basé sur des profils. Il confine les programmes en définissant précisément les fichiers, capabilities et sockets auxquels ils ont accès.

Modes de fonctionnement#

Mode

Description

enforce

Le profil est appliqué ; les violations sont bloquées et journalisées

complain

Les violations sont journalisées mais pas bloquées (phase d’apprentissage)

unconfined

Aucun profil actif pour ce programme

Consulter l’état d’AppArmor#

aa-status
apparmor module is loaded.
63 profiles are loaded.
49 profiles are in enforce mode.
   /usr/bin/evince
   /usr/sbin/nginx
   /usr/sbin/sshd
   ...
14 profiles are in complain mode.
   /usr/bin/firefox
   ...
0 profiles are in kill mode.
2 processes have profiles defined.
2 processes are in enforce mode.
0 processes are in complain mode.

Générer un profil — aa-genprof#

# Démarrer la génération de profil pour une application
aa-genprof /usr/local/bin/mon-service

# Dans un autre terminal, utiliser l'application normalement
# Revenir et valider les accès détectés

# Mettre un profil en mode enforce
aa-enforce /etc/apparmor.d/usr.local.bin.mon-service

# Recharger tous les profils
systemctl reload apparmor

Syntaxe d’un profil AppArmor#

# /etc/apparmor.d/usr.sbin.nginx
#include <tunables/global>

/usr/sbin/nginx {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  capability net_bind_service,
  capability setuid,
  capability setgid,

  /usr/sbin/nginx mr,
  /etc/nginx/** r,
  /var/log/nginx/*.log w,
  /var/www/html/** r,
  /run/nginx.pid rw,

  # Interdire l'accès aux données sensibles
  deny /etc/shadow r,
  deny /root/** rwx,
}

Les permissions dans AppArmor : r (read), w (write), x (execute), m (mmap), l (link), k (lock).

Diagnostiquer les violations#

# Voir les violations AppArmor dans les logs
grep "apparmor" /var/log/syslog | grep "DENIED"

# Ou avec journald
journalctl -k | grep "apparmor.*DENIED"

SELinux — bases#

SELinux (Security-Enhanced Linux) est le système MAC dominant sur Red Hat/CentOS/Fedora. Plus puissant qu’AppArmor, il est aussi plus complexe à administrer.

Contextes de sécurité#

Chaque processus et fichier possède un contexte SELinux de la forme :

utilisateur:rôle:type:niveau
# Voir le contexte d'un processus
ps -eZ | grep httpd
system_u:system_r:httpd_t:s0  1234 ?  00:00:01 httpd
# Voir le contexte d'un fichier
ls -Z /var/www/html/index.html
system_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html

Modes SELinux#

# Voir le mode courant
getenforce
# Enforcing

# Informations détaillées
sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux mount point:            /sys/fs/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
# Passer temporairement en mode permissif (diagnostic)
setenforce 0

# Repasser en enforcing
setenforce 1

# Configuration persistante : /etc/selinux/config
SELINUX=enforcing
SELINUXTYPE=targeted

Diagnostiquer et corriger avec audit2allow#

# Voir les refus SELinux
grep "avc: denied" /var/log/audit/audit.log | tail -20

# Générer une règle de politique à partir des refus
grep "avc: denied" /var/log/audit/audit.log | audit2allow -M mon_module
semodule -i mon_module.pp

Note

audit2allow est utile pour débloquer des applications légitimes, mais il ne faut pas l’utiliser aveuglément. Analyser chaque refus avant de l’autoriser ; parfois la solution correcte est de repositionner le contexte du fichier avec restorecon, pas de créer une exception.


Namespaces et isolation#

Les namespaces Linux permettent d’isoler différents aspects de l’environnement d’un processus.

Types de namespaces#

Namespace

Flag

Isole

UTS

CLONE_NEWUTS

Nom d’hôte et nom de domaine

PID

CLONE_NEWPID

Arbre des processus

NET

CLONE_NEWNET

Interfaces réseau, routes, ports

MNT

CLONE_NEWNS

Points de montage

USER

CLONE_NEWUSER

UIDs/GIDs (mappage utilisateur)

IPC

CLONE_NEWIPC

Queues de messages, sémaphores, mémoire partagée

Cgroup

CLONE_NEWCGROUP

Vue de la hiérarchie cgroups

Time

CLONE_NEWTIME

Horloges système (noyau ≥ 5.6)

Utilisation avec unshare#

# Créer un namespace PID isolé
unshare --pid --fork --mount-proc /bin/bash
# Dans ce shell, le PID 1 est bash

# Namespace réseau isolé (aucune interface réseau sauf lo)
unshare --net /bin/bash
ip link show  # Seulement loopback

# Namespace utilisateur : devenir "root" dans le namespace sans être root sur l'hôte
unshare --user --map-root-user /bin/bash
id  # uid=0(root) — mais seulement dans ce namespace

nsenter — rejoindre un namespace existant#

# Rejoindre le namespace réseau d'un conteneur Docker
PID=$(docker inspect -f '{{.State.Pid}}' mon_conteneur)
nsenter -t $PID -n ip addr

cgroups v2#

Les cgroups (control groups) permettent de limiter, surveiller et prioriser l’utilisation des ressources par les processus.

Hiérarchie cgroups v2#

Sous cgroups v2, tous les contrôleurs partagent une hiérarchie unifiée montée sur /sys/fs/cgroup.

# Voir la hiérarchie
ls /sys/fs/cgroup/system.slice/nginx.service/
cgroup.controllers  cpu.max  cpu.stat  io.max  memory.current
memory.max  memory.stat  pids.current  pids.max

Contrôleurs principaux#

Contrôleur

Ressource contrôlée

cpu

Temps CPU (quota, poids)

memory

Mémoire RAM et swap

io

Débit disque (lecture/écriture)

pids

Nombre maximum de processus

cpuset

Affinité CPU et mémoire NUMA

Limites via systemd#

# /etc/systemd/system/mon-service.service (ou override)
[Service]
# Limiter la mémoire RAM à 512 Mo
MemoryMax=512M
# Limiter le swap à 128 Mo
MemorySwapMax=128M
# Limiter à 50% d'un cœur CPU
CPUQuota=50%
# Limiter les processus fils
TasksMax=100
# Priorité IO (0 à 100, plus bas = plus prioritaire)
IOWeight=50
# Appliquer les changements
systemctl daemon-reload
systemctl restart mon-service

# Vérifier les limites actives
systemctl show mon-service | grep -E "Memory|CPU|Tasks|IO"

auditd#

Le démon auditd est le sous-système d’audit du noyau Linux. Il enregistre les événements système dans /var/log/audit/audit.log avec un niveau de détail très élevé.

Configuration des règles#

# Surveiller les modifications de /etc/passwd
auditctl -w /etc/passwd -p wa -k passwd_changes

# Surveiller les appels système execve de l'utilisateur 1001
auditctl -a always,exit -F arch=b64 -S execve -F uid=1001 -k user_cmds

# Surveiller l'utilisation de sudo
auditctl -w /usr/bin/sudo -p x -k sudo_use

# Lister les règles actives
auditctl -l

# Règles persistantes
# /etc/audit/rules.d/audit.rules

Rechercher dans les logs — ausearch#

# Rechercher par clé
ausearch -k passwd_changes

# Rechercher par utilisateur
ausearch -ua alice --start today

# Rechercher les connexions échouées
ausearch -m USER_LOGIN --success no

# Rechercher les commandes sudo des dernières 24h
ausearch -k sudo_use --start yesterday --end now

Rapports — aureport#

# Rapport général
aureport --summary

# Rapport des authentifications
aureport --auth

# Rapport des commandes exécutées
aureport --executable

# Rapport des accès aux fichiers
aureport --file

# Rapport des anomalies
aureport --anomaly

Exemple de sortie ausearch#

----
time->Mon Mar 24 10:15:32 2026
type=SYSCALL msg=audit(1711274132.456:1234): arch=c000003e syscall=2
success=yes exit=3 a0=7f... a1=0 a2=1b6 a3=...
items=1 ppid=5432 pid=5433 auid=1000 uid=0 gid=0 euid=0
key="passwd_changes"
type=PATH msg=audit(1711274132.456:1234): item=0
name="/etc/passwd" inode=131073 dev=08:01 mode=0100644

chroot et jails#

Principe du chroot#

chroot change le répertoire racine apparent d’un processus. Le processus ne peut plus accéder aux fichiers en dehors de la nouvelle racine.

# Créer un environnement chroot minimal
mkdir -p /srv/jail/{bin,lib,lib64,usr/lib}

# Copier les binaires nécessaires
cp /bin/bash /srv/jail/bin/
ldd /bin/bash  # Voir les dépendances
cp /lib/x86_64-linux-gnu/libtinfo.so.6 /srv/jail/lib/
cp /lib/x86_64-linux-gnu/libc.so.6 /srv/jail/lib/
cp /lib64/ld-linux-x86-64.so.2 /srv/jail/lib64/

# Entrer dans le chroot
chroot /srv/jail /bin/bash

Limites du chroot#

Le chroot présente plusieurs limitations importantes :

  • Un processus avec CAP_SYS_CHROOT peut s’en échapper

  • Les descripteurs de fichiers ouverts avant le chroot restent valides

  • Les sockets réseau restent accessibles

  • Pas d’isolation des processus (on voit toujours le /proc parent si monté)

Avertissement

chroot seul n’est pas une solution de sécurité suffisante. Il doit être combiné avec d’autres mécanismes (suppression des capabilities, namespaces, seccomp) pour offrir un confinement réel. Les conteneurs modernes (Docker, Podman) combinent chroot + namespaces + cgroups + seccomp + AppArmor/SELinux.

Différence avec les conteneurs#

Mécanisme

chroot seul

Conteneur (Docker)

Système de fichiers isolé

Oui

Oui (OverlayFS)

Namespaces PID/NET/UTS

Non

Oui

cgroups (limites ressources)

Non

Oui

Profil seccomp

Non

Oui (défaut)

AppArmor/SELinux

Non

Oui (optionnel)


Checklist de durcissement#

CIS Benchmark#

Le CIS (Center for Internet Security) publie des guides de durcissement détaillés pour chaque distribution. Les recommandations sont classées en deux niveaux :

  • Niveau 1 : mesures de base, impact minimal sur les fonctionnalités

  • Niveau 2 : durcissement avancé, peut affecter certaines fonctionnalités

Exemples de recommandations CIS :

  • Désactiver les protocoles réseau inutilisés (DCCP, SCTP, RDS)

  • Configurer sysctl pour le durcissement réseau (net.ipv4.tcp_syncookies, net.ipv4.conf.all.rp_filter)

  • Configurer PAM pour la politique de mots de passe

  • Activer et configurer auditd

  • S’assurer que /tmp est monté avec noexec,nosuid,nodev

Lynis — audit automatisé#

# Installer lynis
apt install lynis

# Lancer un audit complet
lynis audit system

# Résultat : score sur 100 par domaine
# Rapport dans /var/log/lynis.log
Lynis security scan details:
  Hardening index : 67 [#############       ]
  Tests performed : 243
  Plugins enabled : 2

  Components:
  - Firewall               [ENABLED]
  - Malware scanner        [NOT FOUND]

  Scan mode:
  Normal [ ]  Forensics [ ]  Integration [ ]  Pentest [V] (running non-privileged)

  Lynis modules:
  - Compliance status      [UNKNOWN]
  - Security audit         [ENABLED]
  - Vulnerability scan     [ENABLED]

AIDE — détection d’intrusion par intégrité de fichiers#

# Installer AIDE
apt install aide

# Initialiser la base de données (après durcissement initial)
aideinit
cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# Vérifier l'intégrité
aide --check

# Planifier via cron
echo "0 3 * * * root /usr/bin/aide --check | mail -s 'AIDE check' admin@example.com" \
  >> /etc/crontab

Démonstrations Python#

Analyse d’une sortie aa-status simulée#


import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Sortie aa-status simulée parsée en dictionnaire Python
aa_status = {
    "enforce": [
        "nginx", "sshd", "mysqld", "cupsd", "evince",
        "firefox", "thunderbird", "libreoffice",
        "NetworkManager", "ntpd", "chronyd", "named",
        "dovecot", "postfix", "rsyslogd", "apache2",
        "snapd", "lxc-container", "docker-default",
        "man_filter", "man_groff", "pidgin",
        "totem", "totem-open-location", "totem-pl-parser",
        "usr.lib.snapd.snap-confine",
        "usr.sbin.tcpdump", "usr.sbin.traceroute",
        "usr.bin.ping", "usr.bin.python3.8",
        "usr.bin.ruby", "usr.sbin.avahi-daemon",
        "usr.sbin.cups-browsed", "usr.sbin.dnsmasq",
        "usr.sbin.haveged", "usr.sbin.ippusbxd",
        "usr.sbin.lightdm", "usr.sbin.mdnsd",
        "usr.sbin.ntpd", "usr.sbin.ntp",
        "usr.sbin.unbound", "usr.sbin.useradd",
        "usr.sbin.userdel", "usr.sbin.usermod",
        "usr.sbin.passwd", "xtables-legacy-multi",
        "kerberos5-admin-server", "kerberos5-kdc",
    ],
    "complain": [
        "mozilla-firefox", "opera-stable", "chromium-browser",
        "vlc", "gimp-2.10", "inkscape", "blender",
        "steam", "code", "signal-desktop",
        "usr.bin.perl", "usr.bin.python2",
    ],
    "unconfined": [
        "cron", "bash", "python3", "perl", "ruby",
        "java", "node", "php", "ruby2.7",
        "samba", "vsftpd",
    ],
}

comptes = {k: len(v) for k, v in aa_status.items()}
total = sum(comptes.values())

labels = list(comptes.keys())
valeurs = list(comptes.values())
couleurs = ["#4c9be8", "#f0c040", "#e84c4c"]
explode = [0.04, 0.04, 0.08]

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# Camembert
wedges, texts, autotexts = axes[0].pie(
    valeurs,
    labels=[f"{l.capitalize()}\n({v})" for l, v in zip(labels, valeurs)],
    colors=couleurs,
    explode=explode,
    autopct="%1.1f%%",
    startangle=120,
    textprops={"fontsize": 10},
)
for at in autotexts:
    at.set_fontweight("bold")
axes[0].set_title(f"AppArmor — répartition des profils\n(total : {total} profils)", fontsize=11)

# Barres horizontales avec exemples
y_pos = range(len(labels))
bars = axes[1].barh(
    [l.capitalize() for l in labels],
    valeurs,
    color=couleurs,
    edgecolor="white",
    linewidth=1.2,
    height=0.5,
)
axes[1].set_xlabel("Nombre de profils")
axes[1].set_title("Répartition par mode — détail", fontsize=11)
for bar, val in zip(bars, valeurs):
    axes[1].text(bar.get_width() + 0.3, bar.get_y() + bar.get_height() / 2,
                 str(val), va="center", fontsize=10, fontweight="bold")
axes[1].set_xlim(0, max(valeurs) * 1.15)

# Annotations exemples
exemples = {
    "enforce": "ex: nginx, sshd, mysqld…",
    "complain": "ex: firefox, vlc, gimp…",
    "unconfined": "ex: cron, bash, java…",
}
for i, (label, ex) in enumerate(exemples.items()):
    axes[1].text(0.5, i, ex, va="center", fontsize=8, color="#555555", style="italic")

plt.suptitle("État AppArmor — analyse des profils de sécurité", fontsize=13, y=1.02)
plt.show()
_images/7978da402a359a07e45e9b66994efb994c729215972dac75d3c287ce44058822.png

Visualisation de la hiérarchie cgroups v2#


import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Structure hiérarchique cgroups v2 simulée
# (nœud, parent, ressources)
noeuds = [
    ("/",               None,       "cpu, memory, io, pids"),
    ("system.slice",    "/",        "cpu, memory, io"),
    ("user.slice",      "/",        "cpu, memory"),
    ("machine.slice",   "/",        "cpu, memory, io"),
    ("nginx.service",   "system.slice", "CPUQuota=200%\nMemoryMax=512M"),
    ("postgresql.service", "system.slice", "CPUQuota=400%\nMemoryMax=2G"),
    ("sshd.service",    "system.slice", "TasksMax=100"),
    ("user-1000.slice", "user.slice", "MemoryMax=4G"),
    ("user-1001.slice", "user.slice", "MemoryMax=2G"),
    ("docker.service",  "machine.slice", "MemoryMax=8G"),
]

# Positions manuelles pour lisibilité
positions = {
    "/":                    (0.50, 0.92),
    "system.slice":         (0.22, 0.72),
    "user.slice":           (0.50, 0.72),
    "machine.slice":        (0.78, 0.72),
    "nginx.service":        (0.08, 0.45),
    "postgresql.service":   (0.22, 0.45),
    "sshd.service":         (0.38, 0.45),
    "user-1000.slice":      (0.50, 0.45),
    "user-1001.slice":      (0.64, 0.45),
    "docker.service":       (0.78, 0.45),
}

couleurs_noeud = {
    "/":                    "#2c3e50",
    "system.slice":         "#2980b9",
    "user.slice":           "#27ae60",
    "machine.slice":        "#8e44ad",
    "nginx.service":        "#3498db",
    "postgresql.service":   "#3498db",
    "sshd.service":         "#3498db",
    "user-1000.slice":      "#2ecc71",
    "user-1001.slice":      "#2ecc71",
    "docker.service":       "#9b59b6",
}

fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(0, 1)
ax.set_ylim(0.3, 1.05)
ax.axis("off")

# Arêtes
for nom, parent, _ in noeuds:
    if parent and parent in positions:
        x1, y1 = positions[parent]
        x2, y2 = positions[nom]
        ax.plot([x1, x2], [y1 - 0.03, y2 + 0.04],
                color="#aaaaaa", linewidth=1.5, zorder=1)

# Nœuds
for nom, parent, ressources in noeuds:
    x, y = positions[nom]
    couleur = couleurs_noeud.get(nom, "#3498db")

    # Boîte
    largeur = 0.14 if nom not in ["/"] else 0.08
    hauteur = 0.065
    rect = mpatches.FancyBboxPatch(
        (x - largeur / 2, y - hauteur / 2),
        largeur, hauteur,
        boxstyle="round,pad=0.01",
        facecolor=couleur, edgecolor="white",
        linewidth=1.5, zorder=2
    )
    ax.add_patch(rect)

    ax.text(x, y + 0.005, nom, ha="center", va="center",
            fontsize=8 if nom != "/" else 10,
            color="white", fontweight="bold", zorder=3)

    # Annotations ressources sous les feuilles
    if parent is not None and not any(p == nom for _, p, _ in noeuds):
        ax.text(x, y - 0.065, ressources,
                ha="center", va="top", fontsize=6.5,
                color="#444444", style="italic",
                multialignment="center")

# Légende
legende = [
    mpatches.Patch(color="#2c3e50", label="Racine cgroup"),
    mpatches.Patch(color="#2980b9", label="system.slice (services systemd)"),
    mpatches.Patch(color="#27ae60", label="user.slice (sessions utilisateurs)"),
    mpatches.Patch(color="#8e44ad", label="machine.slice (conteneurs/VMs)"),
]
ax.legend(handles=legende, loc="lower center", ncol=4, fontsize=8,
          bbox_to_anchor=(0.5, 0.0))

ax.set_title("Hiérarchie cgroups v2 — organisation des groupes de contrôle",
             fontsize=13, pad=10)
plt.show()
_images/db9d0f0944641c240200691d2e1b7f9e70413f31884b12284de824ec4efb790a.png

Rapport Lynis simulé — radar par domaine#


import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Scores lynis simulés par domaine (0-100)
domaines = [
    "Démarrage/init",
    "Noyau",
    "Mémoire",
    "Authentification",
    "Réseau",
    "Stockage",
    "Services",
    "Journalisation",
    "Sécurité fichiers",
    "Hardening logiciels",
]

scores_avant = [55, 62, 70, 48, 65, 72, 58, 45, 60, 40]
scores_apres = [80, 78, 82, 85, 88, 79, 75, 82, 87, 78]

N = len(domaines)
angles = [n / float(N) * 2 * np.pi for n in range(N)]
angles_closed = angles + [angles[0]]

scores_avant_closed = scores_avant + [scores_avant[0]]
scores_apres_closed = scores_apres + [scores_apres[0]]

fig, ax = plt.subplots(figsize=(9, 9), subplot_kw=dict(polar=True))

ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)

ax.plot(angles_closed, scores_avant_closed,
        "o-", linewidth=2, color="#e84c4c", label="Avant durcissement", markersize=5)
ax.fill(angles_closed, scores_avant_closed, alpha=0.20, color="#e84c4c")

ax.plot(angles_closed, scores_apres_closed,
        "o-", linewidth=2, color="#4c9be8", label="Après durcissement", markersize=5)
ax.fill(angles_closed, scores_apres_closed, alpha=0.20, color="#4c9be8")

ax.set_xticks(angles)
ax.set_xticklabels(domaines, size=9)
ax.set_ylim(0, 100)
ax.set_yticks([20, 40, 60, 80, 100])
ax.set_yticklabels(["20", "40", "60", "80", "100"], size=8, color="grey")
ax.grid(color="grey", linestyle="--", linewidth=0.5, alpha=0.7)

ax.set_title("Rapport Lynis — scores par domaine\n(simulation avant/après durcissement)",
             size=13, pad=25)
ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.1), fontsize=10)

# Annotations score moyen
moy_avant = np.mean(scores_avant)
moy_apres = np.mean(scores_apres)
fig.text(0.5, 0.02,
         f"Score moyen : {moy_avant:.0f}/100 → {moy_apres:.0f}/100  (+{moy_apres - moy_avant:.0f} points)",
         ha="center", fontsize=11, color="#333333",
         bbox=dict(boxstyle="round,pad=0.4", facecolor="#f0f4ff", edgecolor="#cccccc"))
plt.show()
_images/f77a8c3549fa2345898f362bcb381cdc6822cc55d696cf9f6381368050221a10.png

Matrice risque/effort des mesures de durcissement#


import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Mesures de durcissement : (nom, effort 1-5, réduction risque 1-5, priorité)
mesures = [
    ("Désactiver services\ninutiles",       1.5, 4.0, "haute"),
    ("Mettre à jour\nle système",           1.0, 5.0, "haute"),
    ("Configurer le\npare-feu",             2.0, 4.5, "haute"),
    ("SSH : clés uniquement",               1.5, 4.8, "haute"),
    ("AppArmor enforce",                    3.0, 3.5, "haute"),
    ("SELinux enforcing",                   4.5, 4.5, "haute"),
    ("auditd + règles",                     2.5, 3.0, "moyenne"),
    ("chattr +i fichiers\ncritiques",       1.5, 2.5, "moyenne"),
    ("umask 027",                           1.0, 1.5, "basse"),
    ("Capabilities\nau lieu de SUID",       3.5, 3.0, "moyenne"),
    ("ACL sur partages",                    2.0, 2.0, "basse"),
    ("LUKS partitions\nsensibles",          3.0, 4.0, "haute"),
    ("2FA sur SSH",                         2.5, 4.2, "haute"),
    ("Fail2ban",                            1.5, 3.5, "haute"),
    ("AIDE (intégrité\nfichiers)",          3.0, 3.8, "moyenne"),
    ("Namespaces\nisolation services",      4.0, 3.5, "moyenne"),
    ("cgroups v2 limites",                  2.5, 2.5, "basse"),
    ("Rotation clés\nSSH régulière",        2.0, 2.8, "moyenne"),
]

couleurs_priorite = {"haute": "#e84c4c", "moyenne": "#f0a040", "basse": "#4c9be8"}
tailles_priorite = {"haute": 180, "moyenne": 120, "basse": 80}

fig, ax = plt.subplots(figsize=(12, 7))

for nom, effort, risque, prio in mesures:
    ax.scatter(effort, risque,
               s=tailles_priorite[prio],
               color=couleurs_priorite[prio],
               alpha=0.85,
               edgecolors="white",
               linewidth=1.2,
               zorder=3)
    ax.annotate(nom, (effort, risque),
                textcoords="offset points",
                xytext=(7, 3),
                fontsize=7.5,
                color="#333333")

# Quadrants
ax.axvline(x=2.5, color="grey", linestyle="--", linewidth=0.8, alpha=0.6)
ax.axhline(y=3.0, color="grey", linestyle="--", linewidth=0.8, alpha=0.6)
ax.text(1.1, 4.7, "Wins rapides\n(effort faible, impact élevé)",
        fontsize=8.5, color="#2e7d32", style="italic",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#e8f5e9", alpha=0.7))
ax.text(3.2, 4.7, "Investissements\nstratégiques",
        fontsize=8.5, color="#c62828", style="italic",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#ffebee", alpha=0.7))
ax.text(1.1, 1.2, "Mesures\ncomplémentaires",
        fontsize=8.5, color="#1565c0", style="italic",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#e3f2fd", alpha=0.7))
ax.text(3.2, 1.2, "Effort élevé\nimpact limité",
        fontsize=8.5, color="#777777", style="italic",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#f5f5f5", alpha=0.7))

ax.set_xlabel("Effort d'implémentation (1 = minimal, 5 = élevé)", fontsize=10)
ax.set_ylabel("Réduction du risque (1 = faible, 5 = élevée)", fontsize=10)
ax.set_title("Matrice risque/effort — mesures de durcissement système", fontsize=13)
ax.set_xlim(0.5, 5.5)
ax.set_ylim(0.5, 5.5)

import matplotlib.patches as mpatches
legende = [
    mpatches.Patch(color="#e84c4c", label="Priorité haute"),
    mpatches.Patch(color="#f0a040", label="Priorité moyenne"),
    mpatches.Patch(color="#4c9be8", label="Priorité basse"),
]
ax.legend(handles=legende, fontsize=9, loc="lower right")
plt.show()
_images/3185719619c471d96cad82376eee3c504071efe9454630b13d5e23da3f53e383.png

Résumé#

Ce chapitre a présenté une approche structurée du durcissement système :

Principes directeurs

  • La surface d’attaque doit être réduite à ce qui est strictement nécessaire

  • Le principe du moindre privilège s’applique à chaque composant

  • La défense en profondeur superpose des couches de contrôle indépendantes

Réduction de la surface

  • Désactiver et masquer les services inutiles avec systemd

  • Supprimer les paquets non nécessaires

  • Lier les services à 127.0.0.1 quand l’accès externe n’est pas requis

MAC : AppArmor et SELinux

  • AppArmor confine les programmes par profil (enforce/complain) basé sur les chemins

  • SELinux utilise des contextes de sécurité sur tous les objets du système

  • Les deux systèmes bloquent les actions non autorisées même pour root

Isolation

  • Les namespaces Linux isolent différentes vues du système (PID, réseau, utilisateurs)

  • Les cgroups v2 limitent les ressources avec des directives systemd (MemoryMax, CPUQuota)

  • Le chroot est insuffisant seul ; les conteneurs combinent chroot + namespaces + cgroups

Surveillance

  • auditd offre une traçabilité granulaire des appels système et accès aux fichiers

  • ausearch et aureport permettent d’exploiter les logs d’audit

Outils d’évaluation

  • Lynis fournit un score de durcissement par domaine avec des recommandations concrètes

  • AIDE détecte les modifications non autorisées de fichiers par vérification d’intégrité

  • Les CIS Benchmarks constituent la référence pour des configurations sécurisées standardisées