diff --git a/contrib/debian/openzfs-libpam-zfs.install b/contrib/debian/openzfs-libpam-zfs.install index bafdebe9bb91..cf934fd9f811 100644 --- a/contrib/debian/openzfs-libpam-zfs.install +++ b/contrib/debian/openzfs-libpam-zfs.install @@ -1,2 +1,3 @@ usr/lib/*/security/pam_zfs_key.so +usr/share/man/man8/pam_zfs_key.8 usr/share/pam-configs/zfs_key diff --git a/contrib/pam_zfs_key/pam_zfs_key.c b/contrib/pam_zfs_key/pam_zfs_key.c index 88698dedabbc..d5513b7a43f0 100644 --- a/contrib/pam_zfs_key/pam_zfs_key.c +++ b/contrib/pam_zfs_key/pam_zfs_key.c @@ -754,6 +754,82 @@ zfs_key_config_get_dataset(pam_handle_t *pamh, zfs_key_config_t *config) return (ret); } +/* + * Callback type for foreach_dataset. + * Returns 0 on success, -1 on failure. + */ +typedef int (*dataset_callback_t)(pam_handle_t *, zfs_key_config_t *, + const char *, void *); + +/* + * Iterate over comma-separated homes prefixes and call callback for each + * existing dataset. Returns number of successful callbacks, or -1 if none + * succeeded. + */ +static int +foreach_dataset(pam_handle_t *pamh, zfs_key_config_t *config, + dataset_callback_t callback, void *data) +{ + if (config->homes_prefix == NULL) + return (-1); + + /* Check if this is a comma-separated list */ + if (strchr(config->homes_prefix, ',') == NULL) { + /* Single home - use existing logic */ + char *dataset = zfs_key_config_get_dataset(pamh, config); + if (dataset == NULL) + return (-1); + int ret = callback(pamh, config, dataset, data); + free(dataset); + return (ret == 0 ? 1 : -1); + } + + /* Multiple homes - parse and iterate */ + pam_syslog(pamh, LOG_DEBUG, + "processing multiple home prefixes: %s", config->homes_prefix); + + char *homes_copy = strdup(config->homes_prefix); + if (homes_copy == NULL) + return (-1); + + char *saved_prefix = config->homes_prefix; + char *saveptr; + char *token = strtok_r(homes_copy, ",", &saveptr); + int success_count = 0; + boolean_t failed = B_FALSE; + + while (token != NULL) { + /* Temporarily set homes_prefix to this single prefix */ + config->homes_prefix = token; + char *dataset = zfs_key_config_get_dataset(pamh, config); + if (dataset != NULL) { + pam_syslog(pamh, LOG_DEBUG, + "processing dataset '%s' for prefix '%s'", + dataset, token); + if (callback(pamh, config, dataset, data) == 0) { + success_count++; + } else { + failed = B_TRUE; + pam_syslog(pamh, LOG_WARNING, + "operation failed for dataset '%s'", + dataset); + } + free(dataset); + } else { + pam_syslog(pamh, LOG_DEBUG, + "no dataset found for prefix '%s', skip", token); + } + token = strtok_r(NULL, ",", &saveptr); + } + + config->homes_prefix = saved_prefix; + free(homes_copy); + pam_syslog(pamh, LOG_DEBUG, + "processed %d datasets, %s", + success_count, failed ? "with failures" : "all successful"); + return (!failed && success_count > 0 ? success_count : -1); +} + static int zfs_key_config_modify_session_counter(pam_handle_t *pamh, zfs_key_config_t *config, int delta) @@ -825,6 +901,15 @@ zfs_key_config_modify_session_counter(pam_handle_t *pamh, return (counter_value); } +/* Callback for authentication - verify password works (noop mode) */ +static int +auth_callback(pam_handle_t *pamh, zfs_key_config_t *config, + const char *dataset, void *data) +{ + const char *passphrase = data; + return (decrypt_mount(pamh, config, dataset, passphrase, B_TRUE)); +} + __attribute__((visibility("default"))) PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, @@ -857,21 +942,14 @@ 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(pamh, &config); - if (!dataset) { - pam_zfs_free(); - zfs_key_config_free(&config); - return (PAM_SERVICE_ERR); - } - if (decrypt_mount(pamh, &config, dataset, token->value, B_TRUE) == -1) { - free(dataset); - pam_zfs_free(); - zfs_key_config_free(&config); - return (PAM_AUTH_ERR); - } - free(dataset); + + int ret = foreach_dataset(pamh, &config, auth_callback, + (void *)token->value); pam_zfs_free(); zfs_key_config_free(&config); + if (ret < 0) { + return (PAM_AUTH_ERR); + } return (PAM_SUCCESS); } @@ -884,6 +962,39 @@ pam_sm_setcred(pam_handle_t *pamh, int flags, return (PAM_SUCCESS); } +/* Context for password change callback */ +typedef struct { + const char *old_pass; + const char *new_pass; +} chauthtok_ctx_t; + +/* Callback for password change */ +static int +chauthtok_callback(pam_handle_t *pamh, zfs_key_config_t *config, + const char *dataset, void *data) +{ + chauthtok_ctx_t *ctx = data; + int was_loaded = is_key_loaded(pamh, dataset); + if (!was_loaded) { + int ret = decrypt_mount(pamh, config, dataset, + ctx->old_pass, B_FALSE); + if (ret == -1) { + pam_syslog(pamh, LOG_ERR, + "failed to load key for '%s' during " + "password change", dataset); + return (-1); + } + } + int ret = change_key(pamh, dataset, ctx->new_pass); + if (ret == -1) { + pam_syslog(pamh, LOG_ERR, + "failed to change key for dataset '%s'", dataset); + } + if (!was_loaded) + unmount_unload(pamh, dataset, config); + return (ret); +} + __attribute__((visibility("default"))) PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, @@ -904,34 +1015,27 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, } const pw_password_t *old_token = pw_get(pamh, PAM_OLDAUTHTOK, OLD_PASSWORD_VAR_NAME); - { - if (pam_zfs_init(pamh) != 0) { - zfs_key_config_free(&config); - return (PAM_SERVICE_ERR); - } - char *dataset = zfs_key_config_get_dataset(pamh, &config); - if (!dataset) { - pam_zfs_free(); - zfs_key_config_free(&config); - return (PAM_SERVICE_ERR); - } - if (!old_token) { - pam_syslog(pamh, LOG_ERR, - "old password from PAM stack is null"); - free(dataset); - pam_zfs_free(); - zfs_key_config_free(&config); - return (PAM_SERVICE_ERR); - } - if (decrypt_mount(pamh, &config, dataset, - old_token->value, B_TRUE) == -1) { - pam_syslog(pamh, LOG_ERR, - "old token mismatch"); - free(dataset); - pam_zfs_free(); - zfs_key_config_free(&config); - return (PAM_PERM_DENIED); - } + + if (!old_token) { + pam_syslog(pamh, LOG_ERR, + "old password from PAM stack is 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); + } + + /* First verify old password works for all datasets */ + int ret = foreach_dataset(pamh, &config, auth_callback, + (void *)old_token->value); + if (ret < 0) { + pam_syslog(pamh, LOG_ERR, "old token mismatch"); + pam_zfs_free(); + zfs_key_config_free(&config); + return (PAM_PERM_DENIED); } if ((flags & PAM_UPDATE_AUTHTOK) != 0) { @@ -944,41 +1048,51 @@ 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(pamh, &config); - if (!dataset) { - pam_zfs_free(); - zfs_key_config_free(&config); - pw_clear(pamh, OLD_PASSWORD_VAR_NAME); - pw_clear(pamh, PASSWORD_VAR_NAME); - return (PAM_SERVICE_ERR); - } - int was_loaded = is_key_loaded(pamh, dataset); - if (!was_loaded && decrypt_mount(pamh, &config, dataset, - old_token->value, B_FALSE) == -1) { - free(dataset); - pam_zfs_free(); - zfs_key_config_free(&config); + + chauthtok_ctx_t ctx = { + .old_pass = old_token->value, + .new_pass = token->value + }; + + ret = foreach_dataset(pamh, &config, chauthtok_callback, &ctx); + pam_zfs_free(); + zfs_key_config_free(&config); + + if (ret < 0) { pw_clear(pamh, OLD_PASSWORD_VAR_NAME); pw_clear(pamh, PASSWORD_VAR_NAME); return (PAM_SERVICE_ERR); } - int changed = change_key(pamh, dataset, token->value); - if (!was_loaded) { - unmount_unload(pamh, dataset, &config); - } - free(dataset); - pam_zfs_free(); - zfs_key_config_free(&config); + if (pw_clear(pamh, OLD_PASSWORD_VAR_NAME) == -1 || - pw_clear(pamh, PASSWORD_VAR_NAME) == -1 || changed == -1) { + pw_clear(pamh, PASSWORD_VAR_NAME) == -1) { return (PAM_SERVICE_ERR); } } else { + pam_zfs_free(); zfs_key_config_free(&config); } return (PAM_SUCCESS); } +/* Callback for session open - decrypt and mount */ +static int +open_session_callback(pam_handle_t *pamh, zfs_key_config_t *config, + const char *dataset, void *data) +{ + const char *passphrase = data; + return (decrypt_mount(pamh, config, dataset, passphrase, B_FALSE)); +} + +/* Callback for session close - unmount and unload */ +static int +close_session_callback(pam_handle_t *pamh, zfs_key_config_t *config, + const char *dataset, void *data) +{ + (void) data; + return (unmount_unload(pamh, dataset, config)); +} + PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) @@ -1016,22 +1130,15 @@ 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(pamh, &config); - if (!dataset) { - pam_zfs_free(); - zfs_key_config_free(&config); - return (PAM_SERVICE_ERR); - } - if (decrypt_mount(pamh, &config, dataset, - token->value, B_FALSE) == -1) { - free(dataset); - pam_zfs_free(); - zfs_key_config_free(&config); - return (PAM_SERVICE_ERR); - } - free(dataset); + + int ret = foreach_dataset(pamh, &config, open_session_callback, + (void *)token->value); pam_zfs_free(); zfs_key_config_free(&config); + + if (ret < 0) { + return (PAM_SERVICE_ERR); + } if (pw_clear(pamh, PASSWORD_VAR_NAME) == -1) { return (PAM_SERVICE_ERR); } @@ -1071,20 +1178,15 @@ 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(pamh, &config); - if (!dataset) { - pam_zfs_free(); - zfs_key_config_free(&config); - return (PAM_SESSION_ERR); - } - if (unmount_unload(pamh, dataset, &config) == -1) { - free(dataset); - pam_zfs_free(); + + int ret = foreach_dataset(pamh, &config, + close_session_callback, NULL); + pam_zfs_free(); + + if (ret < 0) { zfs_key_config_free(&config); return (PAM_SESSION_ERR); } - free(dataset); - pam_zfs_free(); } zfs_key_config_free(&config); diff --git a/man/Makefile.am b/man/Makefile.am index 7a63641c1c39..171cc501f8d2 100644 --- a/man/Makefile.am +++ b/man/Makefile.am @@ -111,6 +111,7 @@ endif if BUILD_LINUX dist_man_MANS += \ + %D%/man8/pam_zfs_key.8 \ %D%/man8/zfs-unzone.8 \ %D%/man8/zfs-zone.8 endif diff --git a/man/man8/pam_zfs_key.8 b/man/man8/pam_zfs_key.8 new file mode 100644 index 000000000000..f72602038a06 --- /dev/null +++ b/man/man8/pam_zfs_key.8 @@ -0,0 +1,221 @@ +.\" SPDX-License-Identifier: BSD-3-Clause +.\" +.\" Copyright (c) 2020, Felix Dörre +.\" All rights reserved. +.\" +.Dd December 24, 2025 +.Dt PAM_ZFS_KEY 8 +.Os +. +.Sh NAME +.Nm pam_zfs_key +.Nd PAM module for ZFS encryption key management +.Sh SYNOPSIS +.Nm pam_zfs_key.so +.Op Ar options +. +.Sh DESCRIPTION +.Nm +is a PAM module that automatically manages encryption keys for ZFS +datasets during user authentication and session management. +When a user logs in, the module uses their password to unlock their encrypted +home directory. +When the last session closes, the module unmounts the dataset and unloads +the key. +.Pp +The module tracks active sessions using reference counting to support multiple +simultaneous logins from the same user. +.Ss Multiple Home Prefixes +When configured with multiple home prefixes, the module attempts operations +on all matching datasets. +Operations that succeed are not rolled back if others fail. +The module returns success only if all operations succeed. +.Pp +For example, with datasets 1, 2, 3 where dataset 2 fails: +.Bl -bullet -compact +.It +Auth/session: datasets 1 and 3 are unlocked and mounted, dataset 2 is not. +.It +Password change: datasets 1 and 3 have the new password, +dataset 2 retains the old. +.El +.Pp +With +.Sy required , +login fails even though datasets 1 and 3 succeeded. +With +.Sy optional , +login proceeds. +For password changes, datasets 1 and 3 are updated while dataset 2 +retains the old password. +With +.Sy required , +the user sees an error. +With +.Sy optional , +the user sees success and may not notice the inconsistency. +Either way, passwords are left out of sync. +.Pp +Errors are logged to syslog. +Use +.Xr zfs-change-key 8 +to resync passwords after partial failure. +. +.Sh OPTIONS +.Bl -tag -width "mount_recursively" +.It Sy homes Ns = Ns Ar path Ns Oo , Ns Ar path2 Ns ... Oc +Comma-separated list of dataset prefixes where user home directories +are located. +The module constructs the full dataset path as +.Ar prefix Ns / Ns Ar username . +Default: +.Sy zroot/home +on +.Fx , +.Sy rpool/home +on Linux. +.It Sy runstatedir Ns = Ns Ar path +Directory for storing session reference counts. +Default: +.Pa /var/run/pam_zfs_key . +.It Sy uid_min Ns = Ns Ar uid +Minimum user ID for which the module will operate. +Default: 1000. +.It Sy uid_max Ns = Ns Ar uid +Maximum user ID for which the module will operate. +Default: MAXUID. +.It Sy nounmount +Do not unmount datasets or unload encryption keys when sessions close. +Datasets remain mounted and keys remain loaded. +.It Sy forceunmount +Force unmount datasets even if busy +.Pq Dv MS_FORCE . +.It Sy recursive_homes +Recursively search for encrypted datasets under the homes prefix. +.It Sy mount_recursively +Mount and unmount child datasets recursively. +.It Sy prop_mountpoint +Find the user's dataset by matching the dataset's +.Sy mountpoint +property to the user's home directory from +.Pa /etc/passwd , +instead of constructing the dataset name as +.Ar prefix Ns / Ns Ar username . +.El +. +.Sh FILES +.Bl -tag -width Pa +.It Pa /var/run/pam_zfs_key/ Ns Ar uid +Session reference count files tracking active logins per user. +.El +. +.Sh EXAMPLES +.Ss Example 1: Basic Configuration +Add to +.Pa /etc/pam.d/system-auth : +.Bd -literal -offset indent +auth optional pam_zfs_key.so +password optional pam_zfs_key.so +session optional pam_zfs_key.so +.Ed +.Pp +This configuration uses default settings. +User home datasets are expected at +.Sy zroot/home/ Ns Ar username +on +.Fx +or +.Sy rpool/home/ Ns Ar username +on Linux. +. +.Ss Example 2: Custom Home Directory Prefix +.Bd -literal -offset indent +auth optional pam_zfs_key.so homes=tank/users +password optional pam_zfs_key.so homes=tank/users +session optional pam_zfs_key.so homes=tank/users +.Ed +.Pp +Looks for user datasets at +.Sy tank/users/ Ns Ar username . +. +.Ss Example 3: Multiple Dataset Prefixes +.Bd -literal -offset indent +session optional pam_zfs_key.so homes=rpool/home,tank/users +.Ed +.Pp +Searches for user datasets in both +.Sy rpool/home +and +.Sy tank/users . +. +.Ss Example 4: Keep Datasets Mounted +.Bd -literal -offset indent +session optional pam_zfs_key.so nounmount +.Ed +.Pp +Leaves datasets mounted and keys loaded when sessions close. +Useful for systems with background processes accessing the home directory. +. +.Ss Example 5: Recursive Mounting +.Bd -literal -offset indent +session optional pam_zfs_key.so mount_recursively +.Ed +.Pp +Mounts child datasets recursively, useful when user data is organized +hierarchically like +.Sy rpool/home/alice/documents +and +.Sy rpool/home/alice/photos . +. +.Ss Example 6: Creating an Encrypted Home Dataset +.Bd -literal -offset indent +# zfs create -o encryption=on \e + -o keyformat=passphrase \e + -o keylocation=prompt \e + -o canmount=on \e + -o mountpoint=/home/alice \e + rpool/home/alice +.Ed +.Pp +The user's login password must match the dataset passphrase for automatic +unlocking to work. +The dataset must have a ZFS-managed mountpoint (not legacy) and +.Sy canmount Ns = Ns Sy on +for automatic mounting. +. +.Ss Example 7: Multiple Homes with Password Sync Check +.Bd -literal -offset indent +auth optional pam_zfs_key.so homes=rpool/home,tank/home +password required pam_zfs_key.so homes=rpool/home,tank/home +session optional pam_zfs_key.so homes=rpool/home,tank/home +.Ed +.Pp +Login proceeds even if some datasets are unavailable. +Password changes fail if any dataset cannot be updated, ensuring +the user is notified of sync issues. +See +.Sx Multiple Home Prefixes +for failure behavior. +. +.Sh SEE ALSO +.Xr pam 8 , +.Xr zfs-change-key 8 , +.Xr zfs-load-key 8 , +.Xr zfs-mount 8 +. +.Sh NOTES +.Bl -bullet -compact +.It +Only works with datasets using +.Sy keyformat Ns = Ns Sy passphrase . +.It +Datasets must have +.Sy keylocation Ns = Ns Sy prompt . +.It +Datasets with +.Sy mountpoint Ns = Ns Sy legacy , +.Sy canmount Ns = Ns Sy off , +or +.Sy canmount Ns = Ns Sy noauto +will have keys loaded but not be automatically mounted. +.El diff --git a/man/man8/zfs-load-key.8 b/man/man8/zfs-load-key.8 index 3a11cea99fd6..912f55d753b0 100644 --- a/man/man8/zfs-load-key.8 +++ b/man/man8/zfs-load-key.8 @@ -92,6 +92,9 @@ will ask for the key and mount the dataset see .Xr zfs-mount 8 .Pc . +For automated key management during user login, +.Xr pam_zfs_key 8 +can load keys and mount encrypted home directories on systems with PAM support. Once the key is loaded the .Sy keystatus property will become @@ -301,5 +304,6 @@ written. . .Sh SEE ALSO .Xr zfsprops 7 , +.Xr pam_zfs_key 8 , .Xr zfs-create 8 , .Xr zfs-set 8 diff --git a/man/man8/zfs-mount.8 b/man/man8/zfs-mount.8 index 2689b6dc345b..20a8cbc066d5 100644 --- a/man/man8/zfs-mount.8 +++ b/man/man8/zfs-mount.8 @@ -110,6 +110,9 @@ on each encryption root before mounting it. Note that if a filesystem has .Sy keylocation Ns = Ns Sy prompt , this will cause the terminal to interactively block after asking for the key. +On systems with PAM support, +.Xr pam_zfs_key 8 +can automate this process during user login. .It Fl v Report mount progress. .It Fl f @@ -138,3 +141,6 @@ The command can also be given a path to a ZFS file system mount point on the system. .El .El +. +.Sh SEE ALSO +.Xr pam_zfs_key 8 diff --git a/man/man8/zfs.8 b/man/man8/zfs.8 index b7566a727469..ba535c0f4561 100644 --- a/man/man8/zfs.8 +++ b/man/man8/zfs.8 @@ -805,6 +805,7 @@ don't wait. .Xr exportfs 8 , .Xr mount 8 , .Xr net 8 , +.Xr pam_zfs_key 8 , .Xr selinux 8 , .Xr zfs-allow 8 , .Xr zfs-bookmark 8 , diff --git a/tests/zfs-tests/tests/functional/pam/pam_basic.ksh b/tests/zfs-tests/tests/functional/pam/pam_basic.ksh index 4030a6788c4d..ccb0821e2644 100755 --- a/tests/zfs-tests/tests/functional/pam/pam_basic.ksh +++ b/tests/zfs-tests/tests/functional/pam/pam_basic.ksh @@ -51,4 +51,62 @@ references 0 log_mustnot ismounted "$TESTPOOL/pam/${username}" keystatus unavailable + + +log_mustnot ismounted "$TESTPOOL/pam/${username}" +keystatus unavailable +log_mustnot ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh unavailable + +genconfig "homes=$TESTPOOL/pam,$TESTPOOL/pam-multi-home runstatedir=${runstatedir}" +echo "testpass" | pamtester ${pamservice} ${username} open_session +references 1 +log_must ismounted "$TESTPOOL/pam/${username}" +keystatus available +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh available + +echo "testpass" | pamtester ${pamservice} ${username} open_session +references 2 +log_must ismounted "$TESTPOOL/pam/${username}" +keystatus available +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh available + +log_must pamtester ${pamservice} ${username} close_session +references 1 +log_must ismounted "$TESTPOOL/pam/${username}" +keystatus available +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh available + +log_must pamtester ${pamservice} ${username} close_session +references 0 +log_mustnot ismounted "$TESTPOOL/pam/${username}" +keystatus unavailable +log_mustnot ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh unavailable + +# Test a 'homes' with many entries +allhomes="$TESTPOOL/pam-multi-home1" +for i in {2..$PAM_MULTI_HOME_COUNT} ; do + allhomes="$allhomes,$TESTPOOL/pam-multi-home$i" +done + +genconfig "homes=$allhomes runstatedir=${runstatedir}" + +echo "testpass" | pamtester ${pamservice} ${username} open_session +for i in {1..$PAM_MULTI_HOME_COUNT} ; do + references 1 + log_must ismounted "$TESTPOOL/pam-multi-home$i/${username}" + keystatus_mh available $i +done + +log_must pamtester ${pamservice} ${username} close_session +for i in {1..$PAM_MULTI_HOME_COUNT} ; do + references 0 + log_mustnot ismounted "$TESTPOOL/pam-multi-home$i/${username}" + keystatus_mh unavailable $i +done + log_pass "done." diff --git a/tests/zfs-tests/tests/functional/pam/pam_change_unmounted.ksh b/tests/zfs-tests/tests/functional/pam/pam_change_unmounted.ksh index 3ca4fb810e30..dfd400927374 100755 --- a/tests/zfs-tests/tests/functional/pam/pam_change_unmounted.ksh +++ b/tests/zfs-tests/tests/functional/pam/pam_change_unmounted.ksh @@ -29,28 +29,39 @@ fi log_mustnot ismounted "$TESTPOOL/pam/${username}" keystatus unavailable +log_mustnot ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh unavailable -genconfig "homes=$TESTPOOL/pam runstatedir=${runstatedir}" +genconfig "homes=$TESTPOOL/pam,$TESTPOOL/pam-multi-home runstatedir=${runstatedir}" printf "testpass\nsecondpass\nsecondpass\n" | pamtester -v ${pamservice} ${username} chauthtok log_mustnot ismounted "$TESTPOOL/pam/${username}" keystatus unavailable +log_mustnot ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh unavailable echo "secondpass" | pamtester ${pamservice} ${username} open_session references 1 log_must ismounted "$TESTPOOL/pam/${username}" keystatus available +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh available printf "secondpass\ntestpass\ntestpass\n" | pamtester -v ${pamservice} ${username} chauthtok log_must ismounted "$TESTPOOL/pam/${username}" log_must ismounted "$TESTPOOL/pam/${username}" keystatus available +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh available log_must pamtester ${pamservice} ${username} close_session references 0 log_mustnot ismounted "$TESTPOOL/pam/${username}" keystatus unavailable +log_mustnot ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh 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 index 4f5255a104fd..2832e1ab5d12 100755 --- a/tests/zfs-tests/tests/functional/pam/pam_nounmount.ksh +++ b/tests/zfs-tests/tests/functional/pam/pam_nounmount.ksh @@ -29,28 +29,40 @@ fi log_mustnot ismounted "$TESTPOOL/pam/${username}" keystatus unavailable +log_mustnot ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh unavailable -genconfig "homes=$TESTPOOL/pam runstatedir=${runstatedir} nounmount" +genconfig "homes=$TESTPOOL/pam,$TESTPOOL/pam-multi-home runstatedir=${runstatedir} nounmount" echo "testpass" | pamtester ${pamservice} ${username} open_session references 1 log_must ismounted "$TESTPOOL/pam/${username}" keystatus available +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" +keystatus_mh available echo "testpass" | pamtester ${pamservice} ${username} open_session references 2 keystatus available log_must ismounted "$TESTPOOL/pam/${username}" +keystatus_mh available +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" log_must pamtester ${pamservice} ${username} close_session references 1 keystatus available log_must ismounted "$TESTPOOL/pam/${username}" +keystatus_mh available +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" log_must pamtester ${pamservice} ${username} close_session references 0 keystatus available +keystatus_mh available log_must ismounted "$TESTPOOL/pam/${username}" +log_must ismounted "$TESTPOOL/pam-multi-home/${username}" log_must zfs unmount "$TESTPOOL/pam/${username}" +log_must zfs unmount "$TESTPOOL/pam-multi-home/${username}" log_must zfs unload-key "$TESTPOOL/pam/${username}" +log_must zfs unload-key "$TESTPOOL/pam-multi-home/${username}" log_pass "done." diff --git a/tests/zfs-tests/tests/functional/pam/setup.ksh b/tests/zfs-tests/tests/functional/pam/setup.ksh index e0d81c531df5..685d174f0cea 100755 --- a/tests/zfs-tests/tests/functional/pam/setup.ksh +++ b/tests/zfs-tests/tests/functional/pam/setup.ksh @@ -30,6 +30,7 @@ DISK=${DISKS%% *} create_pool $TESTPOOL "$DISK" log_must zfs create -o mountpoint="$TESTDIR" "$TESTPOOL/pam" +log_must zfs create -o mountpoint="$TESTDIR-multi-home" "$TESTPOOL/pam-multi-home" log_must add_group pamtestgroup log_must add_user pamtestgroup ${username} log_must mkdir -p "$runstatedir" @@ -37,5 +38,15 @@ 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}" +echo "testpass" | zfs create -o encryption=aes-256-gcm -o keyformat=passphrase -o keylocation=prompt "$TESTPOOL/pam-multi-home/${username}" +log_must zfs unmount "$TESTPOOL/pam-multi-home/${username}" +log_must zfs unload-key "$TESTPOOL/pam-multi-home/${username}" + +for i in {1..$PAM_MULTI_HOME_COUNT} ; do + log_must zfs create -o mountpoint="$TESTDIR-multi-home$i" "$TESTPOOL/pam-multi-home$i" + echo "testpass" | zfs create -o encryption=aes-256-gcm -o keyformat=passphrase -o keylocation=prompt "$TESTPOOL/pam-multi-home$i/${username}" + log_must zfs unmount "$TESTPOOL/pam-multi-home$i/${username}" + log_must zfs unload-key "$TESTPOOL/pam-multi-home$i/${username}" +done log_pass diff --git a/tests/zfs-tests/tests/functional/pam/utilities.kshlib.in b/tests/zfs-tests/tests/functional/pam/utilities.kshlib.in index f69a0b097170..65bd1c2e76d3 100644 --- a/tests/zfs-tests/tests/functional/pam/utilities.kshlib.in +++ b/tests/zfs-tests/tests/functional/pam/utilities.kshlib.in @@ -28,11 +28,17 @@ runstatedir="${TESTDIR}_run" pammodule="@pammoduledir@/pam_zfs_key.so" pamservice="pam_zfs_key_test" pamconfig="/etc/pam.d/${pamservice}" +PAM_MULTI_HOME_COUNT=20 function keystatus { log_must [ "$(get_prop keystatus "$TESTPOOL/pam/${username}")" = "$1" ] } +function keystatus_mh { + typeset suffix="${2:-}" + log_must [ "$(get_prop keystatus "$TESTPOOL/pam-multi-home${suffix}/${username}")" = "$1" ] +} + function genconfig { printf '%s\trequired\tpam_permit.so\n%s\toptional\t%s\t%s\n' \ password password "$pammodule" "$1" \