Polkit insists on there always being administrative users
Polkit is how a lot of
things on modern Linux systems decide whether or not to let people do
privileged operations. A while back I investigated making Polkit's
administrative access operate like su does,
where you authenticated using the root password and only some people
could do it. Today I said something related to that on the Fediverse:
It certainly is a choice for Linux's Polkit to decide that there is no way to say "this login does not have administrative access period, no matter what".
(Your only choice is to force people to try to authenticate using the 'password' of a locked account, which will always fail.)
This turns out to not quite be true (you can force people to fail authentication without a password prompt), but getting there is a journey.
Operations using Polkit
can require "authentication by an administrative user", and Polkit
lets you control what an 'administrative user' is. This is done by
writing a JavaScript function that 'specifies what identities may be
used for administrator authentication' (possibly in general or
possibly for a specific operation). You pass this function to Polkit's
polkit.addAdminRule() and then it later gets called and returns
something. There can be multiple functions; they're called in order of
registration (which is generally lexical order of the .rules files)
and the first one to answer wins.
As we can see in _runAdminRules,
what an answer is is a return value that JavaScript 'if (...)' will
consider to be true (a truthy value).
This value is expected to be an array of strings, and .join(",")
will be called on it. If the result of that can't be turned into a
string, there will likely be some sort of exception raised
(also); otherwise, the
string value is split again and Polkit's C code tried to turn each
piece into an identity. If no pieces are valid identities like
'unix-user:0' or 'unix-group:wheel', the code explicitly falls back
to authenticating against the root user.
Then, at a higher level, groups and netgroups are expanded to users,
and if they expand to nothing, there's once again an explicit fallback
to root. Polkit is really insistent that everyone be able to
authenticate as some administrative user and it doesn't have any
particular notion that some people can't do that at all.
The obvious way around all of this mess is what I did in the
original entry, where I returned an existing
login with a locked password. This caused Polkit to ask you to
authenticate as that login, which would always fail. For operations
people weren't supposed to do anyway, this is acceptable even if it's
unaesthetic. Now that I've read the code I've come up with a nominally
better but more alarming way, which is to have your admin rule
function return '["unix-user:NNN"]' for some UID that definitely
doesn't exist and never will. This works today because Polkit doesn't
validate that the UID exists before it attempts to use it for password
authentication; when Polkit finds that the UID doesn't exist, it's too
late for it to do anything but fail. People trying this will get a
pleasant message like:
; run0 echo hi ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ==== Authentication is required to start transient unit 'run-[...].service'. Failed to start transient service unit: Access denied
An 'Access denied' error is about as clear and as good as you can do.
(If you use '["unix-user:nosuchperson"]', Polkit does validate the
login name and rejects the identity, leaving you with the root
fallback.)
However, I suspect that this is a somewhat dangerous approach. Someday Polkit may get smarter about detecting nonexistent Unix UIDs in 'unix-user:...' things in admin rules and start rejecting them early, causing it to fall back to demanding the root password. An existing user with a locked password that people can never enter is less aesthetic but safer in the long term.
(With that said I doubt Polkit is going to change this behavior in the kind of bugfix release that might be picked up by a specific release of a Linux distribution, so given that it works in Ubuntu 26.04 today it's probably going to keep working for the lifetime of 26.04. And you can use very large UID numbers to make sure you'll never have an actual account of that UID.)
Sidebar: What doesn't work and why
Since I went through this exercise, both testing things and reading the combined source code:
- Returning '
[]' from your admin function returns an empty string to the Polkit C layer, which then fails to be converted to an identity. - Both '
["unix-user:nosuch"]' and '["unix-group:nosuch"]' fail at the Polkit conversion from string(s) to identities. - '
["unix-group:emptygrp"]' and '["unix-group:NNNN"]' for a GID that doesn't exist both fail at the Polkit expansion of groups to Unix logins; with no logins generated, Polkit follows the second fallback to root. - Returning '
polkit.Result.NO' from your admin function actually returns the string"no", which also fails to turn into an identity (see init.js).
A nonexistent Unix UID as '["unix-user:NNNN"]' is the only option
that threads all of these needles; it's accepted in the string to
identity conversion step, and it's not currently rejected at the point
when Unix groups are expanded (which would be the natural point to put
such a check, if Polkit wanted to).