diff --git a/config/user-pam.m4 b/config/user-pam.m4 new file mode 100644 index 000000000000..8c2196a5cb5a --- /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 [[/lib/security]]]), + [pammoduledir="$withval"],[pammoduledir=/lib/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='--define "_pam 1" --define "_pammoduledir $(pammoduledir)" --define "_pamconfigsdir $(pamconfigsdir)"' + ]) + AC_SUBST(DEFINE_PAM) + AC_SUBST(pammoduledir) + AC_SUBST(pamconfigsdir) +]) diff --git a/config/user.m4 b/config/user.m4 index b69412fda1e2..c09705bde463 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 016c0fc09537..93bef19ffc80 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 0707384a7333..2c7bb4ff463b 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 diff --git a/contrib/Makefile.am b/contrib/Makefile.am index 1486b28d3cda..d3cfc641cc09 100644 --- a/contrib/Makefile.am +++ b/contrib/Makefile.am @@ -2,4 +2,9 @@ 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 CONFIG_USER +if PAM_ZFS_ENABLED +SUBDIRS += pam_zfs_key +endif +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 000000000000..158783098711 --- /dev/null +++ b/contrib/pam_zfs_key/Makefile.am @@ -0,0 +1,23 @@ +include $(top_srcdir)/config/Rules.am + +VPATH = \ + $(top_srcdir)/module/icp \ + $(top_srcdir)/module/zcommon \ + $(top_srcdir)/lib/libzfs + +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 000000000000..6bd486286ff9 --- /dev/null +++ b/contrib/pam_zfs_key/pam_zfs_key.c @@ -0,0 +1,700 @@ +/* + * 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 + +#include +#include + +#define PAM_SM_AUTH +#define PAM_SM_PASSWORD +#define PAM_SM_SESSION +#include + +#if defined(__linux__) +#include +#elif defined(__FreeBSD__) +#include +void +pam_syslog(pam_handle_t *pamh, int loglevel, const char *str, ...) +{ +} +#endif + +#include + +#include +#include +#include +#include +#include + +#include + +static const char PASSWORD_VAR_NAME[] = "pam_zfs_key_authtok"; + +static libzfs_handle_t *libzfs; + +static void destroy_pw(pam_handle_t *pamh, void *data, int errcode); + +typedef struct { + size_t len; + char *value; +} password; + +static password * +alloc_pw_size(size_t len) +{ + password *pw = malloc(sizeof (password)); + if (!pw) { + return (NULL); + } + pw->len = len; + pw->value = malloc(len); + mlock(pw->value, pw->len); + return (pw); +} + +static password * +alloc_pw_string(const char *source) +{ + password *pw = malloc(sizeof (password)); + 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(password *pw) +{ + bzero(pw->value, pw->len); + munlock(pw->value, pw->len); + free(pw->value); + free(pw); +} + +static password * +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 password * +pw_fetch_lazy(pam_handle_t *pamh) +{ + password *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 password * +pw_get(pam_handle_t *pamh) +{ + const password *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((password*) data); + } +} + +static void +pam_zfs_init(void) +{ + libzfs = libzfs_init(); + libzfs_core_init(); +} + +static void +pam_zfs_free(void) +{ + libzfs_core_fini(); + libzfs_fini(libzfs); +} + +static password * +prepare_passphrase(pam_handle_t *pamh, zfs_handle_t *ds, + const char *passphrase, nvlist_t *nvlist) +{ + password *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(libzfs, 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(libzfs, 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(); + password *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(libzfs, ds_name, ZFS_TYPE_FILESYSTEM); + if (ds == NULL) { + pam_syslog(pamh, LOG_ERR, "dataset %s not found", ds_name); + return (-1); + } + password *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(libzfs, 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; + uid_t uid; + const char *username; + int unmount_and_unload; +} zfs_key_config; + +static int +zfs_key_config_load(pam_handle_t *pamh, zfs_key_config *config, + int argc, const char **argv) +{ + config->homes_prefix = strdup("rpool/home"); + 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->homes_prefix); + return (-1); + } + struct passwd *entry = getpwnam(name); + if (!entry) { + 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 (strcmp(argv[c], "nounmount") == 0) { + config->unmount_and_unload = 0; + } + } + return (0); +} + +static void +zfs_key_config_free(zfs_key_config *config) +{ + free(config->homes_prefix); +} + +static char * +zfs_key_config_get_dataset(zfs_key_config *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 *config, int delta) +{ + const char *runtime_path = RUNSTATEDIR "/pam_zfs_key"; + 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 counter_path_len = strlen(runtime_path) + 1 + 10; + char *counter_path = malloc(counter_path_len + 1); + if (!counter_path) { + return (-1); + } + snprintf(counter_path, counter_path_len, RUNSTATEDIR + "/pam_zfs_key/%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 counterValue = strtol(counter, NULL, 10); + counterValue += delta; + if (counterValue < 0) { + counterValue = 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", counterValue); + remaining = strlen(counter); + pos = counter; + while (remaining > 0 && (ret = write(fd, pos, remaining)) > 0) { + remaining -= ret; + pos += ret; + } + close(fd); + return (counterValue); +} + +__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 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); + } + { + pam_zfs_init(); + 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 password *token = pw_get(pamh); + if (token == NULL) { + zfs_key_config_free(&config); + return (PAM_SERVICE_ERR); + } + pam_zfs_init(); + 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 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 password *token = pw_get(pamh); + if (token == NULL) { + zfs_key_config_free(&config); + return (PAM_SESSION_ERR); + } + pam_zfs_init(); + 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 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) { + pam_zfs_init(); + 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 000000000000..e3ed5c4f2fa7 --- /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 704afd781894..de24e47a60de 100644 --- a/rpm/generic/zfs.spec.in +++ b/rpm/generic/zfs.spec.in @@ -457,6 +457,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 0%{?_pam} +%{_pammoduledir}/* +%{_pamconfigsdir}/* +%endif %files -n libzpool2 %{_libdir}/libzpool.so.* diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run index 01bab0870b05..4ceddbe4e900 100644 --- a/tests/runfiles/common.run +++ b/tests/runfiles/common.run @@ -658,6 +658,10 @@ tests = ['online_offline_001_pos', 'online_offline_002_neg', 'online_offline_003_neg'] tags = ['functional', 'online_offline'] +[tests/functional/pam] +tests = ['001_basic'] +tags = ['functional', 'pam'] + [tests/functional/persist_l2arc] tests = ['persist_l2arc_001_pos', 'persist_l2arc_002_pos', 'persist_l2arc_003_neg', 'persist_l2arc_004_pos', 'persist_l2arc_005_pos', diff --git a/tests/zfs-tests/include/commands.cfg b/tests/zfs-tests/include/commands.cfg index 7bd691e257ae..b27b8d5c6f0f 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/pam/001_basic.ksh b/tests/zfs-tests/tests/functional/pam/001_basic.ksh new file mode 100755 index 000000000000..0e575759bbc3 --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/001_basic.ksh @@ -0,0 +1,99 @@ +#!/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 + +if ! [ -x /usr/bin/pamtester ]; then + log_unsupported "pam tests require the pamtester utility to be installed" +fi + +username="$(id -un)" + +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 "/var/run/pam_zfs_key/$(id -u ${username})")" == "$1" ] +} + +function mounted { + assert $(mount | grep -F "$TESTPOOL/pam/${username} on " | wc -l) == $1 +} + +echo "testpass" | zfs create -o encryption=aes-256-gcm -o keyformat=passphrase -o keylocation=prompt "$TESTPOOL/pam/${username}" +mounted 1 +keystatus available +log_must zfs unmount "$TESTPOOL/pam/${username}" +log_must zfs unload-key "$TESTPOOL/pam/${username}" +mounted 0 +keystatus unavailable + +genconfig "homes=$TESTPOOL/pam" +echo "testpass" | /usr/bin/pamtester pam_zfs_key_test ${username} open_session +references 1 +mounted 1 +keystatus available + +echo "testpass" | /usr/bin/pamtester pam_zfs_key_test ${username} open_session +references 2 +mounted 1 +keystatus available + +log_must /usr/bin/pamtester pam_zfs_key_test ${username} close_session +references 1 +mounted 1 +keystatus available + +log_must /usr/bin/pamtester pam_zfs_key_test ${username} close_session +references 0 +mounted 0 +keystatus unavailable + +genconfig "homes=$TESTPOOL/pam nounmount" +echo "testpass" | /usr/bin/pamtester pam_zfs_key_test ${username} open_session +references 1 +mounted 1 +keystatus available + +echo "testpass" | /usr/bin/pamtester pam_zfs_key_test ${username} open_session +references 2 +keystatus available +mounted 1 + +log_must /usr/bin/pamtester pam_zfs_key_test ${username} close_session +references 1 +keystatus available +mounted 1 + +log_must /usr/bin/pamtester pam_zfs_key_test ${username} close_session +references 0 +keystatus available +mounted 1 + +log_pass "done." 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 000000000000..6fc1758d9389 --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/Makefile.am @@ -0,0 +1,5 @@ +pkgdatadir = $(datadir)/@PACKAGE@/zfs-tests/tests/functional/pam +dist_pkgdata_SCRIPTS = \ + setup.ksh \ + cleanup.ksh \ + 001_basic.ksh 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 000000000000..6b39b6f93bf0 --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/cleanup.ksh @@ -0,0 +1,29 @@ +#!/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 + +log_must destroy_pool $TESTPOOL + +for dir in $TESTDIRS; do + rm -rf $dir +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 000000000000..41513cd6aa5b --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/setup.ksh @@ -0,0 +1,29 @@ +#!/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 + +DISK=${DISKS%% *} +create_pool $TESTPOOL "$DISK" +log_must zfs create -o mountpoint="$TESTDIR" "$TESTPOOL/pam" + +log_pass