Self-hosted team password manager using KeeWeb
Tags: security, opensource, passwordmanager
Published: 2020-03-22 13:55:58

Where I work, we really needed a team password manager that's kept up to date with any password that's been added by a team member. In addition to that, we needed it to be protected by an LDAP authentication and self-hosted (I personally can't trust any cloud-based password manager).

Sadly, most of the open source web-based password managers that offer self-hosting and LDAP authentication were paid.

# Recipe
Lucky for me (and for you), we can build what we want using the following free and open source applications:

- Apache Httpd 2.4
- Apache LDAP & WebDAV modules
- KeeWeb https://keeweb.info/ by the amazing [@antelle](https://github.com/antelle)

We all know what Apache httpd and its mods are, but let's talk a little about KeeWeb ([Github repo](https://github.com/keeweb/keeweb)):

It's basically written in HTML/CSS and JavaScript, and it's based on [KeePass](https://keepass.info/), so you get almost all the features of the KeePass desktop app.
It decrypts your .kdbx database locally on the browser, so security++ ;)

It uses browser cache whenever it can, so keep an eye on that when redeploying your application.

# Time to get your hands dirty!
We'll deploy KeeWeb on an Apache httpd web server, we'll configure KeeWeb to look for our .kdbx database hosted on the same server using WebDAV protocol, and finally, we'll protect the access to the website by configuring the Apache LDAP module.

I'll be working on CentOS 7 so some commands may vary depending on your operating system.

So let's get started by installing Apache httpd.

`sudo yum install -y httpd`

Apache on CentOS loads automatically the necessary mods that we'll use in our configuration so don't worry about that.

Next, we have to get the resources of the KeeWeb application:

1. Download the `gh-pages` branch content: <br>
`wget https://github.com/keeweb/keeweb/archive/gh-pages.zip`

2. Unzip it at the root of your HTTP server.<br>
`sudo unzip gh-pages.zip -d /var/www/html/`

3. You should have a folder `/var/www/html/keeweb-gh-pages`, let's move its content to the parent folder:<br>
`sudo mv /var/www/html/keeweb-gh-pages/* /var/www/html/ && sudo rmdir /var/www/html/keeweb-gh-pages/`<br>
then `chown -R apache.apache /var/www/html/`

4. Next, we are going to write a Virtual Host for KeeWeb:<br>
`sudo vim /etc/httpd/conf.d/01-default.conf`

This is my full virtual host configuration file, we assume that the .kdbx database file will be stored in the `webdav` folder of our website.

```01-default.conf
DavLockDB "/var/www/html/webdav/DavLock"

<VirtualHost *:80>
  DocumentRoot "/var/www/html"

  # According to the dev of KeeWeb, OPTIONS request must work without authorization. So we'll keep it outside the following location tags
  RewriteEngine on
  RewriteCond %{REQUEST_METHOD} OPTIONS
  RewriteRule ^(.*)$ blank.html [R=200,L,E=HTTP_ORIGIN:%{HTTP:ORIGIN}]

  # If your Apache is behind a load-balancer, and the SSL certificate is handled by the Load-balancer, leave this.
  RequestHeader edit Destination ^https: http: early

  # Don't require LDAP authentication for a healthcheck
  SetEnvIf Request_URI "^/healhcheck" accessgranted=1
  
  <Location />
    Order deny,allow
    Satisfy any
    Deny from all
    Allow from env=accessgranted
    AuthName "LDAP Authentication"
    AuthType Basic
    AuthBasicProvider ldap
    AuthLDAPURL "ldap://your-ldap-url:389/dc=example,dc=com?uid"
    AuthLDAPBindDN ${Your-ldap-bind-dn}
    AuthLDAPBindPassword ${Your-ldap-bind-password}
    AuthLDAPGroupAttribute ${Your-ldap-membership-attribute}
    AuthLDAPGroupAttributeIsDN off
    Require ldap-group ${The-ldap-group-dn-you-require-your-users-to-belong-to}
  </Location>

  <Location "/webdav">
    DAV On
    AuthType "Basic"
    AuthName "webdav"
    Options Indexes
    Header always set Access-Control-Allow-Origin "*"
    Header always set Access-Control-Allow-Headers "origin, content-type, cache-control, accept, authorization, if-match, destination, overwrite"
    Header always set Access-Control-Expose-Headers "ETag"
    Header always set Access-Control-Allow-Methods "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK"
    Header always set Access-Control-Allow-Credentials "true"
  </Location>
  
  # Logs
  ErrorLog /dev/stderr
  CustomLog "|/usr/sbin/rotatelogs -l /var/log/httpd/apache_access.log.%Y-%m-%d 86400" combined
</VirtualHost>
```
# Quick explanation
`DavLockDB "/var/www/html/webdav/DavLock"` is a lockfile used by Apache for when our server will receive LOCK or UNLOCK requests to handle write-concurrency for example.

`RequestHeader edit Destination ^https: http: early` If your Apache is behind a load-balancer, and the SSL certificate is handled by the Load-balancer, leave this.

`SetEnvIf Request_URI "^/healhcheck" accessgranted=1` If you need a healthcheck URI that should not be protected by LDAP, set a variable `accessgranted=1` and make sure to add `Allow from env=accessgranted` inside the tag that checks the LDAP authentication.

Then comes a classic location tag that checks LDAP authentication and authorization, here we make sure the user belongs to the specified LDAP group. If you don't need the user to belong to a specific group, you can find plenty of examples on how to do it on the interwebs ;) :
```
<Location />
    Order deny,allow
    Satisfy any
    Deny from all
    Allow from env=accessgranted
    AuthName "LDAP Authentication"
    AuthType Basic
    AuthBasicProvider ldap
    AuthLDAPURL "ldap://your-ldap-url:389/dc=example,dc=com?uid"
    AuthLDAPBindDN ${Your-ldap-bind-dn}
    AuthLDAPBindPassword ${Your-ldap-bind-password}
    AuthLDAPGroupAttribute ${Your-ldap-membership-attribute}
    AuthLDAPGroupAttributeIsDN off
    Require ldap-group ${The-ldap-group-dn-you-require-your-users-to-belong-to}
  </Location>
```
 
Then a classic location tag to configure WebDav:
```
 <Location "/webdav">
    DAV On
    AuthType "Basic"
    AuthName "webdav"
    Options Indexes
    Header always set Access-Control-Allow-Origin "*"
    Header always set Access-Control-Allow-Headers "origin, content-type, cache-control, accept, authorization, if-match, destination, overwrite"
    Header always set Access-Control-Expose-Headers "ETag"
    Header always set Access-Control-Allow-Methods "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK"
    Header always set Access-Control-Allow-Credentials "true"
  </Location>
```

If you don't have a .kdbx file yet, I recommend to create one using the Keepass 2 desktop app, and copy it inside the /webdav folder.

Head off to your browser, and authenticate with your LDAP credentials you should see now the KeeWeb app! You can choose the webdav method by clicking More, then fill in the path to your .kdbx file, in our case it's `https://www.example.com/webdav/database.kdbx`. We didn't set any webdav user or password so hit OK then enter the master password and Voilà!

# App configuration

Not too fast, we don't want our users to go through all this jazz each time they want to access the app...

In the `index.html`, find the meta tag `kw-config` and make sure the attribute `content` has a value of `config.json`, we'll create this file later.
The whole html meta tag looks like this:<br>
`<meta name="kw-config" content="config.json">`

Next, create a file called `config.json` at the root of your website and fill it with the configuration you need from [here](https://github.com/keeweb/keeweb/wiki/Configuration#json-app-config). My config file looked like this:


```
{
	"settings": {
		"theme": "fb",
		"autoSave": true,
		"autoSaveInterval": 1,
		"canOpenDemo": false,
		"dropbox": false,
		"gdrive": false,
		"onedrive": false,
		"canExportXml": false
	},
	"files": [{
		"storage": "webdav",
		"name": "Database",
		"path": "/webdav/database.kdbx"
	}]
}
```

Make sure to delete your browser cache, and you're done!
<center><img src="https://i.imgur.com/fmI3kuq.gif"></center>

# Last words
The downside of this web application is that we can not apply ACL to each entry in the password manager, so it probably can not be shared by many teams at work. If you have any questions I'll be glad to help.

*Note: This article has been first published on my personal blog:* https://www.zaine.me/