ws: Add client certificate authentication

Add a new pam_cockpit_cert PAM module for TLS client certificate based
authentication. This uses sssd's API to map a TLS certificate to a
user [1]. Use the sd-bus API for making the D-Bus calls, instead of
gdbus (glib/gio has too many side effects, which should be avoided in a
setuid root program) or libdbus (which would be a new dependency).

Introduce a new `tls-cert` authorization type into
cockpit-ws/cockpit-session that gets enabled with the new
`ClientCertAuthentication` option in cockpit.conf. If this is enabled,
and cockpit-tls exports a current TLS client certificate,
pam_cockpit_cert maps the certificate to a user name.

Unfortunately the Chrome Devtools Protocol does not currently offer
client certificate import and selecting one for a page that requests a
certificate. Test this with curl instead, and describe in the comments
how to test this interactively.

This requires some more SELinux policy modifications. Apply them locally
in cockpit.spec until they land in Fedora/RHEL.

[1] https://www.freeipa.org/page/V4/User_Certificates

Fixes #8429
Related: https://bugzilla.redhat.com/show_bug.cgi?id=1678465
Jira: COCKPIT-368
Closes #11421
This commit is contained in:
Martin Pitt 2019-03-14 17:40:49 +01:00 committed by Martin Pitt
parent a929d27c1d
commit 60c5fb2f40
23 changed files with 1052 additions and 15 deletions

1
.gitignore vendored
View File

@ -128,6 +128,7 @@ cockpitd
/doc/man/cockpit.conf.5
/doc/man/cockpitd.8
/doc/man/remotectl.8
/doc/man/pam_cockpit_cert.8
/doc/man/pam_ssh_add.8
/doc/version
/doc/guide/html

View File

@ -107,6 +107,11 @@ COCKPIT_SESSION_LIBS="$KRB5_LIBS"
AC_SUBST(COCKPIT_SESSION_CFLAGS)
AC_SUBST(COCKPIT_SESSION_LIBS)
PAM_COCKPIT_CERT_CFLAGS="$COCKPIT_CFLAGS $LIBSYSTEMD_CFLAGS"
PAM_COCKPIT_CERT_LIBS="$LIBSYSTEMD_LIBS"
AC_SUBST(PAM_COCKPIT_CERT_CFLAGS)
AC_SUBST(PAM_COCKPIT_CERT_LIBS)
COCKPIT_WS_CFLAGS="$COCKPIT_CFLAGS"
COCKPIT_WS_LIBS="$COCKPIT_LIBS"
AC_SUBST(COCKPIT_WS_CFLAGS)

View File

@ -21,8 +21,8 @@ The command is then responsible to:
* 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 basic and gssapi
authentication.
The default command is `cockpit-session` it is able to handle basic, gssapi,
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
@ -129,6 +129,24 @@ string response if there is no available key.
}
```
Client certificate authentication
---------------------------------
When a machine is joined to an Identity Management domain (like
[FreeIPA](https://www.freeipa.org) or Active Directory) which has [client-side
user certificates](https://www.freeipa.org/page/V4/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.
Actions
-------

View File

@ -44,6 +44,7 @@ GUIDE_INCLUDES = \
doc/guide/https.xml \
doc/guide/listen.xml \
doc/guide/sso.xml \
doc/guide/cert-authentication.xml \
doc/guide/startup.xml \
doc/guide/urls.xml \
$(NULL)

View File

@ -18,8 +18,10 @@
access is controlled by a cockpit specific pam stack, generally located
at <filename>/etc/pam.d/cockpit</filename>. By default this is configured
to allow you to login with the username and password of any local account on the
system or you can setup a <link linkend="sso">Kerberos based SSO
solution</link>.</para>
system. You can also setup a <link linkend="sso">Kerberos based SSO
solution</link> or <link linkend="cert-authentication">certificate/smart
card authentication</link>.
</para>
<para>The web server can also be run from the
<ulink url="https://hub.docker.com/r/cockpit/ws/">cockpit/ws</ulink>

View File

@ -0,0 +1,124 @@
<?xml version="1.0"?>
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
"http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
<chapter id="cert-authentication">
<title>Certificate/smart card authentication</title>
<para>
Cockpit can use TLS client certificates for authenticating users. Commonly
these are provided by a smart card, but it's equally possible to import
certificates directly into the web browser.
</para>
<para>
This requires the host to be in an Identity Management domain like
<ulink url="https://www.freeipa.org">FreeIPA</ulink> or
<ulink url="https://en.wikipedia.org/wiki/Active_Directory">Active Directory</ulink>,
which can associate certificates to users.
</para>
<section id="certauth-server-idm">
<title>Identity Management setup</title>
<para>To authenticate users from a Identity Management domain, the server that
Cockpit is running on must be joined to that domain. See the
<link linkend="sso-server">SSO server requirements</link> for details.</para>
<para>The domain's users also need to get associated to certificates, usually with
the <command>ipa user-add-cert</command> command. See the
<ulink url="https://www.freeipa.org/page/V4/User_Certificates#Feature_Management">
FreeIPA User Certificates documentation</ulink>
for details. As a simple example, these commands will generate a new certificate/key
pair and associate it to the user <code>alice</code>:</para>
<programlisting>
# create self-signed certificate and key:
openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout alice.key \
-out alice.pem -subj "/CN=alice"
# FreeIPA only accepts DER format, convert it
openssl x509 -outform der -in alice.pem -out alice.der
# browsers and smart cart utilities accept PKCS#12 format, convert it
# this needs to set a transfer/import password
openssl pkcs12 -export -password pass:somepassword \
-in alice.pem -inkey alice.key -out alice.p12
# assign it to the IdM user alice
ipa user-add-cert alice --certificate="$(base64 alice.der)"
</programlisting>
<para>You can now import <code>alice.p12</code> directly into your browser,
with giving the transfer password set above. Or
<ulink url="https://linux.die.net/man/1/pkcs15-init">put the certificate onto a smart card</ulink>:</para>
<programlisting>
pkcs15-init --store-private-key alice.p12 --format pkcs12 --auth-id 01
</programlisting>
</section>
<section id="certauth-server-cockpitconf">
<title>Cockpit web server configuration</title>
<para>Certificate authentication needs to be enabled in
<ulink url="./cockpit.conf.5.html">cockpit.conf</ulink> explicitly:</para>
<programlisting>
[WebService]
ClientCertAuthentication = yes
</programlisting>
<para>When enabling this mode,
<ulink url="https://github.com/cockpit-project/cockpit/blob/master/doc/authentication.md">
other authentication types</ulink> commonly get disabled, so that *only* client certificate
authentication will be accepted. By default, after a failed certificate authentication attempt,
Cockpit's normal login page will appear and permit other login types such as `basic` (passwords)
or `gssapi` (Kerberos). For example, password authentication gets disabled with:</para>
<programlisting>
[Basic]
action = none
</programlisting>
</section>
<section id="certauth-server-resourcelimits">
<title>Cockpit web server resource limits</title>
<para>When using certificate authentication, all requests with a particular
certificate will be handled by a separate and isolated instance of the
<ulink url="./cockpit-ws.8.html">cockpit-ws</ulink> web server. This
protects against possible vulnerabilities in the web server and prevents
an attacker from impersonating another user. However, this introduces a
potential Denial of Service: Some remote attacker could create a
large number of certificates and send a large number of http requests
to Cockpit with these.</para>
<para>To mitigate that, all <code>cockpit-ws</code> instances run
in a <code>system-cockpithttps.slice</code>
<ulink url="https://www.freedesktop.org/software/systemd/man/systemd.slice.html">systemd slice unit</ulink>
which <ulink url="https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html">limits
the collective resources</ulink> of these web server instances: by default,
this slice sets a limit of 200 threads (roughly 100 instances of <code>cockpit-ws</code> -- in other
words, a maximum of 100 parallel user sessions with different certificates) and
a 75% (soft)/90% (hard) memory limit.</para>
<para>You are welcome to adjust these limits to your need through
a <ulink url="https://www.freedesktop.org/software/systemd/man/systemd.unit.html">drop-in</ulink>.
For example:</para>
<programlisting>
# systemctl edit system-cockpithttps.slice
[Slice]
# change existing value
TasksMax=100
# add new restriction
CPUQuota=30%
</programlisting>
</section>
</chapter>

View File

@ -30,6 +30,7 @@
<xi:include href="startup.xml"/>
<xi:include href="authentication.xml"/>
<xi:include href="sso.xml"/>
<xi:include href="cert-authentication.xml"/>
<xi:include href="privileges.xml"/>
</part>

View File

@ -12,6 +12,7 @@ man_MANS += \
doc/man/cockpit-ws.8 \
doc/man/cockpit-tls.8 \
doc/man/cockpit.conf.5 \
doc/man/pam_cockpit_cert.8 \
doc/man/pam_ssh_add.8 \
doc/man/remotectl.8 \
$(NULL)
@ -22,6 +23,7 @@ EXTRA_DIST += \
doc/man/cockpit-tls.xml \
doc/man/cockpit-desktop.xml \
doc/man/cockpit.conf.xml \
doc/man/pam_cockpit_cert.xml \
doc/man/pam_ssh_add.xml \
doc/man/remotectl.xml \
$(NULL)

View File

@ -134,6 +134,16 @@ ProtocolHeader = X-Forwarded-Proto
<code>/cockpit/</code> and <code>/cockpit+new/</code> are not.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>ClientCertAuthentication</option></term>
<listitem>
<para>If true, enable TLS client certificates for authenticating users. Commonly
these are provided by a smart card, but it's equally possible to import
certificates directly into the web browser. Please see the
<ulink url="https://cockpit-project.org/guide/latest/cert-authentication.html">Certificate/smart card authentication</ulink>
section in the Cockpit guide for details.</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
@ -205,6 +215,9 @@ ProtocolHeader = X-Forwarded-Proto
<para>
<citerefentry>
<refentrytitle>cockpit-ws</refentrytitle><manvolnum>8</manvolnum>
</citerefentry>,
<citerefentry>
<refentrytitle>cockpit-tls</refentrytitle><manvolnum>8</manvolnum>
</citerefentry>
</para>
</refsect1>

View File

@ -0,0 +1,168 @@
<refentry id="pam_cockpit_cert.8">
<!--
This file is part of Cockpit.
Copyright (C) 2019 Red Hat, Inc.
Cockpit is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
Cockpit is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
-->
<refentryinfo>
<title>pam_cockpit_cert</title>
<productname>pam_cockpit_cert</productname>
</refentryinfo>
<refmeta>
<refentrytitle>pam_cockpit_cert</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>pam_cockpit_cert</refname>
<refpurpose>PAM module for authenticating to Cockpit with a client certificate</refpurpose>
</refnamediv>
<refsect1><title>DESCRIPTION</title>
<para>
pam_cockpit_cert provides an PAM authentication module for identifying and
authenticating users through a TLS client certificate. Commonly this is
provided by a smart card, but it's equally possible to import certificates
directly into the web browser.
</para>
<para>
This requires the host to be in an Identity Management domain like
<ulink url="https://www.freeipa.org">FreeIPA</ulink> or
<ulink url="https://en.wikipedia.org/wiki/Active_Directory">Active Directory</ulink>,
which can associate certificates to users. See the
<ulink url="https://www.freeipa.org/page/V4/User_Certificates">FreeIPA User Certificates documentation</ulink>
for details. The <filename>sssd-dbus</filename> package must be installed for this to work.
</para>
<para>
In authentication mode, <refname>pam_cockpit_cert</refname> is invoked with the
user name unset. It checks whether the web browser presented and validated
a TLS client certificate to Cockpit. If so, that gets passed to sssd. If
that can successfully map the certificate to a user, this PAM module sets
the user name and succeeds, which should be treated as a sufficient authentication.
</para>
<para>
Cockpit does not use certificate based authentication by default; it has to be
explicitly enabled in <filename>cockpit.conf</filename>. If not enabled, this
PAM module is inert and always returns <literal>ignore</literal>.
</para>
</refsect1>
<refsect1 id="options">
<title>Options</title>
<variablelist>
<varlistentry id="debug">
<term><option>debug</option></term>
<listitem>
<para>This option will turn on debug logging to syslog.</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id="result-codes">
<title>Result codes</title>
<variablelist>
<varlistentry>
<term><option>success</option></term>
<listitem>
<para>Certificate is present, mapped to a user, and the user name is set in the PAM stack.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>user_unknown</option></term>
<listitem>
<para>Certificate is present, but sssd cannot map it to a user. Effectively
a definitive failed authentication.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>ignore</option></term>
<listitem>
<para>The PAM user is already set, so this authentication process does not use a certificate.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>unavail</option></term>
<listitem>
<para>sssd is not available for mapping certificates to users.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>service_err</option></term>
<listitem>
<para>sssd is available in general, but responded with an invalid answer. This
might indicate a compatibility problem with a future version.</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Usage in PAM configuration</title>
<para>The module should be added to service PAM configurations like this:</para>
<programlisting>
-auth [success=done new_authtok_reqd=done user_unknown=die default=ignore] pam_cockpit_cert.so
# fallback authentication methods such as pam_unix
</programlisting>
<para>
This <emphasis>must</emphasis> be first module in the "auth" stack as it
sets the <code>PAM_USER</code> variable on successful mapping of a certificate
to a user name. Also, <emphasis>if</emphasis> a certificate is being presented,
then failure to map that to a user should usually be treated as fatal,
without falling back to other methods such as password. Other errors should
usually be considered non-fatal, and just try the next authentication method in the stack.
</para>
</refsect1>
<refsect1 id="pam-cockpit-cert-see-also">
<title>SEE ALSO</title>
<para>
<citerefentry> <refentrytitle>cockpit.conf</refentrytitle><manvolnum>5</manvolnum> </citerefentry>,
<citerefentry> <refentrytitle>cockpit-tls</refentrytitle><manvolnum>8</manvolnum> </citerefentry>,
<citerefentry> <refentrytitle>pam.d</refentrytitle><manvolnum>5</manvolnum> </citerefentry>,
<citerefentry> <refentrytitle>sssd</refentrytitle><manvolnum>8</manvolnum> </citerefentry>,
<citerefentry> <refentrytitle>sssd-ifp</refentrytitle><manvolnum>5</manvolnum> </citerefentry>
</para>
</refsect1>
<refsect1>
<title>AUTHOR</title>
<para>Cockpit has been written by many
<ulink url="https://github.com/cockpit-project/cockpit/">contributors</ulink>.</para>
</refsect1>
<refsect1>
<title>BUGS</title>
<para>
Please send bug reports to either the distribution bug tracker or the
<ulink url="https://github.com/cockpit-project/cockpit/issues/new">upstream bug tracker</ulink>.
</para>
</refsect1>
</refentry>

View File

@ -26,6 +26,7 @@
#include <stddef.h>
#include <stdlib.h>
#include <common/cockpitconf.h>
#include <common/cockpitwebcertificate.h>
#include "utils.h"
#include "server.h"
@ -92,6 +93,7 @@ int
main (int argc, char **argv)
{
struct arguments arguments;
gnutls_certificate_request_t client_cert_mode = GNUTLS_CERT_IGNORE;
/* default option values */
arguments.no_tls = false;
@ -111,8 +113,10 @@ main (int argc, char **argv)
errx (EXIT_FAILURE, "Could not locate server certificate: %s", error);
debug (SERVER, "Using certificate %s", certfile);
/* TODO: Add cockpit.conf option to enable client-certificate auth, once we support that */
connection_crypto_init (certfile, NULL, GNUTLS_CERT_IGNORE);
if (cockpit_conf_bool ("WebService", "ClientCertAuthentication", false))
client_cert_mode = GNUTLS_CERT_REQUEST;
connection_crypto_init (certfile, NULL, client_cert_mode);
free (certfile);
}

View File

@ -66,9 +66,11 @@ EXTRA_DIST += \
noinst_LIBRARIES += libcockpit-ws.a
libcockpit_ws_a_SOURCES = \
src/ws/cockpitws.h \
src/ws/cockpithandlers.h src/ws/cockpithandlers.c \
src/ws/cockpitauth.h src/ws/cockpitauth.c \
src/ws/cockpitws.h \
src/ws/cockpithandlers.h \
src/ws/cockpithandlers.c \
src/ws/cockpitauth.h \
src/ws/cockpitauth.c \
src/ws/cockpitcertificate.c \
src/ws/cockpitcertificate.h \
src/ws/cockpitcompat.c \
@ -82,6 +84,8 @@ libcockpit_ws_a_SOURCES = \
src/ws/cockpitcreds.h src/ws/cockpitcreds.c \
src/ws/cockpitwebservice.h \
src/ws/cockpitwebservice.c \
src/ws/cockpitwsinstancecert.h \
src/ws/cockpitwsinstancecert.c \
$(NULL)
libcockpit_ws_a_CFLAGS = \
@ -207,6 +211,22 @@ install-exec-hook::
chown -f root:$(COCKPIT_WSINSTANCE_GROUP) $(DESTDIR)$(libexecdir)/cockpit-session || true
test "$(COCKPIT_USER)" != "root" && chmod -f 4750 $(DESTDIR)$(libexecdir)/cockpit-session || true
noinst_PROGRAMS += pam_cockpit_cert.so
pam_cockpit_cert_so_SOURCES = \
src/ws/cockpitwsinstancecert.c \
src/ws/pam_cockpit_cert.c
pam_cockpit_cert.so$(EXEEXT): $(pam_cockpit_cert_so_SOURCES)
$(AM_V_CCLD) $(CC) -fPIC -shared $(PAM_COCKPIT_CERT_CFLAGS) $(CFLAGS) $(DEFS) -I$(builddir) -o $@ $^ $(LDFLAGS) $(PAM_COCKPIT_CERT_LIBS)
CLEANFILES += pam_cockpit_cert.so
install-exec-local::
$(MKDIR_P) $(DESTDIR)$(pamdir)
$(INSTALL) pam_cockpit_cert.so $(DESTDIR)$(pamdir)
uninstall-local::
$(RM) -f $(DESTDIR)$(pamdir)/pam_cockpit_cert.so
libexec_SCRIPTS = cockpit-desktop
cockpit-desktop: src/ws/cockpit-desktop.in

View File

@ -20,6 +20,7 @@
#include "config.h"
#include "cockpitauth.h"
#include "cockpitwsinstancecert.h"
#include "cockpitws.h"
@ -1436,12 +1437,34 @@ cockpit_auth_login_async (CockpitAuth *self,
}
application = cockpit_auth_parse_application (path, NULL);
authorization = cockpit_auth_steal_authorization (headers, connection, &type, &conversation);
if (!application || !authorization)
/* If the client sends a TLS certificate to cockpit-tls, treat this as a
* definitive login type, and don't just silently fall back to other types */
if (https_instance_has_certificate_file (NULL, 0) != -1)
{
g_debug ("TLS connection has peer certificate, using tls-cert auth type");
type = g_strdup ("tls-cert");
/* don't send any actual authorization here; we don't want to put any trust in data sent from cockpit-ws */
authorization = g_strdup ("tls-cert");
}
else
{
g_debug ("No peer certificate");
authorization = cockpit_auth_steal_authorization (headers, connection, &type, &conversation);
if (!authorization)
{
g_simple_async_result_set_error (result, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication required");
g_simple_async_result_complete_in_idle (result);
goto out;
}
}
if (!application)
{
g_simple_async_result_set_error (result, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
"Authentication required");
"Application required");
g_simple_async_result_complete_in_idle (result);
goto out;
}

View File

@ -0,0 +1,212 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "cockpitwsinstancecert.h"
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <regex.h>
#include <stddef.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define CGROUP_REGEX "^(0:|1:name=systemd):/system.slice/system-cockpithttps.slice/" \
"cockpit-wsinstance-https@([0-9a-f]{64}).service$"
#define CGROUP_REGEX_FLAGS (REG_EXTENDED | REG_NEWLINE)
#define CGROUP_REGEX_GROUPS 3 /* number of groups, including the complete match */
#define CGROUP_REGEX_MATCH 2 /* the group which contains the instance */
/* get our cgroup, map it to a systemd unit instance name
* looks like 0::/system.slice/system-cockpit\x2dwsinstance\x2dhttps.slice/cockpit-wsinstance-https@123abc.service
* returns "123abc" instance name (static string)
*/
static const char *
get_ws_https_instance (void)
{
int r;
static char buf[1024];
regmatch_t pmatch[CGROUP_REGEX_GROUPS];
regex_t preg;
int fd;
/* read /proc/self/cgroup */
fd = open ("/proc/self/cgroup", O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (fd < 0)
{
warn ("Failed to open /proc/self/cgroup");
return NULL;
}
do
r = read (fd, buf, sizeof buf);
while (r < 0 && errno == EINTR);
if (r < 0)
{
warn ("Failed to read /proc/self/cgroup");
close (fd);
return NULL;
}
close (fd);
if (r == 0 || r >= sizeof buf)
{
warnx ("Read invalid size %i from /proc/self/cgroup", r);
return NULL;
}
buf[r] = '\0';
/* extract the instance name */
r = regcomp (&preg, CGROUP_REGEX, CGROUP_REGEX_FLAGS);
assert (r == 0);
r = regexec (&preg, buf, CGROUP_REGEX_GROUPS, pmatch, 0);
regfree (&preg);
if (r != 0)
{
/* It's expected that this function will often be called even when
* the client didn't send a certificate, so we shouldn't log about
* that. It might be useful for debugging, though.
*/
// warnx ("Not running in a template cgroup, unable to parse systemd unit instance.\n\n/proc/self/cgroups content follows:\n%s\n", buf);
return NULL;
}
buf[pmatch[CGROUP_REGEX_MATCH].rm_eo] = '\0';
return buf + pmatch[CGROUP_REGEX_MATCH].rm_so;
}
/**
* cockpit_wsinstance_has_certificate_file:
* @contents: an optional buffer to read the certificate into
* @contents_size: the size of @contents
*
* Checks if an active, regular, non-empty https certificate file exists
* for the cgroup of the current wsinstance. This is true if there are
* any active https connections from the client which was responsible
* for this cockpit-ws instance being started.
*
* Optionally, reads the contents of the certificate file into
* @contents (of size @contents_size). The buffer must be large enough
* for the contents of the certificate file, plus a nul terminator
* (which will be added). If @contents is %NULL then no attempt will be
* made to read the file contents, but the other checks are performed.
*
* On sucess, the size of the certificate file (excluding nul
* terminator) is returned. This value is never 0. On error, -1 is
* returned with errno not guaranteed to be set (but a message will be
* logged).
*/
ssize_t
https_instance_has_certificate_file (char *contents,
size_t contents_size)
{
const char *https_instance = get_ws_https_instance ();
int dirfd = -1, filefd = -1;
ssize_t result = -1;
struct stat buf;
ssize_t r;
if (https_instance == NULL) /* already warned */
goto out;
dirfd = open ("/run/cockpit/tls", O_PATH | O_DIRECTORY | O_NOFOLLOW);
if (dirfd == -1)
{
warn ("Failed to open /run/cockpit/tls");
goto out;
}
filefd = openat (dirfd, https_instance, O_RDONLY | O_NOFOLLOW);
if (filefd == -1)
{
warn ("Failed to open certificate file /run/cockpit/tls/%s", https_instance);
goto out;
}
if (fstat (filefd, &buf) != 0)
{
warn ("Failed to stat certificate file /run/cockpit/tls/%s", https_instance);
goto out;
}
if (!S_ISREG (buf.st_mode))
{
warnx ("Could not read certificate: /run/cockpit/tls/%s is not a regular file", https_instance);
goto out;
}
if (buf.st_size == 0)
{
warnx ("Could not read certificate: /run/cockpit/tls/%s is empty", https_instance);
goto out;
}
if (contents != NULL)
{
/* Strictly less than, since we will add a nul */
if (!(buf.st_size < contents_size))
{
warnx ("Insufficient space in read buffer for /run/cockpit/tls/%s", https_instance);
goto out;
}
do
r = pread (filefd, contents, buf.st_size, 0);
while (r == -1 && errno == EINTR);
if (r == -1)
{
warn ("Could not read certificate file /run/cockpit/tls/%s", https_instance);
goto out;
}
if (r != buf.st_size)
{
warnx ("Read incomplete contents of certificate file /run/cockpit/tls/%s: %zu of %zu bytes",
https_instance, r, (size_t) buf.st_size);
goto out;
}
contents[buf.st_size] = '\0';
if (strlen (contents) != buf.st_size)
{
warnx ("Certificate file /run/cockpit/tls/%s contains nul characters", https_instance);
goto out;
}
}
result = buf.st_size;
out:
if (filefd != -1)
close (filefd);
if (dirfd != -1)
close (dirfd);
return result;
}

View File

@ -0,0 +1,26 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <sys/types.h>
ssize_t
https_instance_has_certificate_file (char *contents,
size_t contents_length);

207
src/ws/pam_cockpit_cert.c Normal file
View File

@ -0,0 +1,207 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2019 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
/* Define which PAM interfaces we provide */
#define PAM_SM_AUTH
#include "config.h"
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <syslog.h>
#include <unistd.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <systemd/sd-bus.h>
#include "cockpitwsinstancecert.h"
int enable_debug = 0;
#define debug(format, ...) { if (enable_debug) syslog (LOG_DEBUG, "pam_cockpit_cert: " format, ##__VA_ARGS__); }
#define error(format, ...) syslog (LOG_ERR, "pam_cockpit_cert: " format, ##__VA_ARGS__)
/* This is a bit lame, but having a hard limit on peer certificates is
* desirable: Let's not get DoSed by huge certs */
#define MAX_PEER_CERT_SIZE 100000
/* Parse the module arguments */
static void
parse_args (int argc,
const char **argv)
{
for (int i = 0; i < argc; i++)
{
if (strcmp (argv[i], "debug") == 0)
enable_debug = 1;
else
error ("invalid option: %s", argv[i]);
}
}
static int
sssd_map_certificate (const char *certificate, char** username)
{
int result = PAM_SERVICE_ERR;
sd_bus_error err = SD_BUS_ERROR_NULL;
sd_bus *bus = NULL;
sd_bus_message *user_obj_msg = NULL;
const char *user_obj_path = NULL;
int r;
assert (username);
assert (!*username);
r = sd_bus_open_system (&bus);
if (r < 0)
{
error ("Failed to connect to system bus: %s", strerror (-r));
result = PAM_AUTHINFO_UNAVAIL;
goto out;
}
r = sd_bus_call_method (bus,
"org.freedesktop.sssd.infopipe",
"/org/freedesktop/sssd/infopipe/Users",
"org.freedesktop.sssd.infopipe.Users",
"FindByCertificate",
&err,
&user_obj_msg,
"s",
certificate);
if (r < 0)
{
/* The error name is a bit confusing, and this is the common case; translate to readable error */
if (sd_bus_error_has_name (&err, "sbus.Error.NotFound"))
{
error ("No matching user for certificate");
result = PAM_USER_UNKNOWN;
goto out;
}
error ("Failed to map certificate to user: [%s] %s", err.name, err.message);
result = PAM_AUTHINFO_UNAVAIL;
goto out;
}
assert (user_obj_msg);
r = sd_bus_message_read (user_obj_msg, "o", &user_obj_path);
if (r < 0)
{
error ("Failed to parse response message: %s", strerror (-r));
goto out;
}
debug ("certificate mapped to user object path %s", user_obj_path);
r = sd_bus_get_property_string (bus,
"org.freedesktop.sssd.infopipe",
user_obj_path,
"org.freedesktop.sssd.infopipe.Users.User",
"name",
&err,
username);
if (r < 0)
{
error ("Failed to map user object to name: [%s] %s", err.name, err.message);
goto out;
}
assert (*username);
debug ("mapped certificate to user %s", *username);
result = PAM_SUCCESS;
out:
sd_bus_error_free (&err);
sd_bus_message_unref (user_obj_msg);
sd_bus_unref (bus);
return result;
}
PAM_EXTERN int
pam_sm_authenticate (pam_handle_t *pamh,
int flags,
int argc,
const char **argv)
{
int result = PAM_IGNORE;
int r;
const char *pam_user = NULL;
char cert_pem[MAX_PEER_CERT_SIZE];
char *sssd_user = NULL;
parse_args (argc, argv);
r = pam_get_item (pamh, PAM_USER, (const void**) &pam_user);
if (r != PAM_SUCCESS)
{
error ("couldn't get pam user: %s", pam_strerror (pamh, r));
goto out;
}
/* this PAM module also runs for password auth */
if (pam_user)
{
debug ("user %s is already set, not using client certificate authentication", pam_user);
result = PAM_IGNORE;
goto out;
}
/* read the certificate file from disk */
if (https_instance_has_certificate_file (cert_pem, sizeof cert_pem) < 0)
{
error ("No https instance certificate present");
goto out;
}
/* ask sssd to map cert to a user */
result = sssd_map_certificate (cert_pem, &sssd_user);
debug ("sssd user: %s, result: %s", sssd_user, pam_strerror (pamh, result));
/* sssd_user may be NULL here, which is okay -- we want PAM to know it's an unknown user */
r = pam_set_item (pamh, PAM_USER, sssd_user);
if (r != PAM_SUCCESS)
{
error ("couldn't set pam user: %s", pam_strerror (pamh, r));
result = r;
goto out;
}
out:
free (sssd_user);
return result;
}
PAM_EXTERN int
pam_sm_setcred (pam_handle_t *pamh,
int flags,
int argc,
const char *argv[])
{
return PAM_SUCCESS;
}

View File

@ -521,6 +521,48 @@ out:
return pamh;
}
static int
pam_conv_func_dummy (int num_msg,
const struct pam_message **msg,
struct pam_response **ret_resp,
void *appdata_ptr)
{
/* we don't expect (nor can handle) any actual auth conversation here, but
* PAM sometimes sends messages like "Creating home directory for USER" */
for (int i = 0; i < num_msg; ++i)
debug ("got PAM conversation message, ignoring: %s", msg[i]->msg);
return PAM_CONV_ERR;
}
static pam_handle_t *
perform_tlscert (const char *rhost)
{
struct pam_conv conv = { pam_conv_func_dummy, };
pam_handle_t *pamh;
int res;
debug ("start tls-cert authentication for cockpit-ws %u", getppid ());
/* pam_cockpit_cert sets the user name from the certificate */
res = pam_start ("cockpit", NULL, &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 = pam_authenticate (pamh, 0);
if (res == PAM_SUCCESS)
res = open_session (pamh);
/* Our exit code is a PAM code */
if (res != PAM_SUCCESS)
exit_init_problem (res);
return pamh;
}
static int
session (char **env)
{
@ -648,6 +690,8 @@ main (int argc,
pamh = perform_basic (rhost, authorization);
else if (strcmp (type, "negotiate") == 0)
pamh = perform_gssapi (rhost, authorization);
else if (strcmp (type, "tls-cert") == 0)
pamh = perform_tlscert (rhost);
cockpit_memory_clear (authorization, -1);
free (authorization);

View File

@ -19,6 +19,7 @@
import re
import time
import os
import parent
@ -368,6 +369,147 @@ class TestRealms(MachineCase):
self.allow_journal_messages(".*couldn't introspect /org/freedesktop/realmd.*")
@skipImage("RHEL 8.0 does not yet have the necessary SELinux policy updates", "centos-8-stream")
@skipImage("added in PR #11421", "rhel-8-1-distropkg")
def testClientCertAuthentication(self):
m = self.machine
ipa_machine = self.machines['services']
# need to wait for IPA to start up
ipa_machine.execute("""docker exec -i freeipa sh -ec '
while ! echo foobarfoo | kinit -f admin; do sleep 5; done
export LC_ALL=C.UTF-8 && while ! ipa user-find >/dev/null; do sleep 5; done'
""", timeout=300)
# set up an IPA user with a TLS certificate; can't use "admin" due to https://pagure.io/freeipa/issue/6683
ipa_machine.execute("""docker exec -i freeipa sh -exc '
CERTUSER=jane
ipa user-add --first=$CERTUSER --last="developer" $CERTUSER
yes foobar | ipa user-mod --password $CERTUSER
ipa user-mod --password-expiration='2030-01-01T00:00:00Z' $CERTUSER
openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout ${CERTUSER}.key -out ${CERTUSER}.pem -subj "/CN=$CERTUSER"
openssl x509 -outform der -in ${CERTUSER}.pem -out ${CERTUSER}.der
ipa user-add-cert $CERTUSER --certificate="$(base64 ${CERTUSER}.der)"
# for browser import with manual tests
openssl pkcs12 -export -password pass:foo -in ${CERTUSER}.pem -inkey ${CERTUSER}.key -out ${CERTUSER}.p12
'
for s in key p12 pem; do docker cp freeipa:jane.$s .; done
""")
jane_pem = os.path.join(self.tmpdir, "jane.pem")
jane_key = os.path.join(self.tmpdir, "jane.key")
ipa_machine.download("jane.key", jane_key)
ipa_machine.download("jane.pem", jane_pem)
m.upload([jane_key, jane_pem], "/var/tmp")
# On older Debian/Ubuntu D-Bus activation of ifp is disabled, enable it manually; see https://bugs.debian.org/925026
static_ifp_conf = m.image in ["debian-stable", "ubuntu-1804"]
if static_ifp_conf:
m.write("/etc/sssd/conf.d/ifp.conf", "[sssd]\nservices = nss, sudo, pam, ssh, ifp\n")
m.execute("chmod 600 /etc/sssd/conf.d/ifp.conf")
m.execute("hostnamectl set-hostname x0.cockpit.lan")
# Wait for DNS to work as expected.
# https://bugzilla.redhat.com/show_bug.cgi?id=1071356#c11
wait(lambda: m.execute("nslookup -type=SRV _ldap._tcp.cockpit.lan"))
# join domain, wait until it works
m.execute("printf '[cockpit.lan]\\nfully-qualified-names = no\\n' >> /etc/realmd.conf")
m.execute("echo foobarfoo | realm join -vU admin cockpit.lan")
m.execute('while ! id jane; do sleep 5; systemctl restart sssd; done', timeout=300)
# ensure sssd certificate lookup works
user_obj = m.execute('busctl call org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Users '
'org.freedesktop.sssd.infopipe.Users FindByCertificate s -- '
'''"$(cat /var/tmp/jane.pem)" | sed 's/^o "//; s/"$//' ''')
self.assertEqual(m.execute('busctl get-property org.freedesktop.sssd.infopipe ' + user_obj.strip() +
' org.freedesktop.sssd.infopipe.Users.User name').strip(),
's "jane"')
# These tests have to be run with curl, as chromium-headless does not support selecting/handling client-side
# certificates; it just rejects cert requests. For interactive tests, grab jane.p12 from the services VM and import
# it into the browser.
def do_test(authopts, expected, not_expected=[], session_leader=None):
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:
self.assertNotIn(s, output)
if session_leader:
out = m.execute('loginctl show-user --property=Sessions jane')
sessions = out.split('=')[1].split()
self.assertEqual(len(sessions), 1)
out = m.execute('loginctl session-status ' + sessions[0])
self.assertIn(session_leader, out)
self.assertIn('cockpit-bridge', out)
self.assertIn('cockpit; type web', out)
# sessions time out after 10s, but let's not wait for that
m.execute('loginctl terminate-session ' + sessions[0])
m.stop_cockpit()
# cert auth should not be enabled by default
do_test(['--cert', '/var/tmp/jane.pem', '--key', '/var/tmp/jane.key'],
["HTTP/1.1 401 Authentication required", '"authorize"'])
# password auth should work
do_test(['-u', 'jane:foobar'],
['HTTP/1.1 200 OK', '"csrf-token"'],
session_leader='cockpit-session')
# enable cert based auth
m.execute("printf '[WebService]\nClientCertAuthentication = true\n' >> /etc/cockpit/cockpit.conf")
# cert auth should work now
do_test(['--cert', '/var/tmp/jane.pem', '--key', '/var/tmp/jane.key'],
['HTTP/1.1 200 OK', '"csrf-token"'])
# password auth, too
do_test(['-u', 'jane:foobar'],
['HTTP/1.1 200 OK', '"csrf-token"'],
session_leader='cockpit-session')
# cert auth should go through PAM stack and re-create home dir
home_dir = m.execute("getent passwd jane | cut -d: -f6").strip()
m.execute("rm -r " + home_dir)
do_test(['--cert', '/var/tmp/jane.pem', '--key', '/var/tmp/jane.key'],
['HTTP/1.1 200 OK', '"csrf-token"'])
m.execute('test -f %s/.bashrc' % home_dir)
# another certificate gets rejected
m.execute("openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout /tmp/jane2.key -out /tmp/jane2.pem -subj /CN=jane")
do_test(['--cert', '/tmp/jane2.pem', '--key', '/tmp/jane2.key'],
["HTTP/1.1 401 Authentication failed", '<h1>Authentication failed</h1>'],
not_expected=["crsf-token"])
# check expired certificate
m.execute("openssl req -new -key /var/tmp/jane.key -out /tmp/jane.csr -subj /CN=jane")
m.execute("openssl x509 -in /tmp/jane.csr -out /var/tmp/jane-exp.pem -req -signkey /var/tmp/jane.key -days -1")
m.start_cockpit(tls=True)
m.execute('! curl -ksS --cert /var/tmp/jane-exp.pem --key /var/tmp/jane.key https://localhost:9090/cockpit/login')
m.stop_cockpit()
self.allow_journal_messages('.*Invalid TLS peer certificate.* expired')
self.allow_journal_messages('.*TLS handshake failed: Error in the certificate verification.*')
# disallow password auth
m.execute("printf '[Basic]\naction = none\n' >> /etc/cockpit/cockpit.conf")
do_test(['--cert', '/var/tmp/jane.pem', '--key', '/var/tmp/jane.key'],
['HTTP/1.1 200 OK', '"csrf-token"'])
do_test(['-u', 'jane:foobar'],
['HTTP/1.1 401 Authentication disabled', '<h1>Authentication disabled</h1>'],
not_expected=["crsf-token"])
# sssd-dbus not available
if static_ifp_conf:
m.execute("mv /etc/sssd/conf.d/ifp.conf /etc/sssd/conf.d/ifp.conf.disabled && systemctl restart sssd")
else:
m.execute("systemctl mask sssd-ifp && systemctl stop sssd-ifp")
do_test(['--cert', '/var/tmp/jane.pem', '--key', '/var/tmp/jane.key'],
["HTTP/1.1 401 Authentication failed", '<h1>Authentication failed</h1>'],
not_expected=["crsf-token"])
if static_ifp_conf:
m.execute("mv /etc/sssd/conf.d/ifp.conf.disabled /etc/sssd/conf.d/ifp.conf && systemctl restart sssd")
else:
m.execute("systemctl unmask sssd-ifp")
JOIN_SCRIPT = """
set -ex

View File

@ -1,5 +1,8 @@
#%PAM-1.0
auth required pam_sepermit.so
# this MUST be first in the "auth" stack as it sets PAM_USER
# user_unknown is definitive, so die instead of ignore to avoid subsequent modules mess up the error code
-auth [success=done new_authtok_reqd=done user_unknown=die default=ignore] pam_cockpit_cert.so
auth required pam_sepermit.so
auth substack common-auth
auth optional pam_ssh_add.so
account required pam_nologin.so

View File

@ -1,5 +1,8 @@
#%PAM-1.0
auth required pam_sepermit.so
# this MUST be first in the "auth" stack as it sets PAM_USER
# user_unknown is definitive, so die instead of ignore to avoid subsequent modules mess up the error code
-auth [success=done new_authtok_reqd=done user_unknown=die default=ignore] pam_cockpit_cert.so
auth required pam_sepermit.so
auth substack password-auth
auth include postlogin
auth optional pam_ssh_add.so

View File

@ -395,6 +395,7 @@ Conflicts: firewalld < 0.6.0-1
Recommends: sscg >= 2.3
Recommends: system-logos
Requires: systemd >= 235
Suggests: sssd-dbus
Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd
@ -411,12 +412,16 @@ Requires(post): policycoreutils
%description ws
The Cockpit Web Service listens on the network, and authenticates users.
If sssd-dbus is installed, you can enable client certificate/smart card
authentication via sssd/FreeIPA.
%files ws -f cockpit.lang
%doc %{_mandir}/man1/cockpit-desktop.1.gz
%doc %{_mandir}/man5/cockpit.conf.5.gz
%doc %{_mandir}/man8/cockpit-ws.8.gz
%doc %{_mandir}/man8/cockpit-tls.8.gz
%doc %{_mandir}/man8/remotectl.8.gz
%doc %{_mandir}/man8/pam_cockpit_cert.8.gz
%doc %{_mandir}/man8/pam_ssh_add.8.gz
%config(noreplace) %{_sysconfdir}/cockpit/ws-certs.d
%config(noreplace) %{_sysconfdir}/pam.d/cockpit
@ -439,6 +444,7 @@ The Cockpit Web Service listens on the network, and authenticates users.
%{_prefix}/%{__lib}/tmpfiles.d/cockpit-tempfiles.conf
%{_sbindir}/remotectl
%{_libdir}/security/pam_ssh_add.so
%{_libdir}/security/pam_cockpit_cert.so
%{_libexecdir}/cockpit-ws
%{_libexecdir}/cockpit-wsinstance-factory
%{_libexecdir}/cockpit-tls
@ -471,12 +477,18 @@ module local 1.0;
require {
type cockpit_ws_t;
type cockpit_ws_exec_t;
type cockpit_session_t;
type cockpit_var_run_t;
class unix_stream_socket { create_stream_socket_perms connectto };
class file { execute_no_trans};
class file { open read map getattr execute_no_trans};
class dir { getattr search open read };
}
allow cockpit_ws_t cockpit_ws_t:unix_stream_socket { create_stream_socket_perms connectto };
allow cockpit_ws_t cockpit_ws_exec_t:file { execute_no_trans };
# https://github.com/fedora-selinux/selinux-policy-contrib/pull/130
allow cockpit_session_t cockpit_var_run_t:file { open read map getattr };
EOF
checkmodule -M -m -o $tmp/local.mod $tmp/local.te
semodule_package -o $tmp/local.pp -m $tmp/local.mod

View File

@ -15,6 +15,7 @@ lib/systemd/system/cockpit-wsinstance-https@.service
lib/systemd/system/cockpit-wsinstance-https@.socket
lib/systemd/system/system-cockpithttps.slice
lib/*/security/pam_ssh_add.so
lib/*/security/pam_cockpit_cert.so
usr/lib/tmpfiles.d/cockpit-tempfiles.conf
usr/lib/cockpit/cockpit-session
usr/lib/cockpit/cockpit-ws
@ -31,5 +32,6 @@ usr/share/man/man1/cockpit-desktop.1
usr/share/man/man5/cockpit.conf.5
usr/share/man/man8/cockpit-ws.8
usr/share/man/man8/cockpit-tls.8
usr/share/man/man8/pam_cockpit_cert.8
usr/share/man/man8/pam_ssh_add.8
usr/share/man/man8/remotectl.8

View File

@ -193,9 +193,13 @@ Depends: ${misc:Depends},
openssl,
systemd (>= 235),
Conflicts: ${ws:Conflicts}
Suggests: sssd-dbus
Description: Cockpit Web Service
The Cockpit Web Service listens on the network, and authenticates
users.
.
Install sssd-dbus for supporting client certificate/smart card authentication
via sssd/FreeIPA.
Package: cockpit-sosreport
Architecture: all