Security of the Navin agent
This page describes how the Navin agent protects the host and its link with the Navin Manager. The agent runs as
root because it installs packages, users, systemd units, kernel parameters and limits files; these rules bound what
it may do.
Identity and transport
| Property | Rule |
|---|---|
| Key | EC P-256, made on the host at registration, stored with its certificate chain in agent.pem (0600); never sent |
| Trust of the Manager | CA pinned by its sha256 (--ca-sha256); a different CA stops the registration with ca_mismatch; an address without https is refused with insecure_server (http only toward this host), so the bootstrap token never crosses a network in clear |
| Transport | Mutual TLS on port 8461 with the client certificate issued by the internal CA |
| Request signature | NM-HMAC <host_id>:<ts>:<hmac>, HMAC-SHA256 with the agent secret; 300 seconds of skew at most |
| Renewal | POST /agent/v1/renew with a new certificate request; the previous certificate is revoked |
| Revocation | Hosts > Node agents > Revoke: direct mTLS, API requests and mirror access are refused; existing connections close on the revocation refresh |
| Bootstrap token | used a given number of times during a given time; enroll.env is deleted after the registration |
Atomic client identity
Contract updated on 2026-11-04 for the current 1.0.2 source tree. Do not infer support from the version string alone: older builds with the same version may still use the two-file layout.
The Go and Python agents share state_dir/agent.pem: certificate chain first, then the unencrypted private key,
maximum 64 KiB, private regular file with mode 0600. Registration and renewal validate the matching pair, write a
private temporary file in the same directory, sync it, rename it once, then sync the directory. TLS and pip use one
validated snapshot. An existing TLS connection may retain the previous identity until it reconnects.
- Upgrade: while
agent.pemis absent, the current agents accept the legacyagent.crtandagent.keypair. Registration or successful renewal publishes the new file. No database migration or manual conversion is needed for this direction. No live host has been migrated by the local verification tests. - Refusal: if
agent.pemexists but is unreadable, malformed, mismatched, oversized, a symlink, a non-regular file or accessible to group/others, the new identity is refused. There is no fallback to stale legacy files. A corrupt identity may require recovery or re-enrollment; deleting it to force a fallback is not a safe repair. - Older binaries and external tools: the old separate files are deliberately not refreshed. Before returning to an older binary or a consumer requiring separate files, stop all readers and writers, retain a protected copy of the authoritative bundle, and have an authorized operator export its matching certificate chain and key into protected files, then verify the pair and current Manager trust before restarting. The live two-file export is not atomic and is not an automatic rollback feature. Prefer a compatible binary or controlled re-enrollment.
- Limits: the state directory and its ancestors must remain trusted. Directory-sync failure after rename can report an error while the new complete file is already visible. Filesystem power-loss behavior and production rotation are not certified by the local tests. Decommission removes both new and legacy identity files.
Direct TLS revocation and delivery protocol 2
Updated 2026-11-04 for the current sources. Use the packaged navin-manager serve launcher, not a generic ASGI
runner: production requests outside the verified transport fail closed. No proxy header can impersonate the peer
certificate. The certificate identity must match the HMAC host on API requests; mirror requests also require a
known, non-revoked agent identity checked against the database.
The transport prepares a new CA/CRL context every 5 seconds. Failed refresh, expired CRL or a snapshot older than 15 seconds refuses new handshakes and closes existing agent connections. Revoked connections close at the next refresh; requests are also checked in the database independently. These intervals are polling budgets, not an absolute bound if the OS/event loop is stalled. Revocation does not undo a response or effect already accepted.
Protocol 2 requires the advertised capability delivery_protocols: [2], a signed dispatch, an attempt token and
its whole-envelope SHA256. ACK precedes the durable started state and effect; logs/results are fenced by attempt.
A lost ACK can be retried before execution, a terminal receipt can be replayed, and a started effect without a
receipt is indeterminate. Do not delete journal files or replay an uncertain task under a new id. Go and Python
have distinct delivery-journal layouts: switching implementation with pending/unacknowledged work is not supported.
Migrations V093 (scheduler generations) and V094 (protocol 2) require a coordinated maintenance upgrade with old
Manager replicas stopped and agent binaries updated. Older delivery clients are refused, not silently trusted.
The source changes and tests have not migrated a client environment. See the exact protocol contract (navin-manager/CONTRACT.md).
Execution limits
- No remote shell.
command.run, thecommandhealth checks,preflight.run,smoke.runand the process driver only start an absolute program listed inallowed_commandsor whose real path lies inside the allowed roots, from its argument list./bin/sh -c ...is refused withcommand_not_allowed. - Path guard. Every written path lies below an allowed root (
allowed_roots, by default/etc/navin,/opt/navin,/var/lib/navin,/var/log/navin,/run/navin,/etc/systemd/system/navin-*,/etc/sysctl.d/90-navin-*,/etc/security/limits.d/90-navin-*,/etc/containers/systemd/navin-*). A path with a symbolic link component, a..component or outside every root fails withpath_not_allowed. - Archives. Absolute names,
.., devices, FIFOs and links leaving the destination are refused witharchive_unsafebefore anything is written; a declared content above 200 000 members or 64 GiB fails witharchive_too_large, above the free space minus 1 GiB withdisk_space_low. - Downloads. A file is used only after its sha256 matches; otherwise
checksum_mismatch. A download stops past 64 GiB (download_too_large) or when less than 1 GiB would stay free (disk_space_low). - Secrets. Values of
sensitive_env, smoke test secrets and certificate keys are replaced by******in every log line and result. Task payloads are sealed in the Manager database.
Hardening of the unit
navin-manager-agent.service keeps a hardening compatible with dnf, apt, podman and systemctl:
ProtectHome=read-only, PrivateTmp=yes, ProtectKernelModules=yes, ProtectKernelLogs=yes, ProtectClock=yes,
ProtectHostname=yes, RestrictRealtime=yes, LockPersonality=yes, KeyringMode=private,
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK, SystemCallArchitectures=native, UMask=0027.
Checklist
| Check | Command |
|---|---|
| State directory private | stat -c '%a %U' /var/lib/navin-manager-agent prints 700 root |
enroll.env removed after the registration | test ! -e /etc/navin-manager-agent/enroll.env |
| Commands limited | allowed_commands of agent.json holds only absolute programs you expect |
| Manager in production | navin-manager check passes (client certificates required) |
Adding a broad directory such as /usr/bin to allowed_roots, or a shell to allowed_commands, lets the Manager run
any program of that directory as root on the host.