ADUFRAY

Do you run an nginx-based web server? Do you use the http_auth_basic_module module? This post might be for you!

I just set up a new FreeBSD server (blog post forthcoming) and decided to skip the install of httpd, preferring nginx. All the cool kids are using it, so I figured I should as well. One problem: no htpasswd command.

There are several webapps I’ve written over the years for my own personal uses, and they all use HTTP Basic authentication to regulate access. nginx happily supports this, if you have an existing htpasswd-formatted file to use. Unfortunately, you cannot create one since it does not supply a tool to do so.

On FreeBSD I found a simple enough workaround:

$ echo YOUR_PASSWORD | \
  pw useradd YOUR_USERNAME -h 0 -N | \
  awk -F: '{print $1"$"$2}' | \
  awk -F'$' '{print $1":$6$rounds=5000$"$4"$"$5;}'

Thankfully FreeBSD’s (and perhaps your distro’s) crypt(3) supports strong password hashes, the default being SHA512 with 5,000 rounds. When you supply the -N flag to the pw command, it will not actually perform the action, but instead print out the result (incluiing the hashed password, which is what we’re after).

SHA512 with 5,000 rounds (and a random, 32-character salt) is probably more security than anyone actually needs. If we want more than that, though, we have options — other than installing httpd and gaining htpasswd. I wrote a super-simple PHP script to generate a bcrypt hash with selectable cost (2^x rounds):

bcrypt_hash.php [user]

#!/usr/local/bin/php
<?php
  // 10 is reasonable, but time the output of this script to test what is reasonable for you
  $cost = 10; 

  // write prompt to stderr so we can redirect output with > and only include what we want
  $stderr = fopen('php://stderr', 'w+');
  fwrite($stderr, 'Enter password: ');

  // disable echoing the password to terminal
  system('stty -echo');
  $passwd = stream_get_line(STDIN, 1024, PHP_EOL);
  system('stty echo');

  fwrite($stderr, "\n");

  // at least on FreeBSD you need to replace PHP's bcrypt hash version 2y with OpenBSD's 2b
  // Fixed in secure/lib/libcrypt/crypt-blowfish.c revision 284483 
  // https://svnweb.freebsd.org/base?view=revision&revision=284483
  $hashed = str_replace('$2y$', '$2b$', password_hash($passwd, PASSWORD_BCRYPT, array('cost' => $cost)));
  echo  ( (isset($argv[1])) ? $argv[1] : "user" ) . ":$hashed\n";
?>

FreeBSD’s pw will generate a bcrypt-based hash if you edit /etc/login.conf and add

blf_users:\
    :passwd_format=blf:\
    :tc=default:

Then rebuild the mapping with cap_mkdb /etc/login.conf and add -L blf_users before the -h flag in the aforementioned command. Sadly pw will use the default cost value of 4, which seems pointless to me. Looking over /usr/src/usr.sbin/pw/pw_user.c the pw_pwcrypt() function does not support changing the random salt that is generated to a format specifying the cost value — better to use the above PHP script.