Enabling a Synology NAS to host Keepass files over http

Note that I’m not using this anymore – but rather a local file synchronized with a dropbox-like system. I’ve noticed that this doesn’t work with Ubuntu (latest release) and keepass somehow doesn’t like the https (although it does work with windows).

Introduction

I have been a big fan of lastpass for the last few years. Their service is great, and uptime outstanding. The only problem I have with them is that they completely lack transparency. It works, but nobody knows how, where and why. Additionally (and that actually was the trigger that finally made me think of the switch, although I had some other issues as well), my company has blocked lastpass servers from their proxy, so I cannot sync anymore with lastpass.

So I looked for an alternative, where I could have full control of my passwords. Here are a few requirements that I had:

  • opensource software,
  • should work on both linux and windows,
  • secured location for storage, not depending on a single company,
  • security of the passwords regarding theft,
  • accessible via https proxy on port 80 only (that’s my company’s restrictions, can’t blame them for it),
  • portable solution,
  • enable to share passwords (typically with my wife),
  • possibility of segmenting the password storages (one for work, one for sensitive info, one for non sensitive sites, etc.),

Warning note

Please note that this article is quite technical on some aspects. If you’re not a highly technical person, I believe you should stick with lastpass or any other “on the cloud” solution, since you are more at risk of leaving your passwords unprotected on your own than these professional solutions will do for you. Better have someone professional handling your data than handling your data yourself unprofessionally. 🙂

Here are the notions you should be familiar with before starting reading:

  • keepass or password managers (such as lastpass) and automatic filling password addons/software in forms, well that should be ok otherwise you wouldn’t reach this page at all,
  • apache, configuring websites and their security in general, along with php,
  • good knowledge and understanding of DNS and SSL, along with basic understanding of the HTTP protocol,
  • basic notions in PHP (to understand and adapt one PHP file near the end of this tutorial).

Just to get you thinking a little, after just 24 hours with my synology box on the web, I’ve had:

  • one probe for common admin php files on the web server (in case I had left some admin files for phpmyadmin and other common apps),
  • a couple of probes on other ports,
  • lots of incoming connections on the webserver from unknown IP addresses (God knows what they were looking for – and they were not robots),
  • a couple of probes to see if they can use my webserver as a proxy.

And that’s just in less than 24 hours.

First step: setting up your SSL-enabled web server

First of all, whichever solution you choose, you will need to have a web server that is reachable on the internet if you want to sync all your devices from anywhere. I actually would recommend it only if you have a really good understanding of what this implies security-wise. I’ll cover the basic steps needed for that, but this is not the main point of this article so I won’t go into much detail (which is why you need to have some background on this).

First of all, if you want a web server accessible from the internet on your synology box, there are a few security tips that you should at least follow (not saying this is an exhaustive list, that’s the strict minimum):

  • enable https otherwise any authentication (login/password) you send is sent in clear text over the network, and nobody wants that, for sure, when it comes to storing all your passwords (even though keepass files are encrypted),
  • if you have http enabled, you’ll probably want to have logs… by default the logs for apache are disabled (except Error logs) so I would recommend you activate them by editing /etc/httpd/conf/httpd.conf-user and switching the comments for these lines:
#CustomLog /dev/null combined
CustomLog /var/log/httpd/user-access_log combined

Then restart your http server:

httpd -k restart

To make the change permanent (the httpd.conf file is rewritten by the synology box), you also need to make the same change in /etc.defaults/httpd/conf/httpd.conf.

Then in order to have keepass correctly syncing over https, you’ll need a certificate for your web server (keepass fails to sync otherwise because it cannot trust the server), which means that:

  • you can use Synology’s own reverse DNS (my.synology.me), otherwise you need to acquire a real domain name (I’m afraid you’ll have to pay for that if you want something reliable), along with a valid email address on that domain,
  • you need a certificate for your synology box, you can follow this tutorial to create one and get your synology box to use it (a self-signed certificate is ok to use with keepass as long as you go check the box “Accept invalid SSL certificates” in the section Options/Advanced/File Input/Output Connections in the Keepass UI),

Then of course you need to redirect your local home router to the synology box: go to your router’s admin panel and redirect ports 80 and 443 (that’s the strict minimum for http and https to work, you may need to open more ports if you want to also have your mail on your synology box). Be sure that you have some firewall running somewhere between the internet and your http server, there are rogue people out there, and this is not a joke…

Once that is done, check that your web server is working on https. You can reach it from the outside via your proxy and your certificate is working? Note that you should also make sure that php is working on your web site. You may need to wait for some time (typically a full day) for DNS to correctly redirect you if you’ve just registered your site.

Everything green? Great! We can go to the next part now.

Second part: switching from lastpass to keepass

That part is pretty straightforward. The main point here is to choose a password manager, and as far as open source is concerned, there aren’t many alternatives. Keepass is the most widespread solution, and has quite a large community supporting it.

Download keepass and the appropriate plugin(s) for your navigator(s) (I used keefox on Firefox and chomeipass on Chrome) and create a new keepass file. Connect the plugin(s) to keepass (that should be easy).

Export your lastpass passwords to a csv file. Open that csv file into excel or open office calc (use the comma as separator and double quote as protecting character): last pass sometimes messes up the format, and I ended up with 3 entries that had more columns than they should have.

Once your csv file is fixed, create a new keepass file (anywhere on your computer for now) and import it into keepass (there is a special import format for lastpass, let it do its job, I didn’t have any problem with it).

Third part: enabling your plain web server to deal with keepass

Now comes the tricky part. If you cannot use webdav because of a proxy that’s blocking everything except ports 80 and 443 and accepting only http(s), then you’ll have to do some hacking around for your apache server to accept non standard operations on the directory where you store your keepass encrypted files.

When keepass tries to save its file on a http (and https) server, it uses:

  • the GET http method, that should be ok for any server,
  • the PUT http method, this gets trickier as it doesn’t work on any “normal” web server, you normally need webdav for that,
  • the DELETE http method, same than for PUT,
  • the MOVE http method, again pure webdav (even purer than PUT and DELETE which at least exist on normal http servers, even if they may not be implemented by the server).

So we need to do some little magic here. The main problem we have is that unless you’re willing to write a new plugin for keepass (which would be possible, but what else would it do than the standard keepass behaviour? it would have very little advantage), you have to make the server deal with what keepass is sending it.

The trick will consist of 2 separate things: using url rewriting, we will redirect the PUT/DELETE/MOVE methods to a php file that will do these operations for us. So we need both URL rewriting on that directory, and write the php file.

URL rewriting

We’ll use a .htaccess file to do the URL rewriting. Now lets imagine that we call our php file “methods.php” (this is not a smart name as it can be guessed, I’ll let you be inventive about the name) and your keepass files are in the path “/keepass” of your web server (again not a good idea, come up with some non obvious name).

Here is a .htaccess file you can have in your directory to redirect calls to your php file:

<Limit GET HEAD POST PUT DELETE OPTIONS MOVE>
Order allow,deny
Allow from all
</Limit>
<LimitExcept GET HEAD POST PUT DELETE OPTIONS MOVE>
Order deny,allow
Deny from all
</LimitExcept>
RewriteEngine on
RewriteBase /keepass
RewriteCond %{REQUEST_METHOD} (PUT|DELETE|MOVE)
RewriteRule ^(.*)$ methods.php?url=$1 [L]

We could also imagine redirecting GET calls to the php file (for instance to filter out any intruder using PHP – but we could also filter out from the .htpass file which would probably be more secure in most cases).

Authenticating with the php file

You’ll need a file that I grabbed from somewhere else called “auth.php” (again it would be preferable to rename it – you don’t want anyone “fishing” your website, remember that hacking can actually take place when you leave any crumbs and things to hang onto):

<?php
class auth_digest {
 private $data;

 function __construct($txt) {
 // protect against missing data
 $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
 $this->data = array();

 $txt = explode(',', $txt);
 foreach ($txt as $param) {
 list($k, $v) = explode("=", $param, 2);
 $this->data[$k] = trim($v, "\"'");
 unset($needed_parts[$k]);
 }
 if ($needed_parts) throw new InvalidArgumentException($txt, 1);
 }
 function __get($k) {
 if (!isset($this->data[$k]))
 throw new InvalidArgumentException("Digest does not contain '$k'", 2);
 return $this->data[$k];
 }
 function valid_response($A1) {
 $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $this->data['uri']);
 $valid_response = md5(
 $A1 . ':' .
 $this->data['nonce'] . ':' .
 $this->data['nc'] . ':' .
 $this->data['cnonce'] . ':' .
 $this->data['qop'] . ':' . $A2
 );
 return $this->data['response'] == $valid_response;
 }
}

class authorizer {
 private $realm;
 private $users; // arrayIterator over array ('username' => 'password')
 private $plainpass; // TRUE if $users stores passwords in plain text

 function __construct($auth_realm, ArrayIterator $userIterator, $plainpw = TRUE) {
 $this->realm = $auth_realm;
 $this->users = $userIterator;
 $this->plainpass = $plainpw;
 }
 static private function parse_digest($txt) { // parse the http auth header
 // protect against missing data
 $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
 $data = array();

 $txt = explode(',', $txt);
 foreach ($txt as $param) {
 list($k, $v) = explode("=", $param, 2);
 $data[$k] = trim($v, "\"'");
 unset($needed_parts[$k]);
 }
 return $needed_parts ? FALSE : $data;
 }
 private static function gen_nonce() {
 return date('Ymd') . uniqid();
 }
 private static function nonceok($nonce) {
 return date('Ymd') == substr($nonce, 0, 8);
 }
 private static function unauth($status_text, $body) {
 header("HTTP/1.1 403 $status_text");
 die($body);
 }
 private function getlogin() { // dies
 header('HTTP/1.1 401 Unauthorized');
 header('WWW-Authenticate: Digest realm="'.$this->realm.'",qop="auth",nonce="'.self::gen_nonce().'",opaque="'.md5($this->realm) . '"');
 // display if user hits Cancel
 die('OK, you obviously know when you are beaten.');
 }
 function check() {
 if (empty($_SERVER['PHP_AUTH_DIGEST'])) $this->getlogin();

 // analyze the PHP_AUTH_DIGEST variable
 try {
 $d = new auth_digest($digest_text = $_SERVER['PHP_AUTH_DIGEST']);
 }
 catch (InvalidArgumentException $e) {
 self::unauth('Bad Credentials', "Parse error: $digest_text");
 }
 if (!isset($this->users[$uname = $d->username]))
 self::unauth('Unauthorized user', "Wrong credentials: user '$uname' unknown.");

 // check valid response
 $A1 = $this->users[$uname];
 if ($this->plainpass) $A1 = md5("$uname:$this->realm:$A1");
 if (!$d->valid_response($A1))
 self::unauth('Invalid Response', "Wrong credentials: digest=$digest_text");
 if (!self::nonceok($d->nonce)) $this->getlogin();
 return $uname;
 }
}

The php file that will do the job

Now comes the real php file (called methods.php in our example – rename it and change the red values to your own custom values):

<?php
require 'auth.php';
// customize the following defines:-
define('AUTH_REALM', 'my.website.name/path');
define('URL_BASE', 'https://my.website.name/path');

/* ---
 The following sample authorization function uses an array of
 user names and passwords defined and fixed here.
 An actual implementation might read the array from an external
resource.
 The use of an ArrayIterator makes it moderately easy to extend this
 mechanism.
--- */
function authorize() {
 $auth = new authorizer(AUTH_REALM, new ArrayIterator(array('username1' => 'password1', 'username2' => 'password2')));
 $auth->check(); // dies if not OK
}
function write_log($txt) {
 file_put_contents("<path_to>/log.txt", date('c')." IP:".$_SERVER['REMOTE_ADDR']." User:".$_SERVER['REMOTE_USER']." https:".$_SERVER['HTTPS']." Method:".$_SERVER['REQUEST_METHOD']." URI:".$_SERVER['REQUEST_URI']." ".$txt."\n", FILE_APPEND);
 chmod("<path_to>/log.txt", 0666);
}

function puterror($status, $body, $log = FALSE)
{
 header ("HTTP/1.1 $status");
 if ($log) write_log($log);
 die("<html><head><title>Error $status</title></head><body>$body</body></html>");
}

function getfilename()
{
 $extension = pathinfo($_SERVER['REQUEST_URI'], PATHINFO_EXTENSION);
 if (($extension != 'kdbx') && ($extension != 'tmp')) puterror('403 Forbidden', "Bad file type in $fname");
 return pathinfo($_SERVER['REQUEST_URI'])['basename'];
}

function putfile() {
 $f = fopen($fname = getfilename(), 'w');
 if (!$f) puterror('409 Create error', "Couldn't create file");
 $s = fopen('php://input', 'r'); // read from standard input
 if (!$s) puterror('404 Input Unavailable', "Couldn't open input");
 while($kb = fread($s, 1024)) fwrite($f, $kb, 1024);
 fclose($f);
 fclose($s);
 chmod($fname, 0666);
 $fname = URL_BASE . $fname;
 header("Location: $fname");
 header("HTTP/1.1 201 Created");
 echo "<html><head><title>Success</title></head><body>";
 echo "<p>Created <a href='$fname'>$fname</a> OK.</p></body></html>";
}

function deletefile()
{
 $fname = getfilename();
 if (file_exists($fname))
 {
  unlink($fname);
  if (file_exists($fname)) puterror("403 Could Not Delete", "File is still present after delete");
 }
 $fname = URL_BASE . $fname;
 header("Location: $fname");
 header("HTTP/1.1 201 Deleted");
 echo "<html><head><title>Success</title></head><body>";
 echo "<p>Deleted <a href='$fname'>$fname</a> OK.</p></body></html>";
}

function movefile()
{
 $fname = getfilename();
 write_log("move ".$fname." to ".substr($fname, 0, strlen($fname) - 4));
 if (rename($fname, substr($fname, 0, strlen($fname) - 4)))
 {
  $fname = URL_BASE . $fname;
  header("Location: $fname");
  header("HTTP/1.1 201 Renamed");
  echo "<html><head><title>Success</title></head><body>";
  echo "<p>Renamed <a href='$fname'>$fname</a> OK.</p></body></html>";
 }
 else
 puterror("403 Could not rename", "Could not rename");
}

if ($_SERVER['REQUEST_METHOD'] == 'PUT')
{
 authorize();
 putfile();
}
else if ($_SERVER['REQUEST_METHOD'] == 'DELETE')
{
 authorize();
 deletefile();
}
else if ($_SERVER['REQUEST_METHOD'] == 'MOVE')
{
 authorize();
 movefile();
}
else
 header("HTTP/1.1 403 Bad Request");

write_log($status);

You’ll note that we authorize for reading/writing both .kdbx and .tmp files, because keepass writes and moves tmp files as well as its own keyfiles. Note that you can aslo change the kdbx extension for more security.

You’re all set. Now you can move your kdbx file to your webserver, and redirect keepass to open the file via https://your.server/path/file.kdbx . Of course your kdbx should be encoded with at least 2 methods (I prefer the password+keyfile method, you can’t use the windows account if you’re using several computers anyway – and what happens if your machine goes south and you have to reinstall?).

Conclusion

We now have:

  • a ssl-enabled web server,
  • a directory on the web server that specifically handles the case of accepting the methods needed for keepass,
  • our keepass files stored on the server and accessible from the internet in a secure way using authentication.

And again, I would never stress enough the fact that you should encrypt the files not only with a password but also with a key file that you only keep locally.

Advertisements

8 thoughts on “Enabling a Synology NAS to host Keepass files over http

  1. Thanks for the great tip! I keep receiving 500 internal server error after the setup and the kdbx file cannot be saved/uploade to the web server. Could you suggest possible solutions?

    1. Hi,
      I’ve just changed a few things in the post (including using a different method to get the extension of the file, as the current keepass http plugin uses the extension “.kdbx.tmp” which caused problems with the script).
      Error 500 is definitely not a good sign. I suggest you add some logs in the php file to check where it fails. Is your HTTP server Apache? You’re mentioning that you cannot save, but you can read the file, right? This may be about the URL rewriting? Have you renamed the “methods” file and changed its reference in myauth.php?

      1. Thanks for highlighting the custom values. Unfortunately, it is still spitting out Error 500.
        I initially thought your script authenticates users based on DSM user accounts.
        If the credentials are saved in plain text such as in the sample methods.php, would it not pose a security risk?

      2. Hi,
        as long as nobody has an FTP access to your web server to download the file(s), there is no risk, since the webserver will always expose the php files by “executing” them, so the values of the variables will never be seen by anyone browsing the site. If you wanted to be maybe a little more secure, you could store these in a file located in a directory that is not accessible from the web server. Of course, authenticating with the Synology box’s users is the ideal best solution. Will investigate on that when I have some time (a simple googling gave me this page which seems to have the answer).
        About your Error 500, I guess you’re only left with the possibility of adding traces to see where the script fails. Let me know your findings, in that case I would fix my script here as well.

  2. Thanks for the information! You saved me from having to do the heavy lifting and figuring out how to write the files. I will have to try this VERY soon.

  3. I started to go through this tutorial but hit a problem on configuring the httpd.conf file. I don’t know if there is a difference in file locations when you have another type of Synology but in my case (216 Play) there is no httpd.conf file in etc.defaults\httpd\conf. I had to change the httpd.conf-user file in etc\httpd\conf in order to make the changes in httpd.conf stick after reboot.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s