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 is the file docs/operations/scanner-install.md in
the mipo source checkout or the release bundle your operator received (public publication of the
repository is pending). 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 the same script plus one flag — see "Installing a resolver" below). 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). Provisioning sends only the hash of the API key; at runtime the scanner presents the raw key to the mipo server in the X-API-Key header, inside the TLS connection.
- Write /etc/mipo-scanner.env with the manager URL and the role — 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 an independent channel — the mipo source checkout or the release bundle your operator received, not the manager (public publication of the repository is pending):sudo install -D -m 0644 scanner.pub /etc/mipo/scanner.pub. Verify against the published fingerprint in that checkout'sdocs/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 mipo source checkout or the release bundle
your operator received), not from the manager, and verify scanner.pub against the published
fingerprint.
TLS trust
Provisioning talks to the manager over HTTPS and verifies its certificate against the host's system trust
store. A new mipo deployment serves a self-signed certificate on day one, and many internal deployments use a
private CA, so in both cases set MIPO_SERVER_CA_FILE to the CA's PEM file when running
install.sh: the provisioning step verifies the manager against that CA, and the identity the
manager issues includes the trust bundle the scanner uses for its runtime channel from then on.
MIPO_INSECURE
is refused for provisioning — there is no way to skip verification of the manager that is about to issue the
scanner's identity, and an install that omits the CA file fails at the provision step.
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.
Installing a resolver
A resolver is the SAME mipo-scanner binary's second identity kind (reverse-DNS R2): it performs
reverse-DNS (PTR) lookups for a resolver group — a DNS vantage point — instead of port scans. Which role a
process runs is fixed by the identity the server issues at provisioning, never chosen on the host; the binary
refuses to start if its unit's declared role disagrees with the identity in its own state directory.
Everything else on this page — provisioning, mTLS, TLS trust, signature verification, the systemd
sandboxing described below, upgrades — is identical for a resolver. Only the install command differs, by
one flag:
sudo MIPO_MANAGER_URL=https://mipo.example.com \
MIPO_PROVISIONING_TOKEN=<from-manager-ui> \
MIPO_BINARY_PATH=./mipo-scanner \
bash install.sh --role resolver --role selects its own system user (mipo-resolver), state directory
(/var/lib/mipo-resolver), env file and systemd unit — everything a scanner install writes, but
named for the resolver — so one host can run both roles side by side, each with its own files, sharing only
the one binary at /usr/local/bin/mipo-scanner. Create a resolver group first (Config >
Resolver Groups — every resolver joins exactly one), then add the resolver under Config > Resolvers to
get its provisioning token and this command.
sudo bash uninstall.sh --role resolver removes only the resolver's own files and the
shared binary only once no other role is still installed. Once running, a resolver heartbeats every 30
seconds carrying the DNS job ids it holds, claims up to 4 concurrent jobs every 5 seconds, runs up to 16 PTR
lookups per job concurrently, and reports its findings as observation batches plus one signed receipt per
job — tallied over what the server actually stored, never over what it merely attempted.
The full resolver-specific guide — prerequisites, both role-mismatch refusal messages verbatim, and
403 WRONG_IDENTITY_KIND troubleshooting — ships in the release bundle at
docs/install/resolver.md.
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.
Upgrading an installed host
Upgrade a provisioned host with upgrade.sh, never by re-running install.sh:
the installer always provisions, which needs a fresh one-time token and re-creates the scanner as if it were
new. upgrade.sh runs the same SHA-256 and Ed25519 signature checks as the installer, swaps only
the binary at /usr/local/bin/mipo-scanner, keeps the identity in
/var/lib/mipo-scanner, the env file and the unit untouched, and stops, replaces and restarts
every role installed on the host in one pass — there is no --role flag on upgrade. Upgrade
hosts after the server deploy, not before: the scanner protocol has no compatibility window, so a release
that raises it refuses an un-upgraded scanner's job claims until the host is upgraded (the
upgrade guide explains the ordering). The
procedure and its rollback are in docs/operations/scanner-upgrade.md in the source checkout
or release bundle.
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.