mirror of
https://git.proxmox.com/git/mirror_zfs.git
synced 2026-03-14 06:16:17 +03:00
Added support for multiple homes in pam_zfs_key module (#18084)
This implemented support for having multiple datasets unlocked and mounted when a session is opened. Example: `homes=rpool/home,tank/users` Extra unit tests have been added A man page documents have been added `man 8 pam_zfs_key`. A few references to the new man page have also been added in other documents. Signed-off-by: Dennis Vestergaard Værum <github@varum.dk> Reviewed-by: Tony Hutter <hutter2@llnl.gov> Reviewed-by: Tino Reichardt <milky-zfs@mcmilk.de>
This commit is contained in:
parent
7e33476a7c
commit
07ae463d1a
@ -1,2 +1,3 @@
|
|||||||
usr/lib/*/security/pam_zfs_key.so
|
usr/lib/*/security/pam_zfs_key.so
|
||||||
|
usr/share/man/man8/pam_zfs_key.8
|
||||||
usr/share/pam-configs/zfs_key
|
usr/share/pam-configs/zfs_key
|
||||||
|
|||||||
@ -754,6 +754,82 @@ zfs_key_config_get_dataset(pam_handle_t *pamh, zfs_key_config_t *config)
|
|||||||
return (ret);
|
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
|
static int
|
||||||
zfs_key_config_modify_session_counter(pam_handle_t *pamh,
|
zfs_key_config_modify_session_counter(pam_handle_t *pamh,
|
||||||
zfs_key_config_t *config, int delta)
|
zfs_key_config_t *config, int delta)
|
||||||
@ -825,6 +901,15 @@ zfs_key_config_modify_session_counter(pam_handle_t *pamh,
|
|||||||
return (counter_value);
|
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")))
|
__attribute__((visibility("default")))
|
||||||
PAM_EXTERN int
|
PAM_EXTERN int
|
||||||
pam_sm_authenticate(pam_handle_t *pamh, int flags,
|
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);
|
zfs_key_config_free(&config);
|
||||||
return (PAM_SERVICE_ERR);
|
return (PAM_SERVICE_ERR);
|
||||||
}
|
}
|
||||||
char *dataset = zfs_key_config_get_dataset(pamh, &config);
|
|
||||||
if (!dataset) {
|
int ret = foreach_dataset(pamh, &config, auth_callback,
|
||||||
pam_zfs_free();
|
(void *)token->value);
|
||||||
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);
|
|
||||||
pam_zfs_free();
|
pam_zfs_free();
|
||||||
zfs_key_config_free(&config);
|
zfs_key_config_free(&config);
|
||||||
|
if (ret < 0) {
|
||||||
|
return (PAM_AUTH_ERR);
|
||||||
|
}
|
||||||
return (PAM_SUCCESS);
|
return (PAM_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -884,6 +962,39 @@ pam_sm_setcred(pam_handle_t *pamh, int flags,
|
|||||||
return (PAM_SUCCESS);
|
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")))
|
__attribute__((visibility("default")))
|
||||||
PAM_EXTERN int
|
PAM_EXTERN int
|
||||||
pam_sm_chauthtok(pam_handle_t *pamh, int flags,
|
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,
|
const pw_password_t *old_token = pw_get(pamh,
|
||||||
PAM_OLDAUTHTOK, OLD_PASSWORD_VAR_NAME);
|
PAM_OLDAUTHTOK, OLD_PASSWORD_VAR_NAME);
|
||||||
{
|
|
||||||
if (pam_zfs_init(pamh) != 0) {
|
if (!old_token) {
|
||||||
zfs_key_config_free(&config);
|
pam_syslog(pamh, LOG_ERR,
|
||||||
return (PAM_SERVICE_ERR);
|
"old password from PAM stack is null");
|
||||||
}
|
zfs_key_config_free(&config);
|
||||||
char *dataset = zfs_key_config_get_dataset(pamh, &config);
|
return (PAM_SERVICE_ERR);
|
||||||
if (!dataset) {
|
}
|
||||||
pam_zfs_free();
|
|
||||||
zfs_key_config_free(&config);
|
if (pam_zfs_init(pamh) != 0) {
|
||||||
return (PAM_SERVICE_ERR);
|
zfs_key_config_free(&config);
|
||||||
}
|
return (PAM_SERVICE_ERR);
|
||||||
if (!old_token) {
|
}
|
||||||
pam_syslog(pamh, LOG_ERR,
|
|
||||||
"old password from PAM stack is null");
|
/* First verify old password works for all datasets */
|
||||||
free(dataset);
|
int ret = foreach_dataset(pamh, &config, auth_callback,
|
||||||
pam_zfs_free();
|
(void *)old_token->value);
|
||||||
zfs_key_config_free(&config);
|
if (ret < 0) {
|
||||||
return (PAM_SERVICE_ERR);
|
pam_syslog(pamh, LOG_ERR, "old token mismatch");
|
||||||
}
|
pam_zfs_free();
|
||||||
if (decrypt_mount(pamh, &config, dataset,
|
zfs_key_config_free(&config);
|
||||||
old_token->value, B_TRUE) == -1) {
|
return (PAM_PERM_DENIED);
|
||||||
pam_syslog(pamh, LOG_ERR,
|
|
||||||
"old token mismatch");
|
|
||||||
free(dataset);
|
|
||||||
pam_zfs_free();
|
|
||||||
zfs_key_config_free(&config);
|
|
||||||
return (PAM_PERM_DENIED);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & PAM_UPDATE_AUTHTOK) != 0) {
|
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);
|
pw_clear(pamh, OLD_PASSWORD_VAR_NAME);
|
||||||
return (PAM_SERVICE_ERR);
|
return (PAM_SERVICE_ERR);
|
||||||
}
|
}
|
||||||
char *dataset = zfs_key_config_get_dataset(pamh, &config);
|
|
||||||
if (!dataset) {
|
chauthtok_ctx_t ctx = {
|
||||||
pam_zfs_free();
|
.old_pass = old_token->value,
|
||||||
zfs_key_config_free(&config);
|
.new_pass = token->value
|
||||||
pw_clear(pamh, OLD_PASSWORD_VAR_NAME);
|
};
|
||||||
pw_clear(pamh, PASSWORD_VAR_NAME);
|
|
||||||
return (PAM_SERVICE_ERR);
|
ret = foreach_dataset(pamh, &config, chauthtok_callback, &ctx);
|
||||||
}
|
|
||||||
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);
|
|
||||||
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();
|
pam_zfs_free();
|
||||||
zfs_key_config_free(&config);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
if (pw_clear(pamh, OLD_PASSWORD_VAR_NAME) == -1 ||
|
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);
|
return (PAM_SERVICE_ERR);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
pam_zfs_free();
|
||||||
zfs_key_config_free(&config);
|
zfs_key_config_free(&config);
|
||||||
}
|
}
|
||||||
return (PAM_SUCCESS);
|
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_EXTERN int
|
||||||
pam_sm_open_session(pam_handle_t *pamh, int flags,
|
pam_sm_open_session(pam_handle_t *pamh, int flags,
|
||||||
int argc, const char **argv)
|
int argc, const char **argv)
|
||||||
@ -1016,22 +1130,15 @@ pam_sm_open_session(pam_handle_t *pamh, int flags,
|
|||||||
zfs_key_config_free(&config);
|
zfs_key_config_free(&config);
|
||||||
return (PAM_SERVICE_ERR);
|
return (PAM_SERVICE_ERR);
|
||||||
}
|
}
|
||||||
char *dataset = zfs_key_config_get_dataset(pamh, &config);
|
|
||||||
if (!dataset) {
|
int ret = foreach_dataset(pamh, &config, open_session_callback,
|
||||||
pam_zfs_free();
|
(void *)token->value);
|
||||||
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);
|
|
||||||
pam_zfs_free();
|
pam_zfs_free();
|
||||||
zfs_key_config_free(&config);
|
zfs_key_config_free(&config);
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
return (PAM_SERVICE_ERR);
|
||||||
|
}
|
||||||
if (pw_clear(pamh, PASSWORD_VAR_NAME) == -1) {
|
if (pw_clear(pamh, PASSWORD_VAR_NAME) == -1) {
|
||||||
return (PAM_SERVICE_ERR);
|
return (PAM_SERVICE_ERR);
|
||||||
}
|
}
|
||||||
@ -1071,20 +1178,15 @@ pam_sm_close_session(pam_handle_t *pamh, int flags,
|
|||||||
zfs_key_config_free(&config);
|
zfs_key_config_free(&config);
|
||||||
return (PAM_SERVICE_ERR);
|
return (PAM_SERVICE_ERR);
|
||||||
}
|
}
|
||||||
char *dataset = zfs_key_config_get_dataset(pamh, &config);
|
|
||||||
if (!dataset) {
|
int ret = foreach_dataset(pamh, &config,
|
||||||
pam_zfs_free();
|
close_session_callback, NULL);
|
||||||
zfs_key_config_free(&config);
|
|
||||||
return (PAM_SESSION_ERR);
|
|
||||||
}
|
|
||||||
if (unmount_unload(pamh, dataset, &config) == -1) {
|
|
||||||
free(dataset);
|
|
||||||
pam_zfs_free();
|
|
||||||
zfs_key_config_free(&config);
|
|
||||||
return (PAM_SESSION_ERR);
|
|
||||||
}
|
|
||||||
free(dataset);
|
|
||||||
pam_zfs_free();
|
pam_zfs_free();
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
zfs_key_config_free(&config);
|
||||||
|
return (PAM_SESSION_ERR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zfs_key_config_free(&config);
|
zfs_key_config_free(&config);
|
||||||
|
|||||||
@ -112,6 +112,7 @@ endif
|
|||||||
|
|
||||||
if BUILD_LINUX
|
if BUILD_LINUX
|
||||||
dist_man_MANS += \
|
dist_man_MANS += \
|
||||||
|
%D%/man8/pam_zfs_key.8 \
|
||||||
%D%/man8/zfs-unzone.8 \
|
%D%/man8/zfs-unzone.8 \
|
||||||
%D%/man8/zfs-zone.8
|
%D%/man8/zfs-zone.8
|
||||||
endif
|
endif
|
||||||
|
|||||||
221
man/man8/pam_zfs_key.8
Normal file
221
man/man8/pam_zfs_key.8
Normal file
@ -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
|
||||||
@ -92,6 +92,9 @@ will ask for the key and mount the dataset
|
|||||||
see
|
see
|
||||||
.Xr zfs-mount 8
|
.Xr zfs-mount 8
|
||||||
.Pc .
|
.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
|
Once the key is loaded the
|
||||||
.Sy keystatus
|
.Sy keystatus
|
||||||
property will become
|
property will become
|
||||||
@ -301,5 +304,6 @@ written.
|
|||||||
.
|
.
|
||||||
.Sh SEE ALSO
|
.Sh SEE ALSO
|
||||||
.Xr zfsprops 7 ,
|
.Xr zfsprops 7 ,
|
||||||
|
.Xr pam_zfs_key 8 ,
|
||||||
.Xr zfs-create 8 ,
|
.Xr zfs-create 8 ,
|
||||||
.Xr zfs-set 8
|
.Xr zfs-set 8
|
||||||
|
|||||||
@ -110,6 +110,9 @@ on each encryption root before mounting it.
|
|||||||
Note that if a filesystem has
|
Note that if a filesystem has
|
||||||
.Sy keylocation Ns = Ns Sy prompt ,
|
.Sy keylocation Ns = Ns Sy prompt ,
|
||||||
this will cause the terminal to interactively block after asking for the key.
|
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
|
.It Fl v
|
||||||
Report mount progress.
|
Report mount progress.
|
||||||
.It Fl f
|
.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.
|
system.
|
||||||
.El
|
.El
|
||||||
.El
|
.El
|
||||||
|
.
|
||||||
|
.Sh SEE ALSO
|
||||||
|
.Xr pam_zfs_key 8
|
||||||
|
|||||||
@ -805,6 +805,7 @@ don't wait.
|
|||||||
.Xr exportfs 8 ,
|
.Xr exportfs 8 ,
|
||||||
.Xr mount 8 ,
|
.Xr mount 8 ,
|
||||||
.Xr net 8 ,
|
.Xr net 8 ,
|
||||||
|
.Xr pam_zfs_key 8 ,
|
||||||
.Xr selinux 8 ,
|
.Xr selinux 8 ,
|
||||||
.Xr zfs-allow 8 ,
|
.Xr zfs-allow 8 ,
|
||||||
.Xr zfs-bookmark 8 ,
|
.Xr zfs-bookmark 8 ,
|
||||||
|
|||||||
@ -51,4 +51,62 @@ references 0
|
|||||||
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
||||||
keystatus unavailable
|
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."
|
log_pass "done."
|
||||||
|
|||||||
@ -29,28 +29,39 @@ fi
|
|||||||
|
|
||||||
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
||||||
keystatus unavailable
|
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
|
printf "testpass\nsecondpass\nsecondpass\n" | pamtester -v ${pamservice} ${username} chauthtok
|
||||||
|
|
||||||
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
||||||
keystatus unavailable
|
keystatus unavailable
|
||||||
|
log_mustnot ismounted "$TESTPOOL/pam-multi-home/${username}"
|
||||||
|
keystatus_mh unavailable
|
||||||
|
|
||||||
echo "secondpass" | pamtester ${pamservice} ${username} open_session
|
echo "secondpass" | pamtester ${pamservice} ${username} open_session
|
||||||
references 1
|
references 1
|
||||||
log_must ismounted "$TESTPOOL/pam/${username}"
|
log_must ismounted "$TESTPOOL/pam/${username}"
|
||||||
keystatus available
|
keystatus available
|
||||||
|
log_must ismounted "$TESTPOOL/pam-multi-home/${username}"
|
||||||
|
keystatus_mh available
|
||||||
|
|
||||||
printf "secondpass\ntestpass\ntestpass\n" | pamtester -v ${pamservice} ${username} chauthtok
|
printf "secondpass\ntestpass\ntestpass\n" | pamtester -v ${pamservice} ${username} chauthtok
|
||||||
|
|
||||||
log_must ismounted "$TESTPOOL/pam/${username}"
|
log_must ismounted "$TESTPOOL/pam/${username}"
|
||||||
log_must ismounted "$TESTPOOL/pam/${username}"
|
log_must ismounted "$TESTPOOL/pam/${username}"
|
||||||
keystatus available
|
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
|
log_must pamtester ${pamservice} ${username} close_session
|
||||||
references 0
|
references 0
|
||||||
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
||||||
keystatus unavailable
|
keystatus unavailable
|
||||||
|
log_mustnot ismounted "$TESTPOOL/pam-multi-home/${username}"
|
||||||
|
keystatus_mh unavailable
|
||||||
|
|
||||||
log_pass "done."
|
log_pass "done."
|
||||||
|
|||||||
@ -29,28 +29,40 @@ fi
|
|||||||
|
|
||||||
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
log_mustnot ismounted "$TESTPOOL/pam/${username}"
|
||||||
keystatus unavailable
|
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
|
echo "testpass" | pamtester ${pamservice} ${username} open_session
|
||||||
references 1
|
references 1
|
||||||
log_must ismounted "$TESTPOOL/pam/${username}"
|
log_must ismounted "$TESTPOOL/pam/${username}"
|
||||||
keystatus available
|
keystatus available
|
||||||
|
log_must ismounted "$TESTPOOL/pam-multi-home/${username}"
|
||||||
|
keystatus_mh available
|
||||||
|
|
||||||
echo "testpass" | pamtester ${pamservice} ${username} open_session
|
echo "testpass" | pamtester ${pamservice} ${username} open_session
|
||||||
references 2
|
references 2
|
||||||
keystatus available
|
keystatus available
|
||||||
log_must ismounted "$TESTPOOL/pam/${username}"
|
log_must ismounted "$TESTPOOL/pam/${username}"
|
||||||
|
keystatus_mh available
|
||||||
|
log_must ismounted "$TESTPOOL/pam-multi-home/${username}"
|
||||||
|
|
||||||
log_must pamtester ${pamservice} ${username} close_session
|
log_must pamtester ${pamservice} ${username} close_session
|
||||||
references 1
|
references 1
|
||||||
keystatus available
|
keystatus available
|
||||||
log_must ismounted "$TESTPOOL/pam/${username}"
|
log_must ismounted "$TESTPOOL/pam/${username}"
|
||||||
|
keystatus_mh available
|
||||||
|
log_must ismounted "$TESTPOOL/pam-multi-home/${username}"
|
||||||
|
|
||||||
log_must pamtester ${pamservice} ${username} close_session
|
log_must pamtester ${pamservice} ${username} close_session
|
||||||
references 0
|
references 0
|
||||||
keystatus available
|
keystatus available
|
||||||
|
keystatus_mh available
|
||||||
log_must ismounted "$TESTPOOL/pam/${username}"
|
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/${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/${username}"
|
||||||
|
log_must zfs unload-key "$TESTPOOL/pam-multi-home/${username}"
|
||||||
|
|
||||||
log_pass "done."
|
log_pass "done."
|
||||||
|
|||||||
@ -30,6 +30,7 @@ DISK=${DISKS%% *}
|
|||||||
create_pool $TESTPOOL "$DISK"
|
create_pool $TESTPOOL "$DISK"
|
||||||
|
|
||||||
log_must zfs create -o mountpoint="$TESTDIR" "$TESTPOOL/pam"
|
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_group pamtestgroup
|
||||||
log_must add_user pamtestgroup ${username}
|
log_must add_user pamtestgroup ${username}
|
||||||
log_must mkdir -p "$runstatedir"
|
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}"
|
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 unmount "$TESTPOOL/pam/${username}"
|
||||||
log_must zfs unload-key "$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
|
log_pass
|
||||||
|
|||||||
@ -28,11 +28,17 @@ runstatedir="${TESTDIR}_run"
|
|||||||
pammodule="@pammoduledir@/pam_zfs_key.so"
|
pammodule="@pammoduledir@/pam_zfs_key.so"
|
||||||
pamservice="pam_zfs_key_test"
|
pamservice="pam_zfs_key_test"
|
||||||
pamconfig="/etc/pam.d/${pamservice}"
|
pamconfig="/etc/pam.d/${pamservice}"
|
||||||
|
PAM_MULTI_HOME_COUNT=20
|
||||||
|
|
||||||
function keystatus {
|
function keystatus {
|
||||||
log_must [ "$(get_prop keystatus "$TESTPOOL/pam/${username}")" = "$1" ]
|
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 {
|
function genconfig {
|
||||||
printf '%s\trequired\tpam_permit.so\n%s\toptional\t%s\t%s\n' \
|
printf '%s\trequired\tpam_permit.so\n%s\toptional\t%s\t%s\n' \
|
||||||
password password "$pammodule" "$1" \
|
password password "$pammodule" "$1" \
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user