ADR-0171 : Durcissement de l'installation pour la production bancaire
- Statut : accepté (chantiers 1, 2 et 3 implémentés dans le Manager et les agents, cycle de mise à niveau du chantier 5 en place, chantier 4 porté par le Hub)
- Date : 2026-11-01
- Portée : Navin Platform Manager (
navin-manager/), agents d'installation (agent/,agent-go/), Navin Trusted Hub - Documents liés : ADR-0118 (licences signées), ADR-0119 (agent Go opérateur), ADR-0124 (bootstrap SSH des agents),
ADR-0127 (sauvegarde et PRA),
navin-manager/docs/fr/hardened-installation.md(guide opérateur)
Contexte
Le Manager installe les 51 services du catalogue (49 depuis le retrait de Perses et d'Infisical OSS, ADR-0169) avec quatre méthodes :
| Méthode | Services | Principe |
|---|---|---|
binary | 24 | Archive officielle vérifiée par sha256, unité systemd durcie |
podman | 12 | Image OCI du miroir, podman run piloté par systemd |
python | 7 | venv isolé par service, wheels du miroir |
package | 2 | Paquets officiels Debian/RHEL |
L'installation est hors ligne (miroir / Trusted Hub), le catalogue d'artefacts est validé, le cycle de vie passe par systemd et le Manager sait se connecter aux services existants du client (PostgreSQL, Redis, Keycloak).
Une revue d'architecture avant déploiement en banque a relevé cinq faiblesses :
- tous les conteneurs tournaient avec
podman run --rm --network host --pull never ...: le conteneur partage l'espace réseau de l'hôte, y compris les sockets locaux (127.0.0.1), ce qui affaiblit l'isolation ; - les unités Podman sont des commandes
podman rungénérées, alors que Podman et Red Hat recommandent Quadlet (fichiers.containerdéclaratifs convertis en unités systemd par le générateur) ; - tous les conteneurs sont lancés de la même façon : pas de profil de sécurité par service (capabilities, système de fichiers en lecture seule, quotas CPU/RAM, GPU via CDI, health checks) ;
- le sha256 garantit l'intégrité mais pas l'origine : il manque signatures Cosign, SBOM, scan CVE, contrôle des licences, approbation des versions et manifest signé ;
- le cycle de vie s'arrête à « installer, démarrer, vérifier » : pas de sauvegarde avant changement, de validation applicative ni de rollback qui tienne compte des migrations de schéma.
Décision
Ce qui est conservé
| Élément | Décision |
|---|---|
Les 4 méthodes binary, podman, python, package | Conservées. On ne conteneurise pas systématiquement les 51 services. |
| Installation hors ligne | Conservée |
| Catalogue d'artefacts validés | Conservé, devient la base du Trusted Hub |
| Pilotage systemd | Conservé (Quadlet produit lui aussi des unités systemd) |
| Connexion aux services existants | Conservée |
| Podman plutôt que Docker en production | Conservé |
Chantier 1 : Réseau isolé par défaut (implémenté)
- Chaque composant Podman déclare une politique réseau
run.podman.networkdans le catalogue :isolated(défaut) : réseau bridge propre au clusternavin-<cluster>; seuls les ports déclarés dansportsdu composant sont publiés (--publish <port>:<port>/<proto>), liés à l'adresse de l'hôte ;host: réseau de l'hôte, uniquement avec une justification écrite (network_justification) contrôlée au chargement du catalogue. Cas légitimes : membres de cluster qui annoncent l'adresse de l'hôte (gossip, Raft, JGroups), sondes noyau (Falco, Tetragon), VRRP ;none: aucun réseau (tâches batch hors ligne).
- Les communications internes passent par le réseau du cluster ; les flux entre hôtes restent ceux déclarés et
ouverts par
firewall.open(inchangé). - Les MCP et composants non fiables exécutés pour un tenant ou un projet utiliseront un réseau nommé par tenant
(
navin-<cluster>-<tenant>), sans route vers les autres tenants (à implémenter avec l'exécution des MCP). - État du catalogue : les composants dont la configuration est liée à l'adresse de l'hôte ou à sa boucle locale
(membres de cluster, liaison sur
host.ipou127.0.0.1) déclarent provisoirementnetwork: hostavec une justification ; chaque migration versisolatedest validée service par service (tableau du guide opérateur).
Chantier 2 : Quadlet (implémenté, bascule par paramètre)
- Le Manager rend (
catalog.render.quadlet) un fichier Quadletnavin-<service>-<composant>.container(et le.networkdu cluster) à partir de la même spécification quepodman run: image par digest,Pull=never, réseau, ports publiés, volumes, capabilities,ReadOnly,NoNewPrivileges, quotas et GPU par CDI. Les contrôles de santé restent ceux du Manager et les secrets passent par le fichier d'environnement 0640, comme pourpodman run. - Le fichier est déposé dans
/etc/containers/systemd/; le générateur Quadlet produitnavin-<service>-<composant>.service: le nom d'unité, la supervision, les logs journald et le pilotageservice.controldu Manager restent identiques. - Paramètre
NM_PODMAN_QUADLET(défauttrue) : la tâche agentquadlet.unit(agents Python et Go) écrit le.containeret le.network, retire l'unitépodman runremplacée et recharge systemd. Prérequis hôte : Podman 4.9 ou plus récent ; un hôte plus ancien garde l'unitépodman run. L'unité générée est démarrée, jamais activée : sa section[Install]la démarre au boot.
Chantier 3 : Profil de sécurité par service (implémenté)
| Contrôle | Politique Navin | Champ catalogue |
|---|---|---|
| Utilisateur | Rootless / utilisateur non root dans le conteneur dès que compatible | run.podman.user |
| Réseau | Isolé par défaut | run.podman.network |
| Privilèges | --security-opt no-new-privileges toujours ; --cap-drop ALL par service (cap_drop_all) ; tout ajout est justifié | cap_add, privileged_justification |
| Système de fichiers | --read-only (tmpfs sur /tmp, /var/tmp, /run) quand possible | read_only |
| Volumes | Persistants sous /var/lib/navin/<service>, option Z (SELinux), lecture seule pour la configuration | volumes |
| CPU / RAM | Quotas par composant (--cpus, --memory, --pids-limit) | limits.cpus, limits.memory, limits.pids |
| GPU | Uniquement les GPU autorisés via CDI (--device nvidia.com/gpu=<index>) | gpu, paramètre gpu_devices |
| Secrets | Injectés à l'exécution depuis OpenBao (fichier d'environnement 0640 hors argv aujourd'hui, podman secret avec Quadlet) | secret_params |
| SELinux / AppArmor | Actifs selon le système ; jamais label=disable sans justification | security_opt |
| Health checks | Obligatoires pour les composants permanents : le chargeur du catalogue refuse un composant master ou worker sans health | health |
Les profils spécifiques (Falco, Tetragon : --privileged, --pid=host, accès BPF) sont autorisés seulement
avec privileged_justification dans le catalogue ; ils restent visibles dans l'aperçu du plan.
Chantier 4 : Trusted Hub (porté par l'agent Hub)
Contrôles exigés avant qu'un artefact soit installable :
| Contrôle | Objectif |
|---|---|
| SHA256 | Intégrité (déjà en place : artifact.fetch, images par digest) |
| Signature Cosign | Origine approuvée ; vérification hors ligne avec la clé publique et le bundle fournis par le Hub |
| SBOM (SPDX ou CycloneDX) | Inventaire des composants et dépendances |
| Scan CVE | Vulnérabilités connues, seuil bloquant configurable |
| Licence | Politique ADR-0101 (MIT, Apache-2.0, MPL-2.0 autorisées ; GPL/AGPL selon usage) |
| Approbation de version | Une version non approuvée n'est jamais installée |
| Manifest signé | Versions, paramètres et dépendances autorisés, signé par le Hub |
Le Manager consomme le manifest signé du Hub (mirror.py) : il refuse un artefact absent du manifest, non signé
ou non approuvé. Les champs signature, sbom, cve, license et approved du manifest sont transmis au plan et
à l'audit.
Chantier 5 : Mise à niveau par méthode
| Méthode | Ajouts |
|---|---|
binary | Signature Cosign du fichier, dépendances système déclarées, répertoire de version immuable /opt/navin/<service>/<version>, lien current basculé atomiquement (rollback = rebascule du lien) |
python | Wheels précompilées du Hub, versions figées, pip install --require-hashes --no-index, venv par version |
package | Dépôts signés (GPG) du Hub, versions figées, apt/dnf sans dépôt externe |
podman | Quadlet, images signées par digest, réseau isolé, profil de sécurité |
Cycle de vie cible
- Vérifier les prérequis et les droits (preflight, RBAC, approbation)
- Valider l'artefact signé (Hub : signature, SBOM, CVE, licence, version approuvée)
- Vérifier les dépendances (bindings, services requis, versions compatibles)
- Sauvegarder la configuration et l'état (fichiers gérés, unité, dump ou snapshot si migration de schéma)
- Installer la version demandée (répertoire de version, venv par version, image par digest)
- Démarrer et contrôler la santé (health checks)
- Valider le fonctionnement applicatif (
service_check) - Confirmer, ou déclencher le rollback
Le rollback tient compte des migrations : un service dont la mise à niveau migre le schéma déclare
upgrade.migrates_schema: true ; le rollback restaure alors la sauvegarde de l'étape 4 avant de rebasculer le
binaire. Revenir au binaire précédent ne suffit pas.
Actions privilégiées
- Tâches signées par le Manager (déjà en place :
signing.py, vérification par l'agent) et autorisées par RBAC. - Journal d'audit de chaque installation, mise à niveau et rollback (chaîne d'audit ADR-0116).
- Compte de service à privilèges minimaux sur chaque machine (sudoers limité, ADR-0124).
- La restriction aux unités
navin-*est une garde utile mais n'est pas une barrière de sécurité à elle seule : la liste blanche des types de tâches, le path guard et la signature des tâches restent indispensables.
Haute disponibilité
systemd et Podman restent la bonne brique par hôte. Ils ne font pas d'orchestration multi-hôtes : quorums, réplicas et mises à jour progressives restent coordonnés par le Manager (rolling restart, ordre des composants, ADR-0126).
Conséquences
- Un composant Podman sans politique réseau passe en réseau isolé : les services qui ont besoin du réseau hôte le déclarent avec une justification, contrôlée par le chargeur du catalogue.
- Tous les conteneurs non privilégiés reçoivent
no-new-privileges;--cap-drop ALLet--read-onlysont activés service par service après validation (les images qui changent d'utilisateur au démarrage en ont besoin). - Les quotas
limits.cpus,limits.memory,limits.pidss'appliquent aussi aux méthodes binary et python (CPUQuota,MemoryMax,TasksMaxde l'unité systemd). - Quadlet est utilisé par défaut (
NM_PODMAN_QUADLET) sur chaque hôte systemd dont Podman est en version 4.9 ou plus récente et dont l'agent annoncequadlet.unitdans ses faits (task_types) ; les autres hôtes gardentpodman run. - Le Trusted Hub devient bloquant pour toute installation en production.
Suivi
| Chantier | État |
|---|---|
| 1. Réseau isolé par défaut | Implémenté (navin-manager/server/src/navin_manager/catalog/render.py, chargeur, schéma) : un réseau par service, ports publiés selon leur portée (ports par mode et ports d'une liste compris), ports entre hôtes ouverts aux seuls hôtes du cluster ; réseau de l'hôte gardé seulement pour falco et tetragon (sondes noyau), livekit (médias WebRTC) et vllm en mode distributed, chacun avec sa justification propre |
| 2. Quadlet | Implémenté : rendu (GPU par CDI compris), tâche agent quadlet.unit des agents Python et Go, bascule par NM_PODMAN_QUADLET ; isolation des conteneurs affichée dans l'onglet Sécurité de la fiche service |
| 3. Profil de sécurité par service | Implémenté (no-new-privileges, cap_drop_all, read_only, security_opt, quotas, GPU CDI existant) ; profils à activer service par service |
| 4. Trusted Hub | Confié à l'agent Hub (contrat ci-dessus) |
| 5. Mise à niveau et rollback | Implémenté dans le Manager (navin-manager/server/src/navin_manager/services/upgrades.py) : les 8 étapes du cycle sont rattachées au code et aux tests dans la section 5 du guide de l'installation durcie ; dépendances de la version cible vérifiées avant toute tâche, hôte canari, sauvegarde avant la mise à niveau d'un service à état (upgrade.backup_before), instantané scellé de la version précédente, retour arrière automatique sur demande (auto_rollback), rollback refusé après une migration irréversible (upgrade.migrations[].irreversible) et restauration de la sauvegarde à la place ; --require-hashes et un venv par version pour Python, paquets à version figée sans désactiver la signature des dépôts |