From 221e67040fc47c15b3da2afb09bb48f1e9700fb9 Mon Sep 17 00:00:00 2001 From: felixdoerre Date: Thu, 25 Jun 2020 03:45:44 +0200 Subject: [PATCH] pam: implement a zfs_key pam module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a pam module for automatically loading zfs encryption keys for home datasets. The pam module: - loads a zfs key and mounts the dataset when a session opens. - unmounts the dataset and unloads the key when the session closes. - when the user is logged on and changes the password, the module changes the encryption key. Reviewed-by: Richard Laager Reviewed-by: @jengelh Reviewed-by: Ryan Moeller Reviewed-by: Brian Behlendorf Signed-off-by: Felix Dörre Closes #9886 Closes #9903 --- config/user-pam.m4 | 37 + config/user.m4 | 1 + config/zfs-build.m4 | 2 + configure.ac | 2 + contrib/Makefile.am | 5 +- contrib/pam_zfs_key/Makefile.am | 18 + contrib/pam_zfs_key/pam_zfs_key.c | 741 ++++++++++++++++++ contrib/pam_zfs_key/zfs_key | 13 + rpm/generic/zfs.spec.in | 14 +- tests/runfiles/linux.run | 4 + tests/test-runner/bin/zts-report.py | 1 + tests/zfs-tests/include/commands.cfg | 1 + tests/zfs-tests/tests/functional/Makefile.am | 1 + .../tests/functional/pam/Makefile.am | 7 + .../tests/functional/pam/cleanup.ksh | 32 + .../tests/functional/pam/pam_basic.ksh | 49 ++ .../tests/functional/pam/pam_nounmount.ksh | 51 ++ .../zfs-tests/tests/functional/pam/setup.ksh | 41 + .../tests/functional/pam/utilities.kshlib | 40 + 19 files changed, 1058 insertions(+), 2 deletions(-) create mode 100644 config/user-pam.m4 create mode 100644 contrib/pam_zfs_key/Makefile.am create mode 100644 contrib/pam_zfs_key/pam_zfs_key.c create mode 100644 contrib/pam_zfs_key/zfs_key create mode 100644 tests/zfs-tests/tests/functional/pam/Makefile.am create mode 100755 tests/zfs-tests/tests/functional/pam/cleanup.ksh create mode 100755 tests/zfs-tests/tests/functional/pam/pam_basic.ksh create mode 100755 tests/zfs-tests/tests/functional/pam/pam_nounmount.ksh create mode 100755 tests/zfs-tests/tests/functional/pam/setup.ksh create mode 100644 tests/zfs-tests/tests/functional/pam/utilities.kshlib diff --git a/config/user-pam.m4 b/config/user-pam.m4 new file mode 100644 index 000000000..1d376681d --- /dev/null +++ b/config/user-pam.m4 @@ -0,0 +1,37 @@ +AC_DEFUN([ZFS_AC_CONFIG_USER_PAM], [ + AC_ARG_ENABLE([pam], + AS_HELP_STRING([--enable-pam], + [install pam_zfs_key module [[default: check]]]), + [enable_pam=$enableval], + [enable_pam=check]) + + AC_ARG_WITH(pammoduledir, + AS_HELP_STRING([--with-pammoduledir=DIR], + [install pam module in dir [[$libdir/security]]]), + [pammoduledir="$withval"],[pammoduledir=$libdir/security]) + + AC_ARG_WITH(pamconfigsdir, + AS_HELP_STRING([--with-pamconfigsdir=DIR], + [install pam-config files in dir [[/usr/share/pamconfigs]]]), + [pamconfigsdir="$withval"],[pamconfigsdir=/usr/share/pam-configs]) + + AS_IF([test "x$enable_pam" != "xno"], [ + AC_CHECK_HEADERS([security/pam_modules.h], [ + enable_pam=yes + ], [ + AS_IF([test "x$enable_pam" == "xyes"], [ + AC_MSG_FAILURE([ + *** security/pam_modules.h missing, libpam0g-dev package required + ]) + ],[ + enable_pam=no + ]) + ]) + ]) + AS_IF([test "x$enable_pam" == "xyes"], [ + DEFINE_PAM='--with "pam" --define "_pamconfigsdir $(pamconfigsdir)"' + ]) + AC_SUBST(DEFINE_PAM) + AC_SUBST(pammoduledir) + AC_SUBST(pamconfigsdir) +]) diff --git a/config/user.m4 b/config/user.m4 index b69412fda..c09705bde 100644 --- a/config/user.m4 +++ b/config/user.m4 @@ -17,6 +17,7 @@ AC_DEFUN([ZFS_AC_CONFIG_USER], [ ZFS_AC_CONFIG_USER_LIBUDEV ZFS_AC_CONFIG_USER_LIBSSL ZFS_AC_CONFIG_USER_LIBAIO + ZFS_AC_CONFIG_USER_PAM ZFS_AC_CONFIG_USER_RUNSTATEDIR ZFS_AC_CONFIG_USER_MAKEDEV_IN_SYSMACROS ZFS_AC_CONFIG_USER_MAKEDEV_IN_MKDEV diff --git a/config/zfs-build.m4 b/config/zfs-build.m4 index 016c0fc09..93bef19ff 100644 --- a/config/zfs-build.m4 +++ b/config/zfs-build.m4 @@ -223,6 +223,7 @@ AC_DEFUN([ZFS_AC_CONFIG], [ [test "x$qatsrc" != x ]) AM_CONDITIONAL([WANT_DEVNAME2DEVID], [test "x$user_libudev" = xyes ]) AM_CONDITIONAL([WANT_MMAP_LIBAIO], [test "x$user_libaio" = xyes ]) + AM_CONDITIONAL([PAM_ZFS_ENABLED], [test "x$enable_pam" = xyes]) ]) dnl # @@ -284,6 +285,7 @@ AC_DEFUN([ZFS_AC_RPM], [ RPM_DEFINE_UTIL+=' $(DEFINE_INITRAMFS)' RPM_DEFINE_UTIL+=' $(DEFINE_SYSTEMD)' RPM_DEFINE_UTIL+=' $(DEFINE_PYZFS)' + RPM_DEFINE_UTIL+=' $(DEFINE_PAM)' RPM_DEFINE_UTIL+=' $(DEFINE_PYTHON_VERSION)' RPM_DEFINE_UTIL+=' $(DEFINE_PYTHON_PKG_VERSION)' diff --git a/configure.ac b/configure.ac index 79246833d..a0a2926e5 100644 --- a/configure.ac +++ b/configure.ac @@ -98,6 +98,7 @@ AC_CONFIG_FILES([ contrib/initramfs/hooks/Makefile contrib/initramfs/scripts/Makefile contrib/initramfs/scripts/local-top/Makefile + contrib/pam_zfs_key/Makefile contrib/pyzfs/Makefile contrib/pyzfs/setup.py contrib/zcp/Makefile @@ -351,6 +352,7 @@ AC_CONFIG_FILES([ tests/zfs-tests/tests/functional/no_space/Makefile tests/zfs-tests/tests/functional/nopwrite/Makefile tests/zfs-tests/tests/functional/online_offline/Makefile + tests/zfs-tests/tests/functional/pam/Makefile tests/zfs-tests/tests/functional/persist_l2arc/Makefile tests/zfs-tests/tests/functional/pool_checkpoint/Makefile tests/zfs-tests/tests/functional/pool_names/Makefile diff --git a/contrib/Makefile.am b/contrib/Makefile.am index 1486b28d3..9547878d0 100644 --- a/contrib/Makefile.am +++ b/contrib/Makefile.am @@ -2,4 +2,7 @@ SUBDIRS = bash_completion.d pyzfs zcp if BUILD_LINUX SUBDIRS += bpftrace dracut initramfs endif -DIST_SUBDIRS = bash_completion.d bpftrace dracut initramfs pyzfs zcp +if PAM_ZFS_ENABLED +SUBDIRS += pam_zfs_key +endif +DIST_SUBDIRS = bash_completion.d bpftrace dracut initramfs pam_zfs_key pyzfs zcp diff --git a/contrib/pam_zfs_key/Makefile.am b/contrib/pam_zfs_key/Makefile.am new file mode 100644 index 000000000..0f038bb78 --- /dev/null +++ b/contrib/pam_zfs_key/Makefile.am @@ -0,0 +1,18 @@ +include $(top_srcdir)/config/Rules.am + +pammodule_LTLIBRARIES=pam_zfs_key.la + +pam_zfs_key_la_SOURCES = pam_zfs_key.c + +pam_zfs_key_la_LIBADD = \ + $(top_builddir)/lib/libnvpair/libnvpair.la \ + $(top_builddir)/lib/libuutil/libuutil.la \ + $(top_builddir)/lib/libzfs/libzfs.la \ + $(top_builddir)/lib/libzfs_core/libzfs_core.la + +pam_zfs_key_la_LDFLAGS = -version-info 1:0:0 -avoid-version -module -shared + +pam_zfs_key_la_LIBADD += -lpam $(LIBSSL) + +pamconfigs_DATA = zfs_key +EXTRA_DIST = $(pamconfigs_DATA) diff --git a/contrib/pam_zfs_key/pam_zfs_key.c b/contrib/pam_zfs_key/pam_zfs_key.c new file mode 100644 index 000000000..0a96f19a3 --- /dev/null +++ b/contrib/pam_zfs_key/pam_zfs_key.c @@ -0,0 +1,741 @@ +/* + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * Copyright (c) 2020, Felix Dörre + * All rights reserved. + */ + +#include +#include +#include + +#include + +#include +#include + +#define PAM_SM_AUTH +#define PAM_SM_PASSWORD +#define PAM_SM_SESSION +#include + +#if defined(__linux__) +#include +#elif defined(__FreeBSD__) +#include +static void +pam_syslog(pam_handle_t *pamh, int loglevel, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + vsyslog(loglevel, fmt, args); + va_end(args); +} +#endif + +#include + +#include +#include +#include +#include +#include + +#include + +static const char PASSWORD_VAR_NAME[] = "pam_zfs_key_authtok"; + +static libzfs_handle_t *g_zfs; + +static void destroy_pw(pam_handle_t *pamh, void *data, int errcode); + +typedef struct { + size_t len; + char *value; +} pw_password_t; + +static pw_password_t * +alloc_pw_size(size_t len) +{ + pw_password_t *pw = malloc(sizeof (pw_password_t)); + if (!pw) { + return (NULL); + } + pw->len = len; + pw->value = malloc(len); + if (!pw->value) { + free(pw); + return (NULL); + } + mlock(pw->value, pw->len); + return (pw); +} + +static pw_password_t * +alloc_pw_string(const char *source) +{ + pw_password_t *pw = malloc(sizeof (pw_password_t)); + if (!pw) { + return (NULL); + } + pw->len = strlen(source) + 1; + pw->value = malloc(pw->len); + if (!pw->value) { + free(pw); + return (NULL); + } + mlock(pw->value, pw->len); + memcpy(pw->value, source, pw->len); + return (pw); +} + +static void +pw_free(pw_password_t *pw) +{ + bzero(pw->value, pw->len); + munlock(pw->value, pw->len); + free(pw->value); + free(pw); +} + +static pw_password_t * +pw_fetch(pam_handle_t *pamh) +{ + const char *token; + if (pam_get_authtok(pamh, PAM_AUTHTOK, &token, NULL) != PAM_SUCCESS) { + pam_syslog(pamh, LOG_ERR, + "couldn't get password from PAM stack"); + return (NULL); + } + if (!token) { + pam_syslog(pamh, LOG_ERR, + "token from PAM stack is null"); + return (NULL); + } + return (alloc_pw_string(token)); +} + +static const pw_password_t * +pw_fetch_lazy(pam_handle_t *pamh) +{ + pw_password_t *pw = pw_fetch(pamh); + if (pw == NULL) { + return (NULL); + } + int ret = pam_set_data(pamh, PASSWORD_VAR_NAME, pw, destroy_pw); + if (ret != PAM_SUCCESS) { + pw_free(pw); + pam_syslog(pamh, LOG_ERR, "pam_set_data failed"); + return (NULL); + } + return (pw); +} + +static const pw_password_t * +pw_get(pam_handle_t *pamh) +{ + const pw_password_t *authtok = NULL; + int ret = pam_get_data(pamh, PASSWORD_VAR_NAME, + (const void**)(&authtok)); + if (ret == PAM_SUCCESS) + return (authtok); + if (ret == PAM_NO_MODULE_DATA) + return (pw_fetch_lazy(pamh)); + pam_syslog(pamh, LOG_ERR, "password not available"); + return (NULL); +} + +static int +pw_clear(pam_handle_t *pamh) +{ + int ret = pam_set_data(pamh, PASSWORD_VAR_NAME, NULL, NULL); + if (ret != PAM_SUCCESS) { + pam_syslog(pamh, LOG_ERR, "clearing password failed"); + return (-1); + } + return (0); +} + +static void +destroy_pw(pam_handle_t *pamh, void *data, int errcode) +{ + if (data != NULL) { + pw_free((pw_password_t *)data); + } +} + +static int +pam_zfs_init(pam_handle_t *pamh) +{ + int error = 0; + if ((g_zfs = libzfs_init()) == NULL) { + error = errno; + pam_syslog(pamh, LOG_ERR, "Zfs initialization error: %s", + libzfs_error_init(error)); + } + return (error); +} + +static void +pam_zfs_free(void) +{ + libzfs_fini(g_zfs); +} + +static pw_password_t * +prepare_passphrase(pam_handle_t *pamh, zfs_handle_t *ds, + const char *passphrase, nvlist_t *nvlist) +{ + pw_password_t *key = alloc_pw_size(WRAPPING_KEY_LEN); + if (!key) { + return (NULL); + } + uint64_t salt; + uint64_t iters; + if (nvlist != NULL) { + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + pw_free(key); + return (NULL); + } + int bytes_read = 0; + char *buf = (char *)&salt; + size_t bytes = sizeof (uint64_t); + while (bytes_read < bytes) { + ssize_t len = read(fd, buf + bytes_read, bytes + - bytes_read); + if (len < 0) { + close(fd); + pw_free(key); + return (NULL); + } + bytes_read += len; + } + close(fd); + + if (nvlist_add_uint64(nvlist, + zfs_prop_to_name(ZFS_PROP_PBKDF2_SALT), salt)) { + pam_syslog(pamh, LOG_ERR, + "failed to add salt to nvlist"); + pw_free(key); + return (NULL); + } + iters = DEFAULT_PBKDF2_ITERATIONS; + if (nvlist_add_uint64(nvlist, zfs_prop_to_name( + ZFS_PROP_PBKDF2_ITERS), iters)) { + pam_syslog(pamh, LOG_ERR, + "failed to add iters to nvlist"); + pw_free(key); + return (NULL); + } + } else { + salt = zfs_prop_get_int(ds, ZFS_PROP_PBKDF2_SALT); + iters = zfs_prop_get_int(ds, ZFS_PROP_PBKDF2_ITERS); + } + + salt = LE_64(salt); + if (!PKCS5_PBKDF2_HMAC_SHA1((char *)passphrase, + strlen(passphrase), (uint8_t *)&salt, + sizeof (uint64_t), iters, WRAPPING_KEY_LEN, + (uint8_t *)key->value)) { + pam_syslog(pamh, LOG_ERR, "pbkdf failed"); + pw_free(key); + return (NULL); + } + return (key); +} + +static int +is_key_loaded(pam_handle_t *pamh, const char *ds_name) +{ + zfs_handle_t *ds = zfs_open(g_zfs, ds_name, ZFS_TYPE_FILESYSTEM); + if (ds == NULL) { + pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); + return (-1); + } + int keystatus = zfs_prop_get_int(ds, ZFS_PROP_KEYSTATUS); + zfs_close(ds); + return (keystatus != ZFS_KEYSTATUS_UNAVAILABLE); +} + +static int +change_key(pam_handle_t *pamh, const char *ds_name, + const char *passphrase) +{ + zfs_handle_t *ds = zfs_open(g_zfs, ds_name, ZFS_TYPE_FILESYSTEM); + if (ds == NULL) { + pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); + return (-1); + } + nvlist_t *nvlist = fnvlist_alloc(); + pw_password_t *key = prepare_passphrase(pamh, ds, passphrase, nvlist); + if (key == NULL) { + nvlist_free(nvlist); + zfs_close(ds); + return (-1); + } + if (nvlist_add_string(nvlist, + zfs_prop_to_name(ZFS_PROP_KEYLOCATION), + "prompt")) { + pam_syslog(pamh, LOG_ERR, "nvlist_add failed for keylocation"); + pw_free(key); + nvlist_free(nvlist); + zfs_close(ds); + return (-1); + } + if (nvlist_add_uint64(nvlist, + zfs_prop_to_name(ZFS_PROP_KEYFORMAT), + ZFS_KEYFORMAT_PASSPHRASE)) { + pam_syslog(pamh, LOG_ERR, "nvlist_add failed for keyformat"); + pw_free(key); + nvlist_free(nvlist); + zfs_close(ds); + return (-1); + } + int ret = lzc_change_key(ds_name, DCP_CMD_NEW_KEY, nvlist, + (uint8_t *)key->value, WRAPPING_KEY_LEN); + pw_free(key); + if (ret) { + pam_syslog(pamh, LOG_ERR, "change_key failed: %d", ret); + nvlist_free(nvlist); + zfs_close(ds); + return (-1); + } + nvlist_free(nvlist); + zfs_close(ds); + return (0); +} + +static int +decrypt_mount(pam_handle_t *pamh, const char *ds_name, + const char *passphrase) +{ + zfs_handle_t *ds = zfs_open(g_zfs, ds_name, ZFS_TYPE_FILESYSTEM); + if (ds == NULL) { + pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); + return (-1); + } + pw_password_t *key = prepare_passphrase(pamh, ds, passphrase, NULL); + if (key == NULL) { + zfs_close(ds); + return (-1); + } + int ret = lzc_load_key(ds_name, B_FALSE, (uint8_t *)key->value, + WRAPPING_KEY_LEN); + pw_free(key); + if (ret) { + pam_syslog(pamh, LOG_ERR, "load_key failed: %d", ret); + zfs_close(ds); + return (-1); + } + ret = zfs_mount(ds, NULL, 0); + if (ret) { + pam_syslog(pamh, LOG_ERR, "mount failed: %d", ret); + zfs_close(ds); + return (-1); + } + zfs_close(ds); + return (0); +} + +static int +unmount_unload(pam_handle_t *pamh, const char *ds_name) +{ + zfs_handle_t *ds = zfs_open(g_zfs, ds_name, ZFS_TYPE_FILESYSTEM); + if (ds == NULL) { + pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); + return (-1); + } + int ret = zfs_unmount(ds, NULL, 0); + if (ret) { + pam_syslog(pamh, LOG_ERR, "zfs_unmount failed with: %d", ret); + zfs_close(ds); + return (-1); + } + + ret = lzc_unload_key(ds_name); + if (ret) { + pam_syslog(pamh, LOG_ERR, "unload_key failed with: %d", ret); + zfs_close(ds); + return (-1); + } + zfs_close(ds); + return (0); +} + +typedef struct { + char *homes_prefix; + char *runstatedir; + uid_t uid; + const char *username; + int unmount_and_unload; +} zfs_key_config_t; + +static int +zfs_key_config_load(pam_handle_t *pamh, zfs_key_config_t *config, + int argc, const char **argv) +{ + config->homes_prefix = strdup("rpool/home"); + if (config->homes_prefix == NULL) { + pam_syslog(pamh, LOG_ERR, "strdup failure"); + return (-1); + } + config->runstatedir = strdup(RUNSTATEDIR "/pam_zfs_key"); + if (config->runstatedir == NULL) { + pam_syslog(pamh, LOG_ERR, "strdup failure"); + free(config->homes_prefix); + return (-1); + } + const char *name; + if (pam_get_user(pamh, &name, NULL) != PAM_SUCCESS) { + pam_syslog(pamh, LOG_ERR, + "couldn't get username from PAM stack"); + free(config->runstatedir); + free(config->homes_prefix); + return (-1); + } + struct passwd *entry = getpwnam(name); + if (!entry) { + free(config->runstatedir); + free(config->homes_prefix); + return (-1); + } + config->uid = entry->pw_uid; + config->username = name; + config->unmount_and_unload = 1; + for (int c = 0; c < argc; c++) { + if (strncmp(argv[c], "homes=", 6) == 0) { + free(config->homes_prefix); + config->homes_prefix = strdup(argv[c] + 6); + } else if (strncmp(argv[c], "runstatedir=", 12) == 0) { + free(config->runstatedir); + config->runstatedir = strdup(argv[c] + 12); + } else if (strcmp(argv[c], "nounmount") == 0) { + config->unmount_and_unload = 0; + } + } + return (0); +} + +static void +zfs_key_config_free(zfs_key_config_t *config) +{ + free(config->homes_prefix); +} + +static char * +zfs_key_config_get_dataset(zfs_key_config_t *config) +{ + size_t len = ZFS_MAX_DATASET_NAME_LEN; + size_t total_len = strlen(config->homes_prefix) + 1 + + strlen(config->username); + if (total_len > len) { + return (NULL); + } + char *ret = malloc(len + 1); + if (!ret) { + return (NULL); + } + ret[0] = 0; + strcat(ret, config->homes_prefix); + strcat(ret, "/"); + strcat(ret, config->username); + return (ret); +} + +static int +zfs_key_config_modify_session_counter(pam_handle_t *pamh, + zfs_key_config_t *config, int delta) +{ + const char *runtime_path = config->runstatedir; + if (mkdir(runtime_path, S_IRWXU) != 0 && errno != EEXIST) { + pam_syslog(pamh, LOG_ERR, "Can't create runtime path: %d", + errno); + return (-1); + } + if (chown(runtime_path, 0, 0) != 0) { + pam_syslog(pamh, LOG_ERR, "Can't chown runtime path: %d", + errno); + return (-1); + } + if (chmod(runtime_path, S_IRWXU) != 0) { + pam_syslog(pamh, LOG_ERR, "Can't chmod runtime path: %d", + errno); + return (-1); + } + size_t runtime_path_len = strlen(runtime_path); + size_t counter_path_len = runtime_path_len + 1 + 10; + char *counter_path = malloc(counter_path_len + 1); + if (!counter_path) { + return (-1); + } + counter_path[0] = 0; + strcat(counter_path, runtime_path); + snprintf(counter_path + runtime_path_len, counter_path_len, "/%d", + config->uid); + const int fd = open(counter_path, + O_RDWR | O_CLOEXEC | O_CREAT | O_NOFOLLOW, + S_IRUSR | S_IWUSR); + free(counter_path); + if (fd < 0) { + pam_syslog(pamh, LOG_ERR, "Can't open counter file: %d", errno); + return (-1); + } + if (flock(fd, LOCK_EX) != 0) { + pam_syslog(pamh, LOG_ERR, "Can't lock counter file: %d", errno); + close(fd); + return (-1); + } + char counter[20]; + char *pos = counter; + int remaining = sizeof (counter) - 1; + int ret; + counter[sizeof (counter) - 1] = 0; + while (remaining > 0 && (ret = read(fd, pos, remaining)) > 0) { + remaining -= ret; + pos += ret; + } + *pos = 0; + long int counter_value = strtol(counter, NULL, 10); + counter_value += delta; + if (counter_value < 0) { + counter_value = 0; + } + lseek(fd, 0, SEEK_SET); + if (ftruncate(fd, 0) != 0) { + pam_syslog(pamh, LOG_ERR, "Can't truncate counter file: %d", + errno); + close(fd); + return (-1); + } + snprintf(counter, sizeof (counter), "%ld", counter_value); + remaining = strlen(counter); + pos = counter; + while (remaining > 0 && (ret = write(fd, pos, remaining)) > 0) { + remaining -= ret; + pos += ret; + } + close(fd); + return (counter_value); +} + +__attribute__((visibility("default"))) +PAM_EXTERN int +pam_sm_authenticate(pam_handle_t *pamh, int flags, + int argc, const char **argv) +{ + if (pw_fetch_lazy(pamh) == NULL) { + return (PAM_AUTH_ERR); + } + + return (PAM_SUCCESS); +} + +__attribute__((visibility("default"))) +PAM_EXTERN int +pam_sm_setcred(pam_handle_t *pamh, int flags, + int argc, const char **argv) +{ + return (PAM_SUCCESS); +} + +__attribute__((visibility("default"))) +PAM_EXTERN int +pam_sm_chauthtok(pam_handle_t *pamh, int flags, + int argc, const char **argv) +{ + if (geteuid() != 0) { + pam_syslog(pamh, LOG_ERR, + "Cannot zfs_mount when not being root."); + return (PAM_PERM_DENIED); + } + zfs_key_config_t config; + if (zfs_key_config_load(pamh, &config, argc, argv) == -1) { + return (PAM_SERVICE_ERR); + } + if (config.uid < 1000) { + zfs_key_config_free(&config); + return (PAM_SUCCESS); + } + { + if (pam_zfs_init(pamh) != 0) { + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + char *dataset = zfs_key_config_get_dataset(&config); + if (!dataset) { + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + int key_loaded = is_key_loaded(pamh, dataset); + if (key_loaded == -1) { + free(dataset); + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + free(dataset); + pam_zfs_free(); + if (! key_loaded) { + pam_syslog(pamh, LOG_ERR, + "key not loaded, returning try_again"); + zfs_key_config_free(&config); + return (PAM_PERM_DENIED); + } + } + + if ((flags & PAM_UPDATE_AUTHTOK) != 0) { + const pw_password_t *token = pw_get(pamh); + if (token == NULL) { + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + if (pam_zfs_init(pamh) != 0) { + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + char *dataset = zfs_key_config_get_dataset(&config); + if (!dataset) { + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + if (change_key(pamh, dataset, token->value) == -1) { + free(dataset); + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + free(dataset); + pam_zfs_free(); + zfs_key_config_free(&config); + if (pw_clear(pamh) == -1) { + return (PAM_SERVICE_ERR); + } + } else { + zfs_key_config_free(&config); + } + return (PAM_SUCCESS); +} + +PAM_EXTERN int +pam_sm_open_session(pam_handle_t *pamh, int flags, + int argc, const char **argv) +{ + if (geteuid() != 0) { + pam_syslog(pamh, LOG_ERR, + "Cannot zfs_mount when not being root."); + return (PAM_SUCCESS); + } + zfs_key_config_t config; + zfs_key_config_load(pamh, &config, argc, argv); + if (config.uid < 1000) { + zfs_key_config_free(&config); + return (PAM_SUCCESS); + } + + int counter = zfs_key_config_modify_session_counter(pamh, &config, 1); + if (counter != 1) { + zfs_key_config_free(&config); + return (PAM_SUCCESS); + } + + const pw_password_t *token = pw_get(pamh); + if (token == NULL) { + zfs_key_config_free(&config); + return (PAM_SESSION_ERR); + } + if (pam_zfs_init(pamh) != 0) { + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + char *dataset = zfs_key_config_get_dataset(&config); + if (!dataset) { + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + if (decrypt_mount(pamh, dataset, token->value) == -1) { + free(dataset); + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + free(dataset); + pam_zfs_free(); + zfs_key_config_free(&config); + if (pw_clear(pamh) == -1) { + return (PAM_SERVICE_ERR); + } + return (PAM_SUCCESS); + +} + +__attribute__((visibility("default"))) +PAM_EXTERN int +pam_sm_close_session(pam_handle_t *pamh, int flags, + int argc, const char **argv) +{ + if (geteuid() != 0) { + pam_syslog(pamh, LOG_ERR, + "Cannot zfs_mount when not being root."); + return (PAM_SUCCESS); + } + zfs_key_config_t config; + zfs_key_config_load(pamh, &config, argc, argv); + if (config.uid < 1000) { + zfs_key_config_free(&config); + return (PAM_SUCCESS); + } + + int counter = zfs_key_config_modify_session_counter(pamh, &config, -1); + if (counter != 0) { + zfs_key_config_free(&config); + return (PAM_SUCCESS); + } + + if (config.unmount_and_unload) { + if (pam_zfs_init(pamh) != 0) { + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + char *dataset = zfs_key_config_get_dataset(&config); + if (!dataset) { + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_SESSION_ERR); + } + if (unmount_unload(pamh, dataset) == -1) { + free(dataset); + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_SESSION_ERR); + } + free(dataset); + pam_zfs_free(); + } + + zfs_key_config_free(&config); + return (PAM_SUCCESS); +} diff --git a/contrib/pam_zfs_key/zfs_key b/contrib/pam_zfs_key/zfs_key new file mode 100644 index 000000000..e3ed5c4f2 --- /dev/null +++ b/contrib/pam_zfs_key/zfs_key @@ -0,0 +1,13 @@ +Name: Unlock zfs datasets for user +Default: yes +Priority: 128 +Auth-Type: Additional +Auth: + optional pam_zfs_key.so +Session-Interactive-Only: yes +Session-Type: Additional +Session: + optional pam_zfs_key.so +Password-Type: Additional +Password: + optional pam_zfs_key.so diff --git a/rpm/generic/zfs.spec.in b/rpm/generic/zfs.spec.in index 704afd781..e972a10ee 100644 --- a/rpm/generic/zfs.spec.in +++ b/rpm/generic/zfs.spec.in @@ -52,6 +52,7 @@ %bcond_with debuginfo %bcond_with asan %bcond_with systemd +%bcond_with pam # Generic enable switch for systemd %if %{with systemd} @@ -329,6 +330,12 @@ image which is ZFS aware. %define pyzfs --disable-pyzfs %endif +%if %{with pam} + %define pam --enable-pam +%else + %define pam --disable-pam +%endif + %setup -q %build @@ -342,7 +349,8 @@ image which is ZFS aware. %{debug} \ %{debuginfo} \ %{asan} \ - %{systemd}\ + %{systemd} \ + --with-pammoduledir=%{_libdir}/security %{pam} \ %{pyzfs} make %{?_smp_mflags} @@ -457,6 +465,10 @@ systemctl --system daemon-reload >/dev/null || true %config(noreplace) %{_sysconfdir}/%{name}/zpool.d/* %config(noreplace) %{_sysconfdir}/%{name}/vdev_id.conf.*.example %attr(440, root, root) %config(noreplace) %{_sysconfdir}/sudoers.d/* +%if %{with pam} +%{_libdir}/security/* +%{_pamconfigsdir}/* +%endif %files -n libzpool2 %{_libdir}/libzpool.so.* diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index a800e6bb8..5b22b7fda 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -128,6 +128,10 @@ tags = ['functional', 'mmp'] tests = ['umount_unlinked_drain'] tags = ['functional', 'mount'] +[tests/functional/pam:Linux] +tests = ['pam_basic', 'pam_nounmount'] +tags = ['functional', 'pam'] + [tests/functional/procfs:Linux] tests = ['procfs_list_basic', 'procfs_list_concurrent_readers', 'procfs_list_stale_read', 'pool_state'] diff --git a/tests/test-runner/bin/zts-report.py b/tests/test-runner/bin/zts-report.py index 767d64d1c..0162248ed 100755 --- a/tests/test-runner/bin/zts-report.py +++ b/tests/test-runner/bin/zts-report.py @@ -239,6 +239,7 @@ maybe = { 'userquota/setup': ['SKIP', exec_reason], 'vdev_zaps/vdev_zaps_004_pos': ['FAIL', '6935'], 'zvol/zvol_ENOSPC/zvol_ENOSPC_001_pos': ['FAIL', '5848'], + 'pam/setup': ['SKIP', "pamtester might be not available"], } if sys.platform.startswith('freebsd'): diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index 7bd691e25..b27b8d5c6 100644 --- a/tests/zfs-tests/include/commands.cfg +++ b/tests/zfs-tests/include/commands.cfg @@ -61,6 +61,7 @@ export SYSTEM_FILES_COMMON='arp net od openssl + pamtester pax pgrep ping diff --git a/tests/zfs-tests/tests/functional/Makefile.am b/tests/zfs-tests/tests/functional/Makefile.am index 2df78d260..24f3e50bb 100644 --- a/tests/zfs-tests/tests/functional/Makefile.am +++ b/tests/zfs-tests/tests/functional/Makefile.am @@ -46,6 +46,7 @@ SUBDIRS = \ no_space \ nopwrite \ online_offline \ + pam \ persist_l2arc \ pool_checkpoint \ pool_names \ diff --git a/tests/zfs-tests/tests/functional/pam/Makefile.am b/tests/zfs-tests/tests/functional/pam/Makefile.am new file mode 100644 index 000000000..4d9ae1708 --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/Makefile.am @@ -0,0 +1,7 @@ +pkgdatadir = $(datadir)/@PACKAGE@/zfs-tests/tests/functional/pam +dist_pkgdata_SCRIPTS = \ + setup.ksh \ + cleanup.ksh \ + pam_basic.ksh \ + pam_nounmount.ksh \ + utilities.kshlib diff --git a/tests/zfs-tests/tests/functional/pam/cleanup.ksh b/tests/zfs-tests/tests/functional/pam/cleanup.ksh new file mode 100755 index 000000000..62131c6d6 --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/cleanup.ksh @@ -0,0 +1,32 @@ +#!/bin/ksh -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/tests/functional/pam/utilities.kshlib + +destroy_pool $TESTPOOL +del_user ${username} +del_group pamtestgroup + +rm -rf "$runstatedir" +for dir in $TESTDIRS; do + rm -rf $dir +done diff --git a/tests/zfs-tests/tests/functional/pam/pam_basic.ksh b/tests/zfs-tests/tests/functional/pam/pam_basic.ksh new file mode 100755 index 000000000..96ac59453 --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/pam_basic.ksh @@ -0,0 +1,49 @@ +#!/bin/ksh -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/tests/functional/pam/utilities.kshlib + +log_mustnot ismounted "$TESTPOOL/pam/${username}" +keystatus unavailable + +genconfig "homes=$TESTPOOL/pam runstatedir=${runstatedir}" +echo "testpass" | pamtester pam_zfs_key_test ${username} open_session +references 1 +log_must ismounted "$TESTPOOL/pam/${username}" +keystatus available + +echo "testpass" | pamtester pam_zfs_key_test ${username} open_session +references 2 +log_must ismounted "$TESTPOOL/pam/${username}" +keystatus available + +log_must pamtester pam_zfs_key_test ${username} close_session +references 1 +log_must ismounted "$TESTPOOL/pam/${username}" +keystatus available + +log_must pamtester pam_zfs_key_test ${username} close_session +references 0 +log_mustnot ismounted "$TESTPOOL/pam/${username}" +keystatus unavailable + +log_pass "done." diff --git a/tests/zfs-tests/tests/functional/pam/pam_nounmount.ksh b/tests/zfs-tests/tests/functional/pam/pam_nounmount.ksh new file mode 100755 index 000000000..8179f398d --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/pam_nounmount.ksh @@ -0,0 +1,51 @@ +#!/bin/ksh -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/tests/functional/pam/utilities.kshlib + +log_mustnot ismounted "$TESTPOOL/pam/${username}" +keystatus unavailable + +genconfig "homes=$TESTPOOL/pam runstatedir=${runstatedir} nounmount" +echo "testpass" | pamtester pam_zfs_key_test ${username} open_session +references 1 +log_must ismounted "$TESTPOOL/pam/${username}" +keystatus available + +echo "testpass" | pamtester pam_zfs_key_test ${username} open_session +references 2 +keystatus available +log_must ismounted "$TESTPOOL/pam/${username}" + +log_must pamtester pam_zfs_key_test ${username} close_session +references 1 +keystatus available +log_must ismounted "$TESTPOOL/pam/${username}" + +log_must pamtester pam_zfs_key_test ${username} close_session +references 0 +keystatus available +log_must ismounted "$TESTPOOL/pam/${username}" +log_must zfs unmount "$TESTPOOL/pam/${username}" +log_must zfs unload-key "$TESTPOOL/pam/${username}" + +log_pass "done." diff --git a/tests/zfs-tests/tests/functional/pam/setup.ksh b/tests/zfs-tests/tests/functional/pam/setup.ksh new file mode 100755 index 000000000..23515a598 --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/setup.ksh @@ -0,0 +1,41 @@ +#!/bin/ksh -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/tests/functional/pam/utilities.kshlib + +if ! which pamtester; then + log_unsupported "pam tests require the pamtester utility to be installed" +fi + +DISK=${DISKS%% *} +create_pool $TESTPOOL "$DISK" + +log_must zfs create -o mountpoint="$TESTDIR" "$TESTPOOL/pam" +log_must add_group pamtestgroup +log_must add_user pamtestgroup ${username} +log_must mkdir -p "$runstatedir" + +echo "testpass" | zfs create -o encryption=aes-256-gcm -o keyformat=passphrase -o keylocation=prompt "$TESTPOOL/pam/${username}" +log_must zfs unmount "$TESTPOOL/pam/${username}" +log_must zfs unload-key "$TESTPOOL/pam/${username}" + +log_pass diff --git a/tests/zfs-tests/tests/functional/pam/utilities.kshlib b/tests/zfs-tests/tests/functional/pam/utilities.kshlib new file mode 100644 index 000000000..35371d14a --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/utilities.kshlib @@ -0,0 +1,40 @@ +#!/bin/ksh -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/include/libtest.shlib + +username="pamTestuser" +runstatedir="${TESTDIR}_run" +function keystatus { + log_must [ "$(zfs list -Ho keystatus "$TESTPOOL/pam/${username}")" == "$1" ] +} + +function genconfig { + for i in password auth session; do + printf "%s\trequired\tpam_permit.so\n%s\toptional\tpam_zfs_key.so\t%s\n" "$i" "$i" "$1" + done > /etc/pam.d/pam_zfs_key_test +} + +function references { + log_must [ "$(cat "${runstatedir}/$(id -u ${username})")" == "$1" ] +} +