Implementing Secure Password Storage with PHPCredlocker and a Raspberry Pi

Password storage can be a sensitive business, but no matter whether you're using PHPCredlocker or KeePassX, dedicated hardware is best. The more isolated your password storage solution, the less likely it is that unauthorised access can be obtained.

Of course, dedicated hardware can quickly become expensive. Whilst it might be ideal in terms of security, who can afford to Colo a server just to store their passwords? A VPS is a trade-off - anyone with access to the hypervisor could potentially grab your encryption keys from memory (or the back-end storage).

To try and reduce the cost, whilst maintaining the security ideal of having dedicated hardware, I set out to get PHPCredlocker running on a Raspberry Pi.

This documentation details how to build the system, a Raspberry Pi Model B+ was used, but the B should be fine too

 

 

Assumptions

  • Some familiarity with the Raspberry Pi
  • Some means of creating a DNS Hostname resolving to your Pi (if not, make all NGinx config changes within the default server blocks and access via IP

 

Designing the System

With the exception of SD Card capacity, the hardware limitations are pretty much set into stone, so the system needed to be designed to be as efficient as possible.

So we're going with the following set up

  • MySQL backend
  • NGinx & PHP
  • Raspbian base (for simplicities sake)
  • HTTPS access only
  • 12GB SD Card

The Pi is quite small and easily stolen, so we're also going to want to make sure there are some additional precautions built into the system (whilst there are bigger concerns if your house gets burgled, it'd be nice to reduce the number slightly).

From an OpSec point of view, we're going to work on the assumption that the Pi is connected directly to the Internet - though realistically it's far better if you're able to place it behind a reverse proxy of some form

 


 

Setting up the Base System

As with any Pi based build, the first step is to grab a copy of Raspbian and write it to the SD card. Run through the usual setup steps (resizing to fill the SD, setting a sane hostname and enabling SSH access).

 

NGinx, PHP and MySQL

The first thing to do is to get our various daemons installed (note: run each seperately, don't merge into one apt-get install!)

apt-get update
apt-get install php5-fpm

# When prompted set a good strong password
apt-get install mysql-server
apt-get install php5-mcrypt
apt-get install php5-mysql
apt-get install php5-cli
apt-get install nginx

We'll configure NGinx properly later, but for now we'll adjust the default Nginx server block so that we can do some testing. Add the following in /etc/nginx/sites-enabled/default

      location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}

fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}

Now we need to fix a security issue in PHP-FPM (it'll "helpfully" try and correct incorrect path/filenames, leading to easy information disclosures) 

echo "cgi.fix_pathinfo=0" >> /etc/php5/fpm/php.ini

We're also going to turn off expose_php, in part because it hides the version of PHP we're running, but also because it disables the PHP Easter eggs.


sed -i 's/expose_php = On/expose_php = Off/' /etc/php5/fpm/php.ini

 

Let's give it a test

service nginx restart
service php5-fpm restart
echo "<?php phpinfo(); " > /usr/share/nginx/www/test.php
wget -O http://127.0.0.1/test.php

You should see a lot of HTML being output, if so, NGinx and PHP are set up and ready to go

 

Preparing for Cryptography

The Pi is going to be doing a lot of Cryptography, from generating credential storage keys to establishing SSL connections. Unfortunately, in the configuration we'll be using it in (headless server with no IDE disks) it's a pretty low entropy device. Luckily, the Pi has a hardware Random Number Generator (HWRNG) built in, so we just need to enable that

echo "bcm2708_rng" >> /etc/modules
modprobe bcm2708_rng
apt-get install rng-tools
nano /etc/default/rng-tools

# Uncomment HRNGDEVICE=/dev/hwrng then save and exit

update-rc.d rng-tools defaults
service rng-tools restart

You could also install HAVEGED, but after reading about it on the net I still couldn't decide whether it was safe to use (it passes OK in rngtest and ent but that doesn't mean it's not predictable), so decided to err on the side of caution and skipped it.

Let's run an entropy test

cat /dev/random | rngtest -c 20000
rngtest 2-unofficial-mt.14
Copyright (c) 2004 by Henrique de Moraes Holschuh
This is free software; see the source for copying conditions.  There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

rngtest: starting FIPS tests...
rngtest: bits received from input: 400000032
rngtest: FIPS 140-2 successes: 19982
rngtest: FIPS 140-2 failures: 18
rngtest: FIPS 140-2(2001-10-10) Monobit: 3
rngtest: FIPS 140-2(2001-10-10) Poker: 2
rngtest: FIPS 140-2(2001-10-10) Runs: 5
rngtest: FIPS 140-2(2001-10-10) Long run: 8
rngtest: FIPS 140-2(2001-10-10) Continuous run: 0
rngtest: input channel speed: (min=154.143; avg=472.924; max=1046.859)Kibits/s
rngtest: FIPS tests speed: (min=698.593; avg=5464.670; max=6370.271)Kibits/s
rngtest: Program run time: 897705313 microseconds

You should expect to see some failures from time to time

 


 

Setting Up PHPCredlocker

Next we're going to grab a copy of PHPCredlocker and make a few system level tweaks to help protect our passwords against someone who may have achieved physical access

Make the Webroot

We're going to ensure that PHPCredlocker is accessible only via hostname, so it needs it's own directory root and server block

mkdir /usr/share/nginx/credlocker
cd /usr/share/nginx/credlocker
git clone https://github.com/bentasker/PHPCredLocker.git
mv PHPCredLocker/* ./
rm -rf PHPCredLocker/
chown www-data:www-data ./* -R
echo "tmpfs /usr/share/nginx/credlocker/sessions tmpfs nodev,nosuid,size=30M 0 0" >> /etc/fstab
mount /usr/share/nginx/credlocker/sessions
su www-data
touch sessions/test
ls sessions
rm sessions/test

Next we need to create the server block. In /etc/nginx/sites-enabled/default add the following


server {
listen 443;
server_name creds.PHPCredlockerPi.lan;

root /usr/share/nginx/credlocker;
index index.html index.php;

ssl on;
ssl_certificate /etc/nginx/keys/certs/creds.phpcredlockerpi.lan.crt;
ssl_certificate_key /etc/nginx/keys/private/creds.phpcredlockerpi.lan.key;

ssl_session_timeout 5m;

ssl_protocols SSLv3 TLSv1; # Removed SSLv2
ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
ssl_prefer_server_ciphers on;

try_files = $uri @missing;
location / {
try_files $uri $uri/ /index.html;
}

location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}

fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}

location ~ /\.ht {
deny all;
}
location /conf {
deny all;
}

location /sessions {
deny all;
}
}

We need to generate and drop our SSL certificates in the correct place (Note: If you can get a certificate generated rather than relying on a snake-oil one, it's a very good idea)


cd /root
openssl req -new -x509 -days 365 -nodes -out creds.phpcredlockerpi.lan.crt -keyout creds.phpcredlockerpi.lan.key
mkdir -p /etc/nginx/keys/certs
mkdir -p /etc/nginx/keys/private
mv /root/creds.phpcredlockerpi.lan.crt /etc/nginx/keys/certs
mv /root/creds.phpcredlockerpi.lan.key /etc/nginx/keys/private

We have three tasks left, reload Nginx, add PHPCredlocker's crontask and create a MySQL database

service nginx reload
crontab -e
50 23 * * * CRON_PASS="xxxxx" php /usr/share/nginx/credlocker/cron.php mysql -p #enter the MySQL password when prompted create database PHPCredLocker_Pi;
create user 'credlocker'@'localhost' IDENTIFIED BY 'SET A STRONG PASSWORD';
GRANT ALL PRIVILEGES ON PHPCredLocker_Pi.* TO 'credlocker'@'localhost';

 

Securing Credlocker's Configuration

One of our design requirements was to reduce the capabilities of someone who gains physical access to the Pi, so let's ensure that attempting to move the Pi leaves it in as secure a state as possible

cd /usr/share/nginx/credlocker/
mv conf conf.old
mkdir conf
chown -R www-data:www-data conf
echo "<?php die('Keys havent been decrypted'); ?>" > config.php
apt-get install cryptsetup
cd /root
dd if=/dev/urandom of=credlockerconf bs=1M count=80 # Create an 80MB container
losetup /dev/loop0 credlockerconf
cryptsetup luksFormat /dev/loop0 #Set a strong password when prompted
cryptsetup luksOpen /dev/loop0 testfs
mkfs.ext2 /dev/mapper/testfs
mkdir /tmp/test
mount /dev/mapper/testfs /tmp/test/
umount /tmp/test/
cryptsetup luksClose /dev/mapper/testfs
losetup -d /dev/loop0

We've now created an encrypted container, which we'll be using to store Credlocker's configuration (which should never reach the 80MB limit we've imposed). It's not particularly convenient to have to type all that every time we want to mount it though, so lets grab some utility scripts

git clone https://github.com/bentasker/phpcredlockerpi_utilityscripts.git
cd phpcredlockerpi_utilityscripts/
./decrypt_configuration.sh # Use the password you entered earlier
df -h | grep credlocker
tmpfs 30M 0 30M 0% /usr/share/nginx/credlocker/sessions
/dev/mapper/credlockerconf 76M 1.6M 71M 3% /usr/share/nginx/credlocker/conf

cd /usr/share/nginx/credlocker
mv conf.old/* conf/
chown www-data:www-data -R /usr/share/nginx/credlocker
chmod 770 /usr/share/nginx/credlocker/

So now, if the Pi reboots, the decryption keys will be unavailable and the front-end will simply show Keys havent been decrypted. Let's go a step further though, and add a script so that if the Network link drops out, the keys unmount

nano /etc/network/if-down.d/credlocker

#!/bin/bash
#
# If the interface has gone down, unmount the credlocker config just in case some-ones physically nabbing the device

/root/phpcredlockerpi_utility_scripts/unmount_configuration.sh

# Save and exit
chmod +x /etc/network/if-down.d/credlocker

You can, of course, go further and create similar behaviour for failed console logins (or even if a USB keyboard is plugged into the device) but that's outside the scope of this documentaion

 

We're now ready to run the install itself, so use a browser to access the hostname we set earlier (so in my case https://creds.PHPCredlockerPi.lan) in NGinx's configuration and follow through the installer (see the installation documentation if you need help).

 


 

Tightening Security

We should now have PHPCredlocker installed and configured, however there are still a few key bits to do to help ensure the security of the system as a whole

Firewall

We only ever need two ports to be open - 22 and 443, so let's allow those and drop everything else on the public interface

apt-get install iptables-persistent
iptables -I INPUT -p tcp --dport 22 -j ACCEPT
iptables -I INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT

# Optional - respond to Ping?
iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT

# Change the default policy to drop
iptables -P INPUT DROP
/etc/init.d/iptables-persistent save

 

Changing User Passwords

You may, out of habit, already have done this. But if not, the default passwords must be changed, we're also going to remove the user pi's ability to use sudo without a password

passwd pi # Set something strong
nano /etc/sudoers
# Find the line relating to pi and change to
pi ALL=(ALL) ALL

There should only be one user with a default password at install time, but it's prudent to double check


cut /etc/shadow -d: -f1,2

Any user with anything but * or ! has a password set, if you didn't set it, then change it!

 

Fail2Ban

Although you'll hopefully be sitting the Pi behind a firewall, we're working on the assumption that the system is, or may be exposed. So we'll configure fail2ban's SSH jail

apt-get install fail2ban
update-rc.d fail2ban defaults
cat << EOM > /etc/fail2ban/jail.local
[ssh]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 4
EOM

 

Optional Extras

There are a number of additional steps you can take to tighten security further, but for the sake of brevity I'll simply list them here:

  • Installing PHPChangedBinaries
  • Configuring Outbound Firewall Rules
  • Installing ssmtp and enabling mail notifications
  • Installing RKhunter and ClamAV
  • If using a Reverse Proxy, limiting access on port 443 to that source IP
  • Set a root password, remove pi's sudo privileges completely, and use su instead

 


 

Backups

We should now have a secure password repository, but the data is currently at risk because we haven't configured a backup.

Credlocker's Database is already encrypted, and the keys are in an encrypted container, so theoretically it's probably OK to back them up as is. However, it's no extra hassle to configure some encryption, so let's err on the side of caution and configure some encrypted backups (if you want a more thorough explanation, or to use incrementals, see implementing encrypted incremental backups with s3cmd)

Start by grabbing the utility files

cd /root
git clone https://github.com/bentasker/BackupEncryptionScripts.git

Next we'll generate our backup key-pair

cd /root
mkdir keys
cd keys
openssl genrsa -out backupkey.key 4096
openssl rsa -in backupkey.key -out backupkey-public.pem -outform PEM -pubout

Move the private key (backupkey.key) off the Pi, to somewhere very safe (on a CD guarded by Orcs tends to be the standard advice)

Now let's create our backup script, save the following as /root/backup.sh

#!/bin/bash
#
# CredlockerPi Backup script

DATE=$( date +'%Y-%m-%d' )
echo "Backing up files"
cd /usr/share/nginx/
tar -czf /tmp/credlocker_files-$DATE.tar.gz --exclude="*/conf/*" --exclude="*/sessions/*" credlocker

cd /tmp
/root/BackupEncryptionScripts/OpenSSL_Encryption/opensslencrypt.sh -p 1 -f credlocker_files-$DATE.tar.gz -k /root/keys/backupkey-public.key -D random
rm credlocker_files-$DATE.tar.gz

echo "Backing up Config"
cd /root
/root/BackupEncryptionScripts/OpenSSL_Encryption/opensslencrypt.sh -p 1 -f credlockerconf -k /root/keys/backupkey-public.key -D random
mv credlockerconf.tar.enc /tmp/credlockerconf-$DATE.tar.enc

echo "Backing up Database"

dbname=$(grep dbname /usr/share/nginx/credlocker/conf/config.php | cut -f2 -d= | sed 's/.$//')
dbuser=$(grep dbuser /usr/share/nginx/credlocker/conf/config.php | cut -f2 -d= | sed 's/.$//')
dbpass=$(grep dbpass /usr/share/nginx/credlocker/conf/config.php | cut -f2 -d= | sed 's/.$//')

mysqldump -u$dbuser --password=$dbpass $dbname > credlocker_db-$DATE.sql
/root/BackupEncryptionScripts/OpenSSL_Encryption/opensslencrypt.sh -p 1 -f credlocker_db-$DATE.sql -k /root/keys/backupkey-public.key -D random
mv credlocker_db-$DATE.sql.tar.enc /tmp
rm -f credlocker_db-$DATE.sql

You can then move the backup files from /tmp to your backup server. See Implementing encrypted incremental backups for details on how to decrypt the backups if you ever need to recover them

 


 

Conclusion

Although this article has proven to be quite long, getting PHPCredlocker running on a Raspberry Pi didn't provide as many challenges as I originally expected. I had been concerned that the amount of Crypto in use might prove challenging given the resources available, but with one exception you wouldn't know it was running on a Pi at all.

The single exception to that, is during login. PHPCredlocker uses bcrypt to hash passwords, bcrypt by design is computationally expensive so the Pi can take a second to verify the submitted password, but it's not a huge delay and only ever happens once per session.

The resulting setup should prove to be fairly resilient against attack. Even when explicitly targeted by someone willing to gain physical access to the Pi, there should be enough obstacles present to ensure that there is time to go round and change all the passwords, even if the defences are eventually breached.

The most important thing, once the device is set up, is that it's left to do it's one job. As tempting as it may be to add other services/functionality, the entire premise of using a dedicated device is to help reduce the attack surface.

 It's also pretty important not to lose the passwords/keys we set during setup, so a secure note needs to be made somewhere. Don't store them in PHPCredlocker though, as the only time you're likely to need them is when Credlocker is down!