Local sudo, technical design - cockpit-project/cockpit GitHub Wiki
Superuser bridges are described in the manifests. They are identified
as superuser bridges via the existing privileged
field. Each superuser bridge gets a unique symbolic id that is automatically derived from the spawn
option of the bridge. They are exposed via the bridge D-Bus API in a property.
The "init" message from the bridge has a new "features" element:
{ "features": { "explicit-superuser": true } }
This means that the bridge understands the new "superuser" element in the "init" message from cockpit-ws.
The "init" message from cockpit-ws to a bridge contains a new element to request or prevent starting of a superuser bridge at login:
{ "superuser": false }
{ "superuser": { "id": "sudo", "password": "..." } }
When "superuser" is false, the bridge will not start a superuser bridge. All channel openings requesting one will fail instead of trying to start "sudo".
When "superuser" is not false, the bridge will immediately start the requested superuser bridge and use the provided password. If there are other PAM prompts, starting will fail. No other channels are opened until this is done. If the superuser bridge fails to start, no error is reported back to cockpit-ws, and the bridge continues to behave as if "superuser" was false.
There is a new "cockpit.Superuser" D-Bus interface:
- property Bridges : as
List of the superuser bridges from all manifests, as an array of their ids.
- property Current : s
The currently active superuser bridge, or "none" if none is active.
- method Start (id : s)
Start the indicated superuser bridge. This method returns once the bridge is running or has definitely failed to start. While it is starting, there might be "Prompt" signals that need to be answered with a call to "Answer". The bridge itself does not ever timeout, but the command spawned to get a superuser bridge might.
It's an error if there is already a superuser bridge running, or is currently starting up.
- method Stop ()
Stop the current superuser bridge. All open superuser channels will be closed.
- signal Prompt (message : s, prompt : s, default : s, echo : b)
While a superuser bridge is starting up, this signal might be emitted to prompt for credentials, such as a password or a 2fa code. You need to call the "Answer" method to continue the process.
- method Answer (text : s)
Flows
First ever login, no superuser bridge.
-
login.js examines localStorage, finds nothing.
-
login.js authorizes with "X-Superuser: none" header
-
cockpit-ws remembers password
-
cockpit-session sends "authorize" message, gets password, start ssh-agent, as usual
-
cockpit-ws sends { "superuser": false } init message
-
cockpit-ws could forget the password, but doesn't in order not to break the dashboard.
Become superuser during a session.
-
user clicks on "Not privileged" button.
-
shell.js accesses cockpit.Superuser D-Bus API on current machine.
-
shell.js makes a dialog based on what it finds.
-
user selects "sudo" and clicks "Do it".
-
shell.js calls Start("sudo") and keeps the dialog open
-
cockpit-bridge spawns "sudo -A"
-
sudo-askpass gets invoked, outputs "authorize" message on stdout
-
cockpit-bridge picks up that message, keeps it pending, and emits a "Prompt" D-Bus signal
-
shell.js gets the "Prompt" signal and changes the dialog appropriately.
-
user inputs answer, hits "Do it" again.
-
shell.js calls "Answer"
-
cockpit-bridge replies to the pending "authorize" message.
-
sudo is happy and the privileged bridge starts
-
cockpit-bridge replies to the "Start" method.
-
shell.js makes a note in localStorage that the "sudo" superuser bridge is active.
-
shell.js reloads everything.
Login and become superuser immediately.
-
login.js examines localStorage, finds that a "sudo" superuser bridge should be started.
-
login.js authorizes with "X-Superuser: sudo" header
-
cockpit-ws remembers password
-
cockpit-session sends "authorize" message, gets password, start ssh-agent, as usual
-
cockpit-ws sends { "superuser": { "id": "sudo", "password": "..." } } init message
-
cockpit-ws could forget the password, but doesn't in order not to break the dashboard.
Cease being superuser during session
-
user clicks on "Privileged" button.
-
shell.js opens dialog saying that user is currently superuser via "Sudo".
-
user clicks "Do it"
-
shell.js calls "Stop" method
-
cockpit-bridge sends "logout" to superuser bridge and waits for it to exit
-
cockpit-bridge replies to "Stop" method
-
shell.js makes a note in localStorage that no superuser bridge is active.
-
shell.js reloads everything.
Discussion
Cockpit already passes semi-complex "authorize" control messages around to implement prompting during login. One option would be to build on that and bubble the "authorize" messages all the way from sudo-askpass to shell.js. However, with multi-hop "authorize" messages we need to be sure where they come from and not mix them up. With sudo on remote machines we would have to bubble the "authorize" messages over two bridges from both sudo and ssh. If we keep "authorize" messages single hop and do the rest with D-Bus, things become immediately clear.
When starting a SSH bridge, the parent bridge also needs to dtrt with the "init" message, so we should plan for code sharing between cockpit-ws and cockpit-bridge here.
With our current manifests, we would have "pkexec" and "sudo" superuser bridges. Since we want to expose to the user the full PAM conversation (or similar) that a superuser bridge does, we let the user first choose which supweruser bridge to use. If this is deemed to be bad UX, we should only configure one superuser bridge, probably the "sudo" one.
If there is only one superuser bridge defined in the manifests, the shell would start it right away when the user clicks "Not privileged", without showing any dialog. In this case, when the call to Start is done without any prompting in between, the shell should not immediately reload everything and show a quick confirmation dialog before doing that.
Compatability here is only concerned with the bastion host case, where login+ws and shell+bridge are not necessarily the same version. (But login and ws are always the same version, and shell+bridge as well. Shell and bridge might be different versions when we start looking at multiple machines, but not just yet.)
New login/ws with old shell/bridge
Ws doesn't see explicit-superuser: true
in init message from bridge. Ws will reply once to plain1
auth messages with stored password and then forget it. Ws initially always remembers password, as if the old "Reuse my password for privileged tasks" checkbox would always be on. "logout" message with disconnect: false
needs to work.
Old login/ws with new shell/bridge
Bridge doesn't see superuser
option in init message. Bridge uses old-style on-demand starting of all privileged bridges, sends "plain1" authorize messages when prompting.
Review comments/discussion
-
pitti: We recently introduced
"privileged": true
to the shell's manifest for the root bridges. Your current design removes this again and replaces it with the"superuser":
object. We could probably get away with this API break, given that nothing else outside of cockpit uses that yet, but it's a bit awkward. -
pitti: I'm not a fan of showing an option to select between sudo and polkit, TBH. I see how this could be relevant in specific situations (custom sudo or custom polkit rules), but for the vast majority of users it would just be confusion. If we are going to do that for the "teach the user how Linux works" aspect, then let's please not i18n the title, and in fact drop it completely. We can just show the command (
basename(spawn[0])
). This would also collapse the need for a separatesuperuser
object in the manifest. -
mvo: Having proper PAM conversations pretty much requires that we are only interacting with a single superuser bridge, right? The way to avoid having the user select one is to only configure one superuser bridge, I'd say. So we might want to change our default config to just "sudo" (or just "pkexec") and document how people can change that. [DONE]
-
mvo: So, what about keeping
privileged: true
and deriving the id from the spawn option, like you propose? I would drop the title from the D-Bus API, with the expectation that most people will not actually see the option to choose a bridge since we will configure only one. But the shell would be ready for that. [DONE] -
pitti: Similarly,
"superuser" and
"match": { "superuser": null }` are now very redundant -- could we ever have one without the other? -
mvo: I had the same thought. What do you prefer? Keeping the
match
or keepingprivileged
? Both seem a bit magical to me. But given that we haveprivileged
already in there, maybe we just keep both.