Poser le registre des domaines publics (primaire cache)
Registre declaratif docs/domaines.yml pour la publication externe des hostnames, symetrique de docs/bases-donnees.yml. Modele d'autorite : primaire cache + secondaires. Le bloc « exposition » est le binding nom public -> service interne (cible) -> edge, destine a trois consommateurs (zone publique, vhosts nginx, ACME). - inventory_rules.py : charger_domaines, valider_domaines, fqdn_exposition, expositions_du_groupe, AUTORITES_DNS. - scripts/domaines.py : CLI miroir (lister / verifier). - Makefile : cibles domaines / domaines-verifier + validation dans inventaire-verifier. Premiere entree reelle : forge.alliance-boreale.ca -> serveurs_forgejo. Secondaires, mail et DNSSEC laisses a renseigner (structurel, pas encore operationnel ; l'interne chezlepro.internal n'est pas touche). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5dd3215d91
commit
d7938d4c5a
5 changed files with 175 additions and 1 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
## 2026-06-22
|
## 2026-06-22
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
- Registre déclaratif des domaines publics `docs/domaines.yml` (publication externe des hostnames), symétrique de `docs/bases-donnees.yml`. Modèle d'autorité retenu : **primaire caché + secondaires** (PowerDNS backend PostgreSQL + API non exposé, secondaires publics via AXFR/TSIG). Bloc **`exposition`** = binding `nom public → service interne (cible) → edge`, destiné à trois consommateurs (génération de zone publique, vhosts `serveurs_nginx`, noms à certifier ACME). Validation partagée dans `inventory_rules.py` (`charger_domaines`, `valider_domaines`, `fqdn_exposition`, `expositions_du_groupe` ; autorités connues `primaire-cache|auto-heberge|delegue`, `edge` requis, FQDN uniques). CLI miroir `scripts/domaines.py` (`lister`/`verifier`) + cibles `make domaines` / `make domaines-verifier`, et validation intégrée dans `make inventaire-verifier`. Première entrée réelle : `forge.alliance-boreale.ca → serveurs_forgejo` (DNSSEC, secondaires et enregistrements mail laissés à renseigner).
|
||||||
- Formalisation de l'identité du dépôt : `AGENTS.md` (nouvelle section « Mission et identité ») et `README.md` (section « Mission ») actent que `Set-OPS` **définit et construit l'écosystème numérique souverain de Chezlepro Inc.** — piliers (identité, confiance, nommage/adressage, données, communication, observabilité/supervision, applicatif), propriétés (souverain, déclaratif/convergent) et limites honnêtes (pas d'auto-remédiation ; en grande partie défini/validé avant déploiement réel). Le template Debian 13 reste la fondation, pas la finalité.
|
- Formalisation de l'identité du dépôt : `AGENTS.md` (nouvelle section « Mission et identité ») et `README.md` (section « Mission ») actent que `Set-OPS` **définit et construit l'écosystème numérique souverain de Chezlepro Inc.** — piliers (identité, confiance, nommage/adressage, données, communication, observabilité/supervision, applicatif), propriétés (souverain, déclaratif/convergent) et limites honnêtes (pas d'auto-remédiation ; en grande partie défini/validé avant déploiement réel). Le template Debian 13 reste la fondation, pas la finalité.
|
||||||
- Gestion des bases de données dans la GUI et la CLI : vue « Bases » de `make inventaire-ui` présentant les serveurs de BD, les bases applicatives et leurs **chaînes de connexion** (mot de passe masqué), avec gestion complète (ajout / édition / retrait) et endpoint `POST /api/bases` (validé, jeton). CLI `scripts/bases_donnees.py` (`lister` / `verifier` / `ajouter-serveur` / `ajouter-base` / `retirer-*`) + cibles `make bases` et `make bases-verifier`. Le registre est désormais validé dans `make inventaire-verifier`. GUI et CLI partagent les mêmes règles (`inventory_rules`).
|
- Gestion des bases de données dans la GUI et la CLI : vue « Bases » de `make inventaire-ui` présentant les serveurs de BD, les bases applicatives et leurs **chaînes de connexion** (mot de passe masqué), avec gestion complète (ajout / édition / retrait) et endpoint `POST /api/bases` (validé, jeton). CLI `scripts/bases_donnees.py` (`lister` / `verifier` / `ajouter-serveur` / `ajouter-base` / `retirer-*`) + cibles `make bases` et `make bases-verifier`. Le registre est désormais validé dans `make inventaire-verifier`. GUI et CLI partagent les mêmes règles (`inventory_rules`).
|
||||||
- Rôle `serveurs_forgejo` (forge Git, Phase 5) : binaire officiel Forgejo (version épinglée + lien symbolique), utilisateur `git`, **base PostgreSQL via le registre** (4e consommateur, entrée `forgejo`), publication derrière `serveurs_nginx` (`HTTP_ADDR=127.0.0.1`, `INSTALL_LOCK`), secrets (BD + `SECRET_KEY` + `INTERNAL_TOKEN` + admin) via Vault, mailer vers `serveurs_sendmail`, compte administrateur initial créé une fois. Détails d'installation tirés de la doc officielle Forgejo. Activation de l'entrée `forgejo` dans `docs/bases-donnees.yml`. Playbook de groupe et `group_vars` production associés.
|
- Rôle `serveurs_forgejo` (forge Git, Phase 5) : binaire officiel Forgejo (version épinglée + lien symbolique), utilisateur `git`, **base PostgreSQL via le registre** (4e consommateur, entrée `forgejo`), publication derrière `serveurs_nginx` (`HTTP_ADDR=127.0.0.1`, `INSTALL_LOCK`), secrets (BD + `SECRET_KEY` + `INTERNAL_TOKEN` + admin) via Vault, mailer vers `serveurs_sendmail`, compte administrateur initial créé une fois. Détails d'installation tirés de la doc officielle Forgejo. Activation de l'entrée `forgejo` dans `docs/bases-donnees.yml`. Playbook de groupe et `group_vars` production associés.
|
||||||
|
|
|
||||||
9
Makefile
9
Makefile
|
|
@ -371,14 +371,21 @@ inventaire-verifier: ansible-runtime
|
||||||
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) verifier-playbooks --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
|
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) verifier-playbooks --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
|
||||||
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
|
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES)
|
||||||
python3 scripts/bases_donnees.py verifier
|
python3 scripts/bases_donnees.py verifier
|
||||||
|
python3 scripts/domaines.py verifier
|
||||||
|
|
||||||
.PHONY: bases bases-verifier
|
.PHONY: bases bases-verifier domaines domaines-verifier
|
||||||
bases:
|
bases:
|
||||||
python3 scripts/bases_donnees.py lister
|
python3 scripts/bases_donnees.py lister
|
||||||
|
|
||||||
bases-verifier:
|
bases-verifier:
|
||||||
python3 scripts/bases_donnees.py verifier
|
python3 scripts/bases_donnees.py verifier
|
||||||
|
|
||||||
|
domaines:
|
||||||
|
python3 scripts/domaines.py lister
|
||||||
|
|
||||||
|
domaines-verifier:
|
||||||
|
python3 scripts/domaines.py verifier
|
||||||
|
|
||||||
inventaire-lister: ansible-runtime
|
inventaire-lister: ansible-runtime
|
||||||
ansible-inventory -i $(FICHIER_INVENTAIRE) --list
|
ansible-inventory -i $(FICHIER_INVENTAIRE) --list
|
||||||
|
|
||||||
|
|
|
||||||
43
docs/domaines.yml
Normal file
43
docs/domaines.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
# Registre des domaines publics Set-OPS (publication externe des hostnames).
|
||||||
|
#
|
||||||
|
# Symetrique de docs/bases-donnees.yml : une source unique, plusieurs consommateurs.
|
||||||
|
#
|
||||||
|
# Modele d'autorite retenu : PRIMAIRE CACHE + SECONDAIRES.
|
||||||
|
# - Le primaire (PowerDNS, backend PostgreSQL + API) detient la zone faisant
|
||||||
|
# autorite mais N'EST PAS expose a Internet (NOTIFY + AXFR seulement).
|
||||||
|
# - Les "secondaires" sont la face publique : ce sont eux dans les NS + la glue
|
||||||
|
# chez le registraire. Ils tirent la zone du primaire (AXFR authentifie TSIG).
|
||||||
|
#
|
||||||
|
# Le bloc "exposition" est le binding nom-public <-> service interne, frere du
|
||||||
|
# binding appli<->base : nom public -> service interne (cible) -> edge.
|
||||||
|
# Il est lu par : (1) la generation de la zone publique (nom -> IP de l'edge),
|
||||||
|
# (2) la generation des vhosts serveurs_nginx, (3) ACME (noms a certifier).
|
||||||
|
#
|
||||||
|
# Champs par domaine :
|
||||||
|
# autorite : primaire-cache | auto-heberge | delegue
|
||||||
|
# edge : groupe Ansible qui porte l'IP publique de facade (le reverse proxy)
|
||||||
|
# secondaires : NS publics (FQDN) faisant face a Internet (a renseigner)
|
||||||
|
# dnssec : signature de la zone (jalon ulterieur ; false au depart)
|
||||||
|
# ttl : TTL par defaut de la zone
|
||||||
|
# mail : mx / spf / dmarc (lies a serveurs_sendmail) ; vides si pas de mail
|
||||||
|
# exposition : liste de { nom, cible, type }
|
||||||
|
# nom : etiquette publiee ("@" = apex du domaine) ex. forge
|
||||||
|
# cible : groupe interne servi ex. serveurs_forgejo
|
||||||
|
# type : web (vhost derriere l'edge) | direct | autre defaut: web
|
||||||
|
|
||||||
|
domaines_publics:
|
||||||
|
alliance-boreale.ca:
|
||||||
|
autorite: primaire-cache
|
||||||
|
edge: serveurs_nginx
|
||||||
|
secondaires: [] # FQDN des NS publics secondaires - a renseigner
|
||||||
|
dnssec: false # jalon ulterieur (DS chez le registraire d'abord)
|
||||||
|
ttl: 3600
|
||||||
|
mail:
|
||||||
|
mx: []
|
||||||
|
spf: ""
|
||||||
|
dmarc: ""
|
||||||
|
exposition:
|
||||||
|
- nom: forge # forge.alliance-boreale.ca
|
||||||
|
cible: serveurs_forgejo
|
||||||
|
type: web
|
||||||
52
scripts/domaines.py
Normal file
52
scripts/domaines.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Lit et valide le registre des domaines publics Set-OPS (docs/domaines.yml)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from inventory_rules import charger_domaines, fqdn_exposition, valider_domaines
|
||||||
|
|
||||||
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
|
FICHIER = RACINE / "docs/domaines.yml"
|
||||||
|
|
||||||
|
|
||||||
|
def lister(registre: dict) -> None:
|
||||||
|
domaines = registre.get("domaines_publics", {})
|
||||||
|
if not domaines:
|
||||||
|
print("Aucun domaine public declare.")
|
||||||
|
return
|
||||||
|
for nom_domaine, conf in domaines.items():
|
||||||
|
secondaires = conf.get("secondaires") or []
|
||||||
|
dnssec = "oui" if conf.get("dnssec") else "non"
|
||||||
|
print(f"{nom_domaine} [autorite {conf.get('autorite', '?')}, edge {conf.get('edge', '-')}, dnssec {dnssec}]")
|
||||||
|
print(f" secondaires: {', '.join(secondaires) if secondaires else '(aucun)'}")
|
||||||
|
for entree in conf.get("exposition") or []:
|
||||||
|
fqdn = fqdn_exposition(nom_domaine, entree.get("nom", ""))
|
||||||
|
print(f" exposition: {fqdn} -> {entree.get('cible', '?')} ({entree.get('type', 'web')})")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Registre des domaines publics Set-OPS.")
|
||||||
|
sub = parser.add_subparsers(dest="commande", required=True)
|
||||||
|
sub.add_parser("lister", help="Affiche domaines, autorite et expositions.")
|
||||||
|
sub.add_parser("verifier", help="Valide la coherence du registre.")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
registre = charger_domaines(FICHIER)
|
||||||
|
if args.commande == "lister":
|
||||||
|
lister(registre)
|
||||||
|
elif args.commande == "verifier":
|
||||||
|
valider_domaines(registre)
|
||||||
|
print("Registre des domaines valide.")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"erreur: {exc}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -82,6 +82,77 @@ def chaine_connexion(entree: dict, serveur: dict, secret: str = "***") -> str:
|
||||||
return f"{type_bd}://{entree.get('proprietaire', '?')}:{secret}@{hote}:{port}/{entree.get('base', '?')}"
|
return f"{type_bd}://{entree.get('proprietaire', '?')}:{secret}@{hote}:{port}/{entree.get('base', '?')}"
|
||||||
|
|
||||||
|
|
||||||
|
AUTORITES_DNS = {"primaire-cache", "auto-heberge", "delegue"}
|
||||||
|
|
||||||
|
|
||||||
|
def charger_domaines(path: Path | None) -> dict:
|
||||||
|
"""Charge le registre des domaines publics (domaines_publics)."""
|
||||||
|
if not path or not path.exists():
|
||||||
|
return {"domaines_publics": {}}
|
||||||
|
with path.open("r", encoding="utf-8") as fichier:
|
||||||
|
data = yaml.safe_load(fichier) or {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError(f"{path} ne contient pas une table YAML.")
|
||||||
|
data.setdefault("domaines_publics", {})
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def fqdn_exposition(domaine: str, nom: str) -> str:
|
||||||
|
"""FQDN publie pour une etiquette ('@' ou le domaine lui-meme = apex)."""
|
||||||
|
nom = str(nom).strip()
|
||||||
|
if nom in ("@", "", domaine):
|
||||||
|
return domaine
|
||||||
|
return f"{nom}.{domaine}"
|
||||||
|
|
||||||
|
|
||||||
|
def valider_domaines(registre: dict) -> None:
|
||||||
|
"""Valide la coherence du registre des domaines publics."""
|
||||||
|
domaines = registre.get("domaines_publics") or {}
|
||||||
|
if not isinstance(domaines, dict):
|
||||||
|
raise ValueError("domaines_publics doit etre une table.")
|
||||||
|
fqdns: set[str] = set()
|
||||||
|
for nom_domaine, conf in domaines.items():
|
||||||
|
if not isinstance(conf, dict):
|
||||||
|
raise ValueError(f"Domaine '{nom_domaine}': table attendue.")
|
||||||
|
autorite = str(conf.get("autorite", "")).strip()
|
||||||
|
if autorite not in AUTORITES_DNS:
|
||||||
|
attendu = ", ".join(sorted(AUTORITES_DNS))
|
||||||
|
raise ValueError(f"Domaine '{nom_domaine}': autorite '{autorite}' inconnue (attendu: {attendu}).")
|
||||||
|
if not str(conf.get("edge", "")).strip():
|
||||||
|
raise ValueError(f"Domaine '{nom_domaine}': champ 'edge' requis.")
|
||||||
|
secondaires = conf.get("secondaires") or []
|
||||||
|
if not isinstance(secondaires, list):
|
||||||
|
raise ValueError(f"Domaine '{nom_domaine}': 'secondaires' doit etre une liste.")
|
||||||
|
exposition = conf.get("exposition") or []
|
||||||
|
if not isinstance(exposition, list):
|
||||||
|
raise ValueError(f"Domaine '{nom_domaine}': 'exposition' doit etre une liste.")
|
||||||
|
for entree in exposition:
|
||||||
|
if not isinstance(entree, dict):
|
||||||
|
raise ValueError(f"Domaine '{nom_domaine}': entree d'exposition invalide (table attendue).")
|
||||||
|
for champ in ("nom", "cible"):
|
||||||
|
if not str(entree.get(champ, "")).strip():
|
||||||
|
raise ValueError(f"Domaine '{nom_domaine}': exposition, champ '{champ}' requis.")
|
||||||
|
fqdn = fqdn_exposition(nom_domaine, entree["nom"])
|
||||||
|
if fqdn in fqdns:
|
||||||
|
raise ValueError(f"Exposition en double: {fqdn}")
|
||||||
|
fqdns.add(fqdn)
|
||||||
|
|
||||||
|
|
||||||
|
def expositions_du_groupe(registre: dict, groupe: str) -> list[dict]:
|
||||||
|
"""Expositions publiques dont la cible est un groupe interne donne."""
|
||||||
|
resultat = []
|
||||||
|
for nom_domaine, conf in (registre.get("domaines_publics") or {}).items():
|
||||||
|
for entree in (conf.get("exposition") or []):
|
||||||
|
if entree.get("cible") == groupe:
|
||||||
|
resultat.append({
|
||||||
|
"domaine": nom_domaine,
|
||||||
|
"fqdn": fqdn_exposition(nom_domaine, entree.get("nom", "")),
|
||||||
|
"type": entree.get("type", "web"),
|
||||||
|
"edge": conf.get("edge"),
|
||||||
|
})
|
||||||
|
return resultat
|
||||||
|
|
||||||
|
|
||||||
def charger_nomenclature(path: Path | None) -> dict:
|
def charger_nomenclature(path: Path | None) -> dict:
|
||||||
"""Charge le registre de nomenclature (domaines, categories, adressage)."""
|
"""Charge le registre de nomenclature (domaines, categories, adressage)."""
|
||||||
if not path or not path.exists():
|
if not path or not path.exists():
|
||||||
|
|
|
||||||
Reference in a new issue