Back to Cockpit

Authentication

doc/authentication.md

35916.7 KB
Original Source

Cockpit Authentication

Cockpit authorizes users by looking at the Authorization header on requests to /login. Cockpit will attempt to perform the start the authentication command that is configured for the auth scheme contained in the header.

To configure an auth scheme add a section to cockpit.conf for that scheme. For example to configure an command for the "Bearer" auth scheme your cockpit.conf should contain the following section.

[bearer]
command = example-verify-token
timeout = 300

The command is then responsible to:

  • verify the given credentials
  • setup an appropriate session and environment based on those credentials
  • launch a bridge that speaks the cockpit protocol on stdin and stdout.

The default command is cockpit-session it is able to handle Password ([basic]), Kerberos/GSSAPI ([negotiate]), and TLS client certificate ([tls-cert]) authentication.

Authentication commands are called with a single argument which is the host that the user is connecting to. They communicate with their parent process using the cockpit protocol on stdin and stdout.

Instead of Command, it's also possible to specify UnixPath to connect to a unix socket at a given path. The protocol is the same.

Credentials can then be retrieved by issuing a authorize command with a challenge. The challenge should correspond to the authorization type in header (ei: Basic or Bearer). For example:

{
    "command": "authorize",
    "cookie": "cookie",
    "challenge": "*"
}

The response will look something like this

{
    "command": "authorize",
    "cookie": "cookie",
    "response": "Basic dXNlcjpwYXNzd29yZAo=",
    "remote-peer": "1.2.3.4",
}

remote-peer is the IP address of the connecting user, if known (otherwise unset).

A * challenge requests whatever credentials the parent process has. Most auth commands will want to begin by issuing a * challenge.

By default cockpit-ws will wait a maximum of 30 seconds to receive this response. The number of seconds to wait can be adjusted by adding a timeout parameter along side the auth schema configuration in your config file. The given value should be a number between 1 and 900.

If more information is needed the command should respond with a X-Conversation challenge. This takes the following format.

X-Conversation nonce base64(prompt message)

The message will be displayed to the user and the user will be prompted for a response. If the user does not respond within 60 seconds the command will be closed and the login aborted. The number of seconds to wait can be adjusted by adding a response-timeout parameter along side the auth schema configuration in your config file. The given value should be a number between 1 and 900.

Once a result is known a "init" command should be sent. If the login was successful you can usually just let the bridge do this.

If the login was not successful the JSON should include a problem field. Values of authentication-failed, authentication-unavailable or access-denied are translated to the appropriate cockpit error codes. Any other values are treated as generic errors. Additionally a message field may be included as well.

If an process exits without sending a init command, that will be treated as an internal error.

If the authentication command has additional data that it would like to return with a successful response it can do so by sending a x-login-data challenge. The command should have an additional JSON field login-data. The string placed there will be returned by along with a successful json response.

For a simple python example see cockpit-auth-ssh-key.

Remote machines

Cockpit also supports logging directly into remote machines. The remote machine to connect to is provided by using a application name that begins with cockpit+=. The default command used for this is python3 -m cockpit.beiboot, which invokes ssh.

The section Ssh-Login defines the options for all ssh commands. The section has the same options as the other authentication sections with the following additions.

  • host The default host to log into. Defaults to 127.0.0.1. That host's key will not be checked/validated.
  • connectToUnknownHosts. By default cockpit will refuse to connect to any machines that are not already present in ssh's global known_hosts file (usually /etc/ssh/ssh_known_hosts). Set this to true is to allow those connections to proceed.

After the user authentication with the "*" challenge, if the remote host is not already present in any local known_hosts file, this will send an "x-host-key" challenge:

{
    "command": "authorize",
    "challenge": "x-host-key"
    "cookie": "cookie",
}

The caller responds to that with either a valid key like below, or an empty string response if there is no available key.

{
    "command": "authorize",
    "cookie": "cookie",
    "response": "ssh-rsa AAAA1234...",
}

Client certificate authentication

When a machine is joined to an Identity Management domain (like FreeIPA or Active Directory) which has client-side user certificates set up, then these can be enabled for authentication to Cockpit by setting this option in cockpit.conf:

[WebService]
ClientCertAuthentication = yes

This uses the [tls-cert] authentication scheme.

When enabling this mode, other authentication types commonly get disabled. See the next section for details.

See the Certificate/smart card authentication guide for details how to set this up.

Actions

Setting an action can modify the behavior for an auth scheme. Currently two actions are supported.

  • remote-login-ssh Use the Ssh-Login section instead.
  • none Disable this auth scheme.

To configure an action add the action option. For example to disable basic authentication, cockpit.conf should contain the following section:

[basic]
action = none

Likewise, if the machine is part of a Kerberos domain, but that should not be used to authenticate to cockpit, create the following section:

[negotiate]
action = none

Limits

Cockpit can be configured to limit the number of concurrent login processes. See cockpit.conf for more details. This will affect how many custom authentication processes can be launched.

Environment Variables

The following environment variables are set by cockpit-ws when spawning an auth process for SSH connections:

  • COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS Set to 1 to allow connecting to hosts that are not present in the current known_hosts files. If not set, Cockpit will only connect to unknown hosts if either the remote peer is local, or if the Ssh-Login section in cockpit.conf has an connectToUnknownHosts option set to a true value (1, yes or true).

  • COCKPIT_SSH_KNOWN_HOSTS_FILE Path to knownhost files. Defaults to PACKAGE_SYSCONF_DIR/ssh/ssh_known_hosts

Login process: User/Password

This section entirely ignores cockpit-tls for simplicity, as it is not involved in the authentication. See cockpit-tls docs for details.

  1. User connects to cockpit URL.
  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.
  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
  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.
  5. cockpit-session sends an authorize command with a * challenge (see above) to cockpit-ws, which responds with the user/password
  6. cockpit-session starts a PAM session for the user, and sets the initial credential to the received password
  7. If PAM sends more messages, like e.g. 2FA prompts or changing expired passwords:
    • cockpit-session sends corresponding X-Conversation authorize messages (see above) to cockpit-ws
    • cockpit-ws forwards them to the Login page, which displays the text, and sends the user response as the authorize reply
    • cockpit-ws forwards the authorize reply to PAM
  8. When PAM succeeds, cockpit-session executes the bridge and the session starts.
mermaid
sequenceDiagram
    participant User
    participant cockpit-ws
    participant cockpit-session
    participant PAM

    User->>cockpit-ws: GET https://server:9090
    cockpit-ws->>User: 401 + Login page
    User->>cockpit-ws: GET with Authorization: Basic
    cockpit-ws->>cockpit-session: Spawn
    cockpit-session->>cockpit-ws: authorize command with * challenge
    cockpit-ws->>cockpit-session: user/password
    cockpit-session->>PAM: pam_authenticate()
    PAM-->>cockpit-session: Success or conversation
    opt 2FA/password change
        cockpit-session->>cockpit-ws: X-Conversation challenge
        cockpit-ws->>User: Display prompt
        User->>cockpit-ws: Response
        cockpit-ws->>cockpit-session: Forward response
        cockpit-session->>PAM: Continue conversation
    end
    PAM->>cockpit-session: Success
    cockpit-session->>PAM: open_session()
    cockpit-ws->>User: 200 OK for the login page

Login process: Kerberos/GSSAPI

  1. User connects to cockpit URL.
  2. cockpit-ws responds with "401 Authentication failed" and includes WWW-Authenticate: Negotiate header (if Kerberos is available)
  3. Browser (if configured for SPNEGO/Kerberos) requests a service ticket from the KDC for the HTTP service principal
  4. Browser sends a new request with Authorization: Negotiate <base64-gssapi-token> header
  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.
  6. cockpit-session calls gss_accept_sec_context() with the GSSAPI token to verify the Kerberos ticket
  7. If GSSAPI returns GSS_S_CONTINUE_NEEDED (multi-round negotiation):
    • cockpit-session sends an authorize command with a Negotiate challenge containing the output token to cockpit-ws
    • cockpit-ws responds with "401 Authentication failed" and WWW-Authenticate: Negotiate <token> to the Browser
    • Browser sends another Authorization: Negotiate <token> request
    • This continues until GSSAPI negotiation completes
  8. When GSSAPI succeeds, cockpit-session has the authenticated GSSAPI principal name
  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])
  10. cockpit-session starts PAM, skipping the auth stack (as GSSAPI already authenticated), and runs the account, credential, and session stacks
  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)
  12. Authentication completes, and the session starts as above.

Login process: Client Certificate

Unlike the other sections, this one involves cockpit-tls as well as it provides a crucial part of the authentication.

  1. User connects to cockpit URL with a client certificate, which lands at cockpit-tls
  2. cockpit-tls calculates the SHA256(certificate) as the user fingerprint
  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 and systemd units for details.
  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)
  5. cockpit-tls includes "client-certificate": "<fingerprint>" in its mini JSON protocol to cockpit-ws
  6. cockpit-ws detects the client certificate metadata and uses tls-cert <fingerprint> as the authorization type
  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.
  8. cockpit-session receives the tls-cert <fingerprint> authorization and reads the certificate from /run/cockpit/tls/clients/<fingerprint>
  9. cockpit-session validates that the certificate file exists and matches the expected cockpit-ws cgroup
  10. cockpit-session calls the sssd D-Bus API (org.freedesktop.sssd.infopipe.Users.FindByCertificate) to map the certificate to a username
  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
  12. Authentication completes, and the session starts as above.

Login process: SSH to remote machine

  1. User connects to a URL like https://server:9090/=hostname) or sets the "Connect to:" field in the Login page to hostname.
  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
  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
  4. cockpit-ws spawns the ssh command with the target host as argument
  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)
  6. cockpit.beiboot parses the credentials, writes any received known_hosts to a temporary file, and configures ssh to use it via -o UserKnownHostsfile=...
  7. cockpit.beiboot connects to the remote host via SSH using the ferny API.
  8. If the remote host's SSH key is unknown or has changed:
    • 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.
    • 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).
    • For localhost (127.0.0.1): cockpit.beiboot automatically accepts the key without user interaction.
    • cockpit-ws forwards the X-Conversation challenge to the web UI
    • 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
    • User accepts or rejects the key
    • login.js sends the response back via X-Conversation header
    • cockpit.beiboot returns the response to SSH's askpass mechanism
    • SSH proceeds with the connection (if accepted) or fails (if rejected)
  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
  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). It starts bridge on the remote machine and connects its stdio to it
  11. Authentication completes, and the session starts as above.
  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
  13. login.js stores the received known-hosts entry in localStorage for future connections to this host