Skip to main content

ADR-0171: Hardened installation for banking production

  • Status: accepted (work items 1, 2 and 3 implemented in the Manager and the agents, upgrade cycle of item 5 in place, item 4 carried by the Hub)
  • Date: 2026-11-01
  • Scope: Navin Platform Manager (navin-manager/), installation agents (agent/, agent-go/), Navin Trusted Hub
  • French reference: ADR-0171; operator guide: navin-manager/docs/en/hardened-installation.md

Context​

The Manager installs the catalog services (51 at decision time, 49 since Perses and Infisical OSS were removed, ADR-0169) with four methods: binary (24), podman (12), python (7), package (2), offline, from a validated artifact catalog, driven by systemd, and connects to the customer's existing services. A pre-banking review found five weaknesses: every container ran with --network host; Podman units were generated podman run commands instead of Quadlet; every container was launched the same way; sha256 proves integrity but not origin; the lifecycle had no backup, application validation or schema-aware rollback.

Decision​

Kept: the four methods (no systematic containerization), offline installation, the validated artifact catalog, systemd control, connection to existing services, Podman rather than Docker.

  1. Isolated network by default. run.podman.network: isolated (default, bridge navin-<cluster>, only declared ports published), host (requires network_justification, checked at catalog load), none. Network options are forbidden in extra. Components bound to the host address or loopback declare host with a justification until migrated service by service. Per-tenant networks for untrusted MCP components will follow.
  2. Quadlet. catalog.render.quadlet renders .container and .network files from the same specification; same unit names, so supervision is unchanged. NM_PODMAN_QUADLET (default on) moves the systemd hosts whose Podman is 4.9 or later and whose agent announces quadlet.unit (facts task_types) to the quadlet.unit agent task (Python and Go agents); the other hosts keep the podman run unit. A generated unit is started, never enabled: its [Install] section starts it at boot.
  3. Security profile per service. no-new-privileges always (unless justified privileged), cap_drop_all, read_only, security_opt, justified cap_add / --privileged / host namespaces, CPU / memory / pids quotas for every method (systemd CPUQuota, MemoryMax, TasksMax), GPUs only through CDI, secrets out of argv.
  4. Trusted Hub. sha256, Cosign signature (offline), SBOM, CVE scan, licence check (ADR-0101), version approval, signed manifest. Built by the Hub work stream; the Manager refuses non-compliant artifacts.
  5. Upgrades and rollback. Immutable version directories, pip --require-hashes, signed repositories, signed images; the 8-step cycle (rights, signed artifact, dependencies, backup, install, health, application check, confirm or roll back), schema migrations restored from backup on rollback.

Privileged actions: tasks signed and authorized by the Manager, audit log, least-privilege service account. Restricting to navin-* units is a guard, not a security barrier. Multi-host HA stays orchestrated by the Manager.

Consequences​

A Podman component without a policy is isolated; weakening the isolation needs a written justification that the catalog loader enforces. Capabilities drop and read-only file systems are enabled per service after validation. The Quadlet is used by default (NM_PODMAN_QUADLET) on every systemd host whose Podman is 4.9 or later and whose agent announces quadlet.unit in its facts (task_types); the other hosts keep podman run. Every permanent component (master, worker) declares health checks: the catalog loader refuses it otherwise.

Follow-up​

Work itemState
1. Isolated network by defaultImplemented (navin-manager/server/src/navin_manager/catalog/render.py, loader, schema): one network per service, ports published by their scope (ports per mode and ports of a list included), ports between hosts opened to the hosts of the cluster only; host network kept only for falco and tetragon (kernel probes), livekit (WebRTC media) and vllm in the distributed mode, each with a justification of its own
2. QuadletImplemented: rendering (GPU through CDI included), quadlet.unit task of the Python and Go agents, switch by NM_PODMAN_QUADLET; isolation of the containers shown in the Security tab of the service sheet
3. Security profile per serviceImplemented (no-new-privileges, cap_drop_all, read_only, security_opt, quotas, GPU through CDI); profiles to enable service by service
4. Trusted HubHanded to the Hub agent (contract above)
5. Upgrade and rollbackImplemented in the Manager (navin-manager/server/src/navin_manager/services/upgrades.py): the 8 steps of the cycle are tied to their code and tests in section 5 of the hardened installation guide; dependencies of the target release checked before any task, canary host, backup before the upgrade of a stateful service (upgrade.backup_before), sealed snapshot of the previous release, automatic rollback on request (auto_rollback), rollback refused after an irreversible migration (upgrade.migrations[].irreversible) and restore of the backup instead; --require-hashes and one venv per release for Python, pinned packages without disabling the signature of the repositories