summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorfelixdoerre <[email protected]>2020-06-25 03:45:44 +0200
committerGitHub <[email protected]>2020-06-24 18:45:44 -0700
commit221e67040fc47c15b3da2afb09bb48f1e9700fb9 (patch)
tree4d06425fb5abb067990e8b936b2a909f39e08da5
parent75138073208674967d0fb238f1b6210da224db36 (diff)
pam: implement a zfs_key pam module
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 <[email protected]> Reviewed-by: @jengelh <[email protected]> Reviewed-by: Ryan Moeller <[email protected]> Reviewed-by: Brian Behlendorf <[email protected]> Signed-off-by: Felix Dörre <[email protected]> Closes #9886 Closes #9903
-rw-r--r--config/user-pam.m437
-rw-r--r--config/user.m41
-rw-r--r--config/zfs-build.m42
-rw-r--r--configure.ac2
-rw-r--r--contrib/Makefile.am5
-rw-r--r--contrib/pam_zfs_key/Makefile.am18
-rw-r--r--contrib/pam_zfs_key/pam_zfs_key.c741
-rw-r--r--contrib/pam_zfs_key/zfs_key13
-rw-r--r--rpm/generic/zfs.spec.in14
-rw-r--r--tests/runfiles/linux.run4
-rwxr-xr-xtests/test-runner/bin/zts-report.py1
-rw-r--r--tests/zfs-tests/include/commands.cfg1
-rw-r--r--tests/zfs-tests/tests/functional/Makefile.am1
-rw-r--r--tests/zfs-tests/tests/functional/pam/Makefile.am7
-rwxr-xr-xtests/zfs-tests/tests/functional/pam/cleanup.ksh32
-rwxr-xr-xtests/zfs-tests/tests/functional/pam/pam_basic.ksh49
-rwxr-xr-xtests/zfs-tests/tests/functional/pam/pam_nounmount.ksh51
-rwxr-xr-xtests/zfs-tests/tests/functional/pam/setup.ksh41
-rw-r--r--tests/zfs-tests/tests/functional/pam/utilities.kshlib40
19 files changed, 1058 insertions, 2 deletions
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 <organization> 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 <COPYRIGHT HOLDER> 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 <sys/dsl_crypt.h>
+#include <sys/byteorder.h>
+#include <libzfs.h>
+
+#include <syslog.h>
+
+#include <sys/zio_crypt.h>
+#include <openssl/evp.h>
+
+#define PAM_SM_AUTH
+#define PAM_SM_PASSWORD
+#define PAM_SM_SESSION
+#include <security/pam_modules.h>
+
+#if defined(__linux__)
+#include <security/pam_ext.h>
+#elif defined(__FreeBSD__)
+#include <security/pam_appl.h>
+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 <string.h>
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/file.h>
+#include <sys/wait.h>
+#include <pwd.h>
+
+#include <sys/mman.h>
+
+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" ]
+}
+