La configuration manuelle de serveurs Linux est une tâche répétitive, sujette aux erreurs et difficile à reproduire. Lorsqu'on gère une dizaine, voire des centaines de machines, il devient impératif d'automatiser ce processus. Ansible s'impose comme l'outil idéal pour cette mission : agentless, idempotent et lisible, il permet de décrire l'état souhaité de vos serveurs dans des playbooks YAML clairs et versionnables.
Dans cet article, nous allons construire un playbook complet de hardening et de configuration initiale pour un serveur Linux (Debian/Ubuntu). Ce playbook couvre la gestion des utilisateurs, le durcissement SSH, le pare-feu, fail2ban, la synchronisation horaire, la rotation des logs, le monitoring et les sauvegardes automatiques. À la fin, vous disposerez d'un playbook prêt à l'emploi que vous pourrez adapter à votre infrastructure.
Architecture en un coup d'œil

Prérequis
Avant de commencer, assurez-vous de disposer des éléments suivants :
- Ansible 2.14+ installé sur votre machine de contrôle
- Un ou plusieurs serveurs cibles sous Debian 12 ou Ubuntu 22.04/24.04
- Un accès SSH root initial (ou un utilisateur avec sudo) pour le premier déploiement
- Les clés SSH publiques des administrateurs
Voici la structure de notre projet :
server-hardening/
├── inventory/
│ ├── hosts.yml
│ └── group_vars/
│ └── all.yml
├── roles/
│ ├── users/
│ ├── ssh-hardening/
│ ├── packages/
│ ├── firewall/
│ ├── fail2ban/
│ ├── ntp/
│ ├── logrotate/
│ ├── monitoring/
│ ├── backups/
│ └── auto-updates/
└── site.ymlConfiguration de l'inventaire
Commençons par définir notre inventaire et les variables globales du projet. L'inventaire décrit les serveurs cibles et les variables permettent de centraliser la configuration.
Fichier d'inventaire
# inventory/hosts.yml
all:
children:
webservers:
hosts:
web01:
ansible_host: 192.168.1.10
web02:
ansible_host: 192.168.1.11
dbservers:
hosts:
db01:
ansible_host: 192.168.1.20Variables globales
# inventory/group_vars/all.yml
---
# Utilisateurs à créer
admin_users:
- username: deployer
full_name: "Deploy User"
shell: /bin/bash
groups:
- sudo
- docker
ssh_public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... deployer@codeclan"
- username: sysadmin
full_name: "SysAdmin User"
shell: /bin/bash
groups:
- sudo
ssh_public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... sysadmin@codeclan"
# Configuration SSH
ssh_port: 2222
ssh_permit_root_login: "no"
ssh_password_authentication: "no"
ssh_max_auth_tries: 3
ssh_allowed_users: "deployer sysadmin"
# Packages essentiels
essential_packages:
- vim
- curl
- wget
- git
- htop
- tmux
- unzip
- jq
- tree
- ncdu
- iotop
- net-tools
- dnsutils
- apt-transport-https
- ca-certificates
- gnupg
- lsb-release
- software-properties-common
# Pare-feu
firewall_allowed_tcp_ports:
- "{{ ssh_port }}"
- "80"
- "443"
firewall_allowed_udp_ports: []
# Fail2ban
fail2ban_maxretry: 5
fail2ban_bantime: 3600
fail2ban_findtime: 600
# NTP
ntp_servers:
- 0.fr.pool.ntp.org
- 1.fr.pool.ntp.org
- 2.fr.pool.ntp.org
- 3.fr.pool.ntp.org
ntp_timezone: Europe/Paris
# Monitoring
node_exporter_version: "1.7.0"
node_exporter_port: 9100
# Backups
backup_directories:
- /etc
- /var/log
- /home
backup_destination: /var/backups/automated
backup_retention_days: 30
backup_schedule: "0 2 * * *"Gestion des utilisateurs
La première étape du hardening consiste à créer des utilisateurs dédiés avec des clés SSH et des permissions sudo appropriées. On ne devrait jamais se connecter directement en root sur un serveur de production.
Rôle users
# roles/users/tasks/main.yml
---
- name: Créer les groupes nécessaires
ansible.builtin.group:
name: "{{ item }}"
state: present
loop:
- sudo
- docker
- name: Créer les utilisateurs administrateurs
ansible.builtin.user:
name: "{{ item.username }}"
comment: "{{ item.full_name }}"
shell: "{{ item.shell | default('/bin/bash') }}"
groups: "{{ item.groups | join(',') }}"
append: yes
create_home: yes
state: present
loop: "{{ admin_users }}"
loop_control:
label: "{{ item.username }}"
- name: Déployer les clés SSH autorisées
ansible.posix.authorized_key:
user: "{{ item.username }}"
key: "{{ item.ssh_public_key }}"
state: present
exclusive: yes
loop: "{{ admin_users }}"
loop_control:
label: "{{ item.username }}"
- name: Configurer sudo sans mot de passe pour le groupe sudo
ansible.builtin.copy:
content: |
# Ansible managed - ne pas modifier manuellement
%sudo ALL=(ALL:ALL) NOPASSWD:ALL
dest: /etc/sudoers.d/90-ansible-sudo
mode: "0440"
owner: root
group: root
validate: "visudo -cf %s"
- name: Définir des mots de passe robustes pour les utilisateurs
ansible.builtin.user:
name: "{{ item.username }}"
password: "{{ item.username | password_hash('sha512', 'codeclan_salt') }}"
update_password: on_create
loop: "{{ admin_users }}"
loop_control:
label: "{{ item.username }}"
- name: Configurer le umask par défaut
ansible.builtin.lineinfile:
path: /etc/login.defs
regexp: "^UMASK"
line: "UMASK 027"
state: presentCe rôle crée chaque utilisateur, déploie sa clé SSH publique et configure sudo. L'option exclusive: yes sur les clés autorisées garantit que seules les clés définies dans l'inventaire sont présentes, supprimant toute clé non autorisée.
Durcissement SSH
Le service SSH est la porte d'entrée principale de votre serveur. Son durcissement est donc une priorité absolue. Nous allons désactiver l'authentification par mot de passe, interdire la connexion root, changer le port par défaut et limiter les tentatives de connexion.
Rôle ssh-hardening
# roles/ssh-hardening/tasks/main.yml
---
- name: Sauvegarder la configuration SSH originale
ansible.builtin.copy:
src: /etc/ssh/sshd_config
dest: /etc/ssh/sshd_config.backup
remote_src: yes
force: no
- name: Appliquer la configuration SSH durcie
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: "0600"
validate: "sshd -t -f %s"
notify: Redémarrer SSH
- name: S'assurer que le répertoire ssh config.d existe
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: "0755"
- name: Supprimer les fichiers de configuration par défaut non sécurisés
ansible.builtin.file:
path: "/etc/ssh/sshd_config.d/{{ item }}"
state: absent
loop:
- 50-cloud-init.conf
notify: Redémarrer SSH
- name: Générer des clés hôtes ED25519 si absentes
ansible.builtin.command:
cmd: ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
creates: /etc/ssh/ssh_host_ed25519_key
- name: Supprimer les clés hôtes DSA faibles
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/ssh/ssh_host_dsa_key
- /etc/ssh/ssh_host_dsa_key.pub
notify: Redémarrer SSHTemplate sshd_config
# roles/ssh-hardening/templates/sshd_config.j2
# Ansible managed - ne pas modifier manuellement
# Configuration SSH durcie pour {{ inventory_hostname }}
Port {{ ssh_port }}
AddressFamily inet
ListenAddress 0.0.0.0
# Authentification
PermitRootLogin {{ ssh_permit_root_login }}
PasswordAuthentication {{ ssh_password_authentication }}
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries {{ ssh_max_auth_tries }}
MaxSessions 3
# Restrictions d'accès
AllowUsers {{ ssh_allowed_users }}
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
# Sécurité
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
PermitUserEnvironment no
Banner /etc/ssh/banner
# Algorithmes cryptographiques forts
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
# Logging
LogLevel VERBOSE
SyslogFacility AUTH
# SFTP
Subsystem sftp /usr/lib/openssh/sftp-server -f AUTH -l INFOHandler pour redémarrer SSH
# roles/ssh-hardening/handlers/main.yml
---
- name: Redémarrer SSH
ansible.builtin.systemd:
name: sshd
state: restarted
enabled: yesAttention : Avant de changer le port SSH et de désactiver l'authentification par mot de passe, assurez-vous que votre clé SSH est bien déployée et que le nouveau port est ouvert dans le pare-feu. Sinon, vous risquez de vous verrouiller hors du serveur.
Installation des packages essentiels
Chaque serveur a besoin d'un ensemble d'outils de base pour l'administration quotidienne. Ce rôle installe ces packages et effectue une mise à jour complète du système.
Rôle packages
# roles/packages/tasks/main.yml
---
- name: Mettre à jour le cache APT
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
- name: Mettre à jour tous les packages existants
ansible.builtin.apt:
upgrade: safe
register: apt_upgrade
tags:
- upgrade
- name: Installer les packages essentiels
ansible.builtin.apt:
name: "{{ essential_packages }}"
state: present
- name: Supprimer les packages inutiles
ansible.builtin.apt:
name:
- telnet
- rsh-client
- rsh-server
state: absent
purge: yes
- name: Nettoyer le cache APT
ansible.builtin.apt:
autoclean: yes
autoremove: yes
- name: Vérifier si un redémarrage est nécessaire
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required
- name: Afficher un avertissement si un redémarrage est nécessaire
ansible.builtin.debug:
msg: "ATTENTION : Un redémarrage est nécessaire sur {{ inventory_hostname }}"
when: reboot_required.stat.existsConfiguration du pare-feu avec UFW
Le pare-feu est la première ligne de défense de votre serveur. UFW (Uncomplicated Firewall) offre une interface simplifiée pour iptables, parfaitement adaptée à une gestion via Ansible.
Rôle firewall
# roles/firewall/tasks/main.yml
---
- name: Installer UFW
ansible.builtin.apt:
name: ufw
state: present
- name: Réinitialiser les règles UFW
community.general.ufw:
state: reset
tags:
- firewall-reset
- name: Définir la politique par défaut - refuser le trafic entrant
community.general.ufw:
direction: incoming
policy: deny
- name: Définir la politique par défaut - autoriser le trafic sortant
community.general.ufw:
direction: outgoing
policy: allow
- name: Autoriser les ports TCP définis
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: tcp
comment: "Ansible managed - TCP {{ item }}"
loop: "{{ firewall_allowed_tcp_ports }}"
- name: Autoriser les ports UDP définis
community.general.ufw:
rule: allow
port: "{{ item }}"
proto: udp
comment: "Ansible managed - UDP {{ item }}"
loop: "{{ firewall_allowed_udp_ports }}"
when: firewall_allowed_udp_ports | length > 0
- name: Autoriser le port Node Exporter depuis le réseau interne
community.general.ufw:
rule: allow
port: "{{ node_exporter_port }}"
proto: tcp
from_ip: 10.0.0.0/8
comment: "Ansible managed - Node Exporter interne"
- name: Activer la protection contre le brute force sur SSH
community.general.ufw:
rule: limit
port: "{{ ssh_port }}"
proto: tcp
comment: "Ansible managed - Rate limit SSH"
- name: Activer UFW
community.general.ufw:
state: enabled
logging: "on"
- name: Activer le service UFW au démarrage
ansible.builtin.systemd:
name: ufw
enabled: yes
state: startedLa règle limit sur le port SSH est particulièrement importante : elle bloque automatiquement les adresses IP qui tentent plus de 6 connexions en 30 secondes, offrant une protection supplémentaire contre le brute force.
Mise en place de fail2ban
Fail2ban surveille les logs de vos services et bannit automatiquement les adresses IP qui présentent un comportement suspect (tentatives de connexion échouées, scans, etc.).
Rôle fail2ban
# roles/fail2ban/tasks/main.yml
---
- name: Installer fail2ban
ansible.builtin.apt:
name: fail2ban
state: present
- name: Créer la configuration locale fail2ban
ansible.builtin.template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: "0644"
notify: Redémarrer fail2ban
- name: Créer le filtre personnalisé pour les requêtes HTTP suspectes
ansible.builtin.copy:
content: |
# Ansible managed
[Definition]
failregex = ^ .* "(GET|POST|HEAD) .*(wp-login|xmlrpc|phpmyadmin|\.env|\.git).*" .*$
ignoreregex =
dest: /etc/fail2ban/filter.d/http-suspicious.conf
owner: root
group: root
mode: "0644"
notify: Redémarrer fail2ban
- name: Activer et démarrer fail2ban
ansible.builtin.systemd:
name: fail2ban
state: started
enabled: yesTemplate jail.local
# roles/fail2ban/templates/jail.local.j2
# Ansible managed - ne pas modifier manuellement
[DEFAULT]
bantime = {{ fail2ban_bantime }}
findtime = {{ fail2ban_findtime }}
maxretry = {{ fail2ban_maxretry }}
banaction = ufw
backend = systemd
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
[sshd]
enabled = true
port = {{ ssh_port }}
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 7200
[sshd-ddos]
enabled = true
port = {{ ssh_port }}
filter = sshd-ddos
logpath = /var/log/auth.log
maxretry = 6
bantime = 3600
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
[http-suspicious]
enabled = true
port = http,https
filter = http-suspicious
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 86400Handler fail2ban
# roles/fail2ban/handlers/main.yml
---
- name: Redémarrer fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restartedConfiguration NTP avec Chrony
La synchronisation horaire est cruciale pour la corrélation des logs, les certificats TLS, les tâches cron et la cohérence des bases de données distribuées. Chrony est le successeur moderne de ntpd, plus rapide et plus précis.
Rôle ntp
# roles/ntp/tasks/main.yml
---
- name: Installer chrony
ansible.builtin.apt:
name: chrony
state: present
- name: Supprimer les anciens services NTP si présents
ansible.builtin.apt:
name:
- ntp
- ntpdate
state: absent
purge: yes
- name: Configurer chrony
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony/chrony.conf
owner: root
group: root
mode: "0644"
notify: Redémarrer chrony
- name: Configurer le fuseau horaire
community.general.timezone:
name: "{{ ntp_timezone }}"
- name: Activer et démarrer chrony
ansible.builtin.systemd:
name: chrony
state: started
enabled: yes
- name: Vérifier la synchronisation NTP
ansible.builtin.command:
cmd: chronyc tracking
register: chrony_status
changed_when: false
- name: Afficher le statut de synchronisation
ansible.builtin.debug:
msg: "{{ chrony_status.stdout_lines[:5] }}"Template chrony.conf
# roles/ntp/templates/chrony.conf.j2
# Ansible managed - ne pas modifier manuellement
{% for server in ntp_servers %}
server {{ server }} iburst
{% endfor %}
# Fichier de dérive
driftfile /var/lib/chrony/chrony.drift
# Correction rapide si décalage supérieur à 1 seconde
makestep 1.0 3
# Activer le RTC kernel sync
rtcsync
# Logging
logdir /var/log/chrony
log tracking measurements statisticsHandler chrony
# roles/ntp/handlers/main.yml
---
- name: Redémarrer chrony
ansible.builtin.systemd:
name: chrony
state: restartedConfiguration de Logrotate
Sans rotation des logs, vos disques finiront inévitablement par se remplir. Logrotate gère automatiquement la compression, la rotation et la suppression des anciens fichiers de logs.
Rôle logrotate
# roles/logrotate/tasks/main.yml
---
- name: Installer logrotate
ansible.builtin.apt:
name: logrotate
state: present
- name: Configurer la rotation globale des logs
ansible.builtin.copy:
content: |
# Ansible managed - ne pas modifier manuellement
# Rotation hebdomadaire par défaut
weekly
# Conserver 4 semaines de logs
rotate 4
# Créer de nouveaux fichiers après rotation
create
# Compresser les anciens fichiers
compress
delaycompress
# Ne pas générer d'erreur si le fichier est manquant
missingok
# Ne pas faire de rotation si le fichier est vide
notifempty
# Inclure les configurations spécifiques
include /etc/logrotate.d
dest: /etc/logrotate.conf
owner: root
group: root
mode: "0644"
- name: Configurer la rotation des logs applicatifs personnalisés
ansible.builtin.copy:
content: |
# Ansible managed
/var/log/app/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid) || true
endscript
}
dest: /etc/logrotate.d/app-custom
owner: root
group: root
mode: "0644"
- name: Configurer la rotation des logs syslog
ansible.builtin.copy:
content: |
# Ansible managed
/var/log/syslog
/var/log/mail.log
/var/log/kern.log
/var/log/auth.log
/var/log/user.log
/var/log/cron.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
/usr/lib/rsyslog/rsyslog-rotate
endscript
}
dest: /etc/logrotate.d/rsyslog-custom
owner: root
group: root
mode: "0644"
- name: Tester la configuration logrotate
ansible.builtin.command:
cmd: logrotate --debug /etc/logrotate.conf
register: logrotate_test
changed_when: false
failed_when: falseInstallation de l'agent de monitoring (Node Exporter)
Node Exporter est l'agent standard de l'écosystème Prometheus pour collecter les métriques système (CPU, mémoire, disque, réseau). Son installation via Ansible permet d'assurer un monitoring uniforme sur toute votre infrastructure.
Rôle monitoring
# roles/monitoring/tasks/main.yml
---
- name: Créer le groupe node_exporter
ansible.builtin.group:
name: node_exporter
system: yes
state: present
- name: Créer l'utilisateur node_exporter
ansible.builtin.user:
name: node_exporter
group: node_exporter
system: yes
shell: /usr/sbin/nologin
create_home: no
state: present
- name: Vérifier si node_exporter est déjà installé
ansible.builtin.stat:
path: /usr/local/bin/node_exporter
register: node_exporter_binary
- name: Télécharger Node Exporter
ansible.builtin.get_url:
url: "https://github.com/prometheus/node_exporter/releases/download/v{{ node_exporter_version }}/node_exporter-{{ node_exporter_version }}.linux-amd64.tar.gz"
dest: "/tmp/node_exporter-{{ node_exporter_version }}.tar.gz"
mode: "0644"
when: not node_exporter_binary.stat.exists
- name: Extraire Node Exporter
ansible.builtin.unarchive:
src: "/tmp/node_exporter-{{ node_exporter_version }}.tar.gz"
dest: /tmp/
remote_src: yes
when: not node_exporter_binary.stat.exists
- name: Copier le binaire Node Exporter
ansible.builtin.copy:
src: "/tmp/node_exporter-{{ node_exporter_version }}.linux-amd64/node_exporter"
dest: /usr/local/bin/node_exporter
remote_src: yes
owner: node_exporter
group: node_exporter
mode: "0755"
when: not node_exporter_binary.stat.exists
notify: Redémarrer node_exporter
- name: Créer le service systemd pour Node Exporter
ansible.builtin.copy:
content: |
# Ansible managed
[Unit]
Description=Prometheus Node Exporter
Documentation=https://prometheus.io/docs/guides/node-exporter/
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=node_exporter
Group=node_exporter
ExecStart=/usr/local/bin/node_exporter \
--web.listen-address=:{{ node_exporter_port }} \
--collector.systemd \
--collector.processes \
--collector.logind \
--no-collector.infiniband \
--no-collector.nfs \
--no-collector.nfsd
Restart=always
RestartSec=5
ProtectSystem=strict
ProtectHome=yes
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target
dest: /etc/systemd/system/node_exporter.service
owner: root
group: root
mode: "0644"
notify:
- Recharger systemd
- Redémarrer node_exporter
- name: Activer et démarrer Node Exporter
ansible.builtin.systemd:
name: node_exporter
state: started
enabled: yes
- name: Nettoyer les fichiers temporaires
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "/tmp/node_exporter-{{ node_exporter_version }}.tar.gz"
- "/tmp/node_exporter-{{ node_exporter_version }}.linux-amd64"Handlers monitoring
# roles/monitoring/handlers/main.yml
---
- name: Recharger systemd
ansible.builtin.systemd:
daemon_reload: yes
- name: Redémarrer node_exporter
ansible.builtin.systemd:
name: node_exporter
state: restartedConfiguration des sauvegardes automatiques
Aucune stratégie de sécurité n'est complète sans des sauvegardes régulières et testées. Ce rôle met en place un script de sauvegarde automatisé via cron, avec rotation et compression.
Rôle backups
# roles/backups/tasks/main.yml
---
- name: Créer le répertoire de destination des sauvegardes
ansible.builtin.file:
path: "{{ backup_destination }}"
state: directory
owner: root
group: root
mode: "0700"
- name: Déployer le script de sauvegarde
ansible.builtin.template:
src: backup.sh.j2
dest: /usr/local/bin/automated-backup.sh
owner: root
group: root
mode: "0700"
- name: Configurer la tâche cron de sauvegarde
ansible.builtin.cron:
name: "Sauvegarde automatique quotidienne"
job: "/usr/local/bin/automated-backup.sh >> /var/log/backup.log 2>&1"
minute: "{{ backup_schedule.split(' ')[0] }}"
hour: "{{ backup_schedule.split(' ')[1] }}"
day: "{{ backup_schedule.split(' ')[2] }}"
month: "{{ backup_schedule.split(' ')[3] }}"
weekday: "{{ backup_schedule.split(' ')[4] }}"
user: root
state: present
- name: Configurer logrotate pour les logs de sauvegarde
ansible.builtin.copy:
content: |
# Ansible managed
/var/log/backup.log {
weekly
rotate 8
compress
missingok
notifempty
}
dest: /etc/logrotate.d/backup
owner: root
group: root
mode: "0644"Template du script de sauvegarde
# roles/backups/templates/backup.sh.j2
#!/bin/bash
# Ansible managed - Script de sauvegarde automatique
set -euo pipefail
BACKUP_DIR="{{ backup_destination }}"
DATE=$(date +%Y%m%d_%H%M%S)
HOSTNAME=$(hostname)
RETENTION_DAYS={{ backup_retention_days }}
echo "========================================="
echo "Sauvegarde démarrée : $(date)"
echo "========================================="
# Créer le répertoire du jour
DAILY_DIR="${BACKUP_DIR}/${DATE}"
mkdir -p "${DAILY_DIR}"
# Sauvegarder chaque répertoire configuré
{% for dir in backup_directories %}
echo "Sauvegarde de {{ dir }}..."
ARCHIVE_NAME="${HOSTNAME}_$(echo '{{ dir }}' | tr '/' '_')_${DATE}.tar.gz"
tar czf "${DAILY_DIR}/${ARCHIVE_NAME}" "{{ dir }}" 2>/dev/null || true
echo " -> ${ARCHIVE_NAME} créé"
{% endfor %}
# Sauvegarder la liste des packages installés
dpkg --get-selections > "${DAILY_DIR}/${HOSTNAME}_packages_${DATE}.txt"
echo "Liste des packages sauvegardée"
# Sauvegarder les tâches cron
crontab -l > "${DAILY_DIR}/${HOSTNAME}_crontab_${DATE}.txt" 2>/dev/null || true
echo "Crontab sauvegardé"
# Sauvegarder les règles du pare-feu
ufw status verbose > "${DAILY_DIR}/${HOSTNAME}_ufw_${DATE}.txt" 2>/dev/null || true
echo "Règles UFW sauvegardées"
# Générer un checksum des fichiers
cd "${DAILY_DIR}"
sha256sum * > checksums.sha256
echo "Checksums générés"
# Rotation : supprimer les sauvegardes plus anciennes que la rétention
echo "Nettoyage des sauvegardes de plus de ${RETENTION_DAYS} jours..."
find "${BACKUP_DIR}" -mindepth 1 -maxdepth 1 -type d -mtime +${RETENTION_DAYS} -exec rm -rf {} \;
# Afficher l'espace utilisé
BACKUP_SIZE=$(du -sh "${BACKUP_DIR}" | cut -f1)
echo "Espace total utilisé par les sauvegardes : ${BACKUP_SIZE}"
echo "========================================="
echo "Sauvegarde terminée : $(date)"
echo "========================================="Mises à jour automatiques de sécurité
Les mises à jour de sécurité doivent être appliquées rapidement pour protéger votre serveur contre les vulnérabilités connues. Le package unattended-upgrades automatise ce processus sur les distributions Debian et Ubuntu.
Rôle auto-updates
# roles/auto-updates/tasks/main.yml
---
- name: Installer unattended-upgrades
ansible.builtin.apt:
name:
- unattended-upgrades
- apt-listchanges
state: present
- name: Configurer unattended-upgrades
ansible.builtin.template:
src: 50unattended-upgrades.j2
dest: /etc/apt/apt.conf.d/50unattended-upgrades
owner: root
group: root
mode: "0644"
- name: Activer les mises à jour automatiques
ansible.builtin.copy:
content: |
// Ansible managed - ne pas modifier manuellement
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: "0644"
- name: Activer le service unattended-upgrades
ansible.builtin.systemd:
name: unattended-upgrades
state: started
enabled: yes
- name: Tester la configuration unattended-upgrades
ansible.builtin.command:
cmd: unattended-upgrades --dry-run --debug
register: unattended_test
changed_when: false
failed_when: false
- name: Afficher le résultat du test
ansible.builtin.debug:
msg: "Unattended-upgrades configuré avec succès"
when: unattended_test.rc == 0Template unattended-upgrades
# roles/auto-updates/templates/50unattended-upgrades.j2
// Ansible managed - ne pas modifier manuellement
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
// Supprimer automatiquement les dépendances inutilisées
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
// Redémarrage automatique si nécessaire (à 4h du matin)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
// Ne pas interrompre si des utilisateurs sont connectés
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
// Notification par mail (optionnel)
// Unattended-Upgrade::Mail "admin@codeclan.fr";
// Unattended-Upgrade::MailReport "on-change";
// Taille maximale du téléchargement
Acquire::http::Dl-Limit "500";
// Logging
Unattended-Upgrade::SyslogEnable "true";
Unattended-Upgrade::SyslogFacility "daemon";Le playbook principal : tout assembler
Voici le playbook principal qui orchestre l'exécution de tous les rôles dans l'ordre optimal. L'ordre est important : on crée d'abord les utilisateurs et on configure SSH avant de verrouiller l'accès, puis on installe les services de sécurité et de monitoring.
# site.yml
---
- name: Configuration et hardening complet des serveurs Linux
hosts: all
become: yes
gather_facts: yes
pre_tasks:
- name: Vérifier la connectivité
ansible.builtin.ping:
- name: Afficher les informations du système
ansible.builtin.debug:
msg: >
Serveur: {{ inventory_hostname }} |
OS: {{ ansible_distribution }} {{ ansible_distribution_version }} |
Kernel: {{ ansible_kernel }} |
CPU: {{ ansible_processor_vcpus }} vCPU |
RAM: {{ ansible_memtotal_mb }} MB
- name: Vérifier que le système est supporté
ansible.builtin.assert:
that:
- ansible_distribution in ['Debian', 'Ubuntu']
- ansible_distribution_major_version | int >= 11 or ansible_distribution == 'Ubuntu'
fail_msg: "Ce playbook supporte uniquement Debian 11+ et Ubuntu 20.04+"
success_msg: "Système compatible détecté"
roles:
- role: packages
tags: [packages]
- role: users
tags: [users]
- role: ssh-hardening
tags: [ssh]
- role: firewall
tags: [firewall]
- role: fail2ban
tags: [fail2ban]
- role: ntp
tags: [ntp]
- role: logrotate
tags: [logrotate]
- role: monitoring
tags: [monitoring]
- role: backups
tags: [backups]
- role: auto-updates
tags: [auto-updates]
post_tasks:
- name: Résumé de la configuration
ansible.builtin.debug:
msg:
- "============================================"
- "Configuration terminée pour {{ inventory_hostname }}"
- "============================================"
- "Port SSH : {{ ssh_port }}"
- "Utilisateurs créés : {{ admin_users | map(attribute='username') | list | join(', ') }}"
- "Ports ouverts (TCP) : {{ firewall_allowed_tcp_ports | join(', ') }}"
- "Fail2ban : actif"
- "Node Exporter : port {{ node_exporter_port }}"
- "Sauvegardes : {{ backup_schedule }} vers {{ backup_destination }}"
- "Mises à jour auto : activées"
- "============================================"
- "IMPORTANT : Reconnectez-vous avec :"
- "ssh -p {{ ssh_port }} deployer@{{ ansible_host }}"
- "============================================"Exécution du playbook
Pour lancer le déploiement complet, exécutez la commande suivante :
# Déploiement complet
ansible-playbook -i inventory/hosts.yml site.yml
# Déploiement sur un seul serveur
ansible-playbook -i inventory/hosts.yml site.yml --limit web01
# Exécuter uniquement certains rôles via les tags
ansible-playbook -i inventory/hosts.yml site.yml --tags "ssh,firewall,fail2ban"
# Mode dry-run pour vérifier sans appliquer
ansible-playbook -i inventory/hosts.yml site.yml --check --diff
# Avec verbosité pour le débogage
ansible-playbook -i inventory/hosts.yml site.yml -vvvIl est fortement recommandé de lancer d'abord en mode --check --diff pour visualiser les changements avant de les appliquer réellement. Cela permet d'éviter les mauvaises surprises, surtout sur des serveurs en production.
Vérification post-déploiement
Après le déploiement, vous pouvez vérifier que tout fonctionne correctement avec ces commandes :
# Vérifier la connexion SSH sur le nouveau port
ssh -p 2222 deployer@192.168.1.10
# Vérifier le statut du pare-feu
sudo ufw status verbose
# Vérifier fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
# Vérifier la synchronisation NTP
chronyc tracking
chronyc sources -v
# Vérifier Node Exporter
curl http://localhost:9100/metrics | head -20
# Vérifier les mises à jour automatiques
sudo unattended-upgrades --dry-run --debug
# Vérifier la dernière sauvegarde
ls -la /var/backups/automated/
# Vérifier les services actifs
sudo systemctl is-active node_exporter fail2ban ufw chrony unattended-upgradesBonnes pratiques et recommandations
Pour tirer le meilleur parti de ce playbook, voici quelques recommandations :
- Versionnez votre playbook avec Git. Chaque modification de la configuration serveur doit être tracée et réversible.
- Utilisez Ansible Vault pour chiffrer les données sensibles (mots de passe, clés API) :
ansible-vault encrypt inventory/group_vars/all.yml. - Testez dans un environnement de staging avant de déployer en production. Vagrant ou des conteneurs LXC sont idéaux pour cela.
- Documentez vos variables et gardez les valeurs par défaut dans
defaults/main.ymlde chaque rôle. - Exécutez régulièrement le playbook pour vous assurer que la configuration n'a pas dérivé (configuration drift).
- Surveillez les logs de fail2ban et des mises à jour automatiques pour détecter les anomalies.
Rappel : La sécurité est un processus continu, pas un état. Ce playbook pose les fondations, mais il doit être régulièrement mis à jour pour suivre les nouvelles menaces et les meilleures pratiques.
Conclusion
Nous avons construit un playbook Ansible complet qui automatise la configuration et le durcissement d'un serveur Linux de A à Z. En quelques minutes, vous pouvez désormais provisionner un serveur sécurisé avec une gestion des utilisateurs rigoureuse, un SSH durci, un pare-feu configuré, une protection contre le brute force, une synchronisation horaire fiable, un monitoring opérationnel et des sauvegardes automatiques.
L'avantage majeur de cette approche est la reproductibilité : chaque serveur est configuré de manière identique, éliminant les erreurs humaines et le "snowflake syndrome" où chaque machine a une configuration unique et non documentée. En combinant ce playbook avec un pipeline CI/CD, vous pouvez même automatiser les tests de conformité et le déploiement continu de votre infrastructure.
Dans le prochain article, nous verrons comment aller plus loin en utilisant Ansible pour déployer et gérer des conteneurs Docker, ajoutant une couche supplémentaire d'automatisation à votre infrastructure.