1984 – the techblog

(Hopefully) useful various sysadmin and other stuff.

Setting up virtual users in vsftpd

I’ve set up a FTP server for me and my col­leagues. This was a mat­ter of honor (don’t want to go all Duke Nukem on my friends) and as a such, it had to be done cor­rectly. So, I’ve had some require­ments prior to set­ting up:

  1. It must be secure
  2. It must scale (hey, we start small, but who said fiber is out of the question?)
  3. It must work with trou­ble­some NAT-environments as well
  4. It must allow as much fine-grained con­trol as possible.
  5. Users hav­ing access to the FTP ser­vice shall not be granted access to other aspects of the server
  6. Every user must have per­sonal credentials

Nat­u­rally, that turned me to good ‘ol vsftpd. A true bitch when it comes to con­fig­ur­ing, but as soon as it works, it’s rock solid. To full­fill require­ment 5, not allow­ing FTP users access to other parts of the sys­tem, I chose to cre­ate an inde­pen­dent user data­base, not known to, nor used by the rest of the sys­tem. This is way more secure than defin­ing access lev­els – what doesn’t exist, can’t do any­thing. As it turns out this isn’t exactly triv­ial with vsftpd – but it’s not rocket sci­ence, either. While there are some HOWTO’s float­ing around for this – the major­ity of them suck hairy balls, in my hum­ble opinion.

The fol­low­ing descrip­tion is tai­lored to a Debian instal­la­tion, but can be eas­ily adopted to other dis­tri­b­u­tions – and if you can’t do this, you shouldn’t be set­ting up an FTP server that’s con­nected to the inter­net anyway.

Now, let’s get started. Given the some­what lack­ing nature of vsftpd’s doc­u­men­ta­tion, it took me a while to fig­ure out that one must imple­ment vir­tual users in vsftpd with PAM. vsftpd relies heav­ily on it for authen­ti­ca­tion; it doesn’t imple­ment any user authen­ti­ca­tion back­ends by itself 1 like ProFTPD (which is a good thing). This require­ment makes the next step crys­tal clear:

Con­fig­ure PAM

With PAM, one has a myr­iad of pos­si­bil­i­ties (read: mod­ules) to chose from to imple­ment a user data­base sep­a­rate from the sys­tem (and other ser­vices using PAM, for that mat­ter), rang­ing from sim­ple non-encrypted text files to full fledged battle-hardened authen­ti­ca­tion schemes. As it’s 2011, I don’t rec­om­mend unen­crypted cre­den­tials – but we don’t want to get too com­plex either (KISS!). The PAM mod­ule pam_pwdfile.so is per­fect for this. It’s avail­able in the pack­age libpam-pwdfile.

A word about secu­rity.
pam_pwdfile.so is only as secure as your system’s crypt() func­tion. That can mean any­thing from utterly hor­ri­ble to fairly secure. If this is a con­cern to you, seek an alter­na­tive – vsftpd doesn’t care. Just adjust the instruc­tions given for PAM as necessary.

The next step is to cre­ate a PAM ser­vice def­i­n­i­tion for vsftpd (one could use the stock one, but I pre­ferred to go from scratch; it’s eas­ier to debug). Cre­ate a file with a name to your lik­ing (I chose to name it “vsftpd-virtual”) in /etc/pam.d/. It should look like this:

auth    required        pam_listfile.so item=user sense=deny file=/etc/ftpusers onerr=succeed
auth    [success=done default=ignore]      pam_unix.so nullok_secure
auth    sufficient      pam_pwdfile.so pwdfile /etc/vsftpd.passwd
account required        pam_permit.so
auth    required        pam_deny.so

The first line make sure that some stan­dard behav­iour (per ftpd(8)) is retained, it’s sim­ply copied from Debian’s stock vsftpd PAM ser­vice def­i­n­i­tion. The next two lines allow either sys­tem users or vir­tual users to login – which is what I want. The “account” line is required, but as I don’t care for expired users or sim­i­lar, it’s set to give an OK in any case. The last line is needed because of the “stack­ing” phi­los­o­phy of PAM 2.

Cre­ate the user database

The next step is to cre­ate /etc/vsftpd.passwd. It can be cre­ated with htpasswd:

htpasswd -c /etc/vsftpd.passwd USERNAME

Obvi­ously, you replace USERNAME with any user­name you like. Adding fur­ther users, delet­ing them, updat­ing pass­words is explained in the Man­page of htpasswd.

Note: I found some erratic behav­iour with htpasswd. On some runs, when cre­at­ing new users, it seemed to MD5-hash (to fur­ther it, it uses a cus­tom ver­sion of the algo­rithm) the pass­words, instead of crypt(). That can be reme­died by sim­ply re-running htpasswd (you may want to force the use of crypt() with the –d option).

Test­ing the PAM service

Before going on, make sure that the above things works. That’s were pamtester comes into play. It’s a lit­tle tool that does noth­ing more than val­i­date an username/password pair against par­tic­u­lar PAM ser­vice. For Debian, it’s avail­able in libpam-dotfile. Test by:

pamtester vsftpd-virtual USERNAME

whereas “vsftpd-virtual” is the name of the file you’ve cre­ated ear­lier on. Don’t con­tinue unless wrong logins are rejected, and good ones accepted. You’ll spare your­self a lot of has­sle in case some­thing else doesnt’ work as expected later on.

Con­fig­ure vsftpd

Now hav­ing your PAM sup­port­ing user that aren’t really exisit­ing on the sys­tem, it’s now time to con­fig­ure vsftpd (usu­ally done in /etc/vsftpd.conf). These are the impor­tant bits:


The first impor­tant thing to note ist that vsftpd itself dis­tin­guishes only between two kinds of users: anony­mous and authen­ti­cated. By Default, vsftpd allows only anony­mous logins. To enable authen­ti­ca­tion, one must set local_enable to YES, which allows vsftpd to use the PAM frame­work. For me, that opens up the sys­tem a bit too wide – also, it still wouldn’t allow vir­tual users to login: Since they are not known to the sys­tem, vsftpd would fail to con­tinue the ses­sion after login, since it can­not “become” this user. Hence, the guest_enable  3set­ting. This will cause vsftpd to remap any user that logged in to a sin­gle, pre­set user – “ftp” by default. This is good here.

That’s it! After restart­ing vsftpd, you’ll have vir­tual users working.



  1. The “secure_email_list” isn’t a real back­end, it just val­i­dates login cre­den­tials for oth­er­wise anony­mous logins, whereas vir­tual users are han­dled dif­fer­ently.
  2. If none of the “auth” mod­ules above (so, ear­lier eval­u­ated) returns sucess, access would be granted in any case, since they are both not explic­itly required – just suf­fi­cient. This is an impor­tant dis­tinc­tion. But wait, the last line reads “required”, and pam_deny.so (as the name sug­gest) does return fail­ure in every case, wouldn’t that deny any­one from using the ser­vice? The answer is No – the first “auth” mod­ule to be suf­fi­cient for autho­riza­tion and return­ing suc­cess causes the sub­se­quent “auth” mod­ules not to be eval­u­ated.
  3. This will how­ever, force the vir­tual user’s restric­tions on the reg­u­lar users as well. If you don’t want this, look into the chroot_local_user and chroot_list_enable set­tings.

Leave a Reply