Skip to content

Machine identity

Usage numbers only mean something if you can tell one installation from another: fifty events from one machine and fifty events from fifty machines are very different facts. But "which machine" is exactly the thing a user does not want to hand over. The compromise is a one-way hash.

What is actually in the event

MachineId::derive(salt) reads the host's machine ID via the machine-uid crate and returns sha256(salt || machine_id) as 64 lowercase hex characters. The raw machine ID is never returned, never stored, and never put in an event. What ships is a hash that is stable for a given host and salt, and that cannot be walked backwards to the host it came from.

That is enough to answer "is this the same install as last week" and nothing else. It cannot be joined against another tool's telemetry, matched against a purchased device list, or turned back into a serial number.

Why the salt is per-tool

Without a salt, every tool on a host would hash the same machine ID to the same digest, and two vendors comparing notes could line their data sets up against each other. The salt breaks that: one host produces a different identity for every distinct salt.

The recommended form ties the salt to the crate name and a version tag:

.salt(concat!(env!("CARGO_PKG_NAME"), ".telemetry.v1"))

Two properties fall out of that. Because the salt is unique per tool, your identities are unlinkable with anyone else's. Because it carries a version tag, you can change it.

Rotating the salt is the reset button

Bumping .v1 to .v2 gives every host a new identity and orphans every previously-recorded one. That is the intended mechanism for "reset my telemetry identity": there is no per-user identifier to delete, so the identity is invalidated by changing the input instead.

Rotating is not free. Your backend will see the entire population as new installs on the day you ship it, and any retention or repeat-usage metric that spans the rotation is meaningless. Rotate deliberately.

The salt is only read when collection is enabled

build() derives the machine ID only for an Enabled policy. A Disabled context stores an empty string as its machine ID and never calls machine-uid, so a user who has not opted in has nothing read from their host — not even the value that would have been hashed.

This is also why salt is required only when the policy is Enabled: there is nothing for it to salt otherwise.

When the identity is not stable

machine-uid reads /var/lib/dbus/machine-id, falling back to /etc/machine-id, on Linux; the equivalent host UUID on macOS, BSD, Windows and illumos. When neither file can be read at all — a stripped container image, a sandbox — MachineId::derive substitutes a fresh random UUID so that a missing host ID cannot fail the caller's command.

The consequence is worth stating plainly, because it changes what your numbers mean: that substitution happens per call, and nothing caches it. On a host with no readable machine ID, every derivation produces a different identity, so a fleet of such containers looks like an endless stream of one-run installs rather than a small number of repeat users.

There is a second shape of the same problem. On Linux, an empty /etc/machine-id — which some container images ship deliberately — is not a read failure. The file is read successfully as an empty string, so the digest is sha256(salt), which is identical on every host in that situation. Rather than one identity per machine you get one identity for the whole fleet.

Neither case is detectable from the event: a fabricated identity looks exactly like a real one. If your tool runs mostly in containers, treat install counts as a lower bound at best, and prefer counting events over counting identities.