Hardening as Code : CIS, ANSSI & Ansible
Objectifs pédagogiques¶
- Choisir un référentiel de durcissement (CIS, ANSSI) et le relier à la PSSI
- Écrire un durcissement idempotent et revu sous forme de rôles Ansible
- Détecter la dérive de configuration sans modifier la machine
- Mesurer le durcissement avant/après avec Lynis et OpenSCAP
- Gérer les écarts au référentiel par des dérogations justifiées
- Éviter les pièges réels : Docker qui contourne le pare-feu, forwarding IP
Un serveur Linux « par défaut » n'est pas prêt pour la production¶
Une image d'installation vise la compatibilité, pas la sécurité :
- Authentification SSH par mot de passe souvent possible
- Services démarrés dont personne n'a besoin
- Paramètres noyau permissifs (redirections ICMP, routage source)
- Aucune journalisation des actions sensibles (comptes, sudo, SSH)
- Mises à jour de sécurité appliquées « quand on y pense »
- Aucune trace de ce qui a été modifié à la main depuis l'installation
Durcir, c'est quoi exactement¶
Le durcissement (hardening) réduit la surface d'attaque d'un système :
- Réduire : supprimer ou désactiver ce qui n'est pas nécessaire
- Restreindre : moindre privilège pour les comptes, les services et le réseau
- Tracer : journaliser les actions sensibles pour l'investigation
- Maintenir : appliquer les correctifs et empêcher la dérive
Le durcissement complète la gestion des vulnérabilités, il ne la remplace pas : une machine durcie mais jamais mise à jour reste exposée
Les référentiels¶
CIS Benchmarks¶
Publiés par le Center for Internet Security, rédigés par consensus d'experts, un benchmark par OS ou technologie (Ubuntu 24.04, Docker, Kubernetes…)
- Recommandations numérotées, chacune avec justification, audit et remédiation
- Automated : vérifiable par un outil (ex : valeur d'un paramètre
sysctl) - Manual : demande un jugement humain (ex : revue des comptes)
- Profils : Level 1 et Level 2, déclinés en Server et Workstation
Niveau 1 ou niveau 2¶
| Level 1 | Level 2 | |
|---|---|---|
| Objectif | Socle de sécurité largement applicable | Défense en profondeur |
| Impact fonctionnel | Faible | Peut restreindre des usages ou la performance |
| Cible | Toute machine de production | Environnements sensibles (paiement, santé…) |
| Relation | Base | Étend le niveau 1, ne le remplace pas |
Démarche recommandée : appliquer le niveau 1, puis sélectionner les mesures de niveau 2 justifiées par l'analyse de risques
Guide ANSSI BP-028 et autres sources¶
ANSSI — Recommandations de configuration d'un système GNU/Linux (BP-028)
- Recommandations classées par niveau : Minimal, Intermédiaire, Renforcé, Élevé
- Couvre le partitionnement, le noyau, les comptes, les services, la journalisation
- Référence attendue dans un contexte français ou réglementé
Compléments selon la couche
- CIS Docker Benchmark et CIS Kubernetes Benchmark pour la plateforme
- OWASP (Docker Security Cheat Sheet, ASVS) pour les conteneurs et l'application
- DISA STIG pour les environnements défense ou administration
Du référentiel à la PSSI¶
La PSSI fixe des exigences, le référentiel fournit les mesures, le code les applique, l'audit les prouve
| Exigence PSSI | Mesure | Référence | Contrôle | Preuve |
|---|---|---|---|---|
| Accès d'administration nominatif et fort | SSH par clé, root interdit | CIS (SSH), ANSSI | Rôle ssh | sshd -T + Lynis |
| Traçabilité des actions sensibles | Règles auditd | CIS (auditd), ANSSI | Rôle audit | auditctl -l |
| Correctifs de sécurité sous 24 h | Mises à jour automatiques | CIS (paquets) | Rôle updates | Journal APT |
Citez toujours le numéro exact de la recommandation et la version du benchmark utilisée : la numérotation change entre versions
Hardening as Code avec Ansible¶
Pourquoi « as code »¶
| Durcissement manuel ou script shell | Hardening as Code |
|---|---|
| Dépend de la mémoire de l'administrateur | Décrit dans un dépôt versionné |
| Non relu | Revu en Merge Request |
| Rejoué avec effets de bord | Idempotent : rejouable sans risque |
| Dérive invisible | Dérive détectée par simulation |
| Une machine à la fois | N machines identiques |
Le dépôt devient lui-même une preuve d'audit : qui a changé quelle mesure, quand et pourquoi
Structure du projet¶
ansible/
├── ansible.cfg
├── inventory.ini
├── requirements.yml # collections (community.general, ansible.posix)
├── hardening.yml # playbook principal
├── group_vars/
│ └── all.yml # variables + dérogations documentées
└── roles/
├── ssh/ # tasks/, handlers/, templates/, defaults/
├── accounts/
├── firewall/
├── kernel/
├── audit/
├── updates/
└── services/
Un rôle par domaine : chaque Merge Request reste lisible et chaque mesure retrouve son rôle
Le playbook principal¶
# ansible/hardening.yml
- name: Durcissement de la VM securepay-prod
hosts: prod
become: true
roles:
- role: ssh
tags: [ssh]
- role: accounts
tags: [accounts]
- role: firewall
tags: [firewall]
- role: kernel
tags: [kernel]
- role: audit
tags: [audit]
- role: updates
tags: [updates]
- role: services
tags: [services]
Les tags permettent de rejouer un seul domaine : ansible-playbook hardening.yml --tags ssh
Un rôle : tâche, validation et handler¶
# roles/ssh/tasks/main.yml
- name: Déposer la configuration SSH durcie
ansible.builtin.template:
src: 10-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/10-hardening.conf
owner: root
group: root
mode: "0600"
validate: /usr/sbin/sshd -t -f %s
notify: Recharger SSH
# roles/ssh/handlers/main.yml
- name: Recharger SSH
ansible.builtin.systemd_service:
name: ssh
state: reloaded
validate refuse un fichier invalide avant qu'il ne remplace l'existant : on ne se coupe pas l'accès
Idempotence¶
Un playbook idempotent amène la machine dans l'état décrit, puis ne change plus rien
- Privilégier les modules déclaratifs (
template,sysctl,ufw,apt) àcommandetshell - Si
commandest inévitable :creates:,changed_when:etcheck_mode: falsepour une lecture seule - Les handlers ne s'exécutent que si une tâche a réellement changé quelque chose
Test d'acceptation : la deuxième exécution consécutive doit afficher changed=0
PLAY RECAP *********************************************************
securepay-prod : ok=42 changed=0 unreachable=0 failed=0 skipped=0
Détecter la dérive sans rien modifier¶
--checksimule chaque tâche sans appliquer de changement--diffaffiche l'écart entre l'état réel et l'état déclaré- Toute tâche qui serait
changedrévèle une dérive (modification manuelle, paquet, mise à jour)
Limites à connaître :
- Les tâches
commandetshellsont ignorées en mode check saufcheck_mode: false - Une tâche qui dépend du résultat d'une précédente peut échouer en simulation
Planifié (CI nocturne ou cron), ce contrôle devient une surveillance continue de conformité
Qualité du code : ansible-lint en CI¶
# .gitlab-ci.yml de securepay-platform
ansible:lint:
stage: validate
image: python:3.12-slim # épinglez une version vérifiée
variables:
ANSIBLE_LINT_VERSION: "<version>"
script:
- pip install --no-cache-dir "ansible-lint==${ANSIBLE_LINT_VERSION}"
- cd ansible
- ansible-galaxy collection install -r requirements.yml
- ansible-lint --profile moderate hardening.yml
rules:
- changes: [ansible/**/*]
ansible-lint détecte les modules non qualifiés (FQCN), les shell évitables, les permissions implicites et les tâches non idempotentes ; montez ensuite vers le profil production
Rôle communautaire ou rôle maison¶
Rôle communautaire (ansible-lockdown UBUNTU24-CIS) | Rôle maison | |
|---|---|---|
| Couverture | Quasi complète du benchmark | Limitée aux mesures choisies |
| Maîtrise | Des centaines de tâches à comprendre | Totale |
| Risque | Peut casser un service (Docker, Kubernetes) | Plus faible, périmètre réduit |
| Cible | Installation propre, à appliquer tôt | Machine existante |
| Pilotage | Une variable par recommandation | Variables définies par l'équipe |
Démarche réaliste : auditer d'abord (rôle d'audit UBUNTU24-CIS-Audit, Lynis), activer ensuite les règles par lots, désactiver explicitement et justifier celles qui cassent le service
Les domaines du durcissement¶
SSH¶
# /etc/ssh/sshd_config.d/10-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey
AllowUsers opsadmin ubuntu # ubuntu : compte utilisé par `multipass shell`
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
Banner /etc/issue.net
Ubuntu lit sshd_config.d/*.conf en premier et sshd retient la première valeur : 10-hardening.conf l'emporte sur 50-cloud-init.conf
Vérifier la configuration effective : sudo sshd -T | grep -Ei 'permitroot|password'
Comptes et privilèges¶
- Un compte nominatif par administrateur, aucune connexion directe en root
sudojournalisé (Defaults logfile=/var/log/sudo.log), sansNOPASSWDgénéralisé- Comptes système sans shell (
/usr/sbin/nologin), comptes inutilisés verrouillés umask 027par défaut, permissions strictes sur/etc/shadow,/etc/sudoers.d/- Appartenance au groupe
docker= équivalent root : à réserver aux administrateurs
Pare-feu¶
# roles/firewall/tasks/main.yml (extrait)
- name: Autoriser SSH depuis le réseau d'administration
community.general.ufw:
rule: allow
port: "22"
proto: tcp
src: "{{ admin_cidr }}"
- name: Refuser par défaut le trafic entrant
community.general.ufw:
direction: incoming
default: deny
- name: Activer UFW
community.general.ufw:
state: enabled
logging: "on"
Ordre impératif : ouvrir SSH avant d'activer le refus par défaut
Paramètres noyau¶
# group_vars/all.yml (extrait)
kernel_sysctls:
kernel.randomize_va_space: 2
kernel.kptr_restrict: 2
kernel.dmesg_restrict: 1
fs.suid_dumpable: 0
fs.protected_symlinks: 1
fs.protected_hardlinks: 1
net.ipv4.conf.all.accept_redirects: 0
net.ipv4.conf.all.send_redirects: 0
net.ipv4.conf.all.accept_source_route: 0
net.ipv4.conf.all.log_martians: 1
net.ipv4.tcp_syncookies: 1
net.ipv4.icmp_echo_ignore_broadcasts: 1
# DER-01 : requis par Docker et Kubernetes (le CIS recommande 0)
net.ipv4.ip_forward: 1
Appliqués par ansible.posix.sysctl dans un fichier dédié de /etc/sysctl.d/
Journalisation avec auditd¶
# /etc/audit/rules.d/50-securepay.rules
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/sudoers -p wa -k scope
-w /etc/sudoers.d/ -p wa -k scope
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
-w /usr/bin/dockerd -p x -k docker
-w /etc/docker/ -p wa -k docker
- Chargement :
augenrules --load(handler), vérification :auditctl -l - Investigation :
ausearch -k identity --start today - Option niveau 2 :
-e 2rend la configuration immuable jusqu'au prochain redémarrage
Mises à jour, AppArmor et services¶
- unattended-upgrades : correctifs de sécurité appliqués chaque jour (
/etc/apt/apt.conf.d/20auto-upgrades) - Docker installé depuis son propre dépôt n'est pas couvert par défaut : planifiez ses mises à jour, elles redémarrent le démon et le cluster
- AppArmor : actif par défaut sur Ubuntu, Docker applique le profil
docker-defaultaux conteneurs (sudo aa-status) - Services :
systemctl list-unit-files --state=enabled, chaque service actif doit avoir une raison d'exister - Permissions : fichiers de configuration sensibles en
0600ou0640, propriétaire root
Les pièges du monde réel¶
Piège 1 : Docker contourne UFW¶
Après ufw default deny incoming, un conteneur lancé avec -p 8080:80 reste joignable depuis l'extérieur
- Le trafic vers un port publié est traduit (DNAT) puis traité dans la chaîne
FORWARD, pasINPUT - Docker insère ses propres règles dans
FORWARD, les règles d'entrée d'UFW ne s'appliquent pas - Même chose pour les ports publiés par k3d : load balancer 80/443 et API Kubernetes
Correctifs possibles :
- Publier sur une adresse précise (
-p 127.0.0.1:8080:80) quand l'accès externe est inutile - Filtrer dans la chaîne
DOCKER-USER, évaluée avant les règles de Docker et jamais modifiée par lui
Filtrer dans DOCKER-USER¶
# Ajouté à la fin de /etc/ufw/after.rules (géré par Ansible, blockinfile)
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
-A DOCKER-USER -i enp0s1 -s 192.168.64.0/24 -j RETURN
-A DOCKER-USER -i enp0s1 -j DROP
-A DOCKER-USER -j RETURN
COMMIT
enp0s1: interface externe de la VM,192.168.64.0/24: réseau du poste d'administration (à adapter,ip -br a)- Le trafic entre conteneurs et nœuds k3d passe par les bridges Docker : il n'est pas concerné
- UFW charge ce bloc au démarrage, avant Docker, qui ne fait qu'ajouter son saut vers
DOCKER-USER
Piège 2 : le benchmark contre le besoin¶
Le CIS recommande net.ipv4.ip_forward = 0 : une machine qui n'est pas un routeur ne doit pas router
- Docker et Kubernetes ont besoin du forwarding pour faire transiter le trafic des conteneurs
- Appliquer la règle casse le cluster, ignorer le benchmark entier supprime toute base de conformité
La bonne réponse est une dérogation :
- Justifiée par une exigence technique
- Validée par le responsable sécurité
- Compensée par d'autres mesures (filtrage
DOCKER-USER, NetworkPolicies) - Tracée dans le code et dans la matrice de conformité, revue périodiquement
Registre des dérogations¶
| ID | Recommandation | Justification | Mesure compensatoire | Validée par | Revue |
|---|---|---|---|---|---|
| DER-01 | ip_forward = 0 | Requis par Docker et k3d | DOCKER-USER, NetworkPolicies | RSSI | Semestrielle |
| DER-02 | AllowUsers limité au nominatif | ubuntu requis par multipass shell | Clé uniquement, accès local au poste | RSSI | Semestrielle |
| DER-03 | Partition /tmp séparée | Image cloud sans partitionnement dédié | Options noexec étudiées, AppArmor | Lead SRE | Annuelle |
Une recommandation non appliquée sans dérogation est un écart, pas un choix
Auditer et prouver¶
Lynis¶
sudo apt-get install -y lynis
sudo lynis audit system --no-colors | tee /tmp/lynis-$(date +%F_%H%M).txt
# Score et constats détaillés
sudo grep -E '^hardening_index=' /var/log/lynis-report.dat
sudo grep -E '^(warning|suggestion)\[\]=' /var/log/lynis-report.dat | head
- Hardening index : indicateur de 0 à 100, utile pour comparer avant/après
- Warnings : à traiter en priorité
- Suggestions : à appliquer ou à justifier dans le registre des dérogations
OpenSCAP et ComplianceAsCode¶
sudo apt-get install -y openscap-scanner
# Contenu SCAP du projet ComplianceAsCode (release scap-security-guide)
oscap info ssg-ubuntu2404-ds.xml # liste des profils disponibles
sudo oscap xccdf eval \
--profile xccdf_org.ssgproject.content_profile_cis_level1_server \
--report /tmp/openscap-cis-l1.html \
ssg-ubuntu2404-ds.xml
- Évalue la machine contre un profil (CIS niveau 1 serveur) règle par règle
- Rapport HTML exploitable directement comme preuve d'audit
- Vérifiez l'identifiant exact du profil avec
oscap info: il dépend de la version du contenu
Prouver le durcissement¶
- Avant : audit initial horodaté, conservé tel quel
- Après : même outil, même profil, même machine
- Delta : score, warnings résolus, recommandations restantes
- Écarts : chaque recommandation restante est corrigée ou portée au registre des dérogations
Un score n'est pas une garantie : un indice Lynis élevé ne dit rien d'une CVE applicative ni d'un secret exposé
À retenir¶
- Un référentiel (CIS, ANSSI) donne les mesures, la PSSI dit lesquelles sont exigées
- Hardening as Code : versionné, revu, idempotent (
changed=0), dérive visible avec--check --diff - Ouvrir SSH avant le refus par défaut, valider
sshdavant de recharger - Docker contourne UFW : filtrez dans
DOCKER-USER - Toute recommandation écartée devient une dérogation justifiée, compensée et revue
- Mesurez avant/après, conservez les preuves horodatées
Pour aller plus loin¶
- CIS Benchmarks : https://www.cisecurity.org/cis-benchmarks
- Guide ANSSI BP-028 (v2.0) : https://cyber.gouv.fr/publications, « Recommandations de configuration d'un système GNU/Linux »
- ansible-lockdown : https://github.com/ansible-lockdown
- Lynis : https://cisofy.com/lynis/