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:
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.