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:
parent
a929d27c1d
commit
60c5fb2f40
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
-------
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue