Skip to content

Commit

Permalink
Add recursive dataset mounting support to pam_zfs_key
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
jkolo committed Dec 12, 2024
1 parent e0039c7 commit 15404d0
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 47 deletions.
190 changes: 143 additions & 47 deletions contrib/pam_zfs_key/pam_zfs_key.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pam_syslog(pam_handle_t *pamh, int loglevel, const char *fmt, ...)
#include <sys/file.h>
#include <sys/wait.h>
#include <pwd.h>
#include <lib/libzfs/libzfs_impl.h>

#include <sys/mman.h>

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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++) {
Expand All @@ -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);
Expand All @@ -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)
{
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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");
Expand All @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
82 changes: 82 additions & 0 deletions tests/zfs-tests/tests/functional/pam/pam_mount_recursively.ksh
Original file line number Diff line number Diff line change
@@ -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."

0 comments on commit 15404d0

Please sign in to comment.