• Amazon Linux EC2
  • vsftpd
  • PAM authentication with pam_userdb.so
  • Usernames/passwords written to Berkeley user db from an external source (lsyncd).

There are many 1000s of virtual users, to date I have manually pre-created home directories for them all under /home/vsftpd

drwx------    2 vsftpd users     4096 Apr 11 15:28 user0123
drwx------    2 vsftpd users     4096 Apr 11 15:28 user0124

auth required pam_userdb.so db=/usr/local/vsftpd_auth/vsftpd_userdb crypt=none
account required pam_userdb.so db=/usr/local/vsftpd_auth/vsftpd_userdb

I want to avoid manually pre-creating the directories so that new entries to the Berkeley DB will work automatically without changing each node.

Searching yields similar questions for LDAP and MySQL authentication using:

  • pam_mkhomedir.so
  • /etc/nsswitch.conf

But I can't seem to put it all together to solve this problem for Berkeley DB.

  • what kind of problem you have using pam_mkhomedir.so? any error that you can show us? – c4f4t0r May 9 '18 at 13:31
  • Based on other posts, my understanding is that pam_mkhomedir.so relies on /etc/nsswitch.conf to populate a data structure with home directory and permission values. In tern, /etc/nsswitch.conf gets the values typically from information in the passwd file. Since my vsftpd users are virtual, I don't know how to setup nsswitch.conf for the user db case. – DanielB6 May 9 '18 at 16:27
  • I don't think that's related to my case - or at least I don't understand how to make it work with vsftpd/pam_userdb.so and virtual users. – DanielB6 May 10 '18 at 14:53

I ended up implementing a PAM module for vsftpd virtual users, based loosely on pam_mkhomedir.so. I'm sure it can be improved, but below is a working version.


pam_mkhomedir_vsftpd_virt.so [debug] vsftpd_user=<vsftpd_user> basedir=<basedir> 
  • vsftpd_user - typically vsftpd
  • basedir - typically /home/vsftpd/


auth requisite pam_userdb.so db=/path/to/userdb crypt=none
account requisite pam_userdb.so db=/path/to/userdb
account required pam_mkhomedir_vsftpd_virt.so debug vsftpd_user=vsftpd basedir=/home/vsftpd/
  • I changed auth and account for pam_userdb.so to 'requisite' to avoid creating the home dir if userdb authentication doesn't pass.
  • I implemented the module to act at the account level because sessions aren't used in my vsftpd context.


gcc -fPIC -c pam_mkhomedir_vsftpd_virt.c
gcc -shared -o pam_mkhomedir_vsftpd_virt.so pam_mkhomedir_vsftpd_virt.o -lpam
  • Install pam_mkhomedir_vsftpd_virt.so with the other PAM modules.


#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pwd.h>
#include <unistd.h>
#include <syslog.h>

/* For now we will use the service function for account management
#include <security/pam_modules.h>

#define MAX_HOMEDIR_SIZE 100

typedef struct {
    bool debug;
    const char *vsftpd_user;
    const char *basedir;
    const char *user;
    char homedir[MAX_HOMEDIR_SIZE+1];
} options_t;

static int parse_input(pam_handle_t *pamh, int flags, int argc, const char **argv, options_t *options) {
    int rc;
    int basedir_len;
    int total_len;
    bool add_slash = false;
    int i;

    /* Retrieve the user name
    rc = pam_get_item(pamh, PAM_USER, (void *)&options->user);

    if (rc != PAM_SUCCESS || options->user == NULL || *(options->user) == '\0') {
        pam_syslog(pamh, LOG_ERR, "cannot retrieve the user name");
        return PAM_USER_UNKNOWN;

    /* Retrieve the module parms
    for (i = 0 ; i < argc; *argv++, i++) {
        if (strcmp(*argv, "debug") == 0) {
            options->debug = true;
        else if (strncmp(*argv, "vsftpd_user=", 12) == 0) {
            options->vsftpd_user = *argv+12;
        else if (strncmp(*argv, "basedir=", 8) == 0) {
            options->basedir = *argv+8;
        else {
            pam_syslog(pamh, LOG_ERR, "unknown option '%s'", *argv);

    /* Validate input
    if (options->vsftpd_user == NULL || *(options->vsftpd_user) == '\0') {
        pam_syslog(pamh, LOG_ERR, "cannot retrieve the vsftpd user");
        return PAM_NO_MODULE_DATA;

    if (options->basedir == NULL || *(options->basedir) == '\0') {
        pam_syslog(pamh, LOG_ERR, "cannot retrieve the base dir");
        return PAM_NO_MODULE_DATA;

    if (options->basedir[0] != '/') {
        pam_syslog(pamh, LOG_ERR, "base dir must start with '/'");
        return PAM_NO_MODULE_DATA;

    /* Check whether we need to add a slash to the path
    basedir_len = (int) strlen(options->basedir);

    if (options->basedir[basedir_len-1] != '/')
        add_slash = true;

    /* Verify we haven't exceeded the max dir length
    total_len = basedir_len + (int) strlen(options->user) + (add_slash?1:0);

    if (total_len > MAX_HOMEDIR_SIZE) {
        pam_syslog(pamh, LOG_ERR, "home directory max length of %d exceeded '%d'", MAX_HOMEDIR_SIZE, total_len);
        return PAM_BUF_ERR;

    /* Create the homedir string
    snprintf(options->homedir, MAX_HOMEDIR_SIZE+1, "%s%s%s", options->basedir, add_slash?"/":"", options->user);

    /* Finished parsing input, log what we got...
    if (options->debug) {
        pam_syslog(pamh, LOG_DEBUG, "vsftpd user '%s'", options->vsftpd_user);
        pam_syslog(pamh, LOG_DEBUG, "base directory '%s'", options->basedir);
        pam_syslog(pamh, LOG_DEBUG, "user '%s'", options->user);
        pam_syslog(pamh, LOG_DEBUG, "home directory '%s'", options->homedir);

    return PAM_SUCCESS;

static int create_homedir(pam_handle_t *pamh, options_t *options) {
    struct stat status;
    struct passwd *pwd;
    const char *vsftpd_user = options->vsftpd_user;
    char *homedir = options->homedir;

    /* Retrieve passwd data for the vsftpd user
    pwd = getpwnam(vsftpd_user);

    if (pwd == NULL) {
        pam_syslog(pamh, LOG_ERR, "unable to get user creds for '%s'", vsftpd_user);

    /* Check if home directory already exists
    if (stat(homedir, &status) == 0) {
        if (options->debug)
            pam_syslog(pamh, LOG_DEBUG, "home directory '%s' already exists", homedir);
        return PAM_SUCCESS;

    /* Home directory doesn't exist, create it
    if (options->debug)
        pam_syslog(pamh, LOG_DEBUG, "creating home directory '%s'", homedir);

    if (mkdir(homedir, 0700) != 0) {
        pam_syslog(pamh, LOG_ERR, "unable to create home directory '%s'", homedir);
        return PAM_PERM_DENIED;

    if (chmod(homedir, 0700) != 0 || chown(homedir, pwd->pw_uid, pwd->pw_gid) != 0) {
        pam_syslog(pamh, LOG_ERR, "unable to change perms on directory '%s'", homedir);
        return PAM_PERM_DENIED;

    return PAM_SUCCESS;

/* PAM Account Management function
PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) {
    options_t options;
    int rc;

    memset(&options, 0, sizeof(options_t));

    rc = parse_input(pamh, flags, argc, argv, &options);

    if (rc != PAM_SUCCESS) {
        return rc;

    rc = create_homedir(pamh, &options);

    if (rc != PAM_SUCCESS) {
        return rc;

    return PAM_SUCCESS;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.