# ansible-hardening-linux Configuration et durcissement de serveurs Linux — Debian 12/13, Ubuntu 22.04/24.04, RHEL / Rocky / AlmaLinux 8 et 9 — par Ansible. Reprise du dépôt de scripts `prerequis-install-deb` (Debian uniquement, copie de fichiers à la main) sous forme de rôles idempotents, multi-distributions, avec CI/CD Gitea. Objectif : à la fin d'un passage, tout serveur est configuré, à jour, supervisé, et **n'est joignable à distance qu'en SSH par clé publique** ; l'accès local (console, `su -`) continue de fonctionner comme avant. ## Correspondance avec les scripts d'origine | Script d'origine | Rôle | Ce qui change | |---|---|---| | `01-ssh` (sshd_config, ssh_config, banner, authorized_keys, 00-basic) | `ssh` | Drop-in `sshd_config.d/00-durcissement.conf` au lieu d'écraser tout le fichier ; clé publique obligatoire (`AuthenticationMethods publickey`) ; `AllowUsers` calculé depuis la liste des comptes ; clés centralisées dans `/etc/ssh/authorized_keys/` (le lien symbolique de l'ancien script est remplacé par des fichiers appartenant à root) ; bannière dans `/etc/issue.net` et `/etc/issue` ; MOTD `00-basic` conservé, rendu portable, et équivalent `profile.d` sous Red Hat ; vérification d'une **nouvelle** connexion SSH avant de rendre la main | | `02-apt` (proxy + full-upgrade) | `proxy`, `updates` | Proxy APT **et** DNF, plus `profile.d` ; mises à jour de sécurité automatiques (`unattended-upgrades` / `dnf-automatic`) ; signalement du besoin de redémarrage | | `04-agent-glpi` | `glpi_agent` (+ `common` pour le certificat) | Installateur perl officiel, version pilotée par variable, configuration dans `conf.d/10-ansible.cfg` ; le certificat racine va dans le magasin système (`update-ca-certificates` / `update-ca-trust`) et sert à tout le monde, pas seulement à l'agent | | `06-snmp` | `snmp` | Communautés v2c par source de supervision, lues depuis le vault ; SNMPv3 disponible ; vue en lecture seule | | `07-DNS` | `dns` | Respecte le gestionnaire réel : `systemd-resolved` (drop-in), NetworkManager (`dns=none` puis `resolv.conf`), ou `resolv.conf` direct | | `08-NRPE` | `nrpe` | Installation **et** configuration : `allowed_hosts`, `dont_blame_nrpe=0`, commandes de base | | `03-cortex`, `05-manageengine` | — | Ignorés à la demande | | *(absent)* | `firewall` | nftables (Debian/Ubuntu) ou firewalld (Red Hat), politique `drop` : SSH depuis les réseaux d'administration, ICMP, SNMP/NRPE depuis la supervision | | *(absent)* | `fail2ban` | Bannissement des sources en échec sur SSH, backend `systemd` | | *(absent)* | `os_hardening` | sysctl, core dumps, `login.defs`, auditd, services et paquets inutiles | | *(absent)* | `common` | Contrôle de la distribution, paquets de base, fuseau, certificats racine | ## Ce qui garantit que l'accès local reste possible - Aucun mot de passe n'est verrouillé, aucun compte n'est désactivé, PAM n'est pas modifié. - `PermitRootLogin prohibit-password` : root n'est joignable à distance que par clé, mais son mot de passe reste valable sur la console (Proxmox, KVM, iLO…) et pour `su -`. - Le pare-feu ne filtre que le réseau ; la console n'est pas concernée. - fail2ban ne bannit que des adresses IP distantes ; les réseaux d'administration sont dans `ignoreip`. - Deux garde-fous contre l'auto-verrouillage : le rôle `ssh` refuse une configuration où le compte utilisé par Ansible n'aurait pas de clé ou ne serait pas dans `AllowUsers`, et le rôle `firewall` refuse une liste `firewall_ssh_sources` qui exclurait l'adresse depuis laquelle Ansible est connecté. Après chaque redémarrage de sshd et après chaque application du pare-feu, une **nouvelle** session SSH est ouverte depuis le contrôleur ; si elle échoue, le playbook s'arrête sans fermer la session courante. ## Arborescence Même organisation qu'`ansible-centreon` : playbook et `group_vars` à la racine, inventaire et vault versionnés, collections déclarées dans `collections/`. ``` . ├── ansible.cfg ├── site.yml # point d'entrée ├── collections/requirements.yml # ansible.posix, community.general (installées par Semaphore) ├── inventory/hosts.yml # inventaire versionné ├── group_vars/all/ │ ├── main.yml # réseaux, DNS, proxy, comptes, supervision, activation des rôles │ └── vault.yml.example # à copier vers vault.yml, chiffrer, puis versionner ├── files/ │ ├── keys/*.pub # clés publiques SSH, attribuées par ssh_users[].keys │ └── ca/*.crt # certificats racine internes (versionnés) ├── roles/ # common, proxy, dns, updates, ssh, firewall, fail2ban, │ # os_hardening, snmp, nrpe, glpi_agent ├── .gitea/workflows/ansible-lint.yml └── CICD.md # chaîne Gitea (contrôle) -> Semaphore (exécution) ``` ## Mise en route 1. **Inventaire** : renseigner `inventory/hosts.yml`. Deux modes de connexion : - compte de service `ansible` + sudo (`become` est activé dans `ansible.cfg`) ; - root par clé, comme sur les VM Terraform : `ansible_user: root`, `become` est alors sans effet. 2. **Clés publiques** : fichiers `*.pub` à plat dans `files/keys/`, attribués aux comptes par `ssh_users[].keys` dans `group_vars/all/main.yml`. Le compte utilisé par Ansible (root) **doit** y avoir la clé de Semaphore. 3. **Certificat racine** (facultatif) : `files/ca/root-ca.crt` au format PEM. 4. **Secrets** : `cp group_vars/all/vault.yml.example group_vars/all/vault.yml`, renseigner, puis `ansible-vault encrypt group_vars/all/vault.yml`. 5. **Variables** : relire `group_vars/all/main.yml` — en particulier `firewall_ssh_sources` (réseaux d'administration), `supervision_sources`, `dns_nameservers`, `proxy_url`, `ssh_users`, `glpi_agent_server`. 6. **Simulation** : `ansible-playbook site.yml --check --diff --ask-vault-pass` 7. **Application** : `ansible-playbook site.yml --ask-vault-pass` Un seul rôle : `--tags ssh`, `--tags firewall`, etc. Un rôle peut aussi être désactivé par variable (`enable_: false`) au niveau global, d'un groupe ou d'un hôte. ## Ordre d'exécution et raison `common` → `proxy` → `dns` → `updates` → `ssh` → `firewall` → `fail2ban` → `os_hardening` → `snmp` → `nrpe` → `glpi_agent`. Le proxy et le DNS viennent d'abord (sans eux, aucun paquet ne se télécharge) ; le SSH est durci avant le pare-feu (la règle SSH doit exister avant que la politique passe à `drop`) ; la supervision et l'agent GLPI viennent en dernier, une fois le système stable. ## Points d'attention par distribution - **Ubuntu 22.10+** : sshd est activé par socket (`ssh.socket`). Le rôle bascule sur `ssh.service` pour que `Port` soit pris en compte depuis `sshd_config`. - **Red Hat / Rocky / Alma** : les algorithmes SSH ne sont pas définis dans `sshd_config` (ils viennent de `crypto-policies`) ; fail2ban, NRPE et les greffons Nagios viennent d'EPEL, installé par `common`. SELinux : le port SSH est déclaré si différent de 22. - **Red Hat 8 (OpenSSH 8.0)** : pas de directive `Include` ; le durcissement est inséré en tête de `sshd_config` entre des marqueurs, et les directives contradictoires du fichier d'origine sont commentées. - **VM issues de `terraform-proxmox-serveur-debian-12`** : le jeu nftables et le drop-in sshd sont compatibles avec ceux posés par cloud-init (même structure, même répertoire `/etc/nftables.d/`). Le drop-in Ansible `00-durcissement.conf` passe avant `99-durcissement.conf` de cloud-init : c'est Ansible qui a le dernier mot. ## CI/CD Voir `CICD.md` : Gitea contrôle le code, Semaphore l'exécute depuis la branche `valide`. ## Vérifications après un premier passage ```bash # Depuis le contrôleur : nouvelle session, clé uniquement ssh -o BatchMode=yes -o ControlPath=none ansible@ 'sudo sshd -T | grep -Ei "^(permitrootlogin|passwordauthentication|authenticationmethods|allowusers)"' # Pare-feu : seuls SSH, ICMP et les ports de supervision sont ouverts ssh ansible@ 'sudo nft list chain inet filter input' # Debian/Ubuntu ssh ansible@ 'sudo firewall-cmd --list-all' # Red Hat # fail2ban actif sur sshd ssh ansible@ 'sudo fail2ban-client status sshd' ```