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
+109Lines changed: 109 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -203,3 +203,112 @@ 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
+
1._User_ connects to cockpit URL, which lands at _cockpit-tls_
211
+
2._cockpit-tls_ connects to _cockpit-ws_ via the `cockpit-wsinstance-http@` or `cockpit-wsinstance-https@SHA256_NIL` systemd socket/service. See [cockpit-tls docs](../src/tls/README.md) and [systemd units](../src/systemd/) for details.
212
+
3._cockpit-ws_ responds with "401 Authentication failed" and sends the Login page
213
+
4._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
214
+
5._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.
215
+
6._cockpit-session_ sends an `authorize` command with a `*` challenge (see above) to _cockpit-ws_, which responds with the user/password
216
+
7._cockpit-session_ starts a _PAM_ session for the user, and sets the initial credential to the received password
217
+
8. If _PAM_ sends more messages, like e.g. 2FA prompts or changing expired passwords:
218
+
*_cockpit-session_ sends corresponding `X-Conversation` authorize messages (see above) to _cockpit-ws_
219
+
*_cockpit-ws_ forwards them to the Login page, which displays the text, and sends the user response as the authorize reply
220
+
*_cockpit-ws_ forwards the authorize reply to PAM
221
+
9. When PAM succeeds, _cockpit-session_ executes the bridge, and connects its stdio pipes to it, then _cockpit-ws_ starts the websocket on it
222
+
223
+
```mermaid
224
+
sequenceDiagram
225
+
participant User
226
+
participant cockpit-tls
227
+
participant cockpit-ws
228
+
participant cockpit-session
229
+
participant PAM
230
+
231
+
User->>cockpit-tls: https://server:9090
232
+
cockpit-tls->>cockpit-ws: Connect via systemd socket
233
+
cockpit-ws->>User: 401 + Login page
234
+
User->>cockpit-ws: POST with Authorization: Basic
235
+
cockpit-ws->>cockpit-session: Spawn
236
+
cockpit-session->>cockpit-ws: authorize command with * challenge
1._User_ connects to cockpit URL, which lands at _cockpit-tls_
257
+
2._cockpit-tls_ connects to _cockpit-ws_ via the `cockpit-wsinstance-http@` or `cockpit-wsinstance-https@SHA256_NIL` systemd socket/service. See [cockpit-tls docs](../src/tls/README.md) and [systemd units](../src/systemd/) for details.
258
+
3._cockpit-ws_ responds with "401 Authentication failed" and includes `WWW-Authenticate: Negotiate` header (if Kerberos is available)
259
+
4._Browser_ (if configured for SPNEGO/Kerberos) requests a service ticket from the _KDC_ for the HTTP service principal
260
+
5._Browser_ sends a new request with `Authorization: Negotiate <base64-gssapi-token>` header
261
+
6._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.
262
+
7._cockpit-session_ calls `gss_accept_sec_context()` with the GSSAPI token to verify the Kerberos ticket
263
+
8. If GSSAPI returns `GSS_S_CONTINUE_NEEDED` (multi-round negotiation):
264
+
*_cockpit-session_ sends an authorize command with a `Negotiate` challenge containing the output token to _cockpit-ws_
265
+
*_cockpit-ws_ responds with "401 Authentication failed" and `WWW-Authenticate: Negotiate <token>` to the _Browser_
266
+
*_Browser_ sends another `Authorization: Negotiate <token>` request
267
+
* This continues until GSSAPI negotiation completes
268
+
9. When GSSAPI succeeds, _cockpit-session_ has the authenticated GSSAPI principal name
269
+
10._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]`)
270
+
11._cockpit-session_ starts _PAM_, skipping the auth stack (as GSSAPI already authenticated), and runs the account, credential, and session stacks
271
+
12._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)
272
+
13._cockpit-session_ executes the bridge, and connects its stdio pipes to it, then _cockpit-ws_ starts the websocket on it
273
+
274
+
Login process: Client Certificate
275
+
----------------------------------
276
+
277
+
1._User_ connects to cockpit URL with a client certificate, which lands at _cockpit-tls_
278
+
2._cockpit-tls_ calculates the SHA256(certificate) as the user fingerprint
279
+
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.
280
+
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)
281
+
5._cockpit-tls_ includes `"client-certificate": "<fingerprint>"` in its mini JSON protocol to _cockpit-ws_
282
+
6._cockpit-ws_ detects the client certificate metadata and uses `tls-cert <fingerprint>` as the authorization type
283
+
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.
284
+
8._cockpit-session_ receives the `tls-cert <fingerprint>` authorization and reads the certificate from `/run/cockpit/tls/clients/<fingerprint>`
285
+
9._cockpit-session_ validates that the certificate file exists and matches the expected _cockpit-ws_ cgroup
286
+
10._cockpit-session_ calls the _sssd_ D-Bus API (`org.freedesktop.sssd.infopipe.Users.FindByCertificate`) to map the certificate to a username
287
+
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
288
+
12._cockpit-session_ executes the bridge, and connects its stdio pipes to it, then _cockpit-ws_ starts the websocket on it
289
+
290
+
Login process: SSH to remote machine
291
+
------------------------------------
292
+
293
+
1._User_ connects to a URL like `https://server:9090/=hostname`) or sets the "Connect to:" field in the Login page to `hostname`. See User/Password steps 2 to 4 for the precise _cockpit-tls_ / _cockpit-ws_ connection process
294
+
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`
295
+
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`
296
+
4._cockpit-ws_ spawns the ssh command with the target host as argument
297
+
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 6)
298
+
6._cockpit.beiboot_ parses the credentials, writes any received `known_hosts` to a temporary file, and configures `ssh` to use it via `-o UserKnownHostsfile=...`
299
+
7._cockpit.beiboot_ connects to the remote host via SSH using the [ferny API](https://github.com/allisonkarlitskaya/ferny/).
300
+
8. If the remote host's SSH key is unknown or has changed:
301
+
* 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.
302
+
* 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).
303
+
* For localhost (127.0.0.1): _cockpit.beiboot_ automatically accepts the key without user interaction.
304
+
*_cockpit-ws_ forwards the `X-Conversation` challenge to the web UI
305
+
*_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
306
+
* User accepts or rejects the key
307
+
*_login.js_ sends the response back via `X-Conversation` header
308
+
*_cockpit.beiboot_ returns the response to SSH's askpass mechanism
309
+
* SSH proceeds with the connection (if accepted) or fails (if rejected)
310
+
9. If SSH prompts for additional input (like 2FA): Similar to step 8 in User/Password section: _cockpit.beiboot_ sends `X-Conversation` messages back and forth
311
+
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
312
+
11._cockpit-ws_ starts the websocket on the transport, connecting it to beiboot.py's stdio and hence effectively to the remote bridge
313
+
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`
314
+
13._login.js_ stores the received `known-hosts` entry in localStorage for future connections to this host
0 commit comments