Aller au contenu principal

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éthodeServicesPrincipe
binary24Archive officielle vérifiée par sha256, unité systemd durcie
podman12Image OCI du miroir, podman run piloté par systemd
python7venv isolé par service, wheels du miroir
package2Paquets 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 :

  1. 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 ;
  2. les unités Podman sont des commandes podman run générées, alors que Podman et Red Hat recommandent Quadlet (fichiers .container déclaratifs convertis en unités systemd par le générateur) ;
  3. 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) ;
  4. 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é ;
  5. 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émentDécision
Les 4 méthodes binary, podman, python, packageConservées. On ne conteneurise pas systématiquement les 51 services.
Installation hors ligneConservée
Catalogue d'artefacts validésConservé, devient la base du Trusted Hub
Pilotage systemdConservé (Quadlet produit lui aussi des unités systemd)
Connexion aux services existantsConservée
Podman plutôt que Docker en productionConservé

Chantier 1 : Réseau isolé par défaut (implémenté)​

  • Chaque composant Podman déclare une politique réseau run.podman.network dans le catalogue :
    • isolated (défaut) : réseau bridge propre au cluster navin-<cluster> ; seuls les ports déclarés dans ports du 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.ip ou 127.0.0.1) déclarent provisoirement network: host avec une justification ; chaque migration vers isolated est 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 Quadlet navin-<service>-<composant>.container (et le .network du cluster) à partir de la même spécification que podman 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 pour podman run.
  • Le fichier est déposé dans /etc/containers/systemd/ ; le générateur Quadlet produit navin-<service>-<composant>.service : le nom d'unité, la supervision, les logs journald et le pilotage service.control du Manager restent identiques.
  • Paramètre NM_PODMAN_QUADLET (défaut true) : la tâche agent quadlet.unit (agents Python et Go) écrit le .container et le .network, retire l'unité podman run remplacé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ôlePolitique NavinChamp catalogue
UtilisateurRootless / utilisateur non root dans le conteneur dès que compatiblerun.podman.user
RéseauIsolé par défautrun.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 possibleread_only
VolumesPersistants sous /var/lib/navin/<service>, option Z (SELinux), lecture seule pour la configurationvolumes
CPU / RAMQuotas par composant (--cpus, --memory, --pids-limit)limits.cpus, limits.memory, limits.pids
GPUUniquement les GPU autorisés via CDI (--device nvidia.com/gpu=<index>)gpu, paramètre gpu_devices
SecretsInjectés à l'exécution depuis OpenBao (fichier d'environnement 0640 hors argv aujourd'hui, podman secret avec Quadlet)secret_params
SELinux / AppArmorActifs selon le système ; jamais label=disable sans justificationsecurity_opt
Health checksObligatoires pour les composants permanents : le chargeur du catalogue refuse un composant master ou worker sans healthhealth

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ôleObjectif
SHA256Intégrité (déjà en place : artifact.fetch, images par digest)
Signature CosignOrigine 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 CVEVulnérabilités connues, seuil bloquant configurable
LicencePolitique ADR-0101 (MIT, Apache-2.0, MPL-2.0 autorisées ; GPL/AGPL selon usage)
Approbation de versionUne 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éthodeAjouts
binarySignature 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)
pythonWheels précompilées du Hub, versions figées, pip install --require-hashes --no-index, venv par version
packageDépôts signés (GPG) du Hub, versions figées, apt/dnf sans dépôt externe
podmanQuadlet, images signées par digest, réseau isolé, profil de sécurité

Cycle de vie cible​

  1. Vérifier les prérequis et les droits (preflight, RBAC, approbation)
  2. Valider l'artefact signé (Hub : signature, SBOM, CVE, licence, version approuvée)
  3. Vérifier les dépendances (bindings, services requis, versions compatibles)
  4. Sauvegarder la configuration et l'état (fichiers gérés, unité, dump ou snapshot si migration de schéma)
  5. Installer la version demandée (répertoire de version, venv par version, image par digest)
  6. Démarrer et contrôler la santé (health checks)
  7. Valider le fonctionnement applicatif (service_check)
  8. 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 ALL et --read-only sont 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.pids s'appliquent aussi aux méthodes binary et python (CPUQuota, MemoryMax, TasksMax de 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 annonce quadlet.unit dans ses faits (task_types) ; les autres hôtes gardent podman run.
  • Le Trusted Hub devient bloquant pour toute installation en production.

Suivi​

ChantierÉtat
1. Réseau isolé par défautImplé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. QuadletImplé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 serviceImplémenté (no-new-privileges, cap_drop_all, read_only, security_opt, quotas, GPU CDI existant) ; profils à activer service par service
4. Trusted HubConfié à l'agent Hub (contrat ci-dessus)
5. Mise à niveau et rollbackImplé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