Automatiser la Configuration de Serveurs Linux avec Ansible

Playbook Ansible complet pour sécuriser et configurer un serveur Linux : utilisateurs, SSH hardening, pare-feu UFW, fail2ban, NTP, monitoring, backups et mises à jour automatiques.

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

Diagramme - Automatiser la Configuration de Serveurs Linux avec Ansible

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

Configuration 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.20

Variables 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: present

Ce 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 SSH

Template 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 INFO

Handler pour redémarrer SSH

# roles/ssh-hardening/handlers/main.yml
---
- name: Redémarrer SSH
  ansible.builtin.systemd:
    name: sshd
    state: restarted
    enabled: yes
Attention : 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.exists

Configuration 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: started

La 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: yes

Template 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 = 86400

Handler fail2ban

# roles/fail2ban/handlers/main.yml
---
- name: Redémarrer fail2ban
  ansible.builtin.systemd:
    name: fail2ban
    state: restarted

Configuration 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 statistics

Handler chrony

# roles/ntp/handlers/main.yml
---
- name: Redémarrer chrony
  ansible.builtin.systemd:
    name: chrony
    state: restarted

Configuration 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: false

Installation 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: restarted

Configuration 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 == 0

Template 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 -vvv

Il 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-upgrades

Bonnes 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.yml de 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.

Vous vous êtes abonné avec succès à CodeClan
Parfait ! Ensuite, complétez le paiement pour obtenir un accès complet à tout le contenu premium.
Erreur ! Impossible de s'inscrire. Lien invalide.
Bienvenue ! Vous vous êtes connecté avec succès.
Erreur ! Impossible de se connecter. Veuillez réessayer.
Succès ! Votre compte est entièrement activé, vous avez maintenant accès à tout le contenu.
Erreur ! Le paiement Stripe a échoué.
Succès ! Vos informations de facturation sont mises à jour.
Erreur ! La mise à jour des informations de facturation a échoué.