flux : l'ICMP n'a pas de port — nftables n'avait jamais demarre
Le generateur emettait `{protocole} dport {port}` pour tout flux. L'ICMP a un
type et un code, pas un port : `icmp dport frag-needed` produit un jeu que `nft`
rejette, et un jeu rejete ne se charge PAS — l'hote perd sa barriere au lieu
d'en gagner une.
Le defaut touchait les 14 hotes. La seconde barriere de D-31 n'avait jamais pu
demarrer nulle part ; personne ne l'avait vu parce qu'aucune VM tenant n'avait
encore ete deployee.
`_selecteur_nft()` traduit : `icmp frag-needed` devient
`icmp type destination-unreachable icmp code frag-needed`. Un code inconnu est
refuse a la generation, avec le nom du role fautif. La garde tourne aussi a la
verification, pour attraper un flux declare mais pas encore porte.
Mesure : nftables actif sur infra-pki-01 et infra-dns-01, 16 et 17 regles, dont
la garde anti-lockout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e276e76f32
commit
3786cc055b
3 changed files with 77 additions and 2 deletions
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -2,6 +2,37 @@
|
||||||
|
|
||||||
## 2026-08-06 — le chemin nord-sud devient dérivable
|
## 2026-08-06 — le chemin nord-sud devient dérivable
|
||||||
|
|
||||||
|
### L'ICMP n'a pas de port — et la seconde barrière n'avait jamais démarré
|
||||||
|
|
||||||
|
`nftables.service` refusait de démarrer sur la première VM déployée :
|
||||||
|
|
||||||
|
```
|
||||||
|
/etc/nftables.conf:23 icmp dport frag-needed accept
|
||||||
|
^^^^^ syntax error, unexpected string
|
||||||
|
```
|
||||||
|
|
||||||
|
Le générateur émettait `{protocole} dport {port}` pour **tout** flux. L'ICMP n'a pas de
|
||||||
|
port : il a un type et un code. Les deux flux PMTUD de D-30 produisaient donc un jeu que
|
||||||
|
`nft` rejette — et un jeu rejeté ne se charge pas du tout, si bien que l'hôte **perd** sa
|
||||||
|
barrière au lieu d'en gagner une.
|
||||||
|
|
||||||
|
Le défaut touchait les quatorze hôtes. La seconde barrière de D-31 — les nftables d'hôte,
|
||||||
|
qui doublent le filtrage de l'hyperviseur — n'avait jamais pu démarrer nulle part. Personne
|
||||||
|
ne s'en était aperçu parce qu'aucune VM tenant n'avait encore été déployée.
|
||||||
|
|
||||||
|
`_selecteur_nft()` traduit désormais : `tcp dport 22` reste tel quel,
|
||||||
|
`icmp frag-needed` devient `icmp type destination-unreachable icmp code frag-needed`. Un
|
||||||
|
code ICMP inconnu est **refusé à la génération**, avec le nom du rôle fautif.
|
||||||
|
|
||||||
|
**La garde tourne aussi à la vérification**, pas seulement à la génération. Un rôle peut
|
||||||
|
déclarer un flux que personne ne porte encore : le devis passerait, et la panne arriverait
|
||||||
|
le jour où un hôte prend ce rôle. `nft -c` en local demanderait des privilèges netlink ;
|
||||||
|
construire le sélecteur ne coûte rien et attrape exactement la même faute.
|
||||||
|
|
||||||
|
Mesuré après correction : `nftables=active` sur les deux VM, 16 et 17 règles chargées, dont
|
||||||
|
la garde anti-lockout `ip saddr 10.0.0.0/24 tcp dport 22 accept`.
|
||||||
|
|
||||||
|
|
||||||
### `client_unbound` devient universel — et l'amorçage DNS trouve sa place
|
### `client_unbound` devient universel — et l'amorçage DNS trouve sa place
|
||||||
|
|
||||||
Une VM ne peut pas s'installer sans résoudre des noms : `apt` en dépend. Or le DNS
|
Une VM ne peut pas s'installer sans résoudre des noms : `apt` en dépend. Or le DNS
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@
|
||||||
| `serveur_postgresql` | ingress | 5432 | tcp | serveur_keycloak, serveur_forgejo, serveur_icinga, serveur_nextcloud | tls-requis | Connexions applicatives à PostgreSQL (verify-full ; pg_hba hostssl). |
|
| `serveur_postgresql` | ingress | 5432 | tcp | serveur_keycloak, serveur_forgejo, serveur_icinga, serveur_nextcloud | tls-requis | Connexions applicatives à PostgreSQL (verify-full ; pg_hba hostssl). |
|
||||||
| `serveur_powerdns` | ingress | 53 | udp | flotte | clair | Résolution DNS interne (zone souveraine). DoT/DoH = feuille de route (chiffrement DNS). |
|
| `serveur_powerdns` | ingress | 53 | udp | flotte | clair | Résolution DNS interne (zone souveraine). DoT/DoH = feuille de route (chiffrement DNS). |
|
||||||
| `serveur_powerdns` | ingress | 53 | tcp | flotte | clair | Résolution DNS interne en TCP (réponses volumineuses, AXFR restreint par allow_axfr_ips). |
|
| `serveur_powerdns` | ingress | 53 | tcp | flotte | clair | Résolution DNS interne en TCP (réponses volumineuses, AXFR restreint par allow_axfr_ips). |
|
||||||
|
| `serveur_powerdns` | egress | 53 | udp | externe | clair | Résolution sortante du serveur autoritatif POUR SES PROPRES besoins (apt, NTP) — il ne récurse pour aucun autre hôte. |
|
||||||
|
| `serveur_powerdns` | egress | 53 | tcp | externe | clair | Repli TCP de la résolution sortante du serveur autoritatif (réponses dépassant la taille UDP). |
|
||||||
| `serveur_prometheus` | ingress | 9090 | tcp | localhost | clair | Console Prometheus consommée en local par Grafana co-localisé (pas d'exposition inter-nœud). |
|
| `serveur_prometheus` | ingress | 9090 | tcp | localhost | clair | Console Prometheus consommée en local par Grafana co-localisé (pas d'exposition inter-nœud). |
|
||||||
| `serveur_prometheus` | egress | 9100 | tcp | client_metrique | tls | Scrape des node_exporter (HTTPS via cert step-ca) sur chaque nœud instrumenté. |
|
| `serveur_prometheus` | egress | 9100 | tcp | client_metrique | tls | Scrape des node_exporter (HTTPS via cert step-ca) sur chaque nœud instrumenté. |
|
||||||
| `serveur_redis` | ingress | 6379 | tcp | localhost | clair | Cache/verrous consommés uniquement par l'application co-localisée (ex. Nextcloud). Aucune exposition inter-nœud. |
|
| `serveur_redis` | ingress | 6379 | tcp | localhost | clair | Cache/verrous consommés uniquement par l'application co-localisée (ex. Nextcloud). Aucune exposition inter-nœud. |
|
||||||
|
|
@ -79,7 +81,7 @@
|
||||||
|
|
||||||
## Synthèse chiffrement
|
## Synthèse chiffrement
|
||||||
|
|
||||||
- **clair** : 26 flux
|
- **clair** : 28 flux
|
||||||
- **n-a** : 3 flux
|
- **n-a** : 3 flux
|
||||||
- **ssh** : 3 flux
|
- **ssh** : 3 flux
|
||||||
- **starttls** : 6 flux
|
- **starttls** : 6 flux
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,34 @@ def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict
|
||||||
return sorted({ip_par_hote[h] for h in noms if h in ip_par_hote})
|
return sorted({ip_par_hote[h] for h in noms if h in ip_par_hote})
|
||||||
|
|
||||||
|
|
||||||
|
# Codes ICMP de destination inatteignable qu'un flux peut nommer dans `port`. L'ICMP n'a
|
||||||
|
# PAS de port : il a un type et un code. Emettre `icmp dport frag-needed` produit un jeu
|
||||||
|
# que `nft` refuse — et le service ne demarre plus du tout, donc l'hote perd sa barriere
|
||||||
|
# au lieu d'en gagner une. Constate le 2026-08-06 sur les 14 hotes a la fois.
|
||||||
|
CODES_ICMP = {
|
||||||
|
"frag-needed": "icmp type destination-unreachable icmp code frag-needed",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _selecteur_nft(protocole: str, port) -> str:
|
||||||
|
"""Selecteur nftables pour un flux : `tcp dport 22`, ou l'equivalent ICMP.
|
||||||
|
|
||||||
|
Un `port` non numerique sur de l'ICMP designe un code, jamais un port. Un code
|
||||||
|
inconnu est refuse ici plutot que de produire un fichier que `nft` rejettera —
|
||||||
|
l'erreur se lit au moment de la generation, pas trois deploiements plus tard.
|
||||||
|
"""
|
||||||
|
p = str(protocole).lower()
|
||||||
|
if p in ("icmp", "icmpv6"):
|
||||||
|
cle = str(port).strip()
|
||||||
|
if cle in CODES_ICMP:
|
||||||
|
return CODES_ICMP[cle]
|
||||||
|
raise ErreurFlux(
|
||||||
|
f"Flux ICMP avec `port: {port}` : l'ICMP n'a pas de port. Nommer un code "
|
||||||
|
f"connu ({', '.join(sorted(CODES_ICMP))}) ou etendre CODES_ICMP."
|
||||||
|
)
|
||||||
|
return f"{p} dport {port}"
|
||||||
|
|
||||||
|
|
||||||
def generer_nftables(flux_par_role: dict[str, list[dict]]) -> list[Path]:
|
def generer_nftables(flux_par_role: dict[str, list[dict]]) -> list[Path]:
|
||||||
data = yaml.safe_load(_inventaire().read_text(encoding="utf-8")) or {}
|
data = yaml.safe_load(_inventaire().read_text(encoding="utf-8")) or {}
|
||||||
actifs_noms = set(_hotes_du_groupe(data, "hotes_actifs"))
|
actifs_noms = set(_hotes_du_groupe(data, "hotes_actifs"))
|
||||||
|
|
@ -237,7 +265,8 @@ def generer_nftables(flux_par_role: dict[str, list[dict]]) -> list[Path]:
|
||||||
for port in ports:
|
for port in ports:
|
||||||
if local and not srcs:
|
if local and not srcs:
|
||||||
continue # lo deja accepte
|
continue # lo deja accepte
|
||||||
regles.append(f" {saddr}{fl['protocole']} dport {port} accept # {g}: {fl.get('raison','')}")
|
regles.append(f" {saddr}{_selecteur_nft(fl['protocole'], port)} accept"
|
||||||
|
f" # {g}: {fl.get('raison','')}")
|
||||||
contenu = _rendre_nft(hote, regles)
|
contenu = _rendre_nft(hote, regles)
|
||||||
chemin = sortie_dir / f"{hote}.nft"
|
chemin = sortie_dir / f"{hote}.nft"
|
||||||
chemin.write_text(contenu, encoding="utf-8")
|
chemin.write_text(contenu, encoding="utf-8")
|
||||||
|
|
@ -282,6 +311,19 @@ def main(argv: list[str]) -> int:
|
||||||
flux = charger_flux()
|
flux = charger_flux()
|
||||||
if commande == "verifier":
|
if commande == "verifier":
|
||||||
r, t = valider(flux)
|
r, t = valider(flux)
|
||||||
|
# Chaque flux doit produire un selecteur nftables VALIDE. Le verifier ici, et
|
||||||
|
# pas seulement a la generation, ferme le cas ou un role declare un flux que
|
||||||
|
# personne ne porte encore : le devis passerait, et la panne arriverait le
|
||||||
|
# jour ou un hote prend ce role. `nft -c` local demanderait des privileges
|
||||||
|
# (netlink) ; construire le selecteur ne coute rien et attrape la meme faute.
|
||||||
|
for role, liste in sorted(flux.items()):
|
||||||
|
for fl in liste:
|
||||||
|
ports = fl["port"] if isinstance(fl["port"], list) else [fl["port"]]
|
||||||
|
for port in ports:
|
||||||
|
try:
|
||||||
|
_selecteur_nft(fl.get("protocole", "tcp"), port)
|
||||||
|
except ErreurFlux as e:
|
||||||
|
raise ErreurFlux(f"{role} : {e}") from e
|
||||||
print(f"Flux coherents : {r} rôles, {t} flux, schéma + matrice OK.")
|
print(f"Flux coherents : {r} rôles, {t} flux, schéma + matrice OK.")
|
||||||
elif commande == "registre":
|
elif commande == "registre":
|
||||||
valider(flux)
|
valider(flux)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue