Skip to content

Commit 2168c9d

Browse files
committed
doc: Login process
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)
1 parent 4cb7d1f commit 2168c9d

File tree

1 file changed

+109
-0
lines changed

1 file changed

+109
-0
lines changed

doc/authentication.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,112 @@ SSH connections:
203203

204204
* **COCKPIT_SSH_KNOWN_HOSTS_FILE** Path to knownhost files. Defaults to
205205
`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
237+
cockpit-ws->>cockpit-session: user/password
238+
cockpit-session->>PAM: pam_authenticate()
239+
PAM-->>cockpit-session: Success or conversation
240+
opt 2FA/password change
241+
cockpit-session->>cockpit-ws: X-Conversation challenge
242+
cockpit-ws->>User: Display prompt
243+
User->>cockpit-ws: Response
244+
cockpit-ws->>cockpit-session: Forward response
245+
cockpit-session->>PAM: Continue conversation
246+
end
247+
PAM->>cockpit-session: Success
248+
cockpit-session->>PAM: open_session()
249+
cockpit-session->>cockpit-ws: Execute bridge, connect stdio
250+
cockpit-ws->>User: Start websocket
251+
```
252+
253+
Login process: Kerberos/GSSAPI
254+
-------------------------------
255+
256+
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

Comments
 (0)