Portable 20 | Hiding Machine ID from applications
Historically, there are multiple ways of permanently identifying a user on Linux. Probably the most stable and easy way is querying the Machine ID. In Portable 20, we have gone though many ways to limit tracking via said method to a great extent.
What Machine ID really is
To quote freedesktop.org:
The machine ID is usually generated from a random source during system installation or first boot and stays constant for all subsequent boots. Optionally, for stateless systems, it is generated during runtime during early boot if necessary.
Something IS not looking good: the ID is generated during installation on normal distributions and it seemly does not reset. Adding more insult to injury, the documentation explicitly states that:
This ID uniquely identifies the host.
As this is generally considered unique (they use UUID v4), this drastically lowers the effort to track a user across different applications and even persists through data wipes.
Ways to get it
Filesystem
If you read through the linked document, it should be clear that /etc/machine-id holds the filesystem copy written by systemd-machine-id-commit.service. But /var/lib/dbus/machine-id also holds a copy of D-Bus Machine ID. These are the two primary sources of Machine ID fingerprinting for the filesystem side. Fortunately, Portable presents an empty /var for the sandboxed application, so there is one less exposure point for us to worry about.
Inter-process communication
You might be amazed that D-Bus inter process communication is another source to identify a Machine ID. But the D-Bus specification defines the org.freedesktop.DBus.Peer interface. It implements a method called GetMachineId which is expected to yield either /var/lib/dbus/machine-id or /etc/machine-id. The issue is that many D-Bus services are required for applications to function correctly, such as the Desktop Portal. Furthermore, granting a basic SEE policy or TALK policy implicitly allows calling GetXXX methods to obtain peer information. This is problematic for a number of cases: we did use the SEE policy for A11y bus to work, and several places like the Document Portal has TALK. Moreover, it is insufficient to restrict object path like what we did before. Because Peer interface is located in literally every accessible object path, it requires filtering on the interface or even method level.
A convenient script can be used to test this issue:
1 |
|
Should you run this script on host or in legacy Portable, lots of unique names would return a specific uniquely identifiable Machine ID.
How do we dodge from this
First of all, when we did a full rewrite of Portable this summer, Rust traits played an important role in D-Bus rule generation. The BusAccessLevel dictates a single rule in XDG D-Bus Proxy, defined as follows:
1 | /** |
interface name [.] method name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
A .* suffix may be allowed.
*/
method: String,
/**
possible with a `/ *` suffix
*/
object_path: String,
},
/**
Allows a sandboxed process to receive broadcasts from outside
*/
GetBroadcast {
bus_name: BusName,
/**
"method" may be quite misleading, but it actually maps to
interface name [.] method name
1
2
3
4
5
6
7
8
9
10
11
A .* suffix may be allowed.
*/
method: String,
/**
possible with a `/ *` suffix
*/
object_path: String,
},
}
These rules can then be translated into xdg-dbus-proxy arguments via the ToCmdline trait we mentioned in the last article. It is quite obvious that we stripped SEE and TALK policy from the list because that would definitely cause a machine ID leak. But this is not enough, as improperly written Call rules could still leak Machine ID if they don’t filter interface properly. As such, we started a snooping campaign – that is, searching for every piece of IPC documentation that is involved with the sandbox, and sniffing D-Bus traffic in the same time. As of today, the vast majority of D-Bus rules are fully hardened to prevent such leaks. In fact, if you are using GNOME with iBus, it should now be impossible to obtain Machine ID from D-Bus IPC.
But wait, what about the physical file on disk? Some applications won’t even function without it! Which is why I’m sliding in the system subsystem (no pun intended :p). You see, we already have some interesting modules under the subsystem, specifically nsswitch for a transiently generated /etc/nsswitch.conf file to mitigate socket exposure, passwd for hiding non-related users in /etc/passwd. It would not be inappropriate to create another module, machine_id.rs into this subsystem, whose primary job is to asynchronously bind and generate the per-app Machine ID if missing.
Let’s head back to the documentation. It states that “starting with systemd v30, newly generated machine IDs do qualify as Variant 1 Version 4 UUIDs, as per RFC 4122”. Thus we just need the uuid crate with the relevant feature v4 enabled, and plug it in:
1 | /** |
You’ll notice that this UUID is stored persistently, because some applications may intentionally forget login credentials if the Machine ID is changed. But one can easily reset the per-app Machine ID by simply deleting $XDG_CONFIG_HOME/portable/$sandbox_id/machine-id, or write another desired value. This raises the effort to track users across applications via Machine ID. Ultimately pulling our goals to be a private sandbox closer.
