mirror of
https://github.com/cockpit-project/cockpit.git
synced 2026-11-03 21:49:50 -09:00
Merge fc10880ecc into 19402ce11f
This commit is contained in:
commit
06902995dd
4 changed files with 178 additions and 1 deletions
|
|
@ -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
|
||||
-------
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue