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.
- Isolated network by default.
run.podman.network:isolated(default, bridgenavin-<cluster>, only declared ports published),host(requiresnetwork_justification, checked at catalog load),none. Network options are forbidden inextra. Components bound to the host address or loopback declarehostwith a justification until migrated service by service. Per-tenant networks for untrusted MCP components will follow. - Quadlet.
catalog.render.quadletrenders.containerand.networkfiles 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 announcesquadlet.unit(factstask_types) to thequadlet.unitagent task (Python and Go agents); the other hosts keep thepodman rununit. A generated unit is started, never enabled: its[Install]section starts it at boot. - Security profile per service.
no-new-privilegesalways (unless justified privileged),cap_drop_all,read_only,security_opt, justifiedcap_add/--privileged/ host namespaces, CPU / memory / pids quotas for every method (systemdCPUQuota,MemoryMax,TasksMax), GPUs only through CDI, secrets out of argv. - 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.
- 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 item | State |
|---|---|
| 1. Isolated network by default | Implemented (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. Quadlet | Implemented: 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 service | Implemented (no-new-privileges, cap_drop_all, read_only, security_opt, quotas, GPU through CDI); profiles to enable service by service |
| 4. Trusted Hub | Handed to the Hub agent (contract above) |
| 5. Upgrade and rollback | Implemented 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 |