
🌐 Aussi en: English · Deutsch · Español
Partie 3 de la visite Ansible. La partie 1 a traité du cadre, la partie 2 de la plateforme en dessous – système de base et Kubernetes. Cette partie pose le blog par-dessus. Comme toujours, elle décrit la version v3.2.0.
Ce blog, c’est WordPress. Quatre rôles l’amènent d’un cluster Kubernetes vide à un site en marche, configuré et supervisé, et un playbook de mise à jour le tient à jour :
| Rôle / playbook | Ce qu’il fait |
|---|---|
blog_k3s_deploy | Chaque objet Kubernetes du blog : base de données, cache, WordPress, serveur web, routage, tâches planifiées, règles réseau, règles d’images |
blog_wordpress | Configure le WordPress en marche depuis l’extérieur : installation, plugins, réglages, petits plugins must-use, mises à jour |
blog_visitor_stats | Compte les visiteurs chaque nuit à partir des journaux, anonymement, sur le serveur |
blog_verify + common_verify | Tests de fumée à la fin de chaque exécution |
blog-update.yml | La mise à jour de routine : plugins, cœur, images reconstruites – avec un chemin défini en cas d’échec |
Avant tout code, il aide cependant de savoir ce qui est construit et pourquoi chaque pièce est là. Cette partie commence donc par l’architecture.
🏗️ Ce qui tourne, et pourquoi
Suivons une requête. Un lecteur ouvre un article ; la requête arrive sur le port 443 du serveur.
- Le contrôleur Ingress (de la partie 2) termine TLS – il détient le certificat Let’s Encrypt, déchiffre la requête et vérifie quelques règles (limitation de débit sur la page de connexion, chemins bloqués, en-têtes de sécurité). Il transmet ensuite la requête HTTP en clair, à l’intérieur du cluster, au Service WordPress, un nom interne stable pour « le pod qui fait tourner WordPress en ce moment ».
- nginx, dans le pod WordPress, la reçoit. S’il existe déjà une copie HTML toute prête de la page dans le cache de pages, nginx renvoie ce fichier et c’est fini – sans PHP. Les images, le CSS et le JavaScript sont aussi servis directement par nginx, avec de longs en-têtes de cache.
- Ce n’est que si la page doit être générée que nginx passe la requête à PHP-FPM dans le même pod. FPM (« FastCGI Process Manager ») garde une réserve de processus PHP prêts ; l’un d’eux exécute le code PHP de WordPress pour cette requête.
- WordPress a besoin de données : l’article, le menu, des dizaines de réglages. Il interroge d’abord Redis, un stockage en mémoire. Ce qui n’y est pas, il le lit dans MariaDB, la base de données, et le met dans Redis pour la prochaine fois.
Chacune de ces pièces répond à une question concrète :
| Composant | Pourquoi il est là |
|---|---|
| nginx + PHP-FPM dans un pod | L’image officielle wordpress:fpm contient PHP, mais pas de serveur web. nginx sert les fichiers statiques et les pages en cache bien moins cher que PHP ne le pourrait ; FPM ne tourne que lorsque PHP est vraiment nécessaire. Dans le même pod, ils partagent les fichiers de WordPress et communiquent via 127.0.0.1:9000 – un sidecar : un conteneur auxiliaire à côté du principal. |
| Cache de pages (WP Super Cache) | La plupart des visiteurs – et presque tous les bots – sont anonymes et voient la même page. Le plugin stocke le HTML terminé ; nginx le sert sans démarrer PHP. Lors d’une tempête de scanners, c’est la différence entre un serveur calme et un pool FPM plein. |
| Redis (cache d’objets) | WordPress interroge la base de données de nombreuses fois par page – réglages, menus, métadonnées d’articles, traductions. Un cache d’objets garde ces résultats en mémoire d’une requête à l’autre, si bien qu’une page qui doit tout de même être générée demande bien moins de requêtes. Redis est ici un pur cache : rien n’est écrit sur disque, et le perdre ne signifie que quelques chargements de pages plus lents. |
| MariaDB | WordPress stocke tout dans une base de données compatible MySQL : articles, utilisateurs, réglages, données des plugins. Sur le blog, elle tourne comme pod dans le cluster, comme tout le reste – la partie 4 montre pourquoi Nextcloud fait autrement. |
| Répertoires de l’hôte (hostPath) | Les fichiers de WordPress et ceux de la base de données résident dans deux répertoires du serveur, montés dans les pods. Les pods peuvent être recréés à tout moment ; les données restent. La sauvegarde nocturne copie ces répertoires. |
| Un CronJob au lieu de WP-Cron | WordPress a son propre planificateur qui tourne « à la prochaine visite » – peu fiable sur un site calme, et il ralentit ce visiteur. Un CronJob Kubernetes exécute à la place wp-cron.php toutes les cinq minutes. |
| Une tâche de mise à jour nocturne | Mises à jour des plugins, thèmes, traductions et mises à jour mineures du cœur, une fois par nuit, avec d’abord un dump de la base de données. |
| Exporteurs | Deux sidecars de plus traduisent les compteurs internes de MariaDB et de PHP-FPM en métriques pour Prometheus (partie 6). |
| NetworkPolicies et une politique d’images | Des règles qui limitent quel pod peut parler à quel autre, et quelles images de conteneurs ont le droit de tourner. La sécurité, c’est la partie 5 – mais ces deux-là appartiennent au blog, elles sont donc traitées ici. |
Deux mots de Kubernetes reviennent sans cesse plus bas. Un Deployment décrit « garde un pod de ce type en marche » – si le pod meurt, Kubernetes en démarre un nouveau. Sa stratégie est ici toujours Recreate : arrêter d’abord l’ancien pod, puis démarrer le nouveau. Deux pods qui écrivent en même temps dans le même répertoire de l’hôte, c’est ainsi que les bases de données se corrompent ; quelques secondes d’interruption lors d’un redémarrage en sont le prix. Une ConfigMap est un ensemble de fichiers de configuration stockés dans le cluster et montés dans un pod ; un Secret est la même chose pour les mots de passe.
🚀 blog_k3s_deploy : tout le blog sous forme d’objets Kubernetes
Le rôle transforme l’architecture ci-dessus en objets. Chaque composant est un template Jinja dans templates/ et un fichier de tâches dans tasks/, appliqué avec le motif d’application K8s de la partie 2. Le fichier principal est une liste de contrôle dans l’ordre des dépendances :
- import_tasks: dirs.yml # host directories with the right owners
- import_tasks: namespace_secrets.yml # namespace, Secret, ConfigMaps
- import_tasks: mariadb.yml # database (+ wait until ready)
- import_tasks: external_services.yml # a name for Grafana on the host
- import_tasks: redis.yml # object cache (+ wait)
- import_tasks: wordpress.yml # WordPress + nginx + exporter (+ wait)
- import_tasks: ingress.yml # routing from the internet
- import_tasks: cronjob.yml # wp-cron every 5 min, nightly updates
- import_tasks: exporters.yml # metrics ports for Prometheus
- import_tasks: networkpolicy.yml # who may talk to whom
- import_tasks: image_policy.yml # which images may runWordPress a besoin d’une base de données en marche, donc le rôle attend après MariaDB, Redis et WordPress – avec kubectl rollout status ou kubectl wait, qui bloquent jusqu’à ce que les nouveaux pods se déclarent prêts. Quand le rôle se termine, le blog tourne.
Une garde avant tout le reste
- name: blog_k3s_deploy | Check that the exporter DB password is present in the vault
ansible.builtin.assert:
that:
- blog_db_exporter_pass is defined
- blog_db_exporter_pass | length > 0
fail_msg: >-
blog_db_exporter_pass is missing from this host's vault.yml. Add a line
like blog_db_exporter_pass: "<a fresh random password>" ...Cette variable a été ajoutée plus tard, quand l’exporteur de la base de données a cessé de se connecter en root. Les vaults plus anciens ne l’ont pas. Sans cette vérification, l’exécution échouerait à mi-chemin avec « undefined variable » dans un template. Avec elle, elle échoue dès le début, et le message dit exactement quoi ajouter. assert est l’outil pour de telles gardes : une liste de conditions, toutes doivent être vraies, et un message pour le cas où elles ne le sont pas. (Les argument_specs du rôle la listent comme optionnelle, cette vérification seule ne l’attraperait donc pas.)
dirs.yml : des propriétaires qui restent justes
Deux répertoires de l’hôte sont créés avec le module file : les fichiers de WordPress, appartenant à l’uid 33 (www-data dans le conteneur), et les fichiers de la base de données, appartenant à l’uid 999 (mysql). Les nombres comptent. Une version antérieure les créait en root. Les conteneurs d’initialisation des pods (on y revient plus bas) corrigeaient le propriétaire au démarrage – et l’exécution suivante du playbook le remettait à root. Tout continuait de fonctionner jusqu’à ce qu’un plugin essaie de créer une table et échoue avec un trompeur « Permission denied ». La cause n’était pas du tout la base de données, mais cette tâche qui défaisait le travail du conteneur d’initialisation. Depuis, la tâche définit le même propriétaire que celui qu’attendent les conteneurs – et depuis le 16/10/2026 aussi les mêmes droits : le conteneur d’initialisation de la base remettait encore le répertoire en 0755 alors que le rôle mettait 0750, si bien qu’il dérivait après chaque redémarrage du pod. Désormais les deux disent 0750, et ils ne se battent plus jamais. Le même fichier place aussi la clé publique PGP des mentions légales dans la racine web.
namespace_secrets.yml : le cadre
Un namespace est une zone nommée du cluster ; tout ce qui concerne le blog vit dans wordpress. Trois détails :
- Labels Pod Security, en avertissement et audit seulement. Kubernetes peut imposer des « Pod Security Standards ». Le standard « baseline » interdit les répertoires de l’hôte dans les pods – exactement la manière dont ce blog stocke ses données. L’imposer rejetterait chaque pod, y compris le cron toutes les cinq minutes. Le namespace reçoit donc les labels en mode warn et audit : les violations sont signalées, rien n’est bloqué. Cette tâche utilise
k8ssansapply: une simple fusion ajoute les labels aux existants au lieu de remplacer tout l’objet namespace. - Pas de jeton de service account. Le même patch que dans la partie 2, pour le namespace qui n’existait pas encore à ce moment-là.
- Le Secret, avec
no_log. Mots de passe de la base de données, identifiants d’administration, identifiants de l’exporteur. Le template est rendu en mémoire et envoyé à l’API ;no_log: truegarde tout cela hors de la sortie, même lors d’une simulation avec--diff.
Puis les ConfigMaps : la configuration du site nginx, les réglages PHP (taille d’upload, mémoire, OPcache) et le pool PHP-FPM (douze workers au plus, une page de statut pour l’exporteur). C’est dans la partie nginx qu’une grande partie du comportement du blog est définie : quels fichiers ne sont jamais servis (wp-config.php, suffixes de sauvegarde comme .bak ou .save, .phar, les exports d’un plugin de migration), comment le cache de pages est servi avant même que PHP soit sollicité, et combien de temps les navigateurs peuvent garder en cache images et scripts. Chacun des motifs bloqués porte un commentaire avec la raison, plusieurs avec une date – par exemple un plugin de sauvegarde qui écrit des exports complets du site, base de données comprise, dans un dossier accessible depuis le web et compte sur un fichier .htaccess que nginx ignore.
Un pod redémarre quand sa configuration change
Un piège dans lequel on tombe facilement : les fichiers de configuration sont montés dans le pod avec subPath, et de tels montages ne se mettent jamais à jour dans un pod en marche. Une ConfigMap modifiée ne prenait effet qu’au prochain redémarrage fortuit. La solution est une annotation dans le template de pod de WordPress :
template:
metadata:
annotations:
checksum/config: "{{ lookup('ansible.builtin.template', 'nginx-cm.yml.j2') | hash('sha256') }}"Le template des ConfigMaps est rendu une fois de plus, ici, et réduit à une somme de contrôle SHA-256. Si la configuration change, la somme change – et avec elle le template du pod, ce qui fait déployer un nouveau pod par Kubernetes. Si rien n’a changé, la somme est la même et le pod est laissé tranquille. Une petite astuce qui transforme « penser à redémarrer » en quelque chose qu’on ne peut pas oublier.
mariadb.yml : la base de données et un utilisateur aux droits minimaux
Le Deployment MariaDB utilise l’image officielle, qui crée au premier démarrage la base de données et l’utilisateur WordPress à partir de variables d’environnement. Quelques points du template méritent d’être signalés :
- Les mots de passe en fichiers, pas en variables d’environnement. Les variables d’environnement sont visibles dans
/proc/<pid>/environ, danskubectl describeet dans certaines pages d’erreur. L’image prend en charge la conventionMARIADB_ROOT_PASSWORD_FILE: elle lit le mot de passe dans un fichier à la place, et le fichier vient du Secret, monté en lecture seule. - Un conteneur d’initialisation s’exécute avant le démarrage de la base de données et définit le propriétaire du répertoire de données. Un conteneur d’initialisation est un conteneur éphémère qui doit se terminer avant que les conteneurs principaux démarrent – le bon endroit pour une préparation ponctuelle.
- Les sondes :
mysqladmin pingindique à Kubernetes quand la base de données est prête à accepter des connexions (readiness) et quand elle est bloquée et doit être redémarrée (liveness). - Un conteneur verrouillé : toutes les capabilities Linux retirées et seulement les cinq dont l’entrypoint a besoin rendues, pas d’élévation de privilèges, un système de fichiers racine en lecture seule avec deux petits volumes inscriptibles pour le socket et les tables temporaires. Le volume des tables temporaires est en RAM depuis le 16/10/2026 (auparavant, environ 2 000 tables temporaires par jour partaient sur le disque) ; comme un volume en mémoire compte dans la limite mémoire du conteneur, celle-ci est passée de 768 Mio à 1280 Mio. La partie 5 explique ces réglages en général.
Après le déploiement, le rôle crée un utilisateur de base de données pour l’exporteur de métriques. Avant, il se connectait en root – un accès complet pour un programme qui ne fait que lire des compteurs. Désormais il a exactement trois privilèges et un accès en lecture au performance schema, rien sur les tables de WordPress. L’intéressant est la manière de le faire de façon idempotente sans module MySQL capable d’atteindre l’intérieur du pod :
- name: MariaDB | Check the mysqld_exporter database user
ansible.builtin.command:
argv:
- k3s
- kubectl
- exec
- deployment/mariadb
- --namespace=wordpress
- --container=mariadb
- --
- sh
- -c
- >-
mariadb -N -u root -p"$(cat /etc/secrets/db-root-password)" -e "
SELECT
(SELECT COUNT(*) FROM mysql.user WHERE User = 'exporter' ...
AND authentication_string = PASSWORD('...'))
+ (SELECT COUNT(*) FROM mysql.db WHERE User = 'exporter' ...
AND Db = 'performance_schema' AND Select_priv = 'Y');"
register: exporter_user_check
changed_when: false
no_log: true
- name: MariaDB | Create least-privilege mysqld_exporter database user
ansible.builtin.command:
argv: [ ... CREATE OR REPLACE USER ... GRANT ... ]
when: exporter_user_check.stdout | default('') | trim != '4'
no_log: trueLisez-le comme une question dont la réponse est un nombre. Le SQL compte deux choses : les utilisateurs nommés « exporter » avec exactement ce mot de passe (deux hôtes : localhost et 127.0.0.1), et le droit de lecture sur le performance schema pour les deux. Tout est juste, cela fait 4. Tout autre nombre – l’utilisateur manque, le mot de passe dans le vault a changé, un droit a été perdu – et la deuxième tâche recrée l’utilisateur avec CREATE OR REPLACE. Cela rend aussi la rotation du mot de passe triviale : le changer dans le vault, lancer le playbook.
Notez argv au lieu d’une chaîne de commande : chaque élément de la liste est passé comme exactement un argument, sans shell intermédiaire qui interpréterait guillemets ou signes dollar. Le SQL arrive intact dans le pod, où sh -c l’exécute – et le mot de passe root est lu dans le pod depuis le fichier monté, il n’apparaît donc jamais sur une ligne de commande de l’hôte.
redis.yml et un entrypoint qui n’a jamais tourné
Redis tourne avec la persistance désactivée (--save "" --appendonly no), une limite mémoire de 80 Mo et la politique d’éviction allkeys-lru : quand la mémoire est pleine, l’entrée utilisée le moins récemment s’en va. Les 80 Mo sont volontairement bien en dessous de la limite du conteneur de 128 Mo, parce que la comptabilité mémoire propre de Redis a été mesurée à presque un tiers de la consommation réelle – Redis doit évincer de lui-même bien avant que le noyau ne le tue. WordPress fixe en plus une durée de vie maximale d’un jour par entrée de cache, si bien que le cache se nettoie tout seul.
Le template porte aussi l’un des bugs les plus instructifs de ce dépôt. Le conteneur est démarré avec sa propre command: – et dans Kubernetes, command remplace l’entrypoint de l’image. Cet entrypoint est le script qui serait passé de root à l’utilisateur redis. Jusqu’en octobre 2026, Redis a donc tourné en root, avec deux capabilities ajoutées pour un changement d’utilisateur qui n’a jamais eu lieu. La correction ne consiste pas à rétablir l’entrypoint, mais à démarrer directement le conteneur sous le bon utilisateur :
securityContext:
runAsUser: 999
runAsGroup: 1000
runAsNonRoot: true
fsGroup: 1000Démarré en uid 999, Redis n’a besoin d’aucune capability. fsGroup fait attribuer par Kubernetes les volumes du pod à ce groupe, si bien que le répertoire de données (inutilisé, mais présent) est inscriptible.
wordpress.yml : trois conteneurs et un conteneur d’initialisation
Le pod WordPress est le plus gros template du rôle. Son conteneur d’initialisation a deux tâches : définir le propriétaire de la racine web et télécharger WP-CLI, l’outil en ligne de commande que blog_wordpress utilise pour piloter WordPress. Les deux tâches portent une leçon :
- WP-CLI vit hors de la racine web. Il se trouvait autrefois dans
/var/www/html– et tout ce qui s’y trouve est accessible en HTTP. nginx ne refusait que les fichiers.php, pas.phar, donc n’importe qui pouvait télécharger un outil de 7 Mo depuis le blog. Il va désormais dans un emptyDir (un volume de travail qui vit aussi longtemps que le pod), monté en lecture seule dans le conteneur PHP. - Épinglé et vérifié par somme de contrôle. Le téléchargement vient de GitHub avec une version fixe et est vérifié contre un SHA-512 de
versions.yml. Lewgetde busybox dans le conteneur d’initialisation ne vérifie pas les certificats TLS, et WP-CLI s’exécute ensuite en root – un téléchargement non vérifié serait une exécution de code à distance qui n’attend que son heure. Si la somme ne correspond pas, le conteneur d’initialisation échoue et le pod ne démarre jamais. sleep 5avant le téléchargement. La première connexion sortante d’un tout nouveau pod peut échouer parce que Calico n’a pas encore programmé ses règles réseau. Cela a provoqué une vraie panne : lors d’un redémarrage, le téléchargement s’est bloqué, l’ancien pod avait déjà disparu (une réplique, Recreate), et le site a répondu 502 jusqu’à ce que le conteneur d’initialisation soit relancé à la main. Cinq secondes d’attente coûtent moins cher.
Puis les trois conteneurs – PHP-FPM, l’exporteur FPM et nginx –, chacun avec le même motif de durcissement : toutes les capabilities retirées et seulement les nécessaires rendues, avec un commentaire par capability (« le maître FPM démarre en root et forke des workers en www-data » demande SETUID/SETGID), pas d’élévation de privilèges, le profil seccomp par défaut, un système de fichiers racine en lecture seule avec de petits volumes inscriptibles pour exactement les chemins qui en ont besoin. L’exporteur tourne en utilisateur « nobody » et n’a besoin d’aucune capability.
La configuration propre de WordPress est transmise par une variable d’environnement que l’image comprend, WORDPRESS_CONFIG_EXTRA. Elle définit l’URL publique, la connexion Redis, la durée de vie du cache, le cache de pages – et DISALLOW_FILE_EDIT, qui retire l’éditeur de plugins et de thèmes de l’administration. Sans cela, un mot de passe d’administration volé est à un clic de l’exécution de PHP arbitraire sur le serveur.
Enfin les sondes, trois sortes pour une bonne raison :
| Sonde | Question | Conséquence |
|---|---|---|
| startupProbe | A-t-il déjà démarré ? | Retient les deux autres jusqu’à cinq minutes, pour qu’un démarrage lent ne soit pas pris pour un blocage |
| readinessProbe | Peut-il recevoir du trafic maintenant ? | Sinon, le Service cesse d’envoyer des requêtes à ce pod |
| livenessProbe | Est-il encore vivant ? | Sinon, Kubernetes redémarre le conteneur |
Pour nginx, les sondes vérifient volontairement le port TCP, pas une page HTTP : une vérification HTTP irait jusqu’à PHP et pourrait déclarer nginx mort alors que seul PHP a un problème.
ingress.yml : master, minion et un certificat provisoire
Le routage du blog, ce sont trois objets Ingress. Ce n’est pas un choix, c’est la façon dont le contrôleur de F5 gère plusieurs Ingress pour un même nom d’hôte : un master détient le certificat TLS et les réglages à l’échelle du serveur, et des minions détiennent les chemins. L’Ingress temporaire du challenge de cert-manager (partie 2) s’y joint comme minion supplémentaire. Ici, le master porte la redirection vers HTTPS (sauf pour le chemin du challenge ACME), quelques refus stricts et la limitation de débit sur wp-login.php – dans sa propre location, ce qui a un temps fait partir la page de connexion sans en-tête HSTS, parce que les règles d’en-têtes vivent dans le minion. Un minion route tout vers WordPress et ajoute les en-têtes de sécurité ; un second route /grafana/ vers Grafana sur l’hôte. Grafana a eu son propre minion parce qu’il a besoin d’en-têtes WebSocket que WordPress ne doit pas recevoir, et parce que sinon deux en-têtes de frame contradictoires auraient été envoyés. Un quatrième Ingress redirige le domaine nu sans « www » vers le domaine complet.
Après l’application, un petit bout de logique gère le tout premier déploiement sur un nouveau serveur :
- name: Ingress | Check whether TLS secrets already exist
kubernetes.core.k8s_info:
kind: Secret
namespace: wordpress
name: "{{ item.secret }}"
loop:
- { secret: wordpress-tls, host: "{{ blog_hostname }}" }
- { secret: apt-upgrade-me-apex-tls, host: "{{ blog_hostname | regex_replace('^www\\.', '') }}" }
register: wp_tls_secret_check
no_log: true
- name: Ingress | Seed self-signed placeholder for any TLS secret cert-manager hasn't issued yet
ansible.builtin.shell: |
openssl req -x509 -nodes -newkey rsa:2048 -days 7 ... -subj "/CN={{ item.item.host }}"
k3s kubectl create secret tls {{ item.item.secret }} ...
loop: "{{ wp_tls_secret_check.results }}"
when: item.resources | length == 0Pour un nom d’hôte dont le certificat n’existe pas encore, ce contrôleur rejette la négociation TLS – le site paraît mort jusqu’à ce que Let’s Encrypt ait répondu. Pour chaque Secret de certificat attendu, le rôle demande donc s’il existe, et crée sinon un certificat provisoire auto-signé valable sept jours ; cert-manager l’écrase dès que le vrai certificat arrive. La boucle montre un motif qu’il vaut la peine de connaître : la deuxième tâche boucle sur les résultats de la première. Chaque résultat contient l’élément de boucle d’origine sous item.item (nom d’hôte et nom du Secret) et la réponse sous item.resources (liste vide = introuvable). Le filtre regex_replace déduit le domaine nu du nom en www. Et no_log est là parce que k8s_info renvoie les données d’un Secret – des clés privées.
cronjob.yml : deux tâches planifiées
Le premier CronJob exécute wp-cron.php toutes les cinq minutes, avec concurrencyPolicy: Forbid (jamais deux à la fois). Il a eu deux bugs qui valent d’être racontés. Ses pods portaient autrefois le label app: wordpress – le même label que celui sur lequel sélectionne le Service WordPress. Pendant leurs dix secondes de vie, les pods cron faisaient donc partie du Service, et l’Ingress envoyait de vrais visiteurs vers un pod sans serveur web : un 502 de temps en temps, toutes les cinq minutes. Ils portent désormais le label wordpress-cron, et les NetworkPolicies autorisent les deux labels. Le second bug : le conteneur cron n’avait jamais eu le bloc WORDPRESS_CONFIG_EXTRA, il ne savait donc pas où était Redis, essayait 127.0.0.1, et chaque exécution planifiée mourait sur « Error establishing a Redis connection » – en silence, car WordPress sort avec 0 dans ce cas. Le bloc est désormais identique dans les deux templates, avec un commentaire disant qu’il doit le rester, et le conteneur attend lui aussi cinq secondes pour Calico, comme le conteneur d’initialisation plus haut.
Le second CronJob est la mise à jour nocturne. Les pods cron montent la racine web en lecture seule, si bien que les mises à jour en arrière-plan de WordPress abandonnaient en silence – chaque mise à jour de plugin attendait un clic dans l’administration. Au lieu de rendre les pods cron inscriptibles, une tâche nocturne séparée le fait proprement : un conteneur d’initialisation télécharge WP-CLI (épinglé et vérifié, là encore), un second fait un dump de la base de données dans un répertoire de sauvegarde et garde 14 jours, et seulement si le dump a réussi, le conteneur principal exécute les mises à jour du cœur, des plugins, des thèmes et des traductions en www-data et vérifie ensuite la page d’accueil. Le playbook de mise à jour plus bas réutilise exactement cette tâche.
exporters.yml, networkpolicy.yml, image_policy.yml
Les exporteurs écoutent dans leurs pods. Prometheus tourne sur l’hôte, donc deux Services de type NodePort les rendent joignables sur des ports fixes du serveur – et le pare-feu de la partie 5 rejette ces ports pour tout ce qui n’est pas une connexion locale.
Les NetworkPolicies commencent par un modèle de menace en commentaire, que je trouve être la meilleure partie du fichier. PHP-FPM analyse des entrées non fiables – requêtes, uploads, code de plugins – et c’est de loin l’endroit le plus probable où un attaquant obtiendrait une exécution de code. Il a aussi légitimement besoin de la base de données et de Redis. Le but n’est donc pas de le couper, mais (1) de s’assurer que seuls les pods WordPress peuvent joindre MariaDB et Redis, et (2) de plafonner ce qu’un pod WordPress compromis peut atteindre. Les règles autorisent exactement : l’Ingress (via l’adresse de l’hôte, car le contrôleur tourne dans le réseau de l’hôte), le DNS, MariaDB, Redis, le port de l’exporteur – et le HTTP/HTTPS sortant vers Internet. Le commentaire est honnête sur ce dernier point : les mises à jour viennent de wordpress.org et de GitHub derrière des adresses de CDN changeantes, et les NetworkPolicies standard ne peuvent pas filtrer sur des noms de domaine. Ce qu’elles empêchent, c’est le déplacement à l’intérieur du cluster.
La politique d’images est une ValidatingAdmissionPolicy : une règle que le serveur d’API vérifie avant d’accepter un pod. Sa condition est écrite en CEL, un petit langage d’expressions :
validations:
- expression: >
object.spec.containers.all(c, c.image.split(':')[0] in variables.allowedRepos)
&& (!has(object.spec.initContainers) ||
object.spec.initContainers.all(c, c.image.split(':')[0] in variables.allowedRepos))« L’image de chaque conteneur, sans le tag, figure dans la liste des dépôts autorisés – et de même pour les conteneurs d’initialisation, s’il y en a. » Vérifier le dépôt et non le tag signifie qu’un changement de version ne demande jamais de modification ici ; ce qui est bloqué, c’est une image différente. Une entrée de la liste a une histoire : le solveur de challenge de cert-manager lance son pod dans le namespace du blog, pas dans le sien. Quand le certificat du domaine nu a été ajouté, le solveur a été rejeté, et aucun certificat n’a pu être émis jusqu’à ce que son image soit autorisée.
| Élément | Où | Ce qu’il faut retenir |
|---|---|---|
assert avec fail_msg | main.yml | Échouer tôt, avec des instructions |
import_tasks dans l’ordre des dépendances + attentes de déploiement | main.yml | Le rôle se termine avec une application qui tourne |
| propriétaires en nombres | dirs.yml | S’accorder avec les conteneurs, sinon ils se battent à chaque exécution |
k8s sans apply | namespace_secrets.yml | Fusionner quelques champs dans un objet existant |
lookup(...) | hash('sha256') | template WordPress | Redéployer les pods quand leur configuration change |
argv, une requête SQL qui compte, when: ... != '4' | mariadb.yml | Des changements idempotents sans module |
runAsUser / fsGroup | template Redis | Démarrer sous le bon utilisateur au lieu d’en changer |
boucle sur results, item.item | ingress.yml | Agir sur les réponses d’une boucle précédente |
| CEL dans une ValidatingAdmissionPolicy | politique d’images | Des règles que le serveur d’API impose |
🔧 blog_wordpress : configurer un site en marche depuis l’extérieur
WordPress tourne maintenant, mais il est vide ou – à chaque exécution ultérieure – peut-être dérivé. Ce rôle le configure. Il n’existe pas de module Ansible pour WordPress ; tout passe par WP-CLI dans le pod :
k3s kubectl exec deployment/wordpress --namespace wordpress --container wordpress-fpm --
php /usr/local/lib/wp-cli/wp-cli.phar <command> --path=/var/www/html ...Tout le rôle est donc une longue application du motif « demander d’abord, agir ensuite » de la partie 2. Le rôle le dit dans son en-tête et donne l’ordre : installation, un plugin de durcissement, plugins, apparence, la mention IA et trois petits plugins auxiliaires, réglages, mises à jour.
root ou www-data ?
kubectl exec s’exécute en root dans le conteneur. C’est un problème pour une application PHP : les fichiers créés par root ne peuvent pas être modifiés par PHP-FPM, qui tourne en www-data. Le rôle suit une règle, écrite en haut de main.yml : les commandes qui installent ou mettent à jour du code s’exécutent en root avec --allow-root (sinon WP-CLI refuse root) et sont toujours suivies d’un chown vers www-data. Les requêtes en lecture seule et tout ce qui écrit sans chown ultérieur s’exécutent en www-data via su -s /bin/sh www-data -c "...". Cette règle existe parce que l’inverse a un jour laissé un répertoire de cache appartenant à root, et le plugin de cache ne pouvait plus y écrire.
install.yml : une seule fois
wp core is-installed sort avec 0 quand les tables de WordPress existent et avec 1 sinon. Enregistré avec failed_when: false, le code de sortie devient une décision : when: wp_installed.rc != 0 exécute wp core install sur une base de données neuve et plus jamais ensuite. La tâche d’installation a no_log: true, car le mot de passe d’administration fait partie de sa ligne de commande. Le titre du site passe par le filtre quote, qui l’emballe de façon sûre pour le shell.
security_muplugin.yml : un fichier qui ne change que lorsqu’il change
Un plugin must-use est un fichier PHP dans wp-content/mu-plugins/. WordPress charge chaque fichier qui s’y trouve à chaque requête, avant les plugins normaux, et il ne peut pas être désactivé dans l’administration – le bon endroit pour des règles qui doivent toujours s’appliquer. Celui-ci cache les numéros de version, retire les liens de découverte et bloque pour les visiteurs anonymes le point d’accès REST qui liste les noms d’utilisateurs.
La manière dont il est écrit est la partie intéressante, car le même motif sert pour chaque fichier PHP que le rôle déploie :
- name: WordPress | Deploy security hardening mu-plugin
ansible.builtin.command:
argv:
- k3s
- kubectl
- exec
- deployment/wordpress
- ...
- sh
- -c
- |
F=/var/www/html/wp-content/mu-plugins/security-hardening.php
cat > "$F.new" << 'PHPEOF'
<?php
...
PHPEOF
if [ -f "$F" ] && [ "$(md5sum < "$F.new")" = "$(md5sum < "$F")" ]; then
rm -f "$F.new"
else
mv "$F.new" "$F" && chown www-data:www-data "$F" && echo CHANGED
fi
register: wp_mu_hardening
changed_when: "'CHANGED' in wp_mu_hardening.stdout"
notify: Restart wordpress deployment (OPcache picks up changed PHP files)- Le fichier est écrit sous le nom
security-hardening.php.new– un nom que WordPress ne charge pas, car il ne se termine pas par.php. - Le heredoc entre guillemets (
<< 'PHPEOF') garde le code PHP littéral : aucun$n’est développé par le shell.argvgarantit qu’Ansible n’y touche pas non plus. - Si le nouveau fichier a la même somme de contrôle que l’existant, il est jeté. Sinon, il remplace l’ancien et le script affiche
CHANGED. changed_whencherche ce mot. Ce n’est qu’alors que la tâche est « changed » – et ce n’est qu’alors que le handler est notifié.
Le handler redémarre WordPress, parce que PHP garde le code compilé dans l’OPcache et ne revérifie les fichiers qu’une fois par minute ; un redémarrage rend un fichier modifié actif tout de suite. Avant ce motif, le fichier était simplement écrasé avec changed_when: true – et chaque exécution du playbook redémarrait WordPress, avec trente à soixante secondes de 502 à chaque fois. Le fichier des handlers contient une seule entrée, notifiée par quatre tâches, et s’exécute au plus une fois à la fin du play.
plugins.yml : block, always et une différence de listes
La liste des plugins vit dans l’inventaire de l’hôte (blog_wordpress_plugins). Le rôle lit une fois les plugins installés, en JSON, et calcule le reste :
- name: WordPress | Install and activate missing plugins
ansible.builtin.command: >
... wp-cli.phar plugin install {{ item }} --activate --allow-root
loop: "{{ blog_wordpress_plugins | default([]) | difference(_wp_plugins_installed) }}"
register: wp_plugin_result
until: wp_plugin_result.rc == 0
retries: 3
delay: 10
vars:
_wp_plugins_installed: "{{ wp_plugin_state.stdout | from_json | map(attribute='name') | list }}"
- name: WordPress | Activate installed but inactive plugins
...
when: _wp_plugins_inactive | length > 0
vars:
_wp_plugins_inactive: >-
{{ wp_plugin_state.stdout | from_json
| selectattr('name', 'in', blog_wordpress_plugins | default([]))
| selectattr('status', 'equalto', 'inactive')
| map(attribute='name') | list }}from_json transforme la sortie de WP-CLI en liste de dictionnaires. difference est une soustraction de listes : plugins voulus moins plugins installés – seuls ceux-là sont installés. selectattr filtre une liste de dictionnaires selon un champ : de tous les plugins installés, garder ceux qui sont voulus, et parmi eux les inactifs. Avant, chaque exécution réinstallait chaque plugin avec --force, les signalait tous comme modifiés – et pouvait tirer en silence une version plus récente sans que personne ne le remarque. Les nouvelles versions ne viennent plus que de l’étape de mise à jour.
until avec retries agit par élément de boucle : si un téléchargement dépasse le délai (celui de WP-CLI est fixé à dix secondes), seul ce plugin est retenté. Et tout cela se trouve dans un block avec une section always :
- name: WordPress | Install plugins and fix ownership
block:
- ...read state, install, activate, Redis...
always:
- name: WordPress | Fix wp-content ownership after plugin installation
ansible.builtin.command: >
k3s kubectl exec ... -- chown -R www-data:www-data /var/www/html/wp-contentLes tâches sous always s’exécutent même quand une tâche du bloc échoue. Pourquoi c’est important : WP-CLI en root écrit les fichiers des plugins en root. Un jour, un téléchargement a dépassé le délai à mi-chemin, le play s’est arrêté et le chown ne s’est jamais exécuté – un plugin de sauvegarde n’a plus pu créer son propre dossier de stockage jusqu’à ce que quelqu’un répare à la main. Désormais, la propriété est rétablie quoi qu’il arrive au-dessus.
Le plugin Redis a ses deux propres tâches, parce que wp redis enable place un fichier drop-in qui active le plugin. Son changed_when est un bel exemple de lecture attentive de la sortie d’un outil : WP-CLI affiche « Activating … » même pour un plugin déjà actif, suivi de « already active » sur stderr. Seule une exécution qui ne signale pas à la fois « already installed » et « already active » compte donc comme un changement.
appearance.yml, ai_disclosure.yml, settings.yml
Trois fichiers plus petits, chacun avec une idée :
- L’apparence exécute de petits extraits PHP via
wp evalpour définir le CSS personnalisé et le menu, tous deux stockés dans la base de données. Le commentaire reconnaît honnêtement une limite : le CSS est écrit à chaque exécution, mais seule sa première création compte comme « changed » – un bloc CSS modifié est appliqué, mais le récapitulatif affiche « ok ». - La mention IA crée la page de transparence et son lien depuis la page À propos – d’abord comparés, écrits seulement en cas de différence, si bien qu’une modification manuelle dans l’administration est annulée – et déploie trois autres plugins must-use avec le même motif
.newet somme de contrôle : la mention en pied de page, des optimisations de performance (préchargement des polices et une première image chargée immédiatement, après qu’un test Lighthouse mobile a montré les deux problèmes) et la ligne « Aussi disponible en » pour les traductions. - Les réglages ferment l’inscription publique (elle était exploitée par des bots de spam : dix-huit faux comptes lors d’un seul nettoyage) et désactivent le cron de WordPress déclenché par les visiteurs, puisque le CronJob fait ce travail. Deux réglages, deux manières de rester idempotent :
wp option updatene dit « Value updated » que si quelque chose a vraiment changé, doncchanged_whenlit cela ; la constante du cron est d’abord lue et n’est écrite que si elle n’est pas déjà vraie.
updates.yml : mises à jour, nettoyage et un redémarrage honnête
Le dernier fichier met à jour le cœur, les plugins, les thèmes et les traductions à chaque exécution, dans une autre paire block/always avec la même correction de propriété. Deux techniques le font fonctionner :
- Un
failed_whenqui lit la sortie.wp plugin update --allsort avec 1 quand il n’y a rien à mettre à jour – le cas normal. La tâche n’échoue donc que si le code de sortie n’est pas 0 et que la sortie ne dit pas « No plugins updated ». De même pour les thèmes, et pour la suppression de plugins déjà disparus. - Deux instantanés. Au début de
plugins.yml, le rôle relève toutes les versions du cœur, des plugins et des thèmes ; ici, il les relève à nouveau. WordPress n’est redémarré que si quelque chose a signalé une mise à jour ou si les deux instantanés diffèrent. Une exécution sans mise à jour ne provoque aucune interruption.
Entre les étapes de mise à jour, le rôle supprime aussi les plugins et thèmes inutilisés, car un plugin désactivé reste sur le disque et reste joignable – une vulnérabilité qu’il contient reste exploitable. Les thèmes inactifs sont calculés, pas listés : theme list --status=inactive ne peut jamais renvoyer le thème actif, le thème du site est donc structurellement à l’abri, et un thème par défaut est conservé comme solution de repli de WordPress. La suppression s’exécute volontairement après la mise à jour du cœur : une mise à jour du cœur peut réinstaller les thèmes par défaut fournis.
Une limite est indiquée dans l’en-tête du rôle : les tâches command sont sautées en mode check. Une simulation ne peut donc exécuter ici que les requêtes en lecture seule ; ce que WP-CLI changerait ne peut pas être montré à l’avance.
📊 blog_visitor_stats : compter les lecteurs sans traceur
L’idée a son propre article : pas de script de suivi, pas de cookies, pas de tiers – compter les visiteurs à partir des journaux de l’Ingress, sur le serveur, et ne garder que les chiffres. K3s ne conserve les journaux des conteneurs que quelques jours, il faut donc un instantané nocturne pour voir une tendance sur des mois. Le rôle est court, mais utilise plusieurs fonctionnalités qui n’apparaissent nulle part ailleurs :
copy src: "{{ playbook_dir }}/scripts/analytics/visitor-stats.py"– un fichier situé hors du rôle.playbook_direst le répertoire du playbook en cours ; un simplesrc: namechercherait dans lefiles/du rôle. Le script se trouve dansscripts/parce que c’est aussi un outil à lancer à la main.- Quatre unités systemd issues de templates, dans une
loop, avecnotifyvers un handler qui recharge systemd – etmeta: flush_handlersjuste après, car les timers doivent démarrer avec les nouvelles unités, pas à la fin du play. - Le service quotidien tourne avec les options de bac à sable de systemd : tout le système de fichiers en lecture seule sauf son propre répertoire de données, pas de répertoires personnels, un
/tmpprivé, pas d’élévation de privilèges. Un script de statistiques qui lit des journaux n’a besoin de rien de plus. - Un rattrapage ponctuel : les journaux encore sur le disque couvrent les derniers jours, donc le rôle les enregistre une fois, avec une
loopde 1 à 5 et une date calculée en Jinja –'%Y-%m-%d' | strftime(epoch - item * 86400), « aujourd’hui moins N jours ». Un{% for %}dans la ligne de commande ajoute un argument--excludepour chaque réseau à écarter (mes propres connexions, qui domineraient sinon les chiffres d’un petit blog).creates:sur un fichier marqueur fait que tout le rattrapage ne s’exécute qu’une seule fois.
✅ blog_verify et common_verify : est-ce que ça a marché ?
La partie 1 a décrit l’idée : collecter tous les résultats, les juger, en rendre compte, et seulement ensuite échouer. Voici comment c’est construit. blog_verify a trois fichiers : main.yml définit les codes couleur du rapport et importe les deux autres, checks.yml exécute les vérifications, report.yml les juge.
Une partie des vérifications n’est même pas écrite dans ce rôle. common_verify est un rôle bibliothèque : il n’apparaît dans aucun playbook et n’a pas de tasks/main.yml, seulement trois fichiers de tâches – vérifications de l’hôte, de Kubernetes, HTTP. Les deux rôles de vérification les incluent :
- name: Verify | Shared host checks (common_verify)
ansible.builtin.import_role:
name: common_verify
tasks_from: hosttasks_from choisit un autre fichier que main.yml. Le fichier hôte vérifie nftables (service, table, politique de rejet par défaut, liste de bannissement), fail2ban, K3s, auditd, SELinux et TCP BBR ; le fichier Kubernetes vérifie les pods de l’Ingress et de cert-manager et cherche partout des pods en état d’erreur ; le fichier HTTP vérifie le site depuis l’extérieur, ses en-têtes et un chemin bloqué. Le blog ajoute les siennes : la page de connexion joignable, xmlrpc.php et readme.html bloqués, pas de noms d’utilisateurs via l’API REST, OPcache actif, MariaDB joignable depuis le pod WordPress. Plusieurs d’entre elles sont exactement ce que j’ai essayé de casser un jour.
Chaque tâche de vérification a les mêmes quatre lignes : register: _v_something, failed_when: false, changed_when: false, check_mode: false. Rien n’échoue, rien ne change, tout s’exécute même en simulation. report.yml calcule ensuite un vrai/faux par vérification avec set_fact, les compte, affiche un tableau en couleur et l’envoie par mail – avant l’assert final, pour que le mail parte aussi quand quelque chose de critique est cassé. Seul l’assert peut faire échouer le play. Une dernière étape optionnelle lance un autotest du chien de garde Monit, qui envoie de vrais mails d’alerte – désactivé par défaut, activé par exécution avec -e monit_selftest=true.
🔄 blog-update.yml : la mise à jour de routine
Le playbook complet peut mettre WordPress à jour, mais il touche à tout le serveur. Le playbook de mise à jour ne touche qu’à l’application, et il le fait de la même façon que la tâche nocturne – il lance cette tâche. La stratégie qui le sous-tend est plus ancienne que ce code ; voici ce que fait le code aujourd’hui :
- Gardes. Refuser de tourner sans
--limit. Refuser si WordPress n’a aucun pod disponible – mettre à jour un site déjà en panne mélangerait deux problèmes. - Versions avant, pour le rapport et la décision de redémarrage.
- Lancer la tâche de mise à jour maintenant.
kubectl create job --from=cronjob/wordpress-autoupdatedémarre une copie ponctuelle de la tâche nocturne : dump de la base de données, puis les mises à jour. Le playbook interroge ensuite l’état de la tâche avecuntilau lieu dekubectl wait --for=condition=complete– ce dernier resterait planté jusqu’à son délai si la tâche échoue au lieu de se terminer. Le journal de la tâche est affiché. Toute l’étape est sautée en mode check. - Images reconstruites.
common_image_refreshexécute un petit script Python sur le serveur (avec le modulescriptd’Ansible, qui le copie, l’exécute et le supprime). Pour chaque Deployment, il tire à nouveau le tag d’image et compare le digest à celui avec lequel le pod en cours a démarré. Docker Hub reconstruit les tags inchangés pour les correctifs de sécurité de l’image de base, mais K3s ne retire jamais de lui-même. Le script renvoie du JSON : quels Deployments font tourner un build obsolète. - Ne redémarrer que ce qui a changé, dans un ordre fixe : la base de données d’abord, WordPress en dernier – WordPress se reconnecte à une base de données redémarrée, mais un WordPress redémarré face à une base de données en train de s’arrêter servirait des erreurs. L’expression de boucle respecte cet ordre et ajoute à la fin tout ce qui n’est pas dans la liste.
- Vérification de la page d’accueil avec le module
uri: HTTP 200, ou l’exécution échoue.
Tout cela est dans un block. Si quelque chose échoue, la section rescue montre les pods et les events récents, le dump de base de données le plus récent de la tâche de mise à jour (le point de restauration) et l’état au moment de l’échec – puis s’arrête. Pas de retour arrière automatique : une restauration est une décision, pas un réflexe.
🛠️ En exploitation : le blog au quotidien
Tout ce qui précède se fait automatiquement. Cette section, c’est ce que vous faites à la main – les commandes, la routine et quoi faire quand quelque chose ne va pas. Toutes les commandes s’exécutent en root sur le serveur ; k3s kubectl est le client Kubernetes fourni avec K3s.
Regarder
# Are all pods running? (mariadb, redis, wordpress; cron pods come and go)
k3s kubectl get pods -n wordpress
# Did the scheduled jobs succeed? (wordpress-cron every 5 min, wordpress-autoupdate nightly)
k3s kubectl get jobs -n wordpress
# Logs of a container (add --previous after a crash)
k3s kubectl logs deployment/wordpress -c wordpress-fpm -n wordpress --tail=50
# Is the certificate valid, and when is it renewed?
k3s kubectl get certificate -n wordpress \
-o custom-columns=NAME:.metadata.name,READY:.status.conditions[0].status,RENEWAL:.status.renewalTimeParler à WordPress
WP-CLI est dans le pod. Exécutez-le en www-data, jamais en root, sauf si vous installez du code et corrigez le propriétaire ensuite – les fichiers appartenant à root sont ici le moyen le plus courant de casser plugins et caches :
k3s kubectl exec -n wordpress deployment/wordpress -c wordpress-fpm -- \
su -s /bin/sh www-data -c 'php /usr/local/lib/wp-cli/wp-cli.phar --path=/var/www/html plugin list'La routine
| Quand | Quoi | Comment |
|---|---|---|
| Chaque nuit | Mises à jour des plugins, thèmes, traductions et mineures du cœur, dump de la base de données d’abord | automatique (CronJob de mise à jour) ; vérifier avec kubectl get jobs |
| Quand vous le voulez maintenant | La même chose, plus les images de conteneurs reconstruites | ansible-playbook blog-update.yml --limit <host> – lancez-le d’abord avec --check |
Quand versions.yml change | Nouveaux tags d’images (WordPress, MariaDB, nginx, Redis) | ansible-playbook blog.yml --limit <host> --tags blog_k3s_deploy, après une simulation |
| Après tout changement dans le dépôt | Tout, vérifié | le playbook complet ; scripts/check-drift.sh avant et après |
| Après chaque exécution | Lire le mail de vérification | tout vert, ou les noms des vérifications en échec |
Quand quelque chose tourne mal
- 502 Bad Gateway : nginx tourne, PHP-FPM ne répond pas. Regardez les journaux de FPM (ci-dessus) ; un redémarrage, c’est
k3s kubectl rollout restart deployment/wordpress -n wordpress– environ une demi-minute d’interruption à cause de la stratégie Recreate. - Un pod ne démarre pas :
k3s kubectl describe pod -l app=wordpress -n wordpressmontre les events en bas. Les causes habituelles sont une base de données qui ne tourne pas, un mot de passe dans le Secret qui ne correspond pas à l’utilisateur de la base (relancer le playbook), ou de mauvais propriétaires sur les répertoires de l’hôte (uid 33 pour la racine web, 999 pour la base de données). - Une mise à jour de plugin a cassé le site : la tâche nocturne a d’abord fait un dump de la base de données, dans le répertoire de sauvegarde des mises à jour sur l’hôte (14 jours). Restaurez le dump si la mise à jour a touché à la base, et épinglez le plugin sur la dernière bonne version :
wp plugin install <slug> --version=<x> --force. La sortie rescue du playbook de mise à jour indique le dump le plus récent. - Tout le serveur a disparu : en installer un nouveau avec le playbook complet, puis restaurer fichiers et base de données depuis la sauvegarde externe avec le script de restauration, puis relancer le playbook une fois. La sauvegarde est récupérée depuis l’extérieur chaque jour – les fichiers avec rsync, la base de données sous forme de dump vérifié, versionnés pendant 30 jours – et le chien de garde alerte quand la dernière sauvegarde réussie date de plus de trois jours. Le manuel d’exploitation du dépôt contient chaque étape.
🧭 La suite
La partie 4, c’est Nextcloud : les mêmes briques, mais avec la base de données et Redis sur l’hôte, une suite bureautique à côté, un service push et un rôle de configuration qui fixe chaque valeur selon « lire, comparer, définir ». Si vous n’avez qu’un blog, vous pouvez la sauter et passer directement à la partie 5, les rôles de sécurité – SSH, le pare-feu, le durcissement, SELinux et les scanners nocturnes.


