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 -e50 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
tmpfs 30M 0 30M 0% /usr/share/nginx/credlocker/sessions
cd phpcredlockerpi_utilityscripts/
./decrypt_configuration.sh # Use the password you entered earlier
df -h | grep credlocker
/dev/mapper/credlockerconf 76M 1.6M 71M 3% /usr/share/nginx/credlocker/confcd /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!