DEV Community

Cover image for run0: Password in polkit, Caller in a journalctl Field
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

run0: Password in polkit, Caller in a journalctl Field

The s in the output of ls -l /usr/bin/sudo has been there since 1980: sudo is a binary that starts as root regardless of who runs it, then decides internally what this person may do. In last week's sudo-rs post I looked at the same model rewritten in Rust; the model was the same, the language had changed. systemd 256 (June 2024) brought a tool that changes the model itself: run0. No setuid bit; the thing that runs the command is not your shell but PID 1; the question "is this allowed" is answered not by sudoers but by polkit. The man page sums it up in four points: no execution or security context credentials are inherited from the caller, authentication takes place via polkit and is isolated from the terminal, the command gets an independent pseudo-tty, and no setuid/setgid functionality is used.

My server's Ubuntu 24.04 is on systemd 255; no run0. Ubuntu 25.10 has 257, 26.04 LTS has 259, Debian 13 has 257. To try it I started a Debian 13-based systemd container on the server (257.13, polkitd, sudo, two users: alice in the sudo group, bob in no group) and ran sudo and run0 side by side with the same commands. The tool's promise held; a few expectations that come with the sudo habit broke too, and one of them was my own wrong expectation.

Where the command runs

The first difference is not in ps but in cgroup. I ran the same command with both tools and looked at /proc/self/cgroup:

run0:  0::/…/user.slice/user-0.slice/session-c26.scope
sudo:  0::/…/user.slice/user-1000.slice/session-c18.scope
Enter fullscreen mode Exit fullscreen mode

The cat that sudo runs stays in alice's session scope; in the process tree it is sudo's child, inside alice's logind session. run0's runs in a new session of root: run0 tells PID 1 over D-Bus "start a transient service with this command", PID 1 creates a unit called run-r….service, a session is opened for root through the systemd-run0 PAM stack, and the command runs inside that service. The sequence in the journal:

polkitd[448]: Operator of unix-process:1000:… successfully authenticated as unix-user:alice
              to gain ONE-SHOT authorization for action org.freedesktop.systemd1.manage-units … [run0 true]
(true)[994]: pam_unix(systemd-run0:session): session opened for user root(uid=0) by (uid=0)
systemd[1]: Started run-r67c6dbbf….service - [run0] /usr/bin/true.
Enter fullscreen mode Exit fullscreen mode

The run0 file itself is a symlink to systemd-run; the program looks at the name it was invoked by and changes behaviour. ls -l: sudo is 306 KB and rwsr-xr-x, systemd-run 85 KB and rwxr-xr-x. The whole of privilege elevation is outside the binary, in the hands of PID 1 and polkit.

Diagram

Environment, directory, user

$ cd /tmp; export FOO=bar
$ run0 bash -c 'echo FOO=$FOO HOME=$HOME USER=$USER SUDO_USER=$SUDO_USER SUDO_UID=$SUDO_UID'; run0 pwd
FOO= HOME=/root USER=root SUDO_USER=alice SUDO_UID=1000
/tmp
$ sudo bash -c 'echo FOO=$FOO HOME=$HOME USER=$USER SUDO_USER=$SUDO_USER'
FOO= HOME=/root USER=root SUDO_USER=alice
Enter fullscreen mode Exit fullscreen mode

Neither passes FOO; sudo does this through its env_reset policy and -E overrides it, in run0 there is no policy and no inheritance, you hand over what you want one by one with --setenv=FOO (it worked with --setenv=FOO=bar). Working directory: when switching to root the caller's directory is kept, pwd said /tmp; the man page says that when switching to another user it goes to the target's home directory, and -D changes it. run0 -u bob id ran as bob; the counterpart of sudo -u. SUDO_USER, SUDO_UID and SUDO_GID come along too; run.c does this explicitly to be "roughly sudo-compatible", while SUDO_COMMAND is absent. In the first draft I had written "there is no SUDO_USER in run0", because I had only looked on the sudo side; the review round made me measure it.

If a password is asked, a terminal is required

The first attempt did not work at all:

Failed to start transient service unit: Interactive authentication required.
Enter fullscreen mode Exit fullscreen mode

I was running the command with su - alice -c '…'. su -c opens a new session and the command is left without a controlling terminal; run0's polkit agent looks for three conditions in the source: not root, stdin is a tty, and a controlling terminal exists; if get_ctty_devnr returns ENXIO the agent is never started, and polkit's "interactive authentication required" answer is passed straight to the user. With su --session-command, which keeps the terminal, the agent opened, pkttyagent asked for the password, the command ran.

In daily life: if a password is going to be asked, ssh server run0 systemctl restart nginx (without -t) does not work, it does not work from cron, it does not work through a pipe. If a rule means no password is asked, no terminal is needed either; I measured that below. Authentication is bound to the terminal, authorisation is not.

Passwordless run0 and what comes with it

So what is the counterpart of NOPASSWD? A polkit rule. I wrote a rule giving the sudo group the manage-units action without a password:

polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.systemd1.manage-units" && subject.isInGroup("sudo"))
        return polkit.Result.YES;
});
Enter fullscreen mode Exit fullscreen mode

run0 id became root without asking for a password; so did echo | run0 id and run0 --pipe id < /dev/null, which means terminal-less use opens up with this rule. In the same session, systemctl restart getty@tty1.service also ran without a password. The action ID is not specific to run0; manage-units is also the action of systemctl start/stop/restart. The rule that makes run0 passwordless makes the same group passwordless for all unit management. In sudoers you could write a single command, alice ALL=(root) NOPASSWD: /usr/bin/id; on the polkit side the action has details (unit, verb), a rule can narrow with action.lookup("unit"), but the caller chooses the transient unit's name (--unit=), so no security boundary can be drawn on the name. There is no such thing as a command-level allow list in run0; either you trust the person or you do not.

Where a password is asked, there is another detail different from sudo. Once sudo has a password it does not ask again for a while (15 minutes in the Debian/Ubuntu package, the upstream default is 5). In run0, in this lab, every call asked again; polkitd wrote ONE-SHOT authorization each time. The action's polkit definition says auth_admin_keep for allow_active, but polkit does not consider a session without a seat local, and then it never looks at allow_active and applies allow_any, that is auth_admin without keep; the su session in this lab has no seat. On top of that, in polkit 126 a temporary authorisation is bound to a single process; 127 extends it to new processes under the same parent, cgroup and tty, and Debian 13 has 126. An SSH session has no seat either; I expect the same result there but did not measure it, I did not install sshd in the container.

The behaviour for an unauthorised user shows polkit's logic. When bob ran run0 id, the screen said "Authenticating as: alice": the default rule packaged by Debian (50-default.rules; the group name is chosen at build time, sudo on Debian) defines unix-group:sudo as the admin identity, so polkit asks bob for alice's password. Enter it wrong and you get Access denied and a FAILED to authenticate line in polkitd. In sudo bob would have got the "not in the sudoers file" answer with his own password; in run0 the door opens with the administrator's password, close to sudo's targetpw/rootpw modes.

Where the caller is in the journal

sudo's single line has been the basis of auditing for years:

sudo[850]:    alice : TTY=pts/0 ; PWD=/tmp ; USER=root ; COMMAND=/usr/bin/cat /proc/self/cgroup
Enter fullscreen mode Exit fullscreen mode

Who, from which terminal, in which directory, as whom, ran what. Looking at run0's plain journalctl output with the same eye, alice does not appear: Started run-r….service - [run0] /usr/bin/id gives the unit and the command, the PAM line says session opened for user root(uid=0) by (uid=0), if a password was asked there is polkitd's authenticated as unix-user:alice … [run0 true] line, and with the passwordless rule not even that. In the first draft I had written "no user name" here. It was wrong; the reviewer pointed at run.c: run0 adds LogExtraFields=ELEVATED_USER=…, ELEVATED_UID and ELEVATED_GID properties to the transient unit, and journald stamps these fields on every record related to that unit. I measured:

$ journalctl -o verbose ELEVATED_USER=alice | grep -E "ELEVATED|MESSAGE="
    ELEVATED_UID=1000
    ELEVATED_GID=1000
    ELEVATED_USER=alice
    MESSAGE=pam_unix(systemd-run0:session): session opened for user root(uid=0) by (uid=0)
    …
    ELEVATED_USER=alice
    MESSAGE=probe1.service: Deactivated successfully.
Enter fullscreen mode Exit fullscreen mode

Everything alice ran during the lab, including PID 1's "Started" and "Deactivated" lines, 64 records, carries the ELEVATED_USER=alice field; the passwordless runs too. The source is the /run/systemd/units/log-extra-fields:<unit> files. So the record is not poorer than sudo's but more structured: journalctl ELEVATED_USER=alice lists all of alice's elevations with one command, and _SYSTEMD_UNIT= takes you down to the unit. The only thing missing is that grep alice finds nothing; the MESSAGE text does not carry the caller, and an auditor who does not know the field, or a log shipper that drops fields, sees a blank. If you write audit rules, use journalctl -o json and the ELEVATED_USER field; do not search for COMMAND= text.

Where I would use it

What run0 gains: no setuid, so it also works under NoNewPrivileges=; the command is cut off from the caller's environment, directory and session; the caller is in a structured journal field; the terminal background turns red and a 🦸 appears in the shell prompt, you do not forget in which shell you are root. Reason enough for an interactive administrator to say run0 instead of sudo -i. What it loses: a command-level allow list and password caching; terminal-less use comes only with a rule that makes all of the group's unit management passwordless, there is no narrowed counterpart of a NOPASSWD: /usr/bin/id line. Where automation passes, in cron, in a CI runner, in the ssh host command pattern, that trade is usually too heavy; there it is sudo with a narrowed NOPASSWD line, or better, a service account that needs no privileges. On my own server a switch is not on the agenda; 255 does not have the tool, and if it did, the places where the runners use sudo are exactly this class.

The container can be rebuilt from the Dockerfile in /tmp/run0-lab; the polkit rule was deleted after the test.

Versions: systemd 257.13-1~deb13u1, polkitd 126, sudo 1.9.16p2 on Debian 13 debian:trixie-slim; host Ubuntu 24.04 systemd 255. Password prompts were driven with Python pty; run0's agent did not open until su --session-command was used instead of su -c. Ubuntu versions from packages.ubuntu.com (questing, resolute). Source reading from the systemd v257 tag: src/run/run.c, src/shared/polkit-agent.c.

Official Sources

Top comments (0)