This commit is contained in:
Chris Wiggins 2026-11-03 08:16:30 +00:00 committed by GitHub
commit 06902995dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 178 additions and 1 deletions

View file

@ -158,6 +158,28 @@ the next section for details.
See the [Certificate/smart card authentication guide](https://docs.cockpit-project.org/cockpit-guide/latest/guide/cert-authentication.html)
for details how to set this up.
Session User Command
---------------------------------
It is possible to set up a custom authentication scheme, that calls an external command to validate the incoming authentication header, and if successful, returns a specific username to Cockpit, in turn using PAM to then log in as that user.
A common use-case for this is where you have Cockpit sitting behind a reverse-proxy that performs authentication, and passes an authentication token; for example a JWT token.
To configure this, create a section in `cockpit.conf` for your custom authentication scheme, for example:
```
[x-my-auth-method]
SessionUserCommand = /usr/local/bin/my-auth-method-script
```
The command specified in `SessionUserCommand` will be called with two arguments, which are the remote host IP address, and the authentication data extracted from the Authorization header (excluding the auth scheme prefix). For example, if a curl command like this is issued:
`curl -vvv -k https://localhost:9090/cockpit/login -H 'Authorization: x-my-auth-method some-auth-data'`
The command will be called like this:
`/usr/local/bin/my-auth-method-script <remote-host-ip> some-auth-data`
The command should **only** output the username to log in as on stdout, and exit with status code 0 on success. If the authentication fails, the command should exit with a non-zero status code. Any output to stderr will be logged by Cockpit.
Actions
-------

View file

@ -5,6 +5,7 @@
#include "config.h"
#include "common/cockpitconf.h"
#include "common/cockpitframe.h"
#include "common/cockpitjsonprint.h"
#include "common/cockpitmemory.h"
@ -17,6 +18,7 @@
#include <gssapi/gssapi_generic.h>
#include <gssapi/gssapi_krb5.h>
#include <fcntl.h>
#include <sys/mman.h>
static char *last_txt_msg = NULL;
static char *last_err_msg = NULL;
@ -776,6 +778,75 @@ perform_tlscert (const char *rhost,
return pamh;
}
static pam_handle_t *
perform_session_user_command (
const char *command,
const char *rhost,
const char *type,
const char *authorization)
{
struct pam_conv conv = { pam_conv_func_dummy, };
pam_handle_t *pamh;
int res;
assert (rhost != NULL);
assert (type != NULL);
assert (authorization != NULL);
/* Strip the type from the authorization string */
assert (strncmp (authorization, type, strlen (type)) == 0);
/* Strip the space (+1) */
authorization += strlen (type) + 1;
debug ("start external authentication (%s) for cockpit-ws %u", type, getppid ());
/* Create a memfd to capture */
int memfd = memfd_create("username-output", MFD_CLOEXEC);
if (memfd < 0)
err (EX, "memfd_create failed");
const char *command_argv[] = { command, rhost, authorization, NULL };
const int remap_fds[] = { -1, memfd, -1 }; // only remap stdout to memfd
int status = spawn_and_wait(command_argv, NULL, remap_fds, 3, getuid(), getgid());
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
close(memfd);
errx(EX, "external command failed: %s", command);
}
char username[1025];
/* Reset file offset to start */
lseek(memfd, 0, SEEK_SET);
/* Read the output, max of 1024 bytes */
ssize_t nread = read(memfd, username, 1024);
close(memfd);
if (nread < 0)
err(EX, "read from external memfd failed");
username[nread] = '\0';
username[strcspn(username, "\n")] = '\0'; /* null-terminate at first newline */
if (username[0] == '\0')
errx(EX, "no username provided by external command: %s", command);
res = pam_start ("cockpit", username, &conv, &pamh);
if (res != PAM_SUCCESS)
errx (EX, "couldn't start pam: %s", pam_strerror (NULL, res));
if (pam_set_item (pamh, PAM_RHOST, rhost) != PAM_SUCCESS)
errx (EX, "couldn't setup pam rhost");
res = open_session (pamh);
/* Our exit code is a PAM code */
if (res != PAM_SUCCESS)
exit_pam_init_problem (res);
return pamh;
}
/* Return path of ccache file (including FILE: prefix), clean this up at session end */
static char *
@ -970,6 +1041,12 @@ main (int argc,
pamh = perform_gssapi (rhost, response);
else if (strcmp (type, "tls-cert") == 0)
pamh = perform_tlscert (rhost, response);
else {
const char *command = cockpit_conf_string (type, "SessionUserCommand");
if (command != NULL) {
pamh = perform_session_user_command (command, rhost, type, response);
}
}
cockpit_memory_clear (response, -1);
free (response);

View file

@ -1079,6 +1079,7 @@ cockpit_session_launch (CockpitAuth *self,
const gchar *command = cockpit_conf_string (section, "Command");
const gchar *unix_path = cockpit_conf_string (section, "UnixPath");
const gchar *session_user_command = cockpit_conf_string (section, "SessionUserCommand");
gboolean capture_stderr = FALSE;
if (g_str_equal (section, COCKPIT_CONF_SSH_SECTION))
@ -1100,7 +1101,8 @@ cockpit_session_launch (CockpitAuth *self,
}
else if (g_str_equal (type, "basic") ||
g_str_equal (type, "negotiate") ||
g_str_equal (type, "tls-cert"))
g_str_equal (type, "tls-cert") ||
session_user_command != NULL)
{
if (command == NULL && unix_path == NULL)
{

View file

@ -1177,6 +1177,82 @@ matchrule = <SUBJECT>^DC=LAN,DC=COCKPIT,CN=alice$
# cleans up session
m.execute("while loginctl --no-legend list-sessions | grep admin; do sleep 1; done")
# @testlib.skipWsContainer("sssd/cockpit-session not available with ws container")
# @testlib.skipImage("sssd not currently in testing", "debian-testing")
def testSessionUserCommand(self):
m = self.machine
m.execute("useradd alice; echo alice:foobar123 | chpasswd")
# These tests have to be run with curl, as this is a backend-auth feature, likely implemented by a middleware proxy
def do_test(authopts, expected, not_expected=None):
# avoid running into start-limit-hit
m.execute("systemctl reset-failed")
m.start_cockpit(tls=True)
output = m.execute(['curl', '-ksS', '-D-', *authopts, 'https://localhost:9090/cockpit/login'])
for s in expected:
self.assertIn(s, output)
for s in (not_expected or []):
self.assertNotIn(s, output)
m.stop_cockpit()
# sample auth method should not be enabled by default
do_test(['-H', 'Authorization: x-my-auth-method some-auth-data'], ["HTTP/1.1 401 Authentication disabled"])
# password auth should work
do_test(['-u', 'alice:foobar123'], ['HTTP/1.1 200 OK', '"csrf-token"'])
# enable cert based auth
m.write("/etc/cockpit/cockpit.conf", '[x-my-auth-method]\nSessionUserCommand = /etc/cockpit/test-session-user\n', append=True)
m.write("/etc/cockpit/test-session-user", """
#!/usr/bin/env bash
set -euo pipefail
dbg() {
# STDERR debug logging
printf "my-auth-method-script DEBUG: %s\n" "$1" >&2
}
exit_with_problem() {
printf "my-auth-method-script ERROR: %s\n" "$1" >&2
exit 1
}
main() {
if [[ $# -ne 2 ]]; then
exit_with_problem "Invalid number of arguments, pass rhost and auth value"
fi
local rhost="$1"
local auth_data="$2"
dbg "Got auth_data ${auth_data} for rhost ${rhost}"
if [[ "$auth_data" != "some-auth-data" ]]; then
exit_with_problem "Invalid auth data"
fi
echo "alice"
}
main "$@"
""", perm="0755")
self.allow_journal_messages("my-auth-method-script DEBUG: Got auth_data some-auth-data for rhost .*")
# cert auth should work now
do_test(['-H', 'Authorization: x-my-auth-method some-auth-data'], ['HTTP/1.1 200 OK', '"csrf-token"'])
# password auth, too
do_test(['-u', 'alice:foobar123'], ['HTTP/1.1 200 OK', '"csrf-token"'])
# Other auth data fails
self.allow_journal_messages("my-auth-method-script DEBUG: Got auth_data bad-auth-data for rhost .*")
self.allow_journal_messages("my-auth-method-script ERROR: Invalid auth data")
do_test(['-H', 'Authorization: x-my-auth-method bad-auth-data'],
["HTTP/1.1 401 Authentication failed"],
not_expected=["crsf-token"])
@testlib.skipOstree("/etc customization issues?")
@testlib.nondestructive