Scanner: Linux systemd service
An evaluator-facing overview of what scanner install involves and why the deployment model is the way it is. This is not the operational procedure.
Operators: use the guide in your release bundle
The step-by-step installation procedure ships with every mipo release at
docs/operations/scanner-install.md. That file is the authoritative reference for deploying
scanners to real environments. Follow it instead of this page.
What scanner install involves
- Obtain a one-time provisioning token from the manager UI (Config > Scanners; a reverse-DNS resolver identity is created under Config > Resolvers and installs with this same procedure). It is valid for an hour, single-use, and supplied to install.sh only through the MIPO_PROVISIONING_TOKEN environment variable.
- Copy the scanner binary to the host and verify its SHA-256 and Ed25519 signature against trusted values.
- Create a dedicated non-privileged system user for the service to run as.
- Run install.sh — it provisions the scanner AS that user with `mipo-scanner provision`: the host generates its own key pair and API key, sends a certificate request plus the key's hash with the one-time token, and stores the issued mTLS identity in /var/lib/mipo-scanner (0700). The raw API key never leaves the host.
- Write /etc/mipo-scanner.env with the manager URL only — no secret lives outside the state directory.
- Install the systemd unit with sandboxing directives (plus StateDirectory=mipo-scanner) and enable the service.
- Allow outbound TCP 443 (bootstrap) and TCP 8443 (the mandatory-mTLS runtime) from the host to the manager.
- Confirm the scanner appears in Health > Services > Scanners within about 60 seconds.
An auditable install.sh is available from the manager's Scanner Provisioning modal and performs
the same steps in the same order, so evaluators can read exactly what the install does before committing.
Why the model is Linux-only and manual
Linux systemd as the target platform
The scanner targets enterprise Linux hosts running systemd. The binary is statically compiled Go with no runtime dependencies beyond the kernel, so it works on any Linux distribution. macOS and Windows are not supported scanner hosts — the scanner is a network-edge agent, not a workstation tool.
Why the binary is not auto-distributed
The binary is never pushed to hosts automatically. A compromised manager would otherwise be able to push a tampered binary to every scanner in the fleet. Operators obtain the binary from the release bundle, verify its SHA-256, and deploy it with existing tooling (Ansible, Chef, Puppet, or manual copy).
Verify the binary signature (required)
Releases built by a CI with a configured signing key ship an Ed25519 signature
(mipo-scanner.sig) beside the binary. Verifying it protects you where the SHA-256 cannot: a
compromised manager can serve a tampered binary with a matching hash, but it cannot forge a signature
your independently-pinned public key accepts.
Signature verification is mandatory by default — controlled by
MIPO_REQUIRE_SIG (default 1). When on, install.sh /
upgrade.sh exit with an error if mipo-scanner.sig or
/etc/mipo/scanner.pub is absent. Set MIPO_REQUIRE_SIG=0 only for dev, transition,
or unsigned-build environments. To set up:
-
Install
scanner.pubfrom the source repository (an independent channel — not from the manager):sudo install -D -m 0644 scanner.pub /etc/mipo/scanner.pub. Verify against the published fingerprint indocs/security/scanner-signing.md. -
Place
mipo-scanner.sig(from the release artifacts) next to the binary. -
Run
install.sh— it verifies fail-closed against the pinned key. Manual check at any time:sh scripts/ops/verify-scanner.sh ./mipo-scanner ./mipo-scanner.sig /etc/mipo/scanner.pub.
Who signs, who verifies: the binary is signed once, at build time, by whoever builds it — for
mipo-built releases that is the project's CI, where the private key lives; organizations that build from
source sign with their own key (scripts/ops/sign-scanner.sh, no CI required) and become their own
signing authority. Scanner hosts only ever need the public key: they verify, never sign.
Nobody needs access to the builder's GitLab or build system to verify.
Installer-provenance boundary: the manager also serves install.sh, so a
compromised manager could serve a poisoned installer. For maximum assurance, obtain install.sh
and scanner.pub from the independent channel (the audited source repo / signed release tag),
not from the manager, and verify scanner.pub against the published fingerprint.
Enrolling a scanner installed before mTLS
A host installed before the mTLS release authenticates with an API key alone and has no
/var/lib/mipo-scanner. Enrollment brings it into mTLS in place: after upgrading the binary and
giving the unit a state directory, the operator mints a one-time enrollment token in the manager
(Config > Scanners > Enroll mTLS) and runs mipo-scanner enroll on the host —
as the service user, exactly the way the installer runs provisioning — trading the current API key plus
the token for a client certificate. The scanner keeps its record, name, bindings and history, and its
runtime traffic moves to :8443.
The step-by-step procedure, including the unit-file lines and the exact command, ships in the release
bundle's docs/operations/scanner-install.md ("Enroll An Existing Installed Scanner"); the
row action and the fleet-wide policy that eventually requires mTLS are documented on the
Scanners page, and the ports to allow on the
Scanner ACL page.
Systemd sandboxing
The unit file applies hardening directives (NoNewPrivileges, ProtectSystem=strict,
PrivateTmp, RestrictAddressFamilies=AF_INET AF_INET6, and others). The scanner's
real runtime needs — outbound TCP connect and HTTP heartbeat — don't conflict with any of them, so there is
no operational trade-off.
Scan speed and the connect timeout
By default the scanner waits 0.5 seconds for each TCP connection attempt. An open port answers within one network round trip and a closed port refuses at once, so the wait only matters when nothing answers: an address with no host, or a firewall that drops the probe. On a sparse range almost every probe is like that, so scan speed is roughly concurrent workers ÷ timeout. With 100 workers that is about 200 probes per second at 0.5 s, and about 50 at 2 s. Scanning a /16 on two ports over a LAN took about 11 minutes at 0.5 s and 44 minutes at 2 s, and both runs found exactly the same open ports.
The trade-off: when a connection request gets no answer, Linux sends it again after 1 second, and again at 3 seconds. A 0.5 s timeout gives up before that first retry. If a packet is lost, an open port is reported as not open, and a delta scan then shows a false "closed" change in its audit diff. That is rare on a LAN but real on long or lossy links, so choose the timeout for the network between the scanner and its targets:
- LAN or same datacenter: 0.5 s, the default.
- Cross-region WAN, or links with packet loss: 1.5–2 s. For recovering a lost packet on a path whose round trip is well under 0.5 s, a 1 s timeout gains nothing over 0.5 s, because it gives up exactly when the retry is sent.
- Round trip of 0.5–1 s (satellite, heavy bufferbloat): 1.5–2 s. At 0.5 s, open ports are missed there even with no packet loss.
- High-latency or lossy intercontinental links: 3.5–5 s. A 3 s timeout gives up just as the second retry leaves, so it gains nothing over 2 s.
The timeout is set per scanner host. Add a line such as MIPO_SCAN_TIMEOUT=2s to
/etc/mipo-scanner.env, then run sudo systemctl restart mipo-scanner. Write the value
as a Go duration with no space, such as 2s or 1500ms: an unparseable value such as
1.5 s silently falls back to 0.5 s.
Large local subnets: the load on the scanner host follows the probe rate (workers ÷ timeout),
not the worker count alone. On a subnet the scanner host is directly attached to, each probe to an unused
address briefly holds an entry in the kernel's neighbour (ARP) table, and the default 200 probes per second
already uses much of Linux's default 1,024-entry table. A full table makes ports silently look closed, so raise
the host's net.ipv4.neigh.default.gc_thresh1/2/3 sysctls before adding workers or lowering the
timeout further. The release bundle's docs/operations/scanner-install.md has the numbers and an
example.