You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
While the existing document describes the protocol, it is hard to see
how all the moving parts communicate together. Describe the steps for
user/password, Kerberos, and client certificate authentication schemes.
Assisted-By: Claude code (initial mermaid diagram and proofreading)
Copy file name to clipboardExpand all lines: doc/authentication.md
+108Lines changed: 108 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -203,3 +203,111 @@ SSH connections:
203
203
204
204
***COCKPIT_SSH_KNOWN_HOSTS_FILE** Path to knownhost files. Defaults to
205
205
`PACKAGE_SYSCONF_DIR/ssh/ssh_known_hosts`
206
+
207
+
Login process: User/Password
208
+
----------------------------
209
+
210
+
This section entirely ignores cockpit-tls for simplicity, as it is not involved in the authentication. See [cockpit-tls docs](../src/tls/README.md) for details.
211
+
212
+
1._User_ connects to cockpit URL.
213
+
2._cockpit-ws_ responds with "401 Authentication failed" and sends the Login page, unless when Kerberos is available (see below) or the server is configured to not use any authentication.
214
+
3._User_ fills in username/password and clicks "Log In". The login page sends a new request to _cockpit-ws_ with an `Authorization: Basic base64(user:password)` header
215
+
4._cockpit-ws_ looks at `cockpit.conf` whether it has a customized session `Command` or `UnixPath` for `basic`. If not, defaults to `cockpit-session` (via `UnixPath = /run/cockpit/session`). It parses user/password from the header and spawns the session command with the target host as argument.
216
+
5._cockpit-session_ sends an `authorize` command with a `*` challenge (see above) to _cockpit-ws_, which responds with the user/password
217
+
6._cockpit-session_ starts a _PAM_ session for the user, and sets the initial credential to the received password
218
+
7. If _PAM_ sends more messages, like e.g. 2FA prompts or changing expired passwords:
219
+
*_cockpit-session_ sends corresponding `X-Conversation` authorize messages (see above) to _cockpit-ws_
220
+
*_cockpit-ws_ forwards them to the Login page, which displays the text, and sends the user response as the authorize reply
221
+
*_cockpit-ws_ forwards the authorize reply to PAM
222
+
8. When PAM succeeds, _cockpit-session_ executes the bridge and the session starts.
223
+
224
+
```mermaid
225
+
sequenceDiagram
226
+
participant User
227
+
participant cockpit-ws
228
+
participant cockpit-session
229
+
participant PAM
230
+
231
+
User->>cockpit-ws: GET https://server:9090
232
+
cockpit-ws->>User: 401 + Login page
233
+
User->>cockpit-ws: GET with Authorization: Basic
234
+
cockpit-ws->>cockpit-session: Spawn
235
+
cockpit-session->>cockpit-ws: authorize command with * challenge
2._cockpit-ws_ responds with "401 Authentication failed" and includes `WWW-Authenticate: Negotiate` header (if Kerberos is available)
256
+
3._Browser_ (if configured for SPNEGO/Kerberos) requests a service ticket from the _KDC_ for the HTTP service principal
257
+
4._Browser_ sends a new request with `Authorization: Negotiate <base64-gssapi-token>` header
258
+
5._cockpit-ws_ looks at `cockpit.conf` whether it has a customized session command for `negotiate`. If not, defaults to `cockpit-session` and runs it in the same way as above.
259
+
6._cockpit-session_ calls `gss_accept_sec_context()` with the GSSAPI token to verify the Kerberos ticket
260
+
7. If GSSAPI returns `GSS_S_CONTINUE_NEEDED` (multi-round negotiation):
261
+
*_cockpit-session_ sends an authorize command with a `Negotiate` challenge containing the output token to _cockpit-ws_
262
+
*_cockpit-ws_ responds with "401 Authentication failed" and `WWW-Authenticate: Negotiate <token>` to the _Browser_
263
+
*_Browser_ sends another `Authorization: Negotiate <token>` request
264
+
* This continues until GSSAPI negotiation completes
265
+
8. When GSSAPI succeeds, _cockpit-session_ has the authenticated GSSAPI principal name
266
+
9._cockpit-session_ maps the GSSAPI name to a local username using `gss_localname()` (which applies configured mapping rules), or if that fails, falls back to `gss_display_name()` which returns the principal name as-is (e.g. `[email protected]`)
267
+
10._cockpit-session_ starts _PAM_, skipping the auth stack (as GSSAPI already authenticated), and runs the account, credential, and session stacks
268
+
11._cockpit-session_ stores the delegated Kerberos credentials (if delegation was negotiated) in a credential cache at `/run/user/<uid>/cockpit-session-<pid>.ccache` and sets `KRB5CCNAME` in the PAM environment, so that the bridge can use them for accessing other Kerberos-protected services (like SSH to remote machines)
269
+
12. Authentication completes, and the session starts as above.
270
+
271
+
Login process: Client Certificate
272
+
----------------------------------
273
+
274
+
Unlike the other sections, this one involves _cockpit-tls_ as well as it provides a crucial part of the authentication.
275
+
276
+
1._User_ connects to cockpit URL with a client certificate, which lands at _cockpit-tls_
277
+
2._cockpit-tls_ calculates the SHA256(certificate) as the user fingerprint
278
+
3._cockpit-tls_ connects to the `cockpit-wsinstance-https@<fingerprint>` systemd socket/service (starting a dedicated _cockpit-ws_ instance for this certificate if needed). See [cockpit-tls docs](../src/tls/README.md) and [systemd units](../src/systemd/) for details.
279
+
4._cockpit-tls_ exports the certificate to `/run/cockpit/tls/clients/<fingerprint>` (kept as long as there is at least one active connection with that certificate)
280
+
5._cockpit-tls_ includes `"client-certificate": "<fingerprint>"` in its mini JSON protocol to _cockpit-ws_
281
+
6._cockpit-ws_ detects the client certificate metadata and uses `tls-cert <fingerprint>` as the authorization type
282
+
7._cockpit-ws_ looks at `cockpit.conf` whether it has a customized session command for `tls-cert`. If not, defaults to `cockpit-session` and runs it in the same way as above.
283
+
8._cockpit-session_ receives the `tls-cert <fingerprint>` authorization and reads the certificate from `/run/cockpit/tls/clients/<fingerprint>`
284
+
9._cockpit-session_ validates that the certificate file exists and matches the expected _cockpit-ws_ cgroup
285
+
10._cockpit-session_ calls the _sssd_ D-Bus API (`org.freedesktop.sssd.infopipe.Users.FindByCertificate`) to map the certificate to a username
286
+
11. When successful, _cockpit-session_ sets the username and starts _PAM_, skipping the auth stack (as the certificate itself was the authentication), and runs the account, credential, and session stacks
287
+
13. Authentication completes, and the session starts as above.
288
+
289
+
Login process: SSH to remote machine
290
+
------------------------------------
291
+
292
+
1._User_ connects to a URL like `https://server:9090/=hostname`) or sets the "Connect to:" field in the Login page to `hostname`.
293
+
2._login.js_ sends the login HTTP request with `Authorization: Basic base64(user:password\0known_hosts)` header, where `known_hosts` contains any previously-stored SSH host keys for the target host from the browser's `localStorage`
294
+
3._cockpit-ws_ extracts the target host from the URL. As it has a host name, it looks at `cockpit.conf` for the `[Ssh-Login]` section's `Command` or `UnixPath`. If not customized, defaults to `cockpit.beiboot`
295
+
4._cockpit-ws_ spawns the ssh command with the target host as argument
296
+
5._cockpit.beiboot_ sends an `authorize` command with a `*` challenge to _cockpit-ws_, which responds with the user/password/known_hosts from the `Authorization` header (same as User/Password step 5)
297
+
6._cockpit.beiboot_ parses the credentials, writes any received `known_hosts` to a temporary file, and configures `ssh` to use it via `-o UserKnownHostsfile=...`
298
+
7._cockpit.beiboot_ connects to the remote host via SSH using the [ferny API](https://github.com/allisonkarlitskaya/ferny/).
299
+
8. If the remote host's SSH key is unknown or has changed:
300
+
* For **unknown** hosts: SSH prompts via its `SSH_ASKPASS` mechanism. _ferny_'s interaction agent detects the host key prompt, parses the fingerprint, and _cockpit.beiboot_ sends an `X-Conversation` challenge to _cockpit-ws_ with the host key fingerprint and hostname.
301
+
* For **changed** keys: SSH immediately fails with a changed host key error. _ferny_ detects this as `SshChangedHostKeyError`. _cockpit.beiboot_ fails with `problem=invalid-hostkey`. _login.js_ catches this and retries the login **without** sending the old known_hosts entry, causing SSH to treat it as an unknown host (see above).
302
+
* For localhost (127.0.0.1): _cockpit.beiboot_ automatically accepts the key without user interaction.
303
+
*_cockpit-ws_ forwards the `X-Conversation` challenge to the web UI
304
+
*_login.js_ shows a host key verification dialog with the fingerprint, key type, and hostname to the user; it remembers unknown vs. changed and shows appropriate UI
305
+
* User accepts or rejects the key
306
+
*_login.js_ sends the response back via `X-Conversation` header
307
+
*_cockpit.beiboot_ returns the response to SSH's askpass mechanism
308
+
* SSH proceeds with the connection (if accepted) or fails (if rejected)
309
+
9. If SSH prompts for additional input (like 2FA): Similar to step 7 in User/Password section: _cockpit.beiboot_ sends `X-Conversation` messages back and forth
310
+
10. When SSH authentication succeeds, _cockpit.beiboot_ either runs an existing remote `cockpit-bridge`, or sends its own Python module through the SSH connection (via [beipack](https://github.com/allisonkarlitskaya/beipack)). It starts bridge on the remote machine and connects its stdio to it
311
+
11. Authentication completes, and the session starts as above.
312
+
12. After successful SSH authentication and bridge startup, if a new host key was accepted, _cockpit.beiboot_ reads the updated `known_hosts` file and sends it to the browser in the `init` message's `login-data` field as `known-hosts`
313
+
13._login.js_ stores the received `known-hosts` entry in localStorage for future connections to this host
0 commit comments