Skip to content

Commit 6f63f0f

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 1d23191 commit 6f63f0f

File tree

1 file changed

+108
-0
lines changed

1 file changed

+108
-0
lines changed

doc/authentication.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,111 @@ 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+
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
236+
cockpit-ws->>cockpit-session: user/password
237+
cockpit-session->>PAM: pam_authenticate()
238+
PAM-->>cockpit-session: Success or conversation
239+
opt 2FA/password change
240+
cockpit-session->>cockpit-ws: X-Conversation challenge
241+
cockpit-ws->>User: Display prompt
242+
User->>cockpit-ws: Response
243+
cockpit-ws->>cockpit-session: Forward response
244+
cockpit-session->>PAM: Continue conversation
245+
end
246+
PAM->>cockpit-session: Success
247+
cockpit-session->>PAM: open_session()
248+
cockpit-ws->>User: 200 OK for the login page
249+
```
250+
251+
Login process: Kerberos/GSSAPI
252+
-------------------------------
253+
254+
1. _User_ connects to cockpit URL.
255+
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

Comments
 (0)