artefacts : la forge sert le code, il manquait qui sert les binaires
Some checks failed
verifier / verifier (push) Has been cancelled

serveur_artefacts (apt-cacher-ng) + client_artefacts, integration universelle
qui s'eteint quand aucun hote ne porte le service et RETIRE la direction posee.
Chez patient 0 : colocalise sur forge-01 -- la forge est la source, du code et
des binaires.

La preuve est le mode hors ligne : paquet en cache servi en 0 o/s, paquet absent
refuse par un 503. Tant qu'internet repond, un apt update qui reussit ne dit pas
d'ou vient l'octet.

Trois lecons : apt fait heriter Acquire::https::Proxy de la valeur HTTP (d'ou
403 CONNECT denied sur les depots tiers, et smallstep injoignable) ; un service
ne doit pas dependre de lui-meme pour se reparer (l'apt update du role passait
par le cache hors ligne) ; et rediriger 2>/dev/null, c'est choisir de ne pas
voir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-23 14:41:56 -04:00
parent a3f496a496
commit 32e1f6fbcd
19 changed files with 503 additions and 9 deletions

View file

@ -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/<remap>/…` — 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

View file

@ -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). |

View file

@ -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`

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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://<hôte-source>.<domaine>: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.

View file

@ -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/<remap>/…`. 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

View file

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

View file

@ -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)

View file

@ -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/<remap>/…`. 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.

View file

@ -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"

View file

@ -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

View file

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

View file

@ -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

View file

@ -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)."

View file

@ -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

View file

@ -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 %}