From 756feccb5886b458ceaba10404786cf38560656e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jerzy=20Ko=C5=82osowski?= Date: Thu, 12 Dec 2024 05:26:45 +0100 Subject: [PATCH] Add recursive dataset mounting support to pam_zfs_key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced functionality to recursively mount datasets with a new config option `mount_recursively`. Adjusted existing functions to handle the recursive behavior and added tests to validate the feature. This enhances support for managing hierarchical ZFS datasets within a PAM context. Signed-off-by: Jerzy KoĊ‚osowski --- contrib/pam_zfs_key/pam_zfs_key.c | 190 +++++++++++++----- .../functional/pam/pam_mount_recursively.ksh | 82 ++++++++ 2 files changed, 225 insertions(+), 47 deletions(-) create mode 100755 tests/zfs-tests/tests/functional/pam/pam_mount_recursively.ksh diff --git a/contrib/pam_zfs_key/pam_zfs_key.c b/contrib/pam_zfs_key/pam_zfs_key.c index 08a8640669b3..4cd2fad4f724 100644 --- a/contrib/pam_zfs_key/pam_zfs_key.c +++ b/contrib/pam_zfs_key/pam_zfs_key.c @@ -63,6 +63,7 @@ pam_syslog(pam_handle_t *pamh, int loglevel, const char *fmt, ...) #include #include #include +#include #include @@ -370,42 +371,6 @@ change_key(pam_handle_t *pamh, const char *ds_name, return (0); } -static int -decrypt_mount(pam_handle_t *pamh, const char *ds_name, - const char *passphrase, boolean_t noop) -{ - 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, noop, (uint8_t *)key->value, - WRAPPING_KEY_LEN); - pw_free(key); - if (ret && ret != EEXIST) { - pam_syslog(pamh, LOG_ERR, "load_key failed: %d", ret); - zfs_close(ds); - return (-1); - } - if (noop) { - goto out; - } - ret = zfs_mount(ds, NULL, 0); - if (ret) { - pam_syslog(pamh, LOG_ERR, "mount failed: %d", ret); - zfs_close(ds); - return (-1); - } -out: - zfs_close(ds); - return (0); -} - static int unmount_unload(pam_handle_t *pamh, const char *ds_name, boolean_t force) { @@ -443,6 +408,7 @@ typedef struct { boolean_t unmount_and_unload; boolean_t force_unmount; boolean_t recursive_homes; + boolean_t mount_recursively; } zfs_key_config_t; static int @@ -481,6 +447,7 @@ zfs_key_config_load(pam_handle_t *pamh, zfs_key_config_t *config, config->unmount_and_unload = B_TRUE; config->force_unmount = B_FALSE; config->recursive_homes = B_FALSE; + config->mount_recursively = B_FALSE; config->dsname = NULL; config->homedir = NULL; for (int c = 0; c < argc; c++) { @@ -500,6 +467,8 @@ zfs_key_config_load(pam_handle_t *pamh, zfs_key_config_t *config, config->force_unmount = B_TRUE; } else if (strcmp(argv[c], "recursive_homes") == 0) { config->recursive_homes = B_TRUE; + } else if (strcmp(argv[c], "mount_recursively") == 0) { + config->mount_recursively = B_TRUE; } else if (strcmp(argv[c], "prop_mountpoint") == 0) { if (config->homedir == NULL) config->homedir = strdup(entry->pw_dir); @@ -508,6 +477,132 @@ zfs_key_config_load(pam_handle_t *pamh, zfs_key_config_t *config, return (PAM_SUCCESS); } +typedef struct { + pam_handle_t *pamh; + zfs_key_config_t *target; +} mount_dataset_data_t; + +static int +mount_dataset(zfs_handle_t *zhp, void *void_data) +{ + mount_dataset_data_t *data = void_data; + + zfs_key_config_t *target = data->target; + pam_handle_t *pamh = data->pamh; + + /* Refresh properties to get the latest key status */ + zfs_refresh_properties(zhp); + + int ret = 0; + + /* Check if dataset type is filesystem */ + if (zhp->zfs_type != ZFS_TYPE_FILESYSTEM) { + pam_syslog(pamh, LOG_DEBUG, + "dataset is not filesystem: %s. Skipping.", + zfs_get_name(zhp)); + return (0); + } + + /* Check if encryption key is available */ + if (zfs_prop_get_int(zhp, ZFS_PROP_KEYSTATUS) == + ZFS_KEYSTATUS_UNAVAILABLE) { + pam_syslog(pamh, LOG_WARNING, + "key unavailable for: %s. Skipping.", + zfs_get_name(zhp)); + return (0); + } + + /* Check if prop canmount is on */ + if (zfs_prop_get_int(zhp, ZFS_PROP_CANMOUNT) != ZFS_CANMOUNT_ON) { + pam_syslog(pamh, LOG_INFO, + "canmount is not on for: %s. Skipping.", + zfs_get_name(zhp)); + return (0); // Skip dataset + } + + /* Get mountpoint prop for check */ + char mountpoint[ZFS_MAXPROPLEN]; + if ((ret = zfs_prop_get(zhp, ZFS_PROP_MOUNTPOINT, mountpoint, + sizeof (mountpoint), NULL, NULL, 0, 1)) != 0) { + pam_syslog(pamh, LOG_ERR, + "failed to get mountpoint prop: %d", ret); + return (-1); + } + + /* Check if mountpoint isn't none or legacy */ + if (strcmp(mountpoint, ZFS_MOUNTPOINT_NONE) == 0 || + strcmp(mountpoint, ZFS_MOUNTPOINT_LEGACY) == 0) { + pam_syslog(pamh, LOG_INFO, + "mountpoint is none or legacy for: %s. Skipping.", + zfs_get_name(zhp)); + return (0); // Skip dataset + } + + /* Mount the dataset (if not already mounted) */ + if (zfs_is_mounted(zhp, NULL)) { + pam_syslog(pamh, LOG_INFO, "already mounted: %s", + zfs_get_name(zhp)); + } else if ((ret = zfs_mount(zhp, NULL, 0)) != 0) { + pam_syslog(pamh, LOG_ERR, "mount failed: %d", ret); + return (-1); + } + + /* Recursively mount children if the recursive flag is set */ + if (target->mount_recursively) { + ret = zfs_iter_filesystems_v2(zhp, 0, mount_dataset, data); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, + "child iteration failed: %d", ret); + return (-1); + } + } + + return (ret); +} + +static int +decrypt_mount(pam_handle_t *pamh, zfs_key_config_t *config, const char *ds_name, + const char *passphrase, boolean_t noop) +{ + 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, noop, (uint8_t *)key->value, + WRAPPING_KEY_LEN); + pw_free(key); + if (ret && ret != EEXIST) { + pam_syslog(pamh, LOG_ERR, "load_key failed: %d", ret); + zfs_close(ds); + return (-1); + } + + if (noop) { + zfs_close(ds); + return (0); + } + + mount_dataset_data_t data; + data.pamh = pamh; + data.target = config; + + ret = mount_dataset(ds, &data); + if (ret != 0) { + pam_syslog(pamh, LOG_ERR, "mount failed: %d", ret); + zfs_close(ds); + return (-1); + } + + zfs_close(ds); + return (0); +} + static void zfs_key_config_free(zfs_key_config_t *config) { @@ -548,7 +643,7 @@ find_dsname_by_prop_value(zfs_handle_t *zhp, void *data) } static char * -zfs_key_config_get_dataset(zfs_key_config_t *config) +zfs_key_config_get_dataset(pam_handle_t *pamh, zfs_key_config_t *config) { if (config->homedir != NULL && config->homes_prefix != NULL) { @@ -559,7 +654,7 @@ zfs_key_config_get_dataset(zfs_key_config_t *config) zfs_handle_t *zhp = zfs_open(g_zfs, config->homes_prefix, ZFS_TYPE_FILESYSTEM); if (zhp == NULL) { - pam_syslog(NULL, LOG_ERR, + pam_syslog(pamh, LOG_ERR, "dataset %s not found", config->homes_prefix); return (NULL); @@ -697,13 +792,13 @@ pam_sm_authenticate(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - if (decrypt_mount(pamh, dataset, token->value, B_TRUE) == -1) { + if (decrypt_mount(pamh, &config, dataset, token->value, B_TRUE) == -1) { free(dataset); pam_zfs_free(); zfs_key_config_free(&config); @@ -749,7 +844,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); @@ -763,7 +858,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - if (decrypt_mount(pamh, dataset, + if (decrypt_mount(pamh, &config, dataset, old_token->value, B_TRUE) == -1) { pam_syslog(pamh, LOG_ERR, "old token mismatch"); @@ -784,7 +879,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, pw_clear(pamh, OLD_PASSWORD_VAR_NAME); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); @@ -793,7 +888,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, return (PAM_SERVICE_ERR); } int was_loaded = is_key_loaded(pamh, dataset); - if (!was_loaded && decrypt_mount(pamh, dataset, + if (!was_loaded && decrypt_mount(pamh, &config, dataset, old_token->value, B_FALSE) == -1) { free(dataset); pam_zfs_free(); @@ -856,13 +951,14 @@ pam_sm_open_session(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - if (decrypt_mount(pamh, dataset, token->value, B_FALSE) == -1) { + if (decrypt_mount(pamh, &config, dataset, + token->value, B_FALSE) == -1) { free(dataset); pam_zfs_free(); zfs_key_config_free(&config); @@ -910,7 +1006,7 @@ pam_sm_close_session(pam_handle_t *pamh, int flags, zfs_key_config_free(&config); return (PAM_SERVICE_ERR); } - char *dataset = zfs_key_config_get_dataset(&config); + char *dataset = zfs_key_config_get_dataset(pamh, &config); if (!dataset) { pam_zfs_free(); zfs_key_config_free(&config); diff --git a/tests/zfs-tests/tests/functional/pam/pam_mount_recursively.ksh b/tests/zfs-tests/tests/functional/pam/pam_mount_recursively.ksh new file mode 100755 index 000000000000..d2682dc6d249 --- /dev/null +++ b/tests/zfs-tests/tests/functional/pam/pam_mount_recursively.ksh @@ -0,0 +1,82 @@ +#!/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 https://opensource.org/licenses/CDDL-1.0. +# 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 [ -n "$ASAN_OPTIONS" ]; then + export LD_PRELOAD=$(ldd "$(command -v zfs)" | awk '/libasan\.so/ {print $3}') +fi + +username="${username}rec" + +# Set up a deeper hierarchy, a mountpoint that doesn't interfere with other tests, +# and a user which references that mountpoint +log_must zfs create "$TESTPOOL/pampam" +log_must zfs create -o mountpoint="$TESTDIR/rec" "$TESTPOOL/pampam/pam" +echo "recurpass" | zfs create -o encryption=aes-256-gcm -o keyformat=passphrase \ + -o keylocation=prompt "$TESTPOOL/pampam/pam/${username}" +log_must zfs create "$TESTPOOL/pampam/pam/${username}/deep" +log_must zfs create "$TESTPOOL/pampam/pam/${username}/deep/deeper" +log_must zfs create -o mountpoint=none "$TESTPOOL/pampam/pam/${username}/deep/none" +log_must zfs create -o canmount=noauto "$TESTPOOL/pampam/pam/${username}/deep/noauto" +log_must zfs create -o canmount=off "$TESTPOOL/pampam/pam/${username}/deep/off" +log_must zfs unmount "$TESTPOOL/pampam/pam/${username}" +log_must zfs unload-key "$TESTPOOL/pampam/pam/${username}" +log_must add_user pamtestgroup ${username} "$TESTDIR/rec" + +function keystatus { + log_must [ "$(get_prop keystatus "$TESTPOOL/pampam/pam/${username}")" = "$1" ] +} + +log_mustnot ismounted "$TESTPOOL/pampam/pam/${username}" +keystatus unavailable + +function test_session { + echo "recurpass" | pamtester ${pamservice} ${username} open_session + references 1 + log_must ismounted "$TESTPOOL/pampam/pam/${username}" + log_must ismounted "$TESTPOOL/pampam/pam/${username}/deep" + log_must ismounted "$TESTPOOL/pampam/pam/${username}/deep/deeper" + log_mustnot ismounted "$TESTPOOL/pampam/pam/${username}/deep/none" + log_mustnot ismounted "$TESTPOOL/pampam/pam/${username}/deep/noauto" + log_mustnot ismounted "$TESTPOOL/pampam/pam/${username}/deep/off" + keystatus available + + log_must pamtester ${pamservice} ${username} close_session + references 0 + log_mustnot ismounted "$TESTPOOL/pampam/pam/${username}" + keystatus unavailable +} + +genconfig "homes=$TESTPOOL/pampam/pam prop_mountpoint mount_recursively runstatedir=${runstatedir}" +test_session + +genconfig "homes=$TESTPOOL/pampam recursive_homes prop_mountpoint mount_recursively runstatedir=${runstatedir}" +test_session + +genconfig "homes=$TESTPOOL recursive_homes prop_mountpoint mount_recursively runstatedir=${runstatedir}" +test_session + +genconfig "homes=* recursive_homes prop_mountpoint mount_recursively runstatedir=${runstatedir}" +test_session + +log_pass "done."