Hardening as Code d'une VM Ubuntu avec Ansible¶
Objectifs pédagogiques¶
À la fin de ce TP, vous devez être capables de :
- mesurer l'état de durcissement initial d'une machine et le conserver comme preuve
- écrire un durcissement en rôles Ansible idempotents, alignés sur le CIS Benchmark Ubuntu 24.04 et le guide ANSSI BP-028
- démontrer puis corriger le contournement d'UFW par Docker
- prouver l'idempotence et détecter une dérive de configuration sans modifier la machine
- contrôler la qualité du code Ansible dans la CI
- documenter chaque recommandation non appliquée dans un registre des dérogations
Prérequis¶
- La VM
securepay-prod(Ubuntu 24.04, Multipass) provisionnée par cloud-init, avec un utilisateur d'administration nominatif (iciopsadmin) qui se connecte par clé SSH - Docker et le cluster k3d
securepaydéjà installés sur la VM - Ansible (
ansible-core) installé sur votre poste (via WSL2 sous Windows) - Le dépôt
securepay-platformcloné localement : tout le code du TP va dansansible/
Ne vous enfermez pas dehors
Durcir SSH et le pare-feu à distance peut vous couper l'accès. Avant de commencer :
- prenez un instantané de la VM :
multipass stop securepay-prod && multipass snapshot securepay-prod --name avant-hardening && multipass start securepay-prod - gardez une session
multipass shell securepay-prodouverte pendant tout le TP - n'activez le refus par défaut du pare-feu qu'après avoir autorisé SSH
multipass shell se connecte lui aussi en SSH, avec le compte ubuntu : votre durcissement doit le préserver.
Contexte¶
L'audit PSSI de la plateforme SecurePay a relevé une VM de production configurée « à la main », sans référentiel, avec un accès SSH par mot de passe et aucun journal des actions d'administration. Vous devez livrer un durcissement reproductible, relu en Merge Request, dont l'effet est mesuré avant/après.
Toutes les preuves produites vont dans docs/preuves/, nommées AAAA-MM-JJ_HHMM_<scenario>.<ext>.
Atelier 1 : État des lieux¶
Travail demandé¶
- Installez Lynis sur la VM et lancez un audit complet :
multipass shell securepay-prod
sudo apt-get update && sudo apt-get install -y lynis
sudo lynis audit system --no-colors | tee /tmp/lynis-avant.txt
sudo grep -E '^hardening_index=' /var/log/lynis-report.dat
- Rapatriez le rapport sur votre poste comme preuve :
multipass transfer securepay-prod:/tmp/lynis-avant.txt "docs/preuves/$(date +%F_%H%M)_lynis-avant.txt"
- Relevez la configuration SSH effective et les ports en écoute :
Ce que vous devez observer¶
- un indice de durcissement de référence, des warnings et une longue liste de suggestions
- des ports en écoute publiés par Docker et k3d (load balancer, API Kubernetes)
Critère de réussite¶
Le rapport initial horodaté est versionné dans docs/preuves/ et les cinq constats les plus critiques sont listés dans votre Merge Request.
Atelier 2 : Structurer le projet Ansible¶
Travail demandé¶
- Créez la structure suivante dans
securepay-platform/ansible/:
ansible/
├── ansible.cfg
├── inventory.ini
├── requirements.yml
├── hardening.yml
├── group_vars/
│ └── all.yml
└── roles/
├── ssh/ accounts/ firewall/ kernel/ audit/ updates/ services/
- Renseignez l'inventaire et les collections :
- Écrivez
hardening.ymlqui applique les sept rôles avec un tag par rôle (voir le support de cours), puis vérifiez la connexion :
cd ansible
ansible-galaxy collection install -r requirements.yml
ansible prod -m ansible.builtin.ping
- Centralisez les variables dans
group_vars/all.yml:admin_cidr(réseau de votre poste),external_iface(interface externe de la VM, voirip -br a),ssh_allowed_users,kernel_sysctls.
Critère de réussite¶
ansible prod -m ansible.builtin.ping répond pong et le squelette est poussé dans une branche de travail.
Atelier 3 : SSH et comptes¶
Travail demandé¶
-
Rôle
ssh: déposez/etc/ssh/sshd_config.d/10-hardening.confparansible.builtin.template, avecvalidate: /usr/sbin/sshd -t -f %set un handler qui recharge le servicessh. Contenu minimal : connexion root interdite, authentification par clé uniquement,AllowUserslimité àopsadminetubuntu,MaxAuthTries,LoginGraceTime, redirections désactivées, bannière légale. -
Rôle
accounts:sudojournalisé dans/var/log/sudo.log(fichier dans/etc/sudoers.d/, validé parvisudo -cf %s)umask 027par défaut- liste des membres du groupe
dockervérifiée et justifiée - si
opsadmindispose deNOPASSWD(pratique pour Ansible sans mot de passe), portez-le au registre des dérogations
-
Appliquez uniquement ces rôles :
- Vérifiez depuis votre poste :
# Doit échouer : connexion root
ssh root@<IP de la VM>
# Doit échouer : authentification par mot de passe
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password opsadmin@<IP de la VM>
# Doit réussir
ssh opsadmin@<IP de la VM> 'sudo sshd -T | grep -Ei "permitrootlogin|passwordauthentication"'
# Doit toujours réussir
multipass shell securepay-prod
Ce que vous devez observer¶
Permission denied (publickey)pour les deux premières tentativespermitrootlogin noetpasswordauthentication nodans la configuration effective, même si50-cloud-init.confcontient une autre valeur
Critère de réussite¶
Les refus de connexion sont capturés dans docs/preuves/ et multipass shell fonctionne toujours.
Atelier 4 : Pare-feu et piège Docker¶
Partie A : Démontrer le contournement¶
-
Rôle
firewall: autorisez SSH depuisadmin_cidr, puis passez UFW en refus par défaut et activez-le (voir le support de cours pour l'ordre des tâches). -
Appliquez le rôle, puis lancez un conteneur de test sur la VM :
ansible-playbook hardening.yml --tags firewall
multipass exec securepay-prod -- sudo docker run -d --name ufw-test -p 8080:80 nginx:<version>
curl -sS -m 5 -o /dev/null -w '%{http_code}\n' http://<IP de la VM>:8080
Ce que vous devez observer¶
Le port 8080 répond 200 alors qu'UFW refuse tout trafic entrant non autorisé : Docker a publié le port dans la chaîne FORWARD, que les règles d'entrée d'UFW ne voient pas.
Partie B : Corriger avec DOCKER-USER¶
- Ajoutez au rôle
firewallune tâcheansible.builtin.blockinfilequi ajoute à la fin de/etc/ufw/after.rules:
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
-A DOCKER-USER -i {{ external_iface }} -s {{ admin_cidr }} -j RETURN
-A DOCKER-USER -i {{ external_iface }} -j DROP
-A DOCKER-USER -j RETURN
COMMIT
Le handler associé exécute ufw reload.
- Rejouez le rôle et testez depuis votre poste, puis depuis une autre machine du réseau (ou en retirant temporairement votre poste de
admin_cidr) :
ansible-playbook hardening.yml --tags firewall
curl -sS -m 5 -o /dev/null -w '%{http_code}\n' http://<IP de la VM>:8080
kubectl get nodes
- Supprimez le conteneur de test :
multipass exec securepay-prod -- sudo docker rm -f ufw-test
Le cluster doit continuer de fonctionner
Le trafic entre les nœuds k3d passe par les bridges Docker, pas par l'interface externe : il n'est pas filtré par ces règles. Vérifiez que kubectl get pods -A et l'application répondent toujours depuis votre poste.
Critère de réussite¶
Le contournement est prouvé (avant), puis bloqué pour toute source extérieure à admin_cidr (après), sans perte d'accès au cluster depuis votre poste.
Atelier 5 : Noyau, journalisation, mises à jour et services¶
Travail demandé¶
-
Rôle
kernel: appliquezkernel_sysctlsavecansible.posix.sysctldans/etc/sysctl.d/60-hardening.conf, avecsysctl_set: truepour contrôler la valeur active et pas seulement le fichier. Conserveznet.ipv4.ip_forward: 1avec un commentaire qui référence la dérogationDER-01. -
Rôle
audit: installezauditd, déposez/etc/audit/rules.d/50-securepay.rules(identité, sudoers, configuration SSH, binaire et configuration Docker) et rechargez avecaugenrules --loaddans un handler. Vérifiez :
sudo auditctl -l
sudo touch /etc/sudoers.d/test && sudo rm /etc/sudoers.d/test
sudo ausearch -k scope --start recent
-
Rôle
updates: installezunattended-upgradeset déposez/etc/apt/apt.conf.d/20auto-upgradesqui active la mise à jour quotidienne des listes et des correctifs de sécurité. -
Rôle
services: listez les services actifs, désactivez ceux qui n'ont pas de raison d'être sur ce serveur et justifiez chaque choix dans la Merge Request. Vérifiez qu'AppArmor est actif :
- Après application, vérifiez que la plateforme fonctionne toujours :
kubectl get nodeset un appel à l'API SecurePay.
Critère de réussite¶
Les quatre rôles s'appliquent sans erreur, une action sensible apparaît dans ausearch et le cluster reste opérationnel.
Atelier 6 : Idempotence et dérive¶
Travail demandé¶
- Jouez le playbook complet deux fois de suite :
ansible-playbook hardening.yml
ansible-playbook hardening.yml | tee "../docs/preuves/$(date +%F_%H%M)_ansible-idempotence.txt"
- Simulez une dérive sur la VM, comme le ferait un administrateur pressé :
multipass exec securepay-prod -- sudo sed -i 's/^MaxAuthTries 3/MaxAuthTries 10/' /etc/ssh/sshd_config.d/10-hardening.conf
multipass exec securepay-prod -- sudo sysctl -w net.ipv4.conf.all.accept_redirects=1
- Détectez la dérive sans rien modifier, puis remédiez :
ansible-playbook hardening.yml --check --diff | tee "../docs/preuves/$(date +%F_%H%M)_ansible-derive.txt"
ansible-playbook hardening.yml
Ce que vous devez observer¶
- la deuxième exécution affiche
changed=0: si ce n'est pas le cas, une tâche n'est pas idempotente et doit être corrigée --check --diffsignale exactement les deux écarts introduits, avec le différentiel du fichier SSH- la réexécution normale restaure l'état déclaré
Critère de réussite¶
Les preuves d'idempotence et de détection de dérive sont versionnées.
Atelier 7 : ansible-lint dans la CI¶
Travail demandé¶
-
Ajoutez le job
ansible:lintprésenté dans le support de cours au.gitlab-ci.ymldesecurepay-platform, avec une version d'ansible-lint épinglée. -
Poussez votre branche et ouvrez une Merge Request.
-
Introduisez volontairement un défaut (module non qualifié comme
copy:au lieu deansible.builtin.copy:, ou unshellqui pourrait être un module), vérifiez que le job échoue, puis corrigez.
Critère de réussite¶
Le job passe au vert sur la Merge Request finale et un échec volontaire a été observé.
Atelier 8 : Audit final et registre des dérogations¶
Travail demandé¶
-
Relancez Lynis avec la même commande qu'à l'atelier 1 et rapatriez le rapport
lynis-apres. -
Comparez les deux audits : indice de durcissement, warnings résolus, suggestions restantes.
-
Pour chaque suggestion restante, choisissez : correction dans un rôle, ou dérogation dans
docs/conformite.md:
| ID | Recommandation | Justification | Mesure compensatoire | Validée par | Revue |
|---|---|---|---|---|---|
| DER-01 | net.ipv4.ip_forward = 0 | Requis par Docker et k3d | Filtrage DOCKER-USER, NetworkPolicies |
-
Optionnel : évaluez la VM avec OpenSCAP et le profil CIS niveau 1 serveur du contenu ComplianceAsCode, et versionnez le rapport HTML.
-
Optionnel : appliquez le rôle d'audit
UBUNTU24-CIS-Auditd'ansible-lockdown et comparez sa couverture avec la vôtre.
Ce que vous devez observer¶
- une hausse nette de l'indice de durcissement, sans atteindre 100
- des recommandations qui ne s'appliquent pas à une machine qui héberge des conteneurs
Critère de réussite¶
Le delta avant/après est documenté, et chaque écart restant est soit corrigé, soit porté au registre avec une justification et une mesure compensatoire.
Questions de synthèse¶
- Pourquoi l'idempotence est-elle une condition nécessaire pour utiliser
--check --diffcomme contrôle de conformité - Quelle différence faites-vous entre un écart au benchmark et une dérogation
- Pourquoi l'appartenance au groupe
dockerdoit-elle être traitée comme un accès root - Que se passerait-il si vous appliquiez le rôle
UBUNTU24-CIScomplet sans lecture préalable sur cette VM - Un indice Lynis de 85 prouve-t-il que la machine est sûre ? Que manque-t-il pour l'affirmer