start
Ansible - controle du code / controle (push) Failing after 5m47s
Ansible - controle du code / promouvoir (push) Skipped

This commit is contained in:
hcornet committed 2026-09-15 17:09:39 +02:00
1 parent 4b8bb1427a
commit 975d3e9d1d
84 files changed
+2507 -351

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
---
# Rôle common — prérequis partagés par tous les autres rôles.
# Distributions acceptées (version majeure minimale).
common_supported:
Debian: 12
Ubuntu: 22
RedHat: 8
Rocky: 8
AlmaLinux: 8
OracleLinux: 8
common_timezone: "Europe/Paris"
# Paquets de base, communs aux deux familles.
common_packages:
- ca-certificates
- curl
- wget
- vim
- htop
- less
- rsync
- tar
- unzip
- sudo
# Paquets propres à chaque famille.
common_packages_debian:
- apt-transport-https
- gnupg2
- lsb-release
- python3-apt
common_packages_redhat:
- epel-release
- python3-libselinux
- dnf-plugins-core
# Certificats racine internes (files/ca/*.crt) à installer dans le magasin système.
common_ca_dir: "{{ playbook_dir }}/files/ca"
+5
View File
@@ -0,0 +1,5 @@
---
- name: Mettre à jour le magasin de certificats
ansible.builtin.command:
cmd: "{{ common_ca_update_cmd[ansible_facts['os_family']] }}"
changed_when: true
+52
View File
@@ -0,0 +1,52 @@
---
- name: Vérifier que la distribution est prise en charge
ansible.builtin.assert:
that:
- ansible_facts['distribution'] in common_supported
- ansible_facts['distribution_major_version'] | int >= common_supported[ansible_facts['distribution']]
- ansible_facts['os_family'] in ['Debian', 'RedHat']
fail_msg: >-
{{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
n'est pas pris en charge (voir common_supported).
- name: Exposer le groupe sudo de la famille d'OS
ansible.builtin.set_fact:
common_sudo_group_effective: "{{ common_sudo_group[ansible_facts['os_family']] }}"
- name: Rafraîchir le cache APT
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_facts['os_family'] == 'Debian'
- name: Installer les paquets de base
ansible.builtin.package:
name: "{{ common_packages + (common_packages_debian if ansible_facts['os_family'] == 'Debian' else common_packages_redhat) }}"
state: present
- name: Régler le fuseau horaire
community.general.timezone:
name: "{{ common_timezone }}"
- name: Lister les certificats racine internes à installer
ansible.builtin.find:
paths: "{{ common_ca_dir }}"
patterns: "*.crt"
delegate_to: localhost
become: false
register: common_ca_files
- name: Installer les certificats racine internes
ansible.builtin.copy:
src: "{{ item.path }}"
dest: "{{ common_ca_path[ansible_facts['os_family']] }}/{{ item.path | basename }}"
owner: root
group: root
mode: "0644"
loop: "{{ common_ca_files.files }}"
loop_control:
label: "{{ item.path | basename }}"
notify: Mettre à jour le magasin de certificats
- name: Appliquer immédiatement le magasin de certificats
ansible.builtin.meta: flush_handlers
+11
View File
@@ -0,0 +1,11 @@
---
# Correspondances propres à chaque famille d'OS.
common_ca_path:
Debian: /usr/local/share/ca-certificates
RedHat: /etc/pki/ca-trust/source/anchors
common_ca_update_cmd:
Debian: update-ca-certificates
RedHat: update-ca-trust extract
common_sudo_group:
Debian: sudo
RedHat: wheel
+13
View File
@@ -0,0 +1,13 @@
---
# Rôle dns — reprend 07-DNS/resolv.conf, en respectant le gestionnaire de
# résolution réellement en place (systemd-resolved, NetworkManager, ou aucun).
dns_nameservers: []
dns_search_domains: []
dns_options:
- timeout:2
- attempts:2
# Quand aucun gestionnaire n'est actif, /etc/resolv.conf est écrit directement.
# Un client DHCP peut le réécrire : dns_lock_resolv_conf pose l'attribut
# immuable (chattr +i) pour l'en empêcher.
dns_lock_resolv_conf: false
+10
View File
@@ -0,0 +1,10 @@
---
- name: Redémarrer systemd-resolved
ansible.builtin.systemd:
name: systemd-resolved
state: restarted
- name: Recharger NetworkManager
ansible.builtin.systemd:
name: NetworkManager
state: reloaded
+18
View File
@@ -0,0 +1,18 @@
---
- name: Vérifier qu'au moins un serveur DNS est défini
ansible.builtin.assert:
that: dns_nameservers | length > 0
fail_msg: "dns_nameservers est vide"
- name: Recenser les services de résolution présents
ansible.builtin.service_facts:
- name: Déterminer le gestionnaire de résolution en place
ansible.builtin.set_fact:
dns_manager: >-
{{ 'resolved' if (ansible_facts['services']['systemd-resolved.service'] | default({})).state | default('') == 'running'
else 'networkmanager' if (ansible_facts['services']['NetworkManager.service'] | default({})).state | default('') == 'running'
else 'static' }}
- name: Configurer la résolution
ansible.builtin.include_tasks: "{{ dns_manager }}.yml"
+16
View File
@@ -0,0 +1,16 @@
---
- name: Empêcher NetworkManager de gérer /etc/resolv.conf
ansible.builtin.copy:
dest: /etc/NetworkManager/conf.d/90-dns-none.conf
content: |
# {{ ansible_managed }}
[main]
dns=none
rc-manager=unmanaged
owner: root
group: root
mode: "0644"
notify: Recharger NetworkManager
- name: Écrire /etc/resolv.conf
ansible.builtin.include_tasks: static.yml
+24
View File
@@ -0,0 +1,24 @@
---
- name: Créer le répertoire de surcharge de systemd-resolved
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d
state: directory
owner: root
group: root
mode: "0755"
- name: Déposer la configuration DNS de systemd-resolved
ansible.builtin.template:
src: resolved.conf.j2
dest: /etc/systemd/resolved.conf.d/10-dns.conf
owner: root
group: root
mode: "0644"
notify: Redémarrer systemd-resolved
- name: Pointer /etc/resolv.conf vers le résolveur local
ansible.builtin.file:
src: /run/systemd/resolve/stub-resolv.conf
dest: /etc/resolv.conf
state: link
force: true
+35
View File
@@ -0,0 +1,35 @@
---
- name: Examiner /etc/resolv.conf
ansible.builtin.stat:
path: /etc/resolv.conf
follow: false
register: dns_resolv_stat
- name: Supprimer un éventuel lien symbolique /etc/resolv.conf
ansible.builtin.file:
path: /etc/resolv.conf
state: absent
when: dns_resolv_stat.stat.islnk | default(false)
- name: Retirer l'attribut immuable avant écriture
ansible.builtin.file:
path: /etc/resolv.conf
attributes: -i
when:
- dns_resolv_stat.stat.exists
- not dns_resolv_stat.stat.islnk | default(false)
- "'i' in (dns_resolv_stat.stat.attributes | default([]))"
- name: Écrire /etc/resolv.conf
ansible.builtin.template:
src: resolv.conf.j2
dest: /etc/resolv.conf
owner: root
group: root
mode: "0644"
- name: Poser l'attribut immuable sur /etc/resolv.conf
ansible.builtin.file:
path: /etc/resolv.conf
attributes: +i
when: dns_lock_resolv_conf | bool
+10
View File
@@ -0,0 +1,10 @@
# {{ ansible_managed }}
{% for ns in dns_nameservers %}
nameserver {{ ns }}
{% endfor %}
{% if dns_search_domains | length > 0 %}
search {{ dns_search_domains | join(' ') }}
{% endif %}
{% if dns_options | length > 0 %}
options {{ dns_options | join(' ') }}
{% endif %}
+6
View File
@@ -0,0 +1,6 @@
# {{ ansible_managed }}
[Resolve]
DNS={{ dns_nameservers | join(' ') }}
{% if dns_search_domains | length > 0 %}
Domains={{ dns_search_domains | join(' ') }}
{% endif %}
+7
View File
@@ -0,0 +1,7 @@
---
# Rôle fail2ban — bannissement des sources en échec d'authentification SSH.
fail2ban_bantime: 1h
fail2ban_findtime: 10m
fail2ban_maxretry: 5
# Adresses jamais bannies : boucle locale + réseaux d'administration.
fail2ban_ignoreip: "{{ ['127.0.0.1/8', '::1'] + (firewall_ssh_sources | default([])) }}"
+5
View File
@@ -0,0 +1,5 @@
---
- name: Redémarrer fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted
+20
View File
@@ -0,0 +1,20 @@
---
- name: Installer fail2ban
ansible.builtin.package:
name: "{{ fail2ban_packages[ansible_facts['os_family']] }}"
state: present
- name: Déposer la configuration locale
ansible.builtin.template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: "0644"
notify: Redémarrer fail2ban
- name: Activer et démarrer fail2ban
ansible.builtin.systemd:
name: fail2ban
enabled: true
state: started
+16
View File
@@ -0,0 +1,16 @@
# {{ ansible_managed }}
[DEFAULT]
# Le journal systemd est lu directement : aucun chemin de log à maintenir
# selon la distribution.
backend = systemd
banaction = {{ fail2ban_banaction[ansible_facts['os_family']] }}
banaction_allports = {{ fail2ban_banaction[ansible_facts['os_family']] }}
bantime = {{ fail2ban_bantime }}
findtime = {{ fail2ban_findtime }}
maxretry = {{ fail2ban_maxretry }}
ignoreip = {{ fail2ban_ignoreip | join(' ') }}
[sshd]
enabled = true
port = {{ ssh_port | default(22) }}
mode = aggressive
+8
View File
@@ -0,0 +1,8 @@
---
fail2ban_packages:
Debian: [fail2ban, python3-systemd]
RedHat: [fail2ban, fail2ban-firewalld]
# Action de bannissement cohérente avec le pare-feu de la famille.
fail2ban_banaction:
Debian: nftables-multiport
RedHat: firewallcmd-rich-rules
+34
View File
@@ -0,0 +1,34 @@
---
# Rôle firewall — nouveau par rapport aux scripts d'origine (qui ne posaient
# aucun pare-feu). Politique : tout est fermé en entrée sauf SSH, ICMP et les
# ports de supervision, chacun limité aux réseaux qui en ont besoin.
#
# auto : nftables sous Debian/Ubuntu, firewalld sous Red Hat (chacun est le
# pare-feu natif de sa famille). Peut être forcé à « nftables » ou « firewalld ».
firewall_backend: auto
# Réseaux autorisés à joindre SSH. Vide = tout le monde.
firewall_ssh_sources: []
# Règles standard, activées avec le rôle correspondant.
firewall_rules:
- name: snmp
port: 161
proto: udp
sources: "{{ supervision_sources | default([]) }}"
enabled: "{{ enable_snmp | default(false) }}"
- name: nrpe
port: 5666
proto: tcp
sources: "{{ supervision_sources | default([]) }}"
enabled: "{{ enable_nrpe | default(false) }}"
# Règles supplémentaires propres à un hôte ou un groupe (même format).
# Exemple : { name: https, port: 443, proto: tcp, sources: [], enabled: true }
firewall_extra_rules: []
# Avant d'appliquer, vérifier que l'adresse depuis laquelle Ansible est
# connecté figure bien dans firewall_ssh_sources (sinon : verrouillage).
firewall_check_controller_source: true
# Après application, ouvrir une nouvelle session SSH depuis le contrôleur.
firewall_verify_ssh: true
+5
View File
@@ -0,0 +1,5 @@
---
- name: Recharger nftables
ansible.builtin.systemd:
name: nftables
state: reloaded
+79
View File
@@ -0,0 +1,79 @@
---
- name: Installer firewalld
ansible.builtin.package:
name:
- firewalld
- python3-firewall
state: present
- name: Recenser les services
ansible.builtin.service_facts:
- name: Arrêter et désactiver nftables (pilotage par firewalld)
ansible.builtin.systemd:
name: nftables
enabled: false
state: stopped
when: "'nftables.service' in ansible_facts['services']"
- name: Activer et démarrer firewalld
ansible.builtin.systemd:
name: firewalld
enabled: true
state: started
- name: Retirer les services ouverts par défaut dans la zone public
ansible.posix.firewalld:
zone: public
service: "{{ item }}"
state: disabled
permanent: true
immediate: true
loop:
- cockpit
- dhcpv6-client
- ssh
- name: Autoriser SSH depuis tout le monde (aucune source définie)
ansible.posix.firewalld:
zone: public
port: "{{ ssh_port | default(22) }}/tcp"
state: enabled
permanent: true
immediate: true
when: firewall_ssh_sources | length == 0
- name: Autoriser SSH depuis les réseaux d'administration
ansible.posix.firewalld:
zone: public
rich_rule: >-
rule family="{{ 'ipv6' if ':' in item else 'ipv4' }}" source address="{{ item }}"
port port="{{ ssh_port | default(22) }}" protocol="tcp" accept
state: enabled
permanent: true
immediate: true
loop: "{{ firewall_ssh_sources }}"
- name: Autoriser les services depuis leurs sources
ansible.posix.firewalld:
zone: public
rich_rule: >-
rule family="{{ 'ipv6' if ':' in item.1 else 'ipv4' }}" source address="{{ item.1 }}"
port port="{{ item.0.port }}" protocol="{{ item.0.proto }}" accept
state: enabled
permanent: true
immediate: true
loop: "{{ firewall_rules_effective | subelements('sources', skip_missing=True) }}"
loop_control:
label: "{{ item.0.name }} <- {{ item.1 }}"
- name: Autoriser les services ouverts à tous (sans source)
ansible.posix.firewalld:
zone: public
port: "{{ item.port }}/{{ item.proto }}"
state: enabled
permanent: true
immediate: true
loop: "{{ firewall_rules_effective | rejectattr('sources') | list }}"
loop_control:
label: "{{ item.name }}"
+60
View File
@@ -0,0 +1,60 @@
---
- name: Déterminer le pare-feu à utiliser
ansible.builtin.set_fact:
firewall_backend_effective: >-
{{ firewall_backend if firewall_backend != 'auto' else firewall_backend_by_family[ansible_facts['os_family']] }}
firewall_rules_effective: >-
{{ (firewall_rules + firewall_extra_rules) | selectattr('enabled', 'defined') | selectattr('enabled') | list }}
- name: Relever l'adresse source de la session Ansible
ansible.builtin.shell:
cmd: 'echo "${SSH_CONNECTION%% *}"'
become: false
register: firewall_controller_ip
changed_when: false
when:
- firewall_check_controller_source | bool
- firewall_ssh_sources | length > 0
- name: Refuser une liste firewall_ssh_sources qui exclurait le contrôleur
ansible.builtin.command:
argv:
- python3
- -c
- |
import ipaddress, sys
ip = ipaddress.ip_address(sys.argv[1])
ok = any(ip in ipaddress.ip_network(n, strict=False) for n in ' '.join(sys.argv[2:]).split())
sys.exit(0 if ok else 1)
- "{{ firewall_controller_ip.stdout }}"
- "{{ firewall_ssh_sources | join(' ') }}"
changed_when: false
failed_when: false
register: firewall_controller_check
when:
- firewall_check_controller_source | bool
- firewall_ssh_sources | length > 0
- firewall_controller_ip.stdout | length > 0
- name: Arrêter si le contrôleur serait bloqué
ansible.builtin.fail:
msg: >-
Ansible est connecté depuis {{ firewall_controller_ip.stdout }}, absent de
firewall_ssh_sources ({{ firewall_ssh_sources | join(', ') }}) : appliquer
ce pare-feu couperait l'accès. Compléter la liste ou passer
firewall_check_controller_source=false en connaissance de cause.
when:
- firewall_controller_check is not skipped
- firewall_controller_check.rc != 0
- name: Configurer le pare-feu
ansible.builtin.include_tasks: "{{ firewall_backend_effective }}.yml"
- name: Appliquer les règles maintenant
ansible.builtin.meta: flush_handlers
- name: Vérifier qu'une nouvelle connexion SSH aboutit
ansible.builtin.include_role:
name: ssh
tasks_from: verify
when: firewall_verify_ssh | bool
+54
View File
@@ -0,0 +1,54 @@
---
- name: Installer nftables
ansible.builtin.package:
name: nftables
state: present
- name: Recenser les services
ansible.builtin.service_facts:
- name: Arrêter et désactiver les pare-feux concurrents
ansible.builtin.systemd:
name: "{{ item }}"
enabled: false
state: stopped
loop:
- ufw
- firewalld
- iptables
- netfilter-persistent
when: (item ~ '.service') in ansible_facts['services']
- name: Créer le répertoire des fragments de règles
ansible.builtin.file:
path: /etc/nftables.d
state: directory
owner: root
group: root
mode: "0750"
- name: Déposer un fragment vide pour que l'include soit toujours valide
ansible.builtin.copy:
dest: /etc/nftables.d/00-placeholder.nft
content: |
# Fragments de règles applicatives (un fichier par service). Ne rien mettre ici.
owner: root
group: root
mode: "0640"
force: false
- name: Déposer le jeu de règles principal
ansible.builtin.template:
src: nftables.conf.j2
dest: /etc/nftables.conf
owner: root
group: root
mode: "0640"
validate: nft -c -f %s
notify: Recharger nftables
- name: Activer et démarrer nftables
ansible.builtin.systemd:
name: nftables
enabled: true
state: started
+72
View File
@@ -0,0 +1,72 @@
#!/usr/sbin/nft -f
# {{ ansible_managed }}
# Compatible avec le jeu posé par cloud-init sur les VM Terraform :
# les règles applicatives vont dans /etc/nftables.d/*.nft.
flush ruleset
table inet filter {
{% if firewall_ssh_sources | length > 0 %}
set reseaux_admin_v4 {
type ipv4_addr
flags interval
{% set v4 = firewall_ssh_sources | reject('search', ':') | list %}
{% if v4 | length > 0 %}
elements = { {{ v4 | join(', ') }} }
{% endif %}
}
set reseaux_admin_v6 {
type ipv6_addr
flags interval
{% set v6 = firewall_ssh_sources | select('search', ':') | list %}
{% if v6 | length > 0 %}
elements = { {{ v6 | join(', ') }} }
{% endif %}
}
{% endif %}
chain input {
type filter hook input priority filter; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept
ip6 nexthdr ipv6-icmp accept
{% if firewall_ssh_sources | length > 0 %}
tcp dport {{ ssh_port | default(22) }} ip saddr @reseaux_admin_v4 ct state new accept
tcp dport {{ ssh_port | default(22) }} ip6 saddr @reseaux_admin_v6 ct state new accept
{% else %}
tcp dport {{ ssh_port | default(22) }} ct state new accept
{% endif %}
{% for rule in firewall_rules_effective %}
{% set r4 = rule.sources | reject('search', ':') | list %}
{% set r6 = rule.sources | select('search', ':') | list %}
# {{ rule.name }}
{% if rule.sources | length == 0 %}
{{ rule.proto }} dport {{ rule.port }} accept
{% else %}
{% if r4 | length > 0 %}
ip saddr { {{ r4 | join(', ') }} } {{ rule.proto }} dport {{ rule.port }} accept
{% endif %}
{% if r6 | length > 0 %}
ip6 saddr { {{ r6 | join(', ') }} } {{ rule.proto }} dport {{ rule.port }} accept
{% endif %}
{% endif %}
{% endfor %}
include "/etc/nftables.d/*.nft"
counter comment "paquets rejetes en entree"
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
+4
View File
@@ -0,0 +1,4 @@
---
firewall_backend_by_family:
Debian: nftables
RedHat: firewalld
+15
View File
@@ -0,0 +1,15 @@
---
# Rôle glpi_agent — reprend 04-agent-glpi (installateur perl officiel).
# Le certificat racine n'est plus passé à l'agent : il est installé dans le
# magasin système par le rôle common, ce qui couvre l'agent et tout le reste.
glpi_agent_version: "1.19"
glpi_agent_installer_url: >-
https://github.com/glpi-project/glpi-agent/releases/download/{{ glpi_agent_version
}}/glpi-agent-{{ glpi_agent_version }}-linux-installer.pl
glpi_agent_server: ""
glpi_agent_tag: ""
# Profil d'installation de l'installateur : typical | network | all.
glpi_agent_install_type: all
# Tâches activées côté agent : inventory, netdiscovery, netinventory, deploy, collect, esx…
glpi_agent_tasks: "inventory,deploy,collect"
glpi_agent_no_ssl_check: false
+5
View File
@@ -0,0 +1,5 @@
---
- name: Redémarrer glpi-agent
ansible.builtin.systemd:
name: glpi-agent
state: restarted
+59
View File
@@ -0,0 +1,59 @@
---
- name: Vérifier que le serveur GLPI est renseigné
ansible.builtin.assert:
that: glpi_agent_server is match('^https?://')
fail_msg: "glpi_agent_server doit être l'URL du serveur GLPI (plugin inventaire)"
- name: Installer perl (prérequis de l'installateur)
ansible.builtin.package:
name: perl
state: present
- name: Lire la version installée de l'agent
ansible.builtin.command:
cmd: glpi-agent --version
register: glpi_agent_installed
changed_when: false
failed_when: false
- name: Installer ou mettre à jour l'agent
when: ('(' ~ glpi_agent_version ~ ')') not in (glpi_agent_installed.stdout | default(''))
environment: "{{ {'http_proxy': proxy_url, 'https_proxy': proxy_url} if (proxy_url | default('')) | length > 0 else {} }}"
block:
- name: Télécharger l'installateur
ansible.builtin.get_url:
url: "{{ glpi_agent_installer_url }}"
dest: "/tmp/glpi-agent-{{ glpi_agent_version }}-linux-installer.pl"
mode: "0600"
owner: root
group: root
- name: Exécuter l'installateur
ansible.builtin.command:
argv:
- perl
- "/tmp/glpi-agent-{{ glpi_agent_version }}-linux-installer.pl"
- --install
- --type={{ glpi_agent_install_type }}
- --no-question
changed_when: true
- name: Supprimer l'installateur
ansible.builtin.file:
path: "/tmp/glpi-agent-{{ glpi_agent_version }}-linux-installer.pl"
state: absent
- name: Configurer l'agent
ansible.builtin.template:
src: 10-ansible.cfg.j2
dest: /etc/glpi-agent/conf.d/10-ansible.cfg
owner: root
group: root
mode: "0640"
notify: Redémarrer glpi-agent
- name: Activer et démarrer l'agent
ansible.builtin.systemd:
name: glpi-agent
enabled: true
state: started
@@ -0,0 +1,10 @@
# {{ ansible_managed }}
server = {{ glpi_agent_server }}
{% if glpi_agent_tag | length > 0 %}
tag = {{ glpi_agent_tag }}
{% endif %}
tasks = {{ glpi_agent_tasks }}
no-ssl-check = {{ '1' if glpi_agent_no_ssl_check else '0' }}
{% if proxy_url | default('') | length > 0 %}
proxy = {{ proxy_url }}
{% endif %}
+13
View File
@@ -0,0 +1,13 @@
---
# Rôle nrpe — reprend 08-NRPE (installation) et ajoute la configuration :
# hôtes autorisés et commandes de base.
nrpe_allowed_hosts: []
nrpe_port: 5666
# Vérifications standard exposées au serveur de supervision.
nrpe_commands:
check_users: "check_users -w 5 -c 10"
check_load: "check_load -r -w .15,.10,.05 -c .30,.25,.20"
check_disk_root: "check_disk -w 20% -c 10% -p /"
check_zombie_procs: "check_procs -w 5 -c 10 -s Z"
check_total_procs: "check_procs -w 250 -c 400"
check_swap: "check_swap -w 20% -c 10%"
+5
View File
@@ -0,0 +1,5 @@
---
- name: Redémarrer nrpe
ansible.builtin.systemd:
name: "{{ nrpe_service[ansible_facts['os_family']] }}"
state: restarted
+54
View File
@@ -0,0 +1,54 @@
---
- name: Installer NRPE et les greffons
ansible.builtin.package:
name: "{{ nrpe_packages[ansible_facts['os_family']] }}"
state: present
- name: Vérifier qu'au moins un hôte de supervision est défini
ansible.builtin.assert:
that: nrpe_allowed_hosts | length > 0
fail_msg: "nrpe_allowed_hosts est vide"
- name: Fixer les hôtes autorisés dans nrpe.cfg
ansible.builtin.lineinfile:
path: /etc/nagios/nrpe.cfg
regexp: '^\s*#?\s*allowed_hosts\s*='
line: "allowed_hosts={{ (['127.0.0.1', '::1'] + nrpe_allowed_hosts) | join(',') }}"
notify: Redémarrer nrpe
- name: Interdire les arguments passés par le superviseur
ansible.builtin.lineinfile:
path: /etc/nagios/nrpe.cfg
regexp: '^\s*#?\s*dont_blame_nrpe\s*='
line: "dont_blame_nrpe=0"
notify: Redémarrer nrpe
- name: Fixer le port d'écoute
ansible.builtin.lineinfile:
path: /etc/nagios/nrpe.cfg
regexp: '^\s*#?\s*server_port\s*='
line: "server_port={{ nrpe_port }}"
notify: Redémarrer nrpe
- name: Créer le répertoire des commandes
ansible.builtin.file:
path: "{{ nrpe_include_dir[ansible_facts['os_family']] }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Déposer les commandes de supervision
ansible.builtin.template:
src: commandes.cfg.j2
dest: "{{ nrpe_include_dir[ansible_facts['os_family']] }}/10-ansible.cfg"
owner: root
group: root
mode: "0644"
notify: Redémarrer nrpe
- name: Activer et démarrer NRPE
ansible.builtin.systemd:
name: "{{ nrpe_service[ansible_facts['os_family']] }}"
enabled: true
state: started
+4
View File
@@ -0,0 +1,4 @@
# {{ ansible_managed }}
{% for name, cmd in nrpe_commands.items() %}
command[{{ name }}]={{ nrpe_plugins_dir[ansible_facts['os_family']] }}/{{ cmd }}
{% endfor %}
+13
View File
@@ -0,0 +1,13 @@
---
nrpe_packages:
Debian: [nagios-nrpe-server, monitoring-plugins-basic, monitoring-plugins-standard]
RedHat: [nrpe, nagios-plugins-all]
nrpe_service:
Debian: nagios-nrpe-server
RedHat: nrpe
nrpe_include_dir:
Debian: /etc/nagios/nrpe.d
RedHat: /etc/nrpe.d
nrpe_plugins_dir:
Debian: /usr/lib/nagios/plugins
RedHat: /usr/lib64/nagios/plugins
+70
View File
@@ -0,0 +1,70 @@
---
# Rôle os_hardening — durcissement du système, sans jamais toucher à
# l'authentification locale (PAM, mots de passe, console) : un administrateur
# devant la machine doit toujours pouvoir se connecter.
# Paramètres noyau (repris du cloud-init Terraform et complétés).
os_hardening_sysctl:
net.ipv4.conf.all.accept_redirects: 0
net.ipv4.conf.default.accept_redirects: 0
net.ipv4.conf.all.secure_redirects: 0
net.ipv4.conf.default.secure_redirects: 0
net.ipv4.conf.all.send_redirects: 0
net.ipv4.conf.default.send_redirects: 0
net.ipv4.conf.all.accept_source_route: 0
net.ipv4.conf.default.accept_source_route: 0
net.ipv4.conf.all.log_martians: 1
net.ipv4.conf.default.log_martians: 1
net.ipv4.conf.all.rp_filter: 1
net.ipv4.conf.default.rp_filter: 1
net.ipv4.icmp_echo_ignore_broadcasts: 1
net.ipv4.icmp_ignore_bogus_error_responses: 1
net.ipv4.tcp_syncookies: 1
net.ipv6.conf.all.accept_redirects: 0
net.ipv6.conf.default.accept_redirects: 0
net.ipv6.conf.all.accept_source_route: 0
net.ipv6.conf.all.accept_ra: 0
net.ipv6.conf.default.accept_ra: 0
kernel.dmesg_restrict: 1
kernel.kptr_restrict: 2
kernel.sysrq: 0
kernel.yama.ptrace_scope: 1
kernel.randomize_va_space: 2
fs.protected_hardlinks: 1
fs.protected_symlinks: 1
fs.protected_fifos: 2
fs.protected_regular: 2
fs.suid_dumpable: 0
# Interdire les core dumps.
os_hardening_disable_core_dumps: true
# Politique des NOUVEAUX comptes (login.defs) — n'affecte pas les comptes existants.
os_hardening_login_defs:
PASS_MAX_DAYS: "365"
PASS_MIN_DAYS: "1"
PASS_WARN_AGE: "14"
UMASK: "027"
# Journalisation des appels système sensibles.
os_hardening_auditd: true
# Services inutiles sur un serveur, désactivés s'ils sont présents.
os_hardening_disable_services:
- avahi-daemon
- cups
- bluetooth
# Paquets à retirer (protocoles en clair).
os_hardening_remove_packages:
- telnet
- rsh-client
- rsh-redone-client
- talk
- nis
- ypbind
# Restreindre « su » aux membres du groupe sudo/wheel (pam_wheel).
# false par défaut : les comptes sans sudo doivent pouvoir faire « su - »
# (cas des VM Terraform où sudo est purgé).
os_hardening_restrict_su: false
+114
View File
@@ -0,0 +1,114 @@
---
- name: Appliquer les paramètres noyau
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
sysctl_file: /etc/sysctl.d/99-durcissement.conf
sysctl_set: true
reload: true
state: present
loop: "{{ os_hardening_sysctl | dict2items }}"
loop_control:
label: "{{ item.key }}"
# Certaines clés n'existent pas partout (yama, IPv6 désactivé) : ne pas bloquer.
failed_when: false
- name: Interdire les core dumps (limits)
ansible.builtin.copy:
dest: /etc/security/limits.d/99-durcissement.conf
content: |
# {{ ansible_managed }}
* hard core 0
* soft core 0
owner: root
group: root
mode: "0644"
when: os_hardening_disable_core_dumps | bool
- name: Créer le répertoire coredump.conf.d
ansible.builtin.file:
path: /etc/systemd/coredump.conf.d
state: directory
owner: root
group: root
mode: "0755"
when: os_hardening_disable_core_dumps | bool
- name: Interdire les core dumps (systemd-coredump)
ansible.builtin.copy:
dest: /etc/systemd/coredump.conf.d/99-durcissement.conf
content: |
# {{ ansible_managed }}
[Coredump]
Storage=none
ProcessSizeMax=0
owner: root
group: root
mode: "0644"
when: os_hardening_disable_core_dumps | bool
- name: Politique des nouveaux comptes (login.defs)
ansible.builtin.lineinfile:
path: /etc/login.defs
regexp: '^\s*#?\s*{{ item.key }}\s'
line: "{{ item.key }}\t{{ item.value }}"
loop: "{{ os_hardening_login_defs | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: Installer et activer auditd
ansible.builtin.package:
name: "{{ 'auditd' if ansible_facts['os_family'] == 'Debian' else 'audit' }}"
state: present
when: os_hardening_auditd | bool
- name: Démarrer auditd
ansible.builtin.systemd:
name: auditd
enabled: true
state: started
when: os_hardening_auditd | bool
- name: Recenser les services
ansible.builtin.service_facts:
- name: Désactiver les services inutiles présents
ansible.builtin.systemd:
name: "{{ item }}"
enabled: false
state: stopped
masked: true
loop: "{{ os_hardening_disable_services }}"
when: (item ~ '.service') in ansible_facts['services']
- name: Retirer les paquets aux protocoles en clair
ansible.builtin.package:
name: "{{ os_hardening_remove_packages }}"
state: absent
- name: Restreindre « su » au groupe sudo/wheel
ansible.builtin.lineinfile:
path: /etc/pam.d/su
regexp: '^\s*#?\s*auth\s+required\s+pam_wheel\.so'
line: "auth required pam_wheel.so use_uid group={{ common_sudo_group_effective }}"
insertafter: '^auth\s+sufficient\s+pam_rootok\.so'
when: os_hardening_restrict_su | bool
- name: Protéger les fichiers sensibles
ansible.builtin.file:
path: "{{ item.path }}"
owner: root
group: root
mode: "{{ item.mode }}"
loop:
- { path: /etc/crontab, mode: "0600" }
- { path: /etc/cron.d, mode: "0700" }
- { path: /etc/cron.daily, mode: "0700" }
- { path: /etc/cron.hourly, mode: "0700" }
- { path: /etc/cron.weekly, mode: "0700" }
- { path: /etc/cron.monthly, mode: "0700" }
- { path: /etc/ssh/sshd_config, mode: "0600" }
loop_control:
label: "{{ item.path }}"
failed_when: false
+10
View File
@@ -0,0 +1,10 @@
---
# Rôle proxy — reprend 02-apt/02proxy et les exports du README d'origine.
proxy_url: ""
proxy_no_proxy:
- localhost
- 127.0.0.1
# Le gestionnaire de paquets passe toujours par le proxy.
# L'environnement (profile.d) est optionnel : utile pour wget/curl/pip.
proxy_configure_environment: true
+41
View File
@@ -0,0 +1,41 @@
---
- name: Vérifier que l'URL du proxy est renseignée
ansible.builtin.assert:
that: proxy_url is match('^https?://')
fail_msg: "proxy_url doit être une URL http(s)://hôte:port"
- name: Configurer le proxy APT
ansible.builtin.template:
src: 02proxy.j2
dest: /etc/apt/apt.conf.d/02proxy
owner: root
group: root
mode: "0644"
when: ansible_facts['os_family'] == 'Debian'
- name: Configurer le proxy DNF
community.general.ini_file:
path: /etc/dnf/dnf.conf
section: main
option: proxy
value: "{{ proxy_url }}"
no_extra_spaces: true
owner: root
group: root
mode: "0644"
when: ansible_facts['os_family'] == 'RedHat'
- name: Exposer le proxy dans l'environnement des sessions
ansible.builtin.template:
src: proxy.sh.j2
dest: /etc/profile.d/proxy.sh
owner: root
group: root
mode: "0644"
when: proxy_configure_environment | bool
- name: Retirer le proxy de l'environnement des sessions
ansible.builtin.file:
path: /etc/profile.d/proxy.sh
state: absent
when: not proxy_configure_environment | bool
+4
View File
@@ -0,0 +1,4 @@
// {{ ansible_managed }}
Acquire::http::proxy "{{ proxy_url }}";
Acquire::https::proxy "{{ proxy_url }}";
Acquire::ftp::proxy "{{ proxy_url }}";
+6
View File
@@ -0,0 +1,6 @@
# {{ ansible_managed }}
export http_proxy="{{ proxy_url }}"
export https_proxy="{{ proxy_url }}"
export ftp_proxy="{{ proxy_url }}"
export no_proxy="{{ proxy_no_proxy | join(',') }}"
export HTTP_PROXY="$http_proxy" HTTPS_PROXY="$https_proxy" FTP_PROXY="$ftp_proxy" NO_PROXY="$no_proxy"
+18
View File
@@ -0,0 +1,18 @@
---
# Rôle snmp — reprend 06-snmp (communautés v2c par source de supervision)
# et ajoute SNMPv3 en option.
snmp_listen: "udp:161,udp6:[::1]:161"
snmp_sys_location: "Datacenter"
snmp_sys_contact: "[email protected]"
# Communautés v2c en lecture seule : name (depuis le vault), sources (liste ;
# vide = toutes). Reprise de l'ancien snmpd.conf.
snmp_communities: []
# Utilisateurs SNMPv3 (lecture seule) : name, auth_pass, priv_pass, et en
# option auth (SHA) / priv (AES). Créés une fois dans la base persistante.
snmp_v3_users: []
# Lignes « extend » (ex. docker-stats). Le script doit être déployé à part.
snmp_extends: []
# - { name: docker, command: /etc/snmp/docker-stats.py }
+5
View File
@@ -0,0 +1,5 @@
---
- name: Redémarrer snmpd
ansible.builtin.systemd:
name: snmpd
state: restarted
+30
View File
@@ -0,0 +1,30 @@
---
- name: Installer snmpd
ansible.builtin.package:
name: "{{ snmp_packages[ansible_facts['os_family']] }}"
state: present
- name: Vérifier qu'au moins un accès est défini
ansible.builtin.assert:
that: (snmp_communities | length) + (snmp_v3_users | length) > 0
fail_msg: "snmp_communities et snmp_v3_users sont vides : aucun superviseur ne pourrait interroger l'agent"
- name: Déposer snmpd.conf
ansible.builtin.template:
src: snmpd.conf.j2
dest: /etc/snmp/snmpd.conf
owner: root
group: root
mode: "0600"
no_log: true
notify: Redémarrer snmpd
- name: Créer les utilisateurs SNMPv3
ansible.builtin.include_tasks: v3.yml
when: snmp_v3_users | length > 0
- name: Activer et démarrer snmpd
ansible.builtin.systemd:
name: snmpd
enabled: true
state: started
+40
View File
@@ -0,0 +1,40 @@
---
# createUser n'est lu qu'au démarrage : le service est arrêté, la ligne est
# ajoutée à la base persistante, puis snmpd la convertit en clés au redémarrage.
- name: Vérifier quels utilisateurs SNMPv3 existent déjà
ansible.builtin.shell:
cmd: >-
set -o pipefail;
hex=$(printf '%s' '{{ item.name }}' | od -An -tx1 | tr -d ' \n');
grep -c -e "createUser {{ item.name }} " -e "usmUser .*0x${hex} " {{ snmp_persistent_conf[ansible_facts['os_family']] }}
executable: /bin/bash
loop: "{{ snmp_v3_users }}"
loop_control:
label: "{{ item.name }}"
register: snmp_v3_existing
changed_when: false
failed_when: false
no_log: true
- name: Arrêter snmpd avant la création des utilisateurs
ansible.builtin.systemd:
name: snmpd
state: stopped
when: snmp_v3_existing.results | selectattr('rc', 'ne', 0) | list | length > 0
- name: Déclarer les utilisateurs SNMPv3 manquants
ansible.builtin.lineinfile:
path: "{{ snmp_persistent_conf[ansible_facts['os_family']] }}"
line: >-
createUser {{ item.item.name }} {{ item.item.auth | default('SHA') }} "{{ item.item.auth_pass }}"
{{ item.item.priv | default('AES') }} "{{ item.item.priv_pass }}"
create: true
owner: root
group: root
mode: "0600"
loop: "{{ snmp_v3_existing.results }}"
loop_control:
label: "{{ item.item.name }}"
when: item.rc != 0
no_log: true
notify: Redémarrer snmpd
+26
View File
@@ -0,0 +1,26 @@
# {{ ansible_managed }}
agentaddress {{ snmp_listen }}
sysLocation {{ snmp_sys_location }}
sysContact {{ snmp_sys_contact }}
sysServices 72
# Lecture seule, arbre complet.
view systemonly included .1
{% for c in snmp_communities %}
{% if c.sources | default([]) | length == 0 %}
rocommunity {{ c.name }} default -V systemonly
{% else %}
{% for src in c.sources %}
rocommunity{{ '6' if ':' in src else '' }} {{ c.name }} {{ src }} -V systemonly
{% endfor %}
{% endif %}
{% endfor %}
{% for u in snmp_v3_users %}
rouser {{ u.name }} priv -V systemonly
{% endfor %}
{% for e in snmp_extends %}
extend {{ e.name }} {{ e.command }}
{% endfor %}
+7
View File
@@ -0,0 +1,7 @@
---
snmp_packages:
Debian: [snmpd, snmp]
RedHat: [net-snmp, net-snmp-utils]
snmp_persistent_conf:
Debian: /var/lib/snmp/snmpd.conf
RedHat: /var/lib/net-snmp/snmpd.conf
+84
View File
@@ -0,0 +1,84 @@
---
# Rôle ssh — reprend 01-ssh (sshd_config, ssh_config, banner, motd, clés
# autorisées) et le durcissement du projet terraform-proxmox-serveur-debian-12.
#
# Principe : les accès distants sont exclusivement en SSH par clé publique ;
# le mot de passe des comptes reste utilisable en local (console, su),
# le rôle ne le verrouille jamais.
ssh_port: 22
# yes | prohibit-password | no
# prohibit-password : root par clé uniquement (nécessaire si Ansible se
# connecte en root) ; le mot de passe root reste utilisable sur la console.
ssh_permit_root_login: prohibit-password
# Comptes autorisés en SSH. Chaque entrée : name (obligatoire), groups (liste,
# « sudo_or_wheel » est traduit selon l'OS), sudo_nopasswd (bool), shell,
# password_hash (optionnel, pour un mot de passe local — jamais en clair).
# Les clés publiques sont lues dans files/keys/<name>/*.pub.
ssh_users:
- name: root
ssh_keys_dir: "{{ playbook_dir }}/files/keys"
# Clés centralisées dans /etc/ssh/authorized_keys/<utilisateur>, appartenant à
# root : un utilisateur ne peut pas ajouter de clé lui-même (reprise de
# l'ancien script, qui posait un lien symbolique vers /root/.ssh).
# false : clés classiques dans ~/.ssh/authorized_keys.
ssh_central_authorized_keys: true
# Groupes supplémentaires autorisés en SSH (en plus des ssh_users).
ssh_allow_groups: []
# Durcissement du démon.
ssh_max_auth_tries: 3
ssh_max_sessions: 4
ssh_max_startups: "10:30:60"
ssh_login_grace_time: 30
ssh_client_alive_interval: 300
ssh_client_alive_count_max: 2
ssh_x11_forwarding: false
ssh_agent_forwarding: false
ssh_tcp_forwarding: false
ssh_log_level: VERBOSE
# Algorithmes (Debian/Ubuntu uniquement : sous Red Hat ils sont imposés par
# crypto-policies, ne pas les redéfinir dans sshd_config).
ssh_kex_algorithms:
- [email protected]
- curve25519-sha256
- [email protected]
- diffie-hellman-group16-sha512
- diffie-hellman-group18-sha512
ssh_ciphers:
- [email protected]
- [email protected]
- [email protected]
- aes256-ctr
ssh_macs:
- [email protected]
- [email protected]
- [email protected]
ssh_host_key_algorithms:
- ssh-ed25519
- [email protected]
- rsa-sha2-512
- rsa-sha2-256
ssh_pubkey_accepted_algorithms: "{{ ssh_host_key_algorithms }}"
# Bannière d'avertissement (avant authentification) et MOTD (après).
ssh_banner: true
ssh_banner_organisation: "l'organisation"
# Logo ASCII affiché en tête de bannière (chaîne multiligne, vide = pas de logo).
ssh_banner_logo: ""
ssh_motd: true
# Sous Ubuntu : désactiver les MOTD publicitaires (motd-news, help-text).
ssh_motd_disable_ubuntu_extras: true
# Durcissement du client ssh (ssh_config) : pas d'agent forwarding ni de X11
# par défaut, known_hosts hachés.
ssh_client_hardening: true
# Après redémarrage du démon, ouvrir une NOUVELLE connexion depuis le
# contrôleur pour vérifier qu'on ne s'est pas enfermé dehors.
ssh_verify_new_connection: true
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# Géré par Ansible (rôle ssh) — repris de 01-ssh/00-basic.
# get load averages
IFS=" " read LOAD1 LOAD5 LOAD15 <<<$(awk '{ print $1,$2,$3 }' /proc/loadavg)
# get free memory
IFS=" " read USED AVAIL TOTAL <<<$(free -htm | awk '/Mem/ { print $3,$7,$2 }')
# get processes
PROCESS=$(ps -eo user=|sort|uniq -c | awk '{ print $2 " " $1 }')
PROCESS_ALL=$(echo "$PROCESS"| awk {'print $2'} | awk '{ SUM += $1} END { print SUM }')
PROCESS_ROOT=$(echo "$PROCESS" | awk '/root/ { print $2}')
PROCESS_USER=$(echo "$PROCESS" | awk '!/root/ { SUM += $2} END { print SUM }')
# get processors
PROCESSOR_NAME=$(awk -F": " '/model name/ { print $2 }' /proc/cpuinfo | head -1)
PROCESSOR_COUNT=$(grep -ioPc 'processor\t:' /proc/cpuinfo)
# colors
W="\e[0;39m"
G="\e[1;32m"
R="\e[1;31m"
dim="\e[2m"
undim="\e[0m"
echo -e "${W}System info:
$W Hostname$dim····$undim: $W${HOSTNAME}
$W Distro$dim······$undim: $W$(. /etc/os-release; echo "$PRETTY_NAME")
$W Kernel$dim······$undim: $W$(uname -sr)
$W Uptime$dim······$undim: $W$(uptime -p)
$W Load$dim········$undim: $G$LOAD1$W (1m), $G$LOAD5$W (5m), $G$LOAD15$W (15m)
$W Processes$dim···$undim: $G$PROCESS_ROOT$W (root), $G$PROCESS_USER$W (user), $G$PROCESS_ALL$W (total)
$W CPU$dim·········$undim: $W$PROCESSOR_NAME ($G$PROCESSOR_COUNT$W vCPU)
$W Memory$dim······$undim: $G$USED$W used, $G$AVAIL$W avail, $G$TOTAL$W total"
# config
max_usage=90
bar_width=50
# disk usage: ignore zfs, squashfs & tmpfs
printf "\nDisk usage:\n"
while read line; do
# get disk usage
usage=$(echo "$line" | awk '{print $2}' | sed 's/%//')
used_width=$((($usage*$bar_width)/100))
# color is green if usage < max_usage, else red
if [ "${usage}" -ge "${max_usage}" ]; then
color=$R
else
color=$G
fi
# print green/red bar until used_width
bar="[${color}"
for ((i=0; i<$used_width; i++)); do
bar+="="
done
# print dimmmed bar until end
bar+="${W}${dim}"
for ((i=$used_width; i<$bar_width; i++)); do
bar+="·"
done
bar+="${undim}]"
# print usage line & bar
echo "${line}" | awk '{ printf("%-31s%+3s used out of %+4s\n", $1, $2, $3); }' | sed -e 's/^/ /'
echo -e "${bar}" | sed -e 's/^/ /'
done < <(df -H -x zfs -x squashfs -x tmpfs -x devtmpfs -x overlay -x nfs -x nfs4 -x cifs --output=target,pcent,size | tail -n+2)
printf "\n"
+5
View File
@@ -0,0 +1,5 @@
---
- name: Redémarrer sshd
ansible.builtin.systemd:
name: "{{ ssh_service_name[ansible_facts['os_family']] }}"
state: restarted
+77
View File
@@ -0,0 +1,77 @@
---
- name: Déposer la bannière d'avertissement (SSH et console)
ansible.builtin.template:
src: banner.j2
dest: "{{ item }}"
owner: root
group: root
mode: "0644"
loop:
- /etc/issue.net
- /etc/issue
when: ssh_banner | bool
- name: "Déposer le script MOTD (Debian/Ubuntu : update-motd.d)"
ansible.builtin.copy:
src: 00-basic
dest: /etc/update-motd.d/00-basic
owner: root
group: root
mode: "0755"
when:
- ssh_motd | bool
- ansible_facts['os_family'] == 'Debian'
- name: Désactiver les MOTD annexes d'Ubuntu
ansible.builtin.file:
path: "/etc/update-motd.d/{{ item }}"
mode: "0644"
loop:
- 10-help-text
- 50-motd-news
- 50-landscape-sysinfo
- 88-esm-announce
- 91-contract-ua-esm-status
failed_when: false
when:
- ssh_motd | bool
- ssh_motd_disable_ubuntu_extras | bool
- ansible_facts['distribution'] == 'Ubuntu'
- name: Désactiver motd-news (Ubuntu)
ansible.builtin.lineinfile:
path: /etc/default/motd-news
regexp: '^ENABLED='
line: "ENABLED=0"
create: true
owner: root
group: root
mode: "0644"
when:
- ssh_motd | bool
- ssh_motd_disable_ubuntu_extras | bool
- ansible_facts['distribution'] == 'Ubuntu'
- name: "Déposer le script MOTD (Red Hat : profile.d)"
ansible.builtin.copy:
src: 00-basic
dest: /usr/local/bin/motd-basic
owner: root
group: root
mode: "0755"
when:
- ssh_motd | bool
- ansible_facts['os_family'] == 'RedHat'
- name: Appeler le MOTD à l'ouverture d'un shell interactif (Red Hat)
ansible.builtin.copy:
dest: /etc/profile.d/zz-motd.sh
content: |
# {{ ansible_managed }}
[ -n "$PS1" ] && [ -x /usr/local/bin/motd-basic ] && /usr/local/bin/motd-basic
owner: root
group: root
mode: "0644"
when:
- ssh_motd | bool
- ansible_facts['os_family'] == 'RedHat'
+44
View File
@@ -0,0 +1,44 @@
---
- name: Créer le répertoire central des clés autorisées
ansible.builtin.file:
path: "{{ ssh_central_keys_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Écrire le fichier de clés de chaque compte
ansible.builtin.template:
src: authorized_keys.j2
dest: "{{ ssh_central_keys_dir }}/{{ item.name }}"
owner: root
group: root
mode: "0644"
loop: "{{ ssh_users }}"
loop_control:
label: "{{ item.name }}"
when: ssh_users_keys[item.name] | length > 0
- name: Supprimer le fichier central d'un compte sans clé
ansible.builtin.file:
path: "{{ ssh_central_keys_dir }}/{{ item.name }}"
state: absent
loop: "{{ ssh_users }}"
loop_control:
label: "{{ item.name }}"
when: ssh_users_keys[item.name] | length == 0
- name: Lister les fichiers de clés d'anciens comptes
ansible.builtin.find:
paths: "{{ ssh_central_keys_dir }}"
file_type: file
register: ssh_central_existing
- name: Supprimer les fichiers de clés des comptes qui ne sont plus gérés
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ ssh_central_existing.files }}"
loop_control:
label: "{{ item.path | basename }}"
when: item.path | basename not in ssh_allow_users_effective
+30
View File
@@ -0,0 +1,30 @@
---
- name: Installer le serveur OpenSSH
ansible.builtin.package:
name: openssh-server
state: present
- name: Préparer les comptes et leurs clés
ansible.builtin.import_tasks: users.yml
- name: Configurer le démon sshd
ansible.builtin.import_tasks: sshd.yml
- name: Déployer la bannière et le MOTD
ansible.builtin.import_tasks: banner_motd.yml
- name: Durcir le client ssh
ansible.builtin.template:
src: ssh_config.j2
dest: "{{ ssh_client_dropin_file }}"
owner: root
group: root
mode: "0644"
when: ssh_client_hardening | bool
- name: Appliquer la configuration sshd maintenant
ansible.builtin.meta: flush_handlers
- name: Vérifier qu'une nouvelle connexion SSH aboutit
ansible.builtin.import_tasks: verify.yml
when: ssh_verify_new_connection | bool
+138
View File
@@ -0,0 +1,138 @@
---
- name: Lire la version du paquet openssh-server
ansible.builtin.package_facts:
manager: auto
- name: Extraire la version d'OpenSSH
ansible.builtin.set_fact:
ssh_version: "{{ ansible_facts['packages']['openssh-server'][0].version | regex_search('([0-9]+\\.[0-9]+)') }}"
- name: Déterminer si sshd prend en charge Include (OpenSSH >= 8.2)
ansible.builtin.set_fact:
ssh_supports_include: "{{ ssh_version is version('8.2', '>=') }}"
- name: Sauvegarder le sshd_config d'origine une seule fois
ansible.builtin.copy:
src: /etc/ssh/sshd_config
dest: /etc/ssh/sshd_config.avant-ansible
remote_src: true
force: false
owner: root
group: root
mode: "0600"
- name: Appliquer le durcissement sshd
block:
- name: Créer le répertoire des drop-ins sshd
ansible.builtin.file:
path: "{{ ssh_dropin_dir }}"
state: directory
owner: root
group: root
mode: "0755"
when: ssh_supports_include | bool
- name: S'assurer que sshd_config inclut le répertoire des drop-ins
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^\s*Include\s+/etc/ssh/sshd_config\.d/\*\.conf'
line: "Include /etc/ssh/sshd_config.d/*.conf"
insertbefore: BOF
when: ssh_supports_include | bool
notify: Redémarrer sshd
- name: Déposer le drop-in de durcissement
ansible.builtin.template:
src: sshd_durcissement.conf.j2
dest: "{{ ssh_dropin_file }}"
owner: root
group: root
mode: "0600"
when: ssh_supports_include | bool
notify: Redémarrer sshd
- name: Insérer le durcissement en tête de sshd_config (OpenSSH < 8.2)
ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config
block: "{{ lookup('ansible.builtin.template', 'sshd_durcissement.conf.j2') }}"
marker: "# {mark} DURCISSEMENT ANSIBLE"
insertbefore: BOF
when: not ssh_supports_include | bool
notify: Redémarrer sshd
- name: Neutraliser dans sshd_config les directives qui contrediraient le durcissement
ansible.builtin.replace:
path: /etc/ssh/sshd_config
regexp: '^(\s*(?:PermitRootLogin|PasswordAuthentication|AuthorizedKeysFile|X11Forwarding|Banner|PrintMotd)\s.*)$'
replace: '#\1'
after: "# END DURCISSEMENT ANSIBLE"
when: not ssh_supports_include | bool
notify: Redémarrer sshd
- name: Valider la configuration sshd
ansible.builtin.command:
cmd: sshd -t
changed_when: false
rescue:
- name: Retirer le drop-in invalide
ansible.builtin.file:
path: "{{ ssh_dropin_file }}"
state: absent
when: ssh_supports_include | bool
- name: Restaurer sshd_config
ansible.builtin.copy:
src: /etc/ssh/sshd_config.avant-ansible
dest: /etc/ssh/sshd_config
remote_src: true
owner: root
group: root
mode: "0600"
when: not ssh_supports_include | bool
- name: Arrêter sur configuration sshd invalide
ansible.builtin.fail:
msg: "sshd -t a rejeté la configuration : elle a été retirée, le démon n'a pas été redémarré."
- name: Autoriser le port SSH dans SELinux
community.general.seport:
ports: "{{ ssh_port }}"
proto: tcp
setype: ssh_port_t
state: present
when:
- ansible_facts['os_family'] == 'RedHat'
- ansible_facts['selinux'].status | default('disabled') == 'enabled'
- ssh_port | int != 22
- name: Désactiver l'activation par socket (Ubuntu >= 22.10)
ansible.builtin.systemd:
name: ssh.socket
enabled: false
state: stopped
when:
- ansible_facts['distribution'] == 'Ubuntu'
- ansible_facts['distribution_version'] is version('22.10', '>=')
notify: Redémarrer sshd
- name: Lister les clés privées d'hôte
ansible.builtin.find:
paths: /etc/ssh
patterns: "ssh_host_*_key"
register: ssh_host_keys
- name: Restreindre les droits des clés privées d'hôte
ansible.builtin.file:
path: "{{ item.path }}"
owner: root
group: root
mode: "0600"
loop: "{{ ssh_host_keys.files }}"
loop_control:
label: "{{ item.path | basename }}"
- name: Activer et démarrer sshd
ansible.builtin.systemd:
name: "{{ ssh_service_name[ansible_facts['os_family']] }}"
enabled: true
state: started
+89
View File
@@ -0,0 +1,89 @@
---
- name: Recenser les clés publiques disponibles par compte
ansible.builtin.set_fact:
ssh_users_keys: >-
{{ ssh_users_keys | default({}) | combine({
item.name: lookup('ansible.builtin.fileglob', ssh_keys_dir ~ '/' ~ item.name ~ '/*.pub', wantlist=True)
}) }}
loop: "{{ ssh_users }}"
loop_control:
label: "{{ item.name }}"
- name: Calculer la liste AllowUsers
ansible.builtin.set_fact:
ssh_allow_users_effective: "{{ ssh_users | map(attribute='name') | list }}"
- name: Refuser une configuration qui enfermerait Ansible dehors
ansible.builtin.assert:
that:
- ssh_users | length > 0
- (ssh_users_keys | default({})).values() | map('length') | sum > 0
- ssh_ansible_login in ssh_allow_users_effective or ssh_allow_groups | length > 0
- ssh_users_keys[ssh_ansible_login] | default([]) | length > 0 or ssh_allow_groups | length > 0
- not (ssh_ansible_login == 'root' and ssh_permit_root_login == 'no')
fail_msg: >-
Le compte de connexion Ansible « {{ ssh_ansible_login }} » doit figurer dans ssh_users
avec au moins une clé dans {{ ssh_keys_dir }}/{{ ssh_ansible_login }}/*.pub,
et ssh_permit_root_login ne peut pas valoir « no » si Ansible se connecte en root.
vars:
ssh_ansible_login: "{{ ansible_user | default('root') }}"
- name: Créer les comptes (hors root)
ansible.builtin.user:
name: "{{ item.name }}"
shell: "{{ item.shell | default('/bin/bash') }}"
groups: "{{ item.groups | default([]) | map('replace', 'sudo_or_wheel', common_sudo_group_effective) | list }}"
append: true
password: "{{ item.password_hash | default(omit) }}"
create_home: true
state: present
loop: "{{ ssh_users }}"
loop_control:
label: "{{ item.name }}"
when: item.name != 'root'
no_log: true
- name: Autoriser sudo sans mot de passe pour les comptes de service
ansible.builtin.copy:
dest: "/etc/sudoers.d/90-{{ item.name }}"
content: |
# {{ ansible_managed }}
{{ item.name }} ALL=(ALL) NOPASSWD:ALL
owner: root
group: root
mode: "0440"
validate: /usr/sbin/visudo -cf %s
loop: "{{ ssh_users }}"
loop_control:
label: "{{ item.name }}"
when:
- item.name != 'root'
- item.sudo_nopasswd | default(false) | bool
- name: Retirer le sudo sans mot de passe des comptes qui ne le demandent plus
ansible.builtin.file:
path: "/etc/sudoers.d/90-{{ item.name }}"
state: absent
loop: "{{ ssh_users }}"
loop_control:
label: "{{ item.name }}"
when:
- item.name != 'root'
- not (item.sudo_nopasswd | default(false) | bool)
- name: Déployer les clés autorisées (répertoire central)
ansible.builtin.import_tasks: keys_central.yml
when: ssh_central_authorized_keys | bool
- name: Déployer les clés autorisées (~/.ssh)
ansible.posix.authorized_key:
user: "{{ item.name }}"
key: "{{ ssh_users_keys[item.name] | map('ansible.builtin.file') | join('\n') }}"
exclusive: true
manage_dir: true
loop: "{{ ssh_users }}"
loop_control:
label: "{{ item.name }}"
when:
- not ssh_central_authorized_keys | bool
- ssh_users_keys[item.name] | length > 0
+26
View File
@@ -0,0 +1,26 @@
---
- name: Construire la commande de vérification
ansible.builtin.set_fact:
ssh_verify_argv: >-
{{ ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15', '-o', 'StrictHostKeyChecking=accept-new',
'-o', 'ControlPath=none', '-p', ssh_port | string]
+ (['-i', ansible_ssh_private_key_file] if ansible_ssh_private_key_file is defined else [])
+ [(ansible_user | default('root')) ~ '@' ~ (ansible_host | default(inventory_hostname)), 'true'] }}
- name: Ouvrir une nouvelle session SSH depuis le contrôleur
ansible.builtin.command:
argv: "{{ ssh_verify_argv }}"
delegate_to: localhost
become: false
register: ssh_verify
changed_when: false
failed_when: false
- name: Signaler un échec de connexion après durcissement
ansible.builtin.fail:
msg: >-
Une NOUVELLE connexion SSH vers {{ inventory_hostname }} échoue (code {{ ssh_verify.rc }}).
La session courante reste ouverte : vérifier {{ ssh_dropin_file }} et
{{ ssh_central_keys_dir if ssh_central_authorized_keys else '~/.ssh/authorized_keys' }}
avant de la fermer.
when: ssh_verify.rc != 0
+5
View File
@@ -0,0 +1,5 @@
# {{ ansible_managed }}
# Source : files/keys/{{ item.name }}/*.pub
{% for key_file in ssh_users_keys[item.name] | sort %}
{{ lookup('ansible.builtin.file', key_file) | trim }}
{% endfor %}
+16
View File
@@ -0,0 +1,16 @@
{% if ssh_banner_logo | length > 0 %}
{{ ssh_banner_logo | regex_replace('\\s+$', '') }}
{% endif %}
AVERTISSEMENT : L'accès à ce système est réservé aux utilisateurs
dûment autorisés.
Toute tentative d'accès à ce système sans autorisation, ou de maintien
frauduleux dans ce système, fera l'objet de poursuites conformément à
la politique de {{ ssh_banner_organisation }}.
Tout utilisateur autorisé est informé et reconnaît que ses actions
peuvent être enregistrées, conservées et auditées conformément aux
chartes et politiques internes de {{ ssh_banner_organisation }}.
+10
View File
@@ -0,0 +1,10 @@
# {{ ansible_managed }}
Host *
ForwardAgent no
ForwardX11 no
HashKnownHosts yes
GSSAPIAuthentication no
StrictHostKeyChecking ask
SendEnv LANG LC_*
ServerAliveInterval 60
ServerAliveCountMax 3
@@ -0,0 +1,62 @@
# {{ ansible_managed }}
# Première valeur lue = valeur retenue : ce fichier est lu avant sshd_config.
Port {{ ssh_port }}
LogLevel {{ ssh_log_level }}
# Authentification : clé publique uniquement, jamais de mot de passe à distance.
PermitRootLogin {{ ssh_permit_root_login }}
PubkeyAuthentication yes
AuthenticationMethods publickey
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
HostbasedAuthentication no
IgnoreRhosts yes
StrictModes yes
UsePAM yes
{% if ssh_central_authorized_keys %}
AuthorizedKeysFile {{ ssh_central_keys_dir }}/%u
{% else %}
AuthorizedKeysFile .ssh/authorized_keys
{% endif %}
AllowUsers {{ ssh_allow_users_effective | join(' ') }}
{% if ssh_allow_groups | length > 0 %}
AllowGroups {{ ssh_allow_groups | join(' ') }}
{% endif %}
# Limites anti-force-brute et sessions inactives.
MaxAuthTries {{ ssh_max_auth_tries }}
MaxSessions {{ ssh_max_sessions }}
MaxStartups {{ ssh_max_startups }}
LoginGraceTime {{ ssh_login_grace_time }}
ClientAliveInterval {{ ssh_client_alive_interval }}
ClientAliveCountMax {{ ssh_client_alive_count_max }}
# Surface réduite.
X11Forwarding {{ 'yes' if ssh_x11_forwarding else 'no' }}
AllowAgentForwarding {{ 'yes' if ssh_agent_forwarding else 'no' }}
AllowTcpForwarding {{ 'yes' if ssh_tcp_forwarding else 'no' }}
PermitTunnel no
GatewayPorts no
PermitUserEnvironment no
UseDNS no
{% if ansible_facts['os_family'] == 'Debian' %}
DebianBanner no
# Algorithmes (Red Hat : gérés par crypto-policies).
KexAlgorithms {{ ssh_kex_algorithms | join(',') }}
Ciphers {{ ssh_ciphers | join(',') }}
MACs {{ ssh_macs | join(',') }}
HostKeyAlgorithms {{ ssh_host_key_algorithms | join(',') }}
PubkeyAcceptedAlgorithms {{ ssh_pubkey_accepted_algorithms | join(',') }}
{% endif %}
# Affichage.
{% if ssh_banner %}
Banner /etc/issue.net
{% else %}
Banner none
{% endif %}
PrintMotd no
PrintLastLog yes
+11
View File
@@ -0,0 +1,11 @@
---
ssh_service_name:
Debian: ssh
RedHat: sshd
ssh_dropin_dir: /etc/ssh/sshd_config.d
# « 00- » : les fichiers inclus sont lus par ordre lexical et la PREMIÈRE
# valeur rencontrée l'emporte. Nos réglages passent donc avant
# 50-cloud-init.conf (Ubuntu) et 50-redhat.conf (Red Hat).
ssh_dropin_file: "{{ ssh_dropin_dir }}/00-durcissement.conf"
ssh_client_dropin_file: /etc/ssh/ssh_config.d/00-durcissement.conf
ssh_central_keys_dir: /etc/ssh/authorized_keys
+10
View File
@@ -0,0 +1,10 @@
---
# Rôle updates — reprend 02-apt (update + full-upgrade) et ajoute les mises à
# jour de sécurité automatiques (unattended-upgrades / dnf-automatic).
updates_upgrade_now: true
updates_automatic: true
# Redémarrage automatique en fin de passage si le noyau l'exige.
# false par défaut : le résumé de fin de playbook signale le besoin.
updates_reboot_if_required: false
# Heure des mises à jour automatiques (dnf-automatic uniquement, format OnCalendar).
updates_automatic_schedule: "*-*-* 04:00"
+4
View File
@@ -0,0 +1,4 @@
---
- name: Recharger systemd
ansible.builtin.systemd:
daemon_reload: true
+45
View File
@@ -0,0 +1,45 @@
---
- name: Mettre à jour tous les paquets (full-upgrade)
ansible.builtin.apt:
update_cache: true
upgrade: full
autoremove: true
autoclean: true
when: updates_upgrade_now | bool
- name: Installer unattended-upgrades
ansible.builtin.apt:
name:
- unattended-upgrades
- apt-listchanges
state: present
when: updates_automatic | bool
- name: Activer les mises à jour automatiques
ansible.builtin.copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
content: |
// {{ ansible_managed }}
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
owner: root
group: root
mode: "0644"
when: updates_automatic | bool
- name: Limiter unattended-upgrades aux mises à jour de sécurité
ansible.builtin.template:
src: 52unattended-upgrades-local.j2
dest: /etc/apt/apt.conf.d/52unattended-upgrades-local
owner: root
group: root
mode: "0644"
when: updates_automatic | bool
- name: Activer le service unattended-upgrades
ansible.builtin.systemd:
name: unattended-upgrades
enabled: true
state: started
when: updates_automatic | bool
+31
View File
@@ -0,0 +1,31 @@
---
- name: Mettre à jour le système
ansible.builtin.include_tasks: "{{ ansible_facts['os_family'] | lower }}.yml"
- name: Déterminer si un redémarrage est nécessaire (Debian)
ansible.builtin.stat:
path: /var/run/reboot-required
register: updates_reboot_file
when: ansible_facts['os_family'] == 'Debian'
- name: Déterminer si un redémarrage est nécessaire (Red Hat)
ansible.builtin.command:
cmd: dnf needs-restarting -r
register: updates_needs_restarting
changed_when: false
failed_when: updates_needs_restarting.rc not in [0, 1]
when: ansible_facts['os_family'] == 'RedHat'
- name: Exposer l'état de redémarrage
ansible.builtin.set_fact:
updates_reboot_required: >-
{{ (updates_reboot_file.stat.exists | default(false))
or (updates_needs_restarting.rc | default(0) == 1) }}
- name: Redémarrer le serveur
ansible.builtin.reboot:
msg: "Redémarrage planifié par Ansible (mises à jour)"
reboot_timeout: 600
when:
- updates_reboot_if_required | bool
- updates_reboot_required | bool
+65
View File
@@ -0,0 +1,65 @@
---
- name: Mettre à jour tous les paquets # noqa: package-latest — mise à jour complète voulue
ansible.builtin.dnf:
name: "*"
state: latest
update_cache: true
when: updates_upgrade_now | bool
- name: Installer dnf-automatic
ansible.builtin.dnf:
name: dnf-automatic
state: present
when: updates_automatic | bool
- name: Limiter dnf-automatic aux mises à jour de sécurité
community.general.ini_file:
path: /etc/dnf/automatic.conf
section: "{{ item.section }}"
option: "{{ item.option }}"
value: "{{ item.value }}"
no_extra_spaces: true
owner: root
group: root
mode: "0644"
loop:
- { section: commands, option: upgrade_type, value: security }
- { section: commands, option: apply_updates, value: "yes" }
- { section: commands, option: download_updates, value: "yes" }
- { section: emitters, option: emit_via, value: stdio }
loop_control:
label: "{{ item.option }}"
when: updates_automatic | bool
- name: Créer le répertoire de surcharge du timer
ansible.builtin.file:
path: /etc/systemd/system/dnf-automatic.timer.d
state: directory
owner: root
group: root
mode: "0755"
when: updates_automatic | bool
- name: Planifier dnf-automatic
ansible.builtin.copy:
dest: /etc/systemd/system/dnf-automatic.timer.d/override.conf
content: |
# {{ ansible_managed }}
[Timer]
OnCalendar=
OnCalendar={{ updates_automatic_schedule }}
RandomizedDelaySec=30m
owner: root
group: root
mode: "0644"
when: updates_automatic | bool
notify: Recharger systemd
- name: Activer le timer dnf-automatic
ansible.builtin.systemd:
name: dnf-automatic.timer
enabled: true
state: started
daemon_reload: true
when: updates_automatic | bool
@@ -0,0 +1,14 @@
// {{ ansible_managed }}
Unattended-Upgrade::Origins-Pattern {
{% if ansible_facts['distribution'] == 'Ubuntu' %}
"origin=Ubuntu,archive=${distro_codename}-security";
"origin=UbuntuESMApps,archive=${distro_codename}-apps-security";
"origin=UbuntuESM,archive=${distro_codename}-infra-security";
{% else %}
"origin=Debian,codename=${distro_codename},label=Debian-Security";
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
{% endif %}
};
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";