Aller au contenu

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 (ici opsadmin) qui se connecte par clé SSH
  • Docker et le cluster k3d securepay déjà installés sur la VM
  • Ansible (ansible-core) installé sur votre poste (via WSL2 sous Windows)
  • Le dépôt securepay-platform cloné localement : tout le code du TP va dans ansible/

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-prod ouverte 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é

  1. 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
  1. 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"
  1. Relevez la configuration SSH effective et les ports en écoute :
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|allowusers'
sudo ss -tlnp

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é

  1. 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/
  1. Renseignez l'inventaire et les collections :
# inventory.ini
[prod]
securepay-prod ansible_host=<IP de la VM> ansible_user=opsadmin
# requirements.yml
collections:
  - name: community.general
  - name: ansible.posix
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = roles
  1. Écrivez hardening.yml qui 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
  1. Centralisez les variables dans group_vars/all.yml : admin_cidr (réseau de votre poste), external_iface (interface externe de la VM, voir ip -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é

  1. Rôle ssh : déposez /etc/ssh/sshd_config.d/10-hardening.conf par ansible.builtin.template, avec validate: /usr/sbin/sshd -t -f %s et un handler qui recharge le service ssh. Contenu minimal : connexion root interdite, authentification par clé uniquement, AllowUsers limité à opsadmin et ubuntu, MaxAuthTries, LoginGraceTime, redirections désactivées, bannière légale.

  2. Rôle accounts :

    • sudo journalisé dans /var/log/sudo.log (fichier dans /etc/sudoers.d/, validé par visudo -cf %s)
    • umask 027 par défaut
    • liste des membres du groupe docker vérifiée et justifiée
    • si opsadmin dispose de NOPASSWD (pratique pour Ansible sans mot de passe), portez-le au registre des dérogations
  3. Appliquez uniquement ces rôles :

ansible-playbook hardening.yml --tags ssh,accounts
  1. 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 tentatives
  • permitrootlogin no et passwordauthentication no dans la configuration effective, même si 50-cloud-init.conf contient 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

  1. Rôle firewall : autorisez SSH depuis admin_cidr, puis passez UFW en refus par défaut et activez-le (voir le support de cours pour l'ordre des tâches).

  2. 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

  1. Ajoutez au rôle firewall une tâche ansible.builtin.blockinfile qui 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.

  1. 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
  1. 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é

  1. Rôle kernel : appliquez kernel_sysctls avec ansible.posix.sysctl dans /etc/sysctl.d/60-hardening.conf, avec sysctl_set: true pour contrôler la valeur active et pas seulement le fichier. Conservez net.ipv4.ip_forward: 1 avec un commentaire qui référence la dérogation DER-01.

  2. Rôle audit : installez auditd, déposez /etc/audit/rules.d/50-securepay.rules (identité, sudoers, configuration SSH, binaire et configuration Docker) et rechargez avec augenrules --load dans 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
  1. Rôle updates : installez unattended-upgrades et déposez /etc/apt/apt.conf.d/20auto-upgrades qui active la mise à jour quotidienne des listes et des correctifs de sécurité.

  2. 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 :

systemctl list-unit-files --state=enabled --type=service
sudo aa-status | head -5
  1. Après application, vérifiez que la plateforme fonctionne toujours : kubectl get nodes et 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é

  1. 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"
  1. 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
  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 --diff signale 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é

  1. Ajoutez le job ansible:lint présenté dans le support de cours au .gitlab-ci.yml de securepay-platform, avec une version d'ansible-lint épinglée.

  2. Poussez votre branche et ouvrez une Merge Request.

  3. Introduisez volontairement un défaut (module non qualifié comme copy: au lieu de ansible.builtin.copy:, ou un shell qui 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é

  1. Relancez Lynis avec la même commande qu'à l'atelier 1 et rapatriez le rapport lynis-apres.

  2. Comparez les deux audits : indice de durcissement, warnings résolus, suggestions restantes.

  3. 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
  1. Optionnel : évaluez la VM avec OpenSCAP et le profil CIS niveau 1 serveur du contenu ComplianceAsCode, et versionnez le rapport HTML.

  2. Optionnel : appliquez le rôle d'audit UBUNTU24-CIS-Audit d'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 --diff comme contrôle de conformité
  • Quelle différence faites-vous entre un écart au benchmark et une dérogation
  • Pourquoi l'appartenance au groupe docker doit-elle être traitée comme un accès root
  • Que se passerait-il si vous appliquiez le rôle UBUNTU24-CIS complet 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