diff --git a/CHANGELOG.md b/CHANGELOG.md index 6800af8..294e206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,71 @@ # CHANGELOG — Set-OPS +## 2026-08-23 — La source d'artefacts : la forge sert le code, il manquait qui sert les binaires + +Pour poser une seule machine, un écosystème allait chercher chez **six serveurs +étrangers** : `deb.debian.org`, `security.debian.org`, `packages.smallstep.com`, +`apt.grafana.com`, `packages.icinga.com`, `codeberg.org`. La forge héberge le code ; rien +n'hébergeait les binaires. + +Deux rôles neufs — `serveur_artefacts` (apt-cacher-ng) et `client_artefacts`, intégration +**universelle** qui s'éteint d'elle-même quand aucun hôte ne porte le service, et qui +**retire** la direction posée auparavant : une intégration qui ne sait pas se retirer est +un piège différé. + +Chez patient 0, le service est colocalisé sur `forge-01`. C'était l'intuition de départ, +prise au mot : *la forge est la source* — du code **et** des binaires. + +### Ce qui était déjà couvert, et ce qui ne l'était pas + +| | | +|---|---| +| téléchargements **directs** (Forgejo, Keycloak, Nextcloud, oauth2-proxy, collections) | déjà couverts — le contrôleur télécharge une fois et pousse par SSH | +| **dépôts apt** | c'est ce qui manquait | + +Deux mécanismes, parce que ce sont deux problèmes : on ne sert pas un dépôt apt par `scp`. + +### La preuve : couper l'amont + +Tant qu'internet répond, un `apt update` qui réussit ne dit pas d'où vient l'octet. D'où +le mode hors ligne, qui est autant une fonction qu'un instrument : + +``` +paquet DÉJÀ en cache 235 ko réceptionnés en 0s (0 o/s) ← servi localement +paquet ABSENT du cache 503 Unable to download in offline mode ← refusé +``` + +Le `0 o/s` est le témoin : rien n'a traversé le réseau. Contrôle positif et négatif dans +la même minute. + +### Trois choses apprises en le construisant + +**`apt` fait hériter `Acquire::https::Proxy` de la valeur HTTP.** Poser le seul proxy HTTP +envoyait donc aussi les dépôts tiers en HTTPS dans le cache, qui refuse les tunnels — à +juste titre : `403 CONNECT denied`. `packages.smallstep.com` devenait injoignable pour +toute la flotte. Il faut écrire `DIRECT` explicitement. + +**Un service ne doit pas dépendre de lui-même pour se réparer.** La première version +faisait `apt update` à chaque passage. Sur l'hôte qui *porte* le cache, cet `apt update` +passe par le cache — et en mode hors ligne, il est refusé. Le rôle qui devait remettre le +service en ligne ne pouvait plus s'exécuter, et il a fallu réparer la machine à la main. +L'index n'est désormais rafraîchi qu'à la **première** installation. + +**Mes sondes ont menti deux fois de plus.** `apt-get update >/dev/null 2>&1 && echo ok` a +rendu « ok » sur cinq hôtes où le proxy était injoignable : rediriger la sortie d'erreur, +c'est choisir de ne pas voir. Et `grep -c` rend un code de sortie **1** quand il compte +zéro — un instrument qui crie à l'échec en constatant le succès attendu. + +### Ce que ça ne règle pas encore + +Les dépôts tiers en **HTTPS** vont toujours en direct. Les faire passer par le cache +demande de réécrire leurs sources en `http://cache//…` — propre, faisable, pas dans +cet incrément. + +Et un cache ne sert **jamais** la machine qui le construit : `client_artefacts` est +déployé en dernière couche, donc lors d'une construction *from-zero* les premières +machines vont encore à l'amont. Il sert dès le deuxième passage, et à chaque +reconstruction — c'est-à-dire exactement le scénario pour lequel il existe. + ## 2026-08-23 — Le résolveur : un service qui répondait à des questions que personne ne posait Les hôtes de patient 0 interrogeaient **Quad9**, alors que `infra-dns-01` fait tourner un diff --git a/docs/audit/preuve-2026-08-23.md b/docs/audit/preuve-2026-08-23.md index cbd3c6e..9321250 100644 --- a/docs/audit/preuve-2026-08-23.md +++ b/docs/audit/preuve-2026-08-23.md @@ -20,8 +20,8 @@ | P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | | P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | | P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | -| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 31 groupes classes, aucun cycle, aucune arete en arriere. | -| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 30 rôles, 80 flux, schéma + matrice OK. | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 33 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 31 rôles, 82 flux, schéma + matrice OK. | | P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | | P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml | | P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | @@ -36,21 +36,21 @@ | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. | | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 7 reseau(x), aucune collision avec la plage tenant. | -| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 51 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,192.168.254.2/32,192.168.255.2/32. | -| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 47 groupe(s), 76 regle(s). | -| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 4 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 52 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,192.168.254.2/32,192.168.255.2/32. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 48 groupe(s), 77 regle(s). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). | | P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 33 VM placee(s), aucun nom ni VMID en collision. | -| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 24 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 13, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 25 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 14, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | -| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 47 scripts expliques et atteignables, 98 cibles make documentees, 55 roles avec README. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 47 scripts expliques et atteignables, 98 cibles make documentees, 57 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 34 exigence(s) de role, toutes satisfaites (118 cle(s) declaree(s) par l'instance). | -| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 41 document(s) declarent leur lecteur (21 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | | P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. | -| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 30 role(s) serveur/client tous nommes, 31 groupe(s) cite(s) en table existent tous. | +| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 32 role(s) serveur/client tous nommes, 33 groupe(s) cite(s) en table existent tous. | | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | | P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 43 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | diff --git a/docs/catalogue-services.md b/docs/catalogue-services.md index 5435748..68493db 100644 --- a/docs/catalogue-services.md +++ b/docs/catalogue-services.md @@ -50,6 +50,7 @@ Ce que la reconstruction couvre, par capacité : | Plateforme webapp | `serveur_web_frontal`, `serveur_web_dorsal` | sites statiques et webapps natives (venv + systemd + nginx), **zéro conteneur** | | Sauvegardes | `serveur_backup`, `client_backup` | restic hors-nœud, **restauration éprouvée** (2026-08-12 : la donnée revient) | | Exploitation | `serveur_ops` | le poste depuis lequel l'ecosysteme se reconstruit : Ansible epingle, genome clone depuis **sa propre forge**, cle SSH propre — **sans** la voute ni son mot de passe | +| Source d'artefacts | `serveur_artefacts`, `client_artefacts` | cache apt de l'ecosysteme (apt-cacher-ng) : les paquets viennent de chez soi, pas de six serveurs etrangers — **mode hors ligne** pour prouver ce que le cache detient vraiment | | Agents de flotte | `client_metrique`, `client_journal`, `client_smtp` | collecte et relais sur toute la flotte | *Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail` diff --git a/docs/couches-deploiement.yml b/docs/couches-deploiement.yml index 33b3030..1f9c0d5 100644 --- a/docs/couches-deploiement.yml +++ b/docs/couches-deploiement.yml @@ -47,6 +47,9 @@ couches: - serveur_dovecot - serveur_postfix - serveur_backup + # La source d'artefacts vient AVANT ceux qui installent des paquets — c'est tout + # son objet. Placee plus tard, elle serait remplie apres avoir servi. + - serveur_artefacts - nom: apps raison: "Les applications métier, qui consomment les services (base, SSO, courriel, edge)." @@ -73,3 +76,4 @@ couches: - client_smtp - client_backup - client_unbound + - client_artefacts diff --git a/docs/dependances-groupes.yml b/docs/dependances-groupes.yml index f9d3cd3..eefef31 100644 --- a/docs/dependances-groupes.yml +++ b/docs/dependances-groupes.yml @@ -100,6 +100,24 @@ groupes: raison: "Le poste d'exploitation clone le genome depuis la forge de l'ecosysteme ; sans elle, il n'a pas de source." surveillance: "Verifier que les depots clones suivent leur amont et qu'ansible repond dans le venv." + client_artefacts: + requiert_groupes_actifs: + - serveur_artefacts + # L'INTEGRATION SUIT L'EXISTENCE DU SERVICE. Un ecosysteme sans source d'artefacts + # prend ses paquets a l'amont : c'est un choix valide, pas une panne. Le role se + # desactive alors seul (`client_artefacts_actif` derive de l'inventaire) et RETIRE la + # direction posee auparavant -- sans quoi les hotes resteraient braques sur une + # machine disparue. + sauf_si: + serveur_artefacts: { variable: client_artefacts_actif, vaut: false } + raison: "Un hote ne peut prendre ses paquets chez lui que si l'ecosysteme heberge une source." + surveillance: "Verifier que le cache repond sur 3142 et que les hotes le designent bien." + + serveur_artefacts: + requiert_groupes_actifs: [] + raison: "Un cache apt ne depend d'aucun service de l'ecosysteme : il ne fait que relayer et retenir." + surveillance: "Verifier l'ecoute sur 3142, le taux de service depuis le journal, et l'espace du cache." + serveur_nextcloud: requiert_groupes_actifs: - serveur_postgresql diff --git a/playbooks/groupes/client_artefacts.yml b/playbooks/groupes/client_artefacts.yml new file mode 100644 index 0000000..534bbcc --- /dev/null +++ b/playbooks/groupes/client_artefacts.yml @@ -0,0 +1,19 @@ +--- +- name: Appliquer le groupe client_artefacts + hosts: client_artefacts + become: true + module_defaults: + ansible.builtin.apt: + lock_timeout: 300 + + gather_facts: true + + pre_tasks: + - name: Vérifier que la cible est Debian + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Debian" + fail_msg: "Ce playbook est prévu pour Debian." + + roles: + - client_artefacts diff --git a/playbooks/groupes/serveur_artefacts.yml b/playbooks/groupes/serveur_artefacts.yml new file mode 100644 index 0000000..ccd7375 --- /dev/null +++ b/playbooks/groupes/serveur_artefacts.yml @@ -0,0 +1,19 @@ +--- +- name: Appliquer le groupe serveur_artefacts + hosts: serveur_artefacts + become: true + module_defaults: + ansible.builtin.apt: + lock_timeout: 300 + + gather_facts: true + + pre_tasks: + - name: Vérifier que la cible est Debian + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Debian" + fail_msg: "Ce playbook est prévu pour Debian." + + roles: + - serveur_artefacts diff --git a/roles/client_artefacts/README.md b/roles/client_artefacts/README.md new file mode 100644 index 0000000..b00f18e --- /dev/null +++ b/roles/client_artefacts/README.md @@ -0,0 +1,39 @@ +# client_artefacts + +**Intégration cliente** : l'hôte prend ses paquets à la source de l'écosystème +(`serveur_artefacts`) plutôt que chez Debian. + +## Principe + +Une ligne dans `/etc/apt/apt.conf.d/`. C'est tout ce que ce rôle fait, et c'est +délibérément tout ce qu'il fait. + +``` +Acquire::http::Proxy "http://.:3142"; +``` + +## Elle suit l'existence du service — elle ne se déclare pas + +`client_artefacts_actif` est **dérivé de l'inventaire** : un groupe `serveur_artefacts` +sans hôte, c'est un écosystème qui prend ses paquets à l'amont, et c'est un choix valide. +Le rôle ne pose alors rien — et **retire** la direction posée auparavant, sans quoi les +hôtes resteraient braqués sur une machine disparue et n'installeraient plus rien. Une +intégration qui ne sait pas se retirer est un piège différé. + +C'est la même leçon que pour le SSO, les bases et les dépendances causales : le moteur ne +doit pas supposer l'écosystème complet. + +## Un cache ne sert jamais la machine qui le construit + +`client_artefacts` est déployé dans la couche **agents**, en dernier — quand sa cible est +debout. Conséquence à connaître : lors d'une construction *from-zero*, les premières +machines prennent encore leurs paquets à l'amont, puisque le cache n'existe pas encore. + +Il sert dès le **deuxième passage**, et à chaque reconstruction — c'est-à-dire exactement +le scénario pour lequel il existe. + +## Seul le HTTP passe par le cache + +Les dépôts Debian sont servis en HTTP et leur intégrité vient de leurs signatures. Les +dépôts tiers en HTTPS continuent d'aller en direct : `client_artefacts_https` existe mais +reste à `false` tant que le cache ne sait pas les remapper. diff --git a/roles/client_artefacts/defaults/main.yml b/roles/client_artefacts/defaults/main.yml new file mode 100644 index 0000000..b3434d9 --- /dev/null +++ b/roles/client_artefacts/defaults/main.yml @@ -0,0 +1,33 @@ +--- +# INTÉGRATION CLIENTE : passer par la source d'artefacts de l'écosystème. +# +# Une ligne dans `/etc/apt/apt.conf.d/` suffit à ce qu'un hôte prenne ses paquets chez lui +# plutôt que chez Debian. C'est tout ce que ce rôle fait — et c'est délibérément tout ce +# qu'il fait. + +# DÉRIVÉ de l'inventaire : l'hôte qui porte `serveur_artefacts`. Un écosystème sans source +# d'artefacts rend une valeur vide, et le rôle ne pose rien (voir `client_artefacts_actif`). +client_artefacts_hote: >- + {{ (groups['serveur_artefacts'] | default([]) | first | default('')) }} +client_artefacts_port: 3142 +client_artefacts_fichier: "/etc/apt/apt.conf.d/00-setops-artefacts" + +# L'INTÉGRATION SUIT L'EXISTENCE DU SERVICE — elle ne se déclare pas. +# +# C'est la quatrième fois que la même leçon se paie dans ce dépôt : le moteur supposait +# l'écosystème COMPLET. Un groupe `serveur_artefacts` sans hôte, c'est un écosystème qui +# prend ses paquets à la source d'origine, et c'est un choix valide — pas une panne. +client_artefacts_actif: "{{ (groups['serveur_artefacts'] | default([])) | length > 0 }}" + +# SEUL LE HTTP PASSE PAR LE CACHE, et c'est voulu. +# +# Les dépôts Debian (`deb.debian.org`, `security.debian.org`) sont servis en HTTP : leur +# intégrité ne vient pas du transport mais des SIGNATURES du dépôt, qu'apt vérifie de +# toute façon. Un cache intermédiaire ne peut donc pas les altérer sans se faire prendre. +# +# Les dépôts tiers en HTTPS (Smallstep, Grafana, Icinga) continuent d'aller en direct : +# les faire passer par le cache exigerait soit de casser le TLS de bout en bout, soit de +# réécrire leurs sources en `http://cache//…`. La seconde voie est propre et +# reste à faire — elle n'est pas dans cet incrément, et le dire vaut mieux que le laisser +# croire. +client_artefacts_https: false diff --git a/roles/client_artefacts/meta/integration.yml b/roles/client_artefacts/meta/integration.yml new file mode 100644 index 0000000..10c1d5e --- /dev/null +++ b/roles/client_artefacts/meta/integration.yml @@ -0,0 +1,12 @@ +--- +# Politique d'integration. Voir roles/client_metrique/meta/integration.yml pour le +# raisonnement, et docs/decisions-architecture.md (D-33). +integration: + universelle: true + raison: >- + Tout hote installe des paquets, et doit les prendre chez lui quand l'ecosysteme a + une source. Une machine restee braquee sur deb.debian.org est une dependance + exterieure de plus, invisible tant que le lien tient. + # AUCUNE exemption, pas meme l'hote qui PORTE le cache : lui aussi installe des + # paquets, et se servir soi-meme est le cas le plus simple. L'exempter reviendrait a + # dire que le cache ne se fait pas confiance. diff --git a/roles/client_artefacts/tasks/main.yml b/roles/client_artefacts/tasks/main.yml new file mode 100644 index 0000000..01e877e --- /dev/null +++ b/roles/client_artefacts/tasks/main.yml @@ -0,0 +1,42 @@ +--- +- name: Exiger un hôte de source d'artefacts quand l'intégration est active + ansible.builtin.assert: + that: + - client_artefacts_hote | length > 0 + fail_msg: >- + client_artefacts est actif mais aucun hôte ne porte `serveur_artefacts`. + Déclarer le service au plan, ou forcer client_artefacts_actif=false. + when: client_artefacts_actif | bool + +- name: Diriger apt vers la source de l'écosystème + ansible.builtin.copy: + dest: "{{ client_artefacts_fichier }}" + content: | + // GÉNÉRÉ par Set-OPS (rôle client_artefacts). NE PAS éditer à la main. + // Les paquets viennent de l'écosystème, pas de six serveurs étrangers. + Acquire::http::Proxy "http://{{ client_artefacts_hote }}.{{ domaine_interne }}:{{ client_artefacts_port }}"; + {% if client_artefacts_https | bool %} + Acquire::https::Proxy "http://{{ client_artefacts_hote }}.{{ domaine_interne }}:{{ client_artefacts_port }}"; + {% else %} + // « DIRECT » EST OBLIGATOIRE, PAS DECORATIF (mesure du 2026-08-23). + // + // apt fait HERITER `Acquire::https::Proxy` de la valeur HTTP quand elle n'est pas + // definie. Poser le seul proxy HTTP envoyait donc AUSSI les depots tiers en HTTPS + // dans le cache, qui refuse les tunnels — a juste titre : + // « Invalid response from proxy: HTTP/1.0 403 CONNECT denied » + // et packages.smallstep.com devenait injoignable pour toute la flotte. + Acquire::https::Proxy "DIRECT"; + {% endif %} + owner: root + group: root + mode: "0644" + when: client_artefacts_actif | bool + +# L'écosystème qui RETIRE sa source d'artefacts doit voir ses hôtes revenir à l'amont, +# sans quoi ils resteraient braqués sur une machine disparue et n'installeraient plus +# rien. Une intégration qui ne sait pas se retirer est un piège différé. +- name: Retirer la direction quand l'écosystème n'a pas de source + ansible.builtin.file: + path: "{{ client_artefacts_fichier }}" + state: absent + when: not (client_artefacts_actif | bool) diff --git a/roles/serveur_artefacts/README.md b/roles/serveur_artefacts/README.md new file mode 100644 index 0000000..ead00a8 --- /dev/null +++ b/roles/serveur_artefacts/README.md @@ -0,0 +1,63 @@ +# serveur_artefacts + +**La source d'artefacts de l'écosystème** : un cache apt (apt-cacher-ng) qui sert à toute +la flotte les paquets qu'elle installe, au lieu de les faire chercher chez Debian. + +## Principe + +La forge héberge le **code**. Il manquait qui héberge les **binaires**. Pour poser une +seule machine, un écosystème allait chercher chez six serveurs étrangers : +`deb.debian.org`, `security.debian.org`, `packages.smallstep.com`, `apt.grafana.com`, +`packages.icinga.com`, `codeberg.org`. + +Ce n'est pas théorique. `packages.smallstep.com` est intermittent — la même URL pend au +premier essai et répond en 0,48 s au second — et a fait échouer un déploiement de +quarante minutes sur la **première** machine. + +## Ce que ce rôle couvre, et ce qui est couvert ailleurs + +| | | +|---|---| +| **dépôts apt** | ce rôle — un cache, servi à la flotte | +| **téléchargements directs** (Forgejo, Keycloak, Nextcloud, oauth2-proxy, collections) | le **cache du contrôleur** : il télécharge une fois et pousse par SSH | + +Deux mécanismes parce que ce sont deux problèmes : on ne sert pas un dépôt apt par `scp`. + +## Ce qu'il ne couvre pas encore + +**Les dépôts tiers en HTTPS** (Smallstep, Grafana, Icinga) continuent d'aller en direct. +Les faire passer par le cache exigerait soit de casser le TLS de bout en bout, soit de +réécrire leurs sources en `http://cache//…`. La seconde voie est propre et reste à +faire. Le dire vaut mieux que le laisser croire. + +## Le mode hors ligne, qui est aussi l'épreuve + +```yaml +serveur_artefacts_hors_ligne: true +``` + +Le cache cesse d'aller en amont : il sert ce qu'il détient et refuse le reste. C'est +l'état d'un écosystème coupé du monde — et **le seul moyen de prouver que le cache sert +vraiment**. Tant qu'internet répond, un `apt update` qui réussit ne dit pas d'où vient +l'octet. + +## Pourquoi le trafic est en clair, et pourquoi c'est correct + +L'intégrité d'un dépôt apt ne vient pas du transport mais des **signatures**, qu'`apt` +vérifie de toute façon. Un intermédiaire ne peut pas altérer un paquet sans se faire +prendre. Restreindre *qui* interroge le cache est le travail du pare-feu est-ouest, +dérivé de `meta/flux.yml` — pas d'une authentification. + +## Variables principales + +| Variable | Défaut | Rôle | +|---|---|---| +| `serveur_artefacts_port` | `3142` | port d'écoute | +| `serveur_artefacts_cache` | `/var/cache/apt-cacher-ng` | où les paquets sont retenus | +| `serveur_artefacts_expiration_jours` | `30` | rétention sans redemande | +| `serveur_artefacts_hors_ligne` | `false` | couper l'amont (et éprouver) | + +## Ce que ce rôle ne fait pas + +Il ne sauvegarde rien : un cache se reconstitue en le re-remplissant. Le sauvegarder +reviendrait à sauvegarder les binaires de Debian. diff --git a/roles/serveur_artefacts/defaults/main.yml b/roles/serveur_artefacts/defaults/main.yml new file mode 100644 index 0000000..8959a5b --- /dev/null +++ b/roles/serveur_artefacts/defaults/main.yml @@ -0,0 +1,47 @@ +--- +# LA SOURCE D'ARTEFACTS DE L'ÉCOSYSTÈME — apt-cacher-ng. +# +# Un écosystème qui prétend se reproduire depuis son propre génome allait chercher, pour +# poser une seule machine, les binaires de **six serveurs étrangers** : deb.debian.org, +# security.debian.org, packages.smallstep.com, apt.grafana.com, packages.icinga.com, +# codeberg.org. La forge héberge le CODE ; il manquait qui héberge les BINAIRES. +# +# CE N'EST PAS THÉORIQUE. Deux cicatrices dans ce dépôt : +# - `packages.smallstep.com` est intermittent — la même URL pend au premier essai et +# répond en 0,48 s au second. Ça a fait échouer un déploiement de quarante minutes +# sur la PREMIÈRE machine (2026-08-23) ; +# - `galaxy.ansible.com` est carrément injoignable depuis l'overlay, et c'est très bien +# ainsi — on l'a réglé par le cache du contrôleur, pas en ouvrant la frontière. +# +# CE RÔLE COUVRE LES DÉPÔTS APT. Les téléchargements DIRECTS (binaire Forgejo, Keycloak, +# Nextcloud, oauth2-proxy, collections Ansible) sont déjà couverts, autrement : le +# contrôleur télécharge une fois dans son cache et pousse par SSH. Deux mécanismes, +# parce que ce sont deux problèmes — on ne sert pas un dépôt apt par `scp`. + +serveur_artefacts_paquets: + - apt-cacher-ng +serveur_artefacts_service: "apt-cacher-ng" +serveur_artefacts_port: 3142 +# L'écoute est large : les clients sont sur d'autres hôtes de la flotte. C'est le pare-feu +# est-ouest, dérivé de meta/flux.yml, qui restreint QUI peut s'y adresser — pas l'adresse +# d'écoute, qui ne saurait distinguer un voisin légitime d'un autre. +serveur_artefacts_ecoute: "0.0.0.0" +serveur_artefacts_cache: "/var/cache/apt-cacher-ng" + +# Combien de temps un paquet reste en cache sans être redemandé. 30 jours : assez pour +# qu'une reconstruction complète retrouve tout ce que la précédente a téléchargé. +serveur_artefacts_expiration_jours: 30 + +# --- LE MODE HORS LIGNE, QUI EST AUSSI L'ÉPREUVE ----------------------------- +# +# `true` : le cache ne va PLUS rien chercher en amont — il sert ce qu'il détient, et +# refuse le reste. C'est le mode d'un écosystème coupé du monde, et c'est surtout le seul +# moyen de PROUVER que le cache sert vraiment : tant qu'internet répond, un `apt update` +# qui réussit ne dit pas d'où vient l'octet. +# +# Laisser à `false` en exploitation normale. +serveur_artefacts_hors_ligne: false + +# Journalisation : apt-cacher-ng tient un journal des requêtes servies. C'est lui qui +# permet de répondre à « ce paquet, l'avons-nous servi ou est-il venu du dehors ? ». +serveur_artefacts_journal: "/var/log/apt-cacher-ng" diff --git a/roles/serveur_artefacts/handlers/main.yml b/roles/serveur_artefacts/handlers/main.yml new file mode 100644 index 0000000..8197201 --- /dev/null +++ b/roles/serveur_artefacts/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Redémarrer le cache d'artefacts + ansible.builtin.systemd: + name: "{{ serveur_artefacts_service }}" + state: restarted + when: not ansible_check_mode diff --git a/roles/serveur_artefacts/meta/authentification.yml b/roles/serveur_artefacts/meta/authentification.yml new file mode 100644 index 0000000..b64ecca --- /dev/null +++ b/roles/serveur_artefacts/meta/authentification.yml @@ -0,0 +1,14 @@ +--- +# Position de ce role dans la directive d'authentification (D-38..D-41). +# Voir docs/authentification.md. Gardee par la preuve P29. +authentification: + portee: sans-auth-humaine + mecanisme: aucun + formulaire_local: sans-objet + secours: "Acces SSH a l'hote" + raison: >- + Un cache de paquets ne sert que des machines, jamais des humains : il n'expose + aucune interface a federer. Ce qu'il sert est PUBLIC par nature -- les depots + Debian le sont -- et son integrite ne repose pas sur qui demande, mais sur les + signatures que le client verifie. Restreindre QUI peut l'interroger est le travail + du pare-feu est-ouest, pas d'une authentification. diff --git a/roles/serveur_artefacts/meta/empreinte.yml b/roles/serveur_artefacts/meta/empreinte.yml new file mode 100644 index 0000000..3ecdbb6 --- /dev/null +++ b/roles/serveur_artefacts/meta/empreinte.yml @@ -0,0 +1,8 @@ +--- +# Empreinte ressources — un cache apt ne calcule rien, il stocke. Le disque est la seule +# dimension qui compte : un socle Debian complet plus les paquets des rôles tiennent +# largement sous 15 Go, et l'expiration recycle le reste. +setops_empreinte: + coeurs: 1 + memoire_mo: 512 + disque_go: 15 diff --git a/roles/serveur_artefacts/meta/flux.yml b/roles/serveur_artefacts/meta/flux.yml new file mode 100644 index 0000000..5475c05 --- /dev/null +++ b/roles/serveur_artefacts/meta/flux.yml @@ -0,0 +1,18 @@ +--- +# Flux réseau de la source d'artefacts. Voir docs/flux-conception.md. +flux: + - sens: ingress + port: 3142 + protocole: tcp + pair: flotte + chiffrement: clair + raison: >- + Toute la flotte prend ses paquets ici. En clair, et c'est correct : l'intégrité + d'un dépôt apt vient de ses signatures, qu'apt vérifie de toute façon — un + intermédiaire ne peut pas altérer un paquet sans se faire prendre. + - sens: egress + port: 80 + protocole: tcp + pair: externe + chiffrement: clair + raison: "Remplir le cache depuis les dépôts Debian amont (deb.debian.org, security)." diff --git a/roles/serveur_artefacts/tasks/main.yml b/roles/serveur_artefacts/tasks/main.yml new file mode 100644 index 0000000..3a8cb20 --- /dev/null +++ b/roles/serveur_artefacts/tasks/main.yml @@ -0,0 +1,66 @@ +--- +# LE CACHE NE DOIT PAS DÉPENDRE DE LUI-MÊME POUR SE RÉPARER. +# +# La première version faisait `update_cache: true` à chaque passage. Sur l'hôte qui PORTE +# le cache, cet `apt update` passe par le cache lui-même — et en mode hors ligne, il est +# refusé. Le rôle qui devait remettre le service en ligne ne pouvait donc plus s'exécuter : +# +# « Failed to update apt cache after 5 retries » (mesuré le 2026-08-23, après +# l'épreuve hors ligne : il a fallu réparer la machine à la main) +# +# Un service dont la remise en route exige qu'il fonctionne déjà est un piège. On ne +# rafraîchit donc l'index qu'à la PREMIÈRE installation, quand aucun cache n'existe encore +# et qu'`apt` va forcément à l'amont. +- name: Le cache est-il déjà installé ? + ansible.builtin.stat: + path: /usr/sbin/apt-cacher-ng + register: serveur_artefacts_present + +- name: Installer le cache d'artefacts + ansible.builtin.apt: + name: "{{ serveur_artefacts_paquets }}" + state: present + update_cache: "{{ not serveur_artefacts_present.stat.exists }}" + when: not ansible_check_mode + +- name: Assurer le répertoire de cache + ansible.builtin.file: + path: "{{ serveur_artefacts_cache }}" + state: directory + owner: apt-cacher-ng + group: apt-cacher-ng + mode: "0755" + when: not ansible_check_mode + +# apt-cacher-ng lit TOUS les `*.conf` de son répertoire, par ordre alphabétique, et le +# dernier lu gagne. Le préfixe `zzz-` place donc notre configuration après celle du +# paquet Debian et celle de debconf, sans avoir à les éditer — on ajoute, on ne remplace +# pas. Une valeur posée ici l'emporte, et `apt purge` rend la machine à son état d'origine. +- name: Déployer la configuration Set-OPS + ansible.builtin.template: + src: zzz-setops.conf.j2 + dest: /etc/apt-cacher-ng/zzz-setops.conf + owner: root + group: root + mode: "0644" + notify: Redémarrer le cache d'artefacts + +- name: Activer et démarrer le cache d'artefacts + ansible.builtin.systemd: + name: "{{ serveur_artefacts_service }}" + enabled: true + state: started + when: not ansible_check_mode + +- name: Appliquer les redémarrages avant de vérifier + ansible.builtin.meta: flush_handlers + +# ÉCRIRE, PUIS RELIRE (D-68). Un service « active » qui n'écoute pas sur le port attendu +# est un vert sur un périmètre vide : les clients le découvriraient à leur premier +# `apt update`, c'est-à-dire au pire moment. +- name: Le cache écoute-t-il vraiment ? + ansible.builtin.wait_for: + host: "127.0.0.1" + port: "{{ serveur_artefacts_port }}" + timeout: 30 + when: not ansible_check_mode diff --git a/roles/serveur_artefacts/templates/zzz-setops.conf.j2 b/roles/serveur_artefacts/templates/zzz-setops.conf.j2 new file mode 100644 index 0000000..488b731 --- /dev/null +++ b/roles/serveur_artefacts/templates/zzz-setops.conf.j2 @@ -0,0 +1,19 @@ +# GÉNÉRÉ par Set-OPS (rôle serveur_artefacts). NE PAS éditer à la main. +# +# Lu APRÈS acng.conf et zz_debconf.conf : ces valeurs l'emportent. + +Port: {{ serveur_artefacts_port }} +BindAddress: {{ serveur_artefacts_ecoute }} +CacheDir: {{ serveur_artefacts_cache }} +LogDir: {{ serveur_artefacts_journal }} + +ExThreshold: {{ serveur_artefacts_expiration_jours }} + +{% if serveur_artefacts_hors_ligne | bool %} +# MODE HORS LIGNE : le cache ne va plus rien chercher en amont. Il sert ce qu'il détient +# et refuse le reste — c'est l'état d'un écosystème coupé du monde, et l'épreuve qui +# montre ce que le cache détient VRAIMENT. +Offlinemode: 1 +{% else %} +Offlinemode: 0 +{% endif %}