git » kxd » commit 9133d48

Initial commit

author Alberto Bertogli
2014-01-12 19:20:24 UTC
committer Alberto Bertogli
2014-04-22 23:56:35 UTC

Initial commit

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>

.gitignore +5 -0
Makefile +69 -0
README +79 -0
cryptsetup/README +46 -0
cryptsetup/initramfs-hooks/kxc +27 -0
cryptsetup/initramfs-scripts/premount-net +27 -0
cryptsetup/kxc-cryptsetup +29 -0
doc/man/.gitignore +1 -0
doc/man/Makefile +11 -0
doc/man/kxc-cryptsetup.rst +62 -0
doc/man/kxc.rst +52 -0
doc/man/kxd.rst +104 -0
doc/quick_start.rst +98 -0
kxc/kxc.go +186 -0
kxd/email.go +106 -0
kxd/key_config.go +205 -0
kxd/kxd.go +228 -0
scripts/create-kxd-config +36 -0
scripts/default/kxd +11 -0
scripts/init.d/kxd +82 -0
scripts/kxc-add-key +58 -0
scripts/kxd-add-client-key +37 -0
scripts/systemd/kxd.service +10 -0
tests/.pylintrc +8 -0
tests/run_tests +328 -0

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c4db43c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+out/
+*.swp
+
+# Just in case, we ignore all .pem so noone commits them by accident.
+*.pem
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..22b86ac
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,69 @@
+
+GO = go
+OUTDIR = ./out
+
+default: kxd kxc
+
+kxd:
+	$(GO) build -o $(OUTDIR)/kxd ./kxd
+
+# For the client, because it can be run in a very limited environment without
+# glibc (like initramfs), we build it using the native go networking so it can
+# work even when glibc's resolvers are missing.
+kxc:
+	$(GO) build --tags netgo -a -o $(OUTDIR)/kxc ./kxc
+
+fmt:
+	gofmt -w .
+
+vet:
+	$(GO) tool vet .
+
+test: kxd kxc
+	python tests/run_tests -b
+
+tests: test
+
+
+# Prefixes for installing the files.
+PREFIX=/usr
+ETCDIR=/etc
+SYSTEMDDIR=$(shell pkg-config systemd --variable=systemdsystemunitdir)
+
+# Install utility, we assume it's GNU/BSD compatible.
+INSTALL=install
+
+install-all: install-kxd install-init.d install-kxc install-initramfs
+
+install-kxd: kxd
+	$(INSTALL) -d $(PREFIX)/bin
+	$(INSTALL) -m 0755 out/kxd $(PREFIX)/bin/
+	$(INSTALL) -m 0755 scripts/create-kxd-config $(PREFIX)/bin/
+	$(INSTALL) -m 0755 scripts/kxd-add-client-key $(PREFIX)/bin/
+
+install-init.d: install-kxd
+	$(INSTALL) -m 0755 scripts/init.d/kxd $(ETCDIR)/init.d/kxd
+	$(INSTALL) -m 0644 scripts/default/kxd $(ETCDIR)/default/kxd
+
+install-systemd: install-kxd
+	$(INSTALL) -m 0644 scripts/default/kxd $(ETCDIR)/default/kxd
+	$(INSTALL) -m 0644 scripts/systemd/kxd.service $(SYSTEMDDIR)
+
+install-kxc: kxc
+	$(INSTALL) -m 0755 out/kxc $(PREFIX)/bin/
+	$(INSTALL) -m 0755 cryptsetup/kxc-cryptsetup $(PREFIX)/bin/
+	$(INSTALL) -m 0755 scripts/kxc-add-key $(PREFIX)/bin/
+
+install-initramfs: install-kxc
+	$(INSTALL) -d $(PREFIX)/share/initramfs-tools/hooks/
+	$(INSTALL) -m 0755 cryptsetup/initramfs-hooks/kxc \
+		$(PREFIX)/share/initramfs-tools/hooks/
+	$(INSTALL) -d $(PREFIX)/share/initramfs-tools/scripts/init-premount
+	$(INSTALL) -m 0755 cryptsetup/initramfs-scripts/premount-net \
+		$(PREFIX)/share/initramfs-tools/scripts/init-premount/
+
+
+.PHONY: kxd kxc
+.PHONY: install-all install-kxd install-init.d install-kxc install-initramfs
+.PHONY: test tests
+
diff --git a/README b/README
new file mode 100644
index 0000000..41cb678
--- /dev/null
+++ b/README
@@ -0,0 +1,79 @@
+
+kxd - Key exchange daemon
+=========================
+
+kxd is a key exchange daemon, which serves blobs of data (keys) over https.
+
+It can be used to get keys remotely instead of using local storage.
+The main use case is to get keys to open dm-crypt devices automatically,
+without having to store them on the local machine.
+
+
+Quick start
+-----------
+
+The document at doc/quick_start.rst contains a step by step guide of a typical
+server and client setups.
+
+
+Server configuration
+--------------------
+
+The server configuration is stored in a root directory (/etc/kxd/data), and
+within there, with per-key directories (e.g. /etc/kxd/data/host1/key1), each
+containing the following files:
+
+  - key: Contains the key to give to the client.
+  - allowed_clients: Contains one or more PEM-encoded client certificates that
+    will be allowed to request the key.
+    If not present, then no clients will be allowed to access this key.
+  - allowed_hosts: Contains one or more host names (one per line).
+    If not present, then all hosts will be allowed to access that key (as long
+    as they are authorized with a valid client certificate).
+  - email_to: Contains one or more email destinations to notify (one per line).
+    If not present, then no notifications will be sent upon key accesses.
+
+
+Client configuration
+--------------------
+
+The basic command line client (kxc) will take the client key and certificate,
+the expected server certificate, and a URL to the server (like
+kxd://server/host1/key1), and it will print on standard output the returned
+key (the contents of the corresponding key file).
+
+There are scripts to tie this with cryptsetup's infrastructure to make the
+opening of encrypted devices automatic; see cryptsetup/ for the details.
+
+
+Security
+--------
+
+All traffic between the server and the clients goes over SSL, using the
+provided server certificate.
+
+The clients are authenticated and authorized based on their SSL client
+certificates matching the ones associated with the key in the server
+configuration, not using a root of trust (for now).
+
+Likewise, the clients will authenticate the server based on a certificate
+given on the command line, and will only accept keys from it.
+
+
+Note the server will return reasonably detailed information on errors, for
+example it will tell when a key is not found vs. when the client is not
+allowed. While this leaks some information about existance of keys, it makes
+troubleshooting much easier.
+
+The server itself makes no effort to protect the data internally; for example,
+there is no on-disk encryption, and memory is not locked. We work under the
+assumption that the server's host is secure and trusted.
+
+
+Bugs and contact
+----------------
+
+Please report bugs to albertito@blitiri.com.ar.
+
+The latest version can be found at http://blitiri.com.ar/p/kxd/.
+
diff --git a/cryptsetup/README b/cryptsetup/README
new file mode 100644
index 0000000..03ea7fa
--- /dev/null
+++ b/cryptsetup/README
@@ -0,0 +1,46 @@
+
+These are scripts for integration with cryptsetup (and initramfs).
+
+They are tested on a Debian install, so they may not be vendor-neutral
+although they should work with an standard initramfs-tools and cryptsetup
+environment.
+
+For an example of how to use it, see doc/quick_start.rst.
+
+
+What if something goes wrong
+============================
+
+If the key fetch fails or is incorrect it will be retried, and after 3
+attempts, it will give up and return an initramfs prompt, which you can use to
+manually recover.
+
+In modern Debian installs, you can just unlock the device (for example using
+"cryptsetup luksOpen /dev/sdXX sdXX_crypt"), and then exit.
+
+The init scripts will recognise they can now proceed with the usual boot
+process.
+
+
+How does it work
+================
+
+The first part of the work happens when update-initramfs runs:
+
+ - The initramfs hook script copies the kxc binary and all the configuration
+   from /etc/kxc.
+ - The standard cryptsetup hook will copy kxc-cryptsetup if it sees it
+   appearing in /etc/crypttab.
+ - The premount-net script will be copied.
+
+Then, when the machine boots:
+
+ - Before attempting to mount root, the premount-net script will run,
+   configure networking, and create a minimal /etc/resolv.conf.
+ - When attempting to mount root, assuming it is encrypted and properly
+   configured, the cryptsetup scripts will invoke the keyfile, kxc-cryptsetup.
+ - kxc-cryptsetup will run the kxc client with the right configuration taken
+   from /etc/kxc.
+ - The device is unlocked with the key, and boot continues as usual.
+
+
diff --git a/cryptsetup/initramfs-hooks/kxc b/cryptsetup/initramfs-hooks/kxc
new file mode 100755
index 0000000..ab78e6f
--- /dev/null
+++ b/cryptsetup/initramfs-hooks/kxc
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+set -e
+
+PREREQ="cryptroot"
+
+prereqs()
+{
+        echo "$PREREQ"
+}
+
+case $1 in
+prereqs)
+        prereqs
+        exit 0
+        ;;
+esac
+
+. /usr/share/initramfs-tools/hook-functions
+
+# Install binaries into initramfs.
+# Note we don't need to install kxc-cryptsetup, as the cryptroot hook will do
+# it for us if it sees it being used as a keyscript.
+copy_exec /usr/bin/kxc /bin
+
+# Install the configuration into initramfs
+cp -a /etc/kxc/ ${DESTDIR}/etc
diff --git a/cryptsetup/initramfs-scripts/premount-net b/cryptsetup/initramfs-scripts/premount-net
new file mode 100755
index 0000000..b131058
--- /dev/null
+++ b/cryptsetup/initramfs-scripts/premount-net
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+# Configure networking before mounting.
+
+PREREQ=""
+
+prereqs()
+{
+	echo "$PREREQ"
+}
+
+case $1 in
+# get pre-requisites
+prereqs)
+        prereqs
+        exit 0
+        ;;
+esac
+
+. /scripts/functions
+
+configure_networking
+
+# Configure a basic resolv.conf based on our networking.
+echo "nameserver $IPV4DNS0" >> /etc/resolv.conf
+
+
diff --git a/cryptsetup/kxc-cryptsetup b/cryptsetup/kxc-cryptsetup
new file mode 100755
index 0000000..7ccfc49
--- /dev/null
+++ b/cryptsetup/kxc-cryptsetup
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+# Script to use as a crypttab keyscript, to automatically get keys with kxc.
+# It will use the configuration from /etc/kxc/.
+#
+# The only argument is the base name of the configuration.
+
+CONFIG_BASE="/etc/kxc"
+
+CLIENT_CERT="${CONFIG_BASE}/cert.pem"
+CLIENT_KEY="${CONFIG_BASE}/key.pem"
+SERVER_CERT="${CONFIG_BASE}/${1}.server_cert.pem"
+SERVER_URL=$(cat "${CONFIG_BASE}/${1}.url")
+
+# Find the binary. We search because it can be in one place in the initramfs,
+# and in another in the normal distribution, and we want to support both
+# easily.
+for KXC in /bin/kxc /sbin/kxc /usr/bin/kxc /usr/sbin/kxc; do
+	if [ -x $KXC ]; then
+		break;
+	fi
+done
+
+exec $KXC \
+	--client_cert=$CLIENT_CERT \
+	--client_key=$CLIENT_KEY \
+	--server_cert=$SERVER_CERT \
+	$SERVER_URL
+
diff --git a/doc/man/.gitignore b/doc/man/.gitignore
new file mode 100644
index 0000000..f7e585b
--- /dev/null
+++ b/doc/man/.gitignore
@@ -0,0 +1 @@
+*.1
diff --git a/doc/man/Makefile b/doc/man/Makefile
new file mode 100644
index 0000000..22c9bec
--- /dev/null
+++ b/doc/man/Makefile
@@ -0,0 +1,11 @@
+
+default: manpages
+
+%.1: %.rst
+	rst2man < $^ > $@
+
+manpages: kxd.1 kxc.1 kxc-cryptsetup.1
+
+clean:
+	rm -f kxd.1 kxc.1 kxc-cryptsetup.1
+
diff --git a/doc/man/kxc-cryptsetup.rst b/doc/man/kxc-cryptsetup.rst
new file mode 100644
index 0000000..299e156
--- /dev/null
+++ b/doc/man/kxc-cryptsetup.rst
@@ -0,0 +1,62 @@
+
+================
+ kxc-cryptsetup
+================
+
+------------------------
+Cryptsetup helper to kxc
+------------------------
+
+:Author: Alberto Bertogli <albertito@blitiri.com.ar>
+:Manual section: 1
+
+
+SYNOPSIS
+========
+
+kxc-cryptsetup <NAME>
+
+
+DESCRIPTION
+===========
+
+``kxc(1)`` is a client for kxd, a key exchange daemon.
+
+kxc-cryptsetup is a convenience wrapper for invoking kxc while taking the
+options from the files in ``/etc/kxc/``.
+
+It can be used as a cryptsetup keyscript, to automatically get keys to open
+encrypted devices with kxc.
+
+
+OPTIONS
+=======
+
+Its only command-line argument is a descriptive name, which will be used to
+find the configuration files.
+
+
+FILES
+=====
+
+For a given *NAME* that is passed as the only command-line argument, the
+following files are needed:
+
+/etc/kxc/NAME.key.pem
+  Private key to use.
+
+/etc/kxc/NAME.cert.pem
+  Certificate to use. Must match the given key.
+
+/etc/kxc/NAME.server_cert.pem
+  Server certificate, used to validate the server.
+
+/etc/kxc/NAME.url
+  Contains the URL to the key; usually in the form of ``kxd://server/name``.
+
+
+SEE ALSO
+========
+
+``kxc(1)``, ``kxd(1)``, ``crypttab(5)``, ``cryptsetup(8)``.
+
diff --git a/doc/man/kxc.rst b/doc/man/kxc.rst
new file mode 100644
index 0000000..c6cd762
--- /dev/null
+++ b/doc/man/kxc.rst
@@ -0,0 +1,52 @@
+
+=====
+ kxc
+=====
+
+-------------------
+Key exchange client
+-------------------
+:Author: Alberto Bertogli <albertito@blitiri.com.ar>
+:Manual section: 1
+
+
+SYNOPSIS
+========
+
+kxc --client_cert=<file> --client_key=<file> --server_cert=<file> <URL>
+
+
+DESCRIPTION
+===========
+
+kxc is a client for kxd, a key exchange daemon.
+
+It will take a client key and certificate, the expected server certificate,
+and a URL to the server (like ``kxd://server/host1/key1``), and it will print
+on standard output the returned key (the contents of the corresponding key
+file on the server).
+
+There are scripts to tie this with cryptsetup's infrastructure to make the
+opening of encrypted devices automatic; see ``kxc-cryptsetup(1)`` for the
+details.
+
+
+OPTIONS
+=======
+
+--client_key=<file>
+  File containing the client private key (in PAM format).
+
+--client_cert=<file>
+  File containing the client certificate that corresponds to the given key (in
+  PAM format).
+
+--server_cert=<file>
+  File containing valid server certificate(s).
+
+
+SEE ALSO
+========
+
+``kxc-cryptsetup(1)``, ``kxd(1)``.
+
diff --git a/doc/man/kxd.rst b/doc/man/kxd.rst
new file mode 100644
index 0000000..5383da0
--- /dev/null
+++ b/doc/man/kxd.rst
@@ -0,0 +1,104 @@
+
+=====
+ kxd
+=====
+
+-------------------
+Key exchange daemon
+-------------------
+
+:Author: Alberto Bertogli <albertito@blitiri.com.ar>
+:Manual section: 1
+
+
+SYNOPSIS
+========
+
+kxd [--key=<file>] [--cert=<file>] [--data_dir=<directory>]
+[--email_from=<email-address>] [--ip_addr=<ip-address>] [--logfile=<file>]
+[--port=<port>] [--smtp_addr=<host:port>]
+
+
+DESCRIPTION
+===========
+
+kxd is a key exchange daemon, which serves blobs of data (keys) over https.
+
+It can be used to get keys remotely instead of using local storage.
+The main use case is to get keys to open dm-crypt devices automatically,
+without having to store them on the local machine.
+
+
+SETUP
+=====
+
+The server configuration is stored in a root directory (``/etc/kxd/data/`` by
+default), and within there, with per-key directories (e.g.
+``/etc/kxd/data/host1/key1/``), each containing the following files:
+
+  - ``key``: Contains the key to give to the client.
+  - ``allowed_clients``: Contains one or more PEM-encoded client certificates
+    that will be allowed to request the key.
+    If not present, then no clients will be allowed to access this key.
+  - ``allowed_hosts``: Contains one or more host names (one per line).
+    If not present, then all hosts will be allowed to access that key (as long
+    as they are authorized with a valid client certificate).
+  - ``email_to``: Contains one or more email destinations to notify (one per
+    line).  If not present, then no notifications will be sent upon key
+    accesses.
+
+
+OPTIONS
+=======
+
+--key=<file>
+  Private key to use.
+  Defaults to /etc/kxd/key.pem.
+
+--cert=<file>
+  Certificate to use; must match the given key.
+  Defaults to /etc/kxd/cert.pem.
+
+--data_dir=<directory>
+  Data directory, where the key and configuration live (see the SETUP section
+  above).
+  Defaults to /etc/kxd/data.
+
+--email_from=<email-address>
+  Email address to send email from.
+
+--ip_addr=<ip-address>
+  IP address to listen on.
+  Defaults to 0.0.0.0, which means all.
+
+--logfile=<file>
+  File to write logs to, use '-' for stdout.
+  By default, the daemon will log to syslog.
+
+--port=<port>
+  Port to listen on.
+  The default port is 19840.
+
+--smtp_addr=<host:port>
+  Address of the SMTP server to use to send emails.
+  If none is given, then emails will not be sent.
+
+
+FILES
+=====
+
+/etc/kxd/key.pem
+  Private key to use for SSL.
+
+/etc/kxd/cert.pem
+  Certificate to use for SSL. Must match the given private key.
+
+/etc/kxd/data/
+  Directory where the keys and their configuration are stored.
+
+
+SEE ALSO
+========
+
+``kxc(1)``, ``kxc-cryptsetup(1)``.
+
diff --git a/doc/quick_start.rst b/doc/quick_start.rst
new file mode 100644
index 0000000..5a53a46
--- /dev/null
+++ b/doc/quick_start.rst
@@ -0,0 +1,98 @@
+
+===================================
+ Key Exchange Daemon - Quick start
+===================================
+
+In this guide we show how to set up a key exchange daemon and client
+on a typical scenario where the keys are used to open a device encrypted with
+dm-crypt (the standard Linux disk encryption).
+
+``server`` is the hostname of the server.
+``client`` is the hostname of the client.
+``sda2`` is the encrypted drive.
+
+
+Initial server setup
+====================
+
+First of all, install *kxd* on the server, usually via your distribution
+packages, or directly from source.
+
+Then, run ``create-kxd-config``, which will create the configuration
+directories, and generate the server key/cert pair. Everything is in
+``/etc/kxd/``.
+
+
+Initial client setup
+====================
+
+Install *kxc* on the client machine, usually via your distribution packages,
+or directly from source.
+
+
+Then, run ``kxc-add-key server sda2``, which will create the configuration
+directories, and generate the client key/cert pair, and also create an entry
+for an ``client/sda2`` key to be fetched from the server.
+Everything is in ``/etc/kxc/``.
+
+Finally, copy the server public certificate over, using
+``scp server:/etc/kxd/cert.pem /etc/kxc/sda2.server_cert.pem`` (or something
+equivalent).
+
+
+Adding the key to the server
+============================
+
+On the server, run ``kxd-add-client-key client sda2`` to generate the basic
+configuration for that client's key, including the key itself (generated
+randomly).
+
+Then, copy the client public certificate over, using
+``scp client:/etc/kxc/cert.pem /etc/kxd/data/client/sda2/allowed_clients``
+(or something equivalent).
+
+That allows the client to fetch the key.
+
+
+Updating the drive's key
+========================
+
+On the client, run ``kxc-cryptsetup sda2 | wc -c`` to double-check that the
+output length is as expected (you could also compare it by running sha256 or
+something equivalent).
+
+Assuming that goes well, all you need is to add that key to your drives' key
+ring so it can be decrypted with it::
+
+  # Note we copy to /dev/shm which should not be written to disk.
+  kxc-cryptsetup sda2 > /dev/shm/key
+
+  cryptsetup luksAddKey /dev/sda2 /dev/shm/key
+
+  rm /dev/shm/key
+
+Note this *adds* a new key, but your existing ones are still valid. Always
+have more than one key, so if something goes wrong with kxd, you can still
+unlock the drive manually.
+
+
+Configuring crypttab
+====================
+
+In order to get kxc to be run automatically to fetch the key, we need to edit
+``/etc/crypttab`` and tell it to use a keyscript::
+
+  sda2_crypt UUID=blah-blah-blah sda2 luks,keyscript=kxc-cryptsetup
+                                 ^^^^      ^^^^^^^^^^^^^^^^^^^^^^^^
+
+Note the "sda2" field corresponds to the name we've been passing around in
+previous sections. The ``keyscript=kxc-cryptsetup`` is our way of telling the
+cryptsetup infrastructure to use our script to fetch the key for this target.
+
+
+You can test that this works by using::
+
+  cryptdisks_stop sda2_crypt
+  cryptdisks_start sda2_crypt
+
+The second command should issue a request to your server to get the key.
diff --git a/kxc/kxc.go b/kxc/kxc.go
new file mode 100644
index 0000000..66860e9
--- /dev/null
+++ b/kxc/kxc.go
@@ -0,0 +1,186 @@
+// kxc is a client for the key exchange daemon kxd.
+//
+// It connects to the given server using the provided certificate,
+// and authorizes the server against the given server certificate.
+//
+// If everything goes well, it prints the obtained key to standard output.
+package main
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+const defaultPort = 19840
+
+var server_cert = flag.String(
+	"server_cert", "", "File containing valid server certificate(s)")
+var client_cert = flag.String(
+	"client_cert", "", "File containing the client certificate")
+var client_key = flag.String(
+	"client_key", "", "File containing the client private key")
+
+func LoadServerCerts() ([]*x509.Certificate, error) {
+	pemData, err := ioutil.ReadFile(*server_cert)
+	if err != nil {
+		return nil, err
+	}
+
+	var certs []*x509.Certificate
+	for len(pemData) > 0 {
+		var block *pem.Block
+		block, pemData = pem.Decode(pemData)
+		if block == nil {
+			return certs, nil
+		}
+
+		if block.Type != "CERTIFICATE" {
+			continue
+		}
+
+		cert, err := x509.ParseCertificate(block.Bytes)
+		if err != nil {
+			return certs, err
+		}
+
+		certs = append(certs, cert)
+	}
+
+	return certs, nil
+}
+
+// We can't ask the http client for the TLS connection, so we make it
+// ourselves and save it here.
+// It is hacky but it simplifies the code.
+var tlsConn *tls.Conn
+
+func OurDial(network, addr string) (net.Conn, error) {
+	var err error
+
+	tlsConf := &tls.Config{}
+	tlsConf.Certificates = make([]tls.Certificate, 1)
+	tlsConf.Certificates[0], err = tls.LoadX509KeyPair(
+		*client_cert, *client_key)
+	if err != nil {
+		log.Fatalf("Failed to load keys: %s", err)
+	}
+
+	// We don't want the TLS stack to check the cert, as we will compare
+	// it ourselves.
+	tlsConf.InsecureSkipVerify = true
+
+	tlsConn, err = tls.Dial(network, addr, tlsConf)
+	return tlsConn, err
+}
+
+// Check if any cert from requestedCerts matches any cert in validCerts.
+func AnyCertMatches(requestedCerts, validCerts []*x509.Certificate) bool {
+	for _, cert := range requestedCerts {
+		for _, validCert := range validCerts {
+			if cert.Equal(validCert) {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+// Check if the given network address has a port.
+func hasPort(s string) bool {
+	// Consider the IPv6 case (where the host part contains ':') by
+	// checking if the last ':' comes after the ']' which closes the host.
+	return strings.LastIndex(s, ":") > strings.LastIndex(s, "]")
+}
+
+func ExtractURL(rawurl string) (*url.URL, error) {
+	// Because we handle the transport ourselves, the http library has to
+	// be told to use plain http; otherwise it will attempt to do its own
+	// TLS on top of our TLS.
+	serverURL, err := url.Parse(rawurl)
+	if err != nil {
+		return nil, err
+	}
+
+	// Fix up the scheme to use http.
+	// Because we handle the transport ourselves, the http library has to
+	// be told to use plain http; otherwise it will attempt to do its own
+	// TLS on top of our TLS (and obviously fails, in this case with an
+	// "local error: record overflow" error).
+	switch serverURL.Scheme {
+	case "http":
+		// Nothing to do here.
+	case "https", "kxd":
+		serverURL.Scheme = "http"
+	default:
+		return nil, fmt.Errorf("Unsupported URL schema (try kxd://)")
+	}
+
+	// The path must begin with /v1/, although we hide that from the user
+	// for forward compatibility.
+	if !strings.HasPrefix(serverURL.Path, "/v1/") {
+		serverURL.Path = "/v1" + serverURL.Path
+	}
+
+	// Add the default port, if none was given.
+	if !hasPort(serverURL.Host) {
+		serverURL.Host += fmt.Sprintf(":%d", defaultPort)
+	}
+
+	return serverURL, nil
+}
+
+func main() {
+	var err error
+	flag.Parse()
+
+	serverCerts, err := LoadServerCerts()
+	if err != nil {
+		log.Fatalf("Failed to load server certs: %s", err)
+	}
+
+	tr := &http.Transport{
+		Dial: OurDial,
+	}
+
+	client := &http.Client{
+		Transport: tr,
+	}
+
+	serverURL, err := ExtractURL(flag.Arg(0))
+	if err != nil {
+		log.Fatalf("Failed to extract the URL: %s", err)
+	}
+
+	resp, err := client.Get(serverURL.String())
+	if err != nil {
+		log.Fatalf("Failed to get key: %s", err)
+	}
+
+	content, err := ioutil.ReadAll(resp.Body)
+	resp.Body.Close()
+	if err != nil {
+		log.Fatalf("Error reading key body: %s", err)
+	}
+
+	if resp.StatusCode != 200 {
+		log.Fatalf("HTTP error %q getting key: %s",
+			resp.Status, content)
+	}
+
+	if !AnyCertMatches(
+		tlsConn.ConnectionState().PeerCertificates, serverCerts) {
+		log.Fatalf("No server certificate matches")
+	}
+
+	fmt.Printf("%s", content)
+}
diff --git a/kxd/email.go b/kxd/email.go
new file mode 100644
index 0000000..d6ca469
--- /dev/null
+++ b/kxd/email.go
@@ -0,0 +1,106 @@
+package main
+
+import (
+	"bytes"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"fmt"
+	"net/smtp"
+	"strings"
+	"text/template"
+	"time"
+)
+
+type EmailBody struct {
+	From       string
+	To         string
+	Key        string
+	Time       time.Time
+	TimeString string
+	Req        *Request
+	Cert       *x509.Certificate
+}
+
+const emailTmplBody = (`Date: {{.TimeString}}
+From: Key Exchange Daemon <{{.From}}>
+To: {{.To}}
+Subject: Access to key {{.Key}}
+
+Key: {{.Key}}
+Accessed by: {{.Req.RemoteAddr}}
+On: {{.TimeString}}
+
+Client certificate:
+  Signature: {{printf "%.16s" (printf "%x" .Cert.Signature)}}...
+  Issuer: {{NameToString .Cert.Issuer}}
+  Subject: {{NameToString .Cert.Subject}}
+
+`)
+
+var emailTmpl = template.New("email")
+
+func init() {
+	emailTmpl.Funcs(map[string]interface{}{
+		"NameToString": NameToString,
+	})
+
+	template.Must(emailTmpl.Parse(emailTmplBody))
+}
+
+func NameToString(name pkix.Name) string {
+	s := make([]string, 0)
+	for _, c := range name.Country {
+		s = append(s, fmt.Sprintf("C=%s", c))
+	}
+	for _, o := range name.Organization {
+		s = append(s, fmt.Sprintf("O=%s", o))
+	}
+
+	if name.CommonName != "" {
+		s = append(s, fmt.Sprintf("N=%s", name.CommonName))
+	}
+
+	return strings.Join(s, " ")
+}
+
+func SendMail(kc *KeyConfig, req *Request, cert *x509.Certificate) error {
+	if *smtp_addr == "" {
+		req.Printf("Skipping notifications")
+		return nil
+	}
+
+	emailTo, err := kc.EmailTo()
+	if err != nil {
+		return err
+	}
+
+	if emailTo == nil {
+		return nil
+	}
+
+	keyPath, err := req.KeyPath()
+	if err != nil {
+		return err
+	}
+
+	now := time.Now()
+	body := EmailBody{
+		From:       *email_from,
+		To:         strings.Join(emailTo, ", "),
+		Key:        keyPath,
+		Time:       now,
+		TimeString: now.Format(time.RFC1123Z),
+		Req:        req,
+		Cert:       cert,
+	}
+
+	msg := new(bytes.Buffer)
+
+	err = emailTmpl.Execute(msg, body)
+	if err != nil {
+		return err
+	}
+
+	return smtp.SendMail(*smtp_addr, nil, *email_from, emailTo,
+		msg.Bytes())
+}
diff --git a/kxd/key_config.go b/kxd/key_config.go
new file mode 100644
index 0000000..5b9e491
--- /dev/null
+++ b/kxd/key_config.go
@@ -0,0 +1,205 @@
+package main
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"os"
+	"strings"
+)
+
+func FileStat(path string) (os.FileInfo, error) {
+	fd, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+
+	return fd.Stat()
+}
+
+func IsDir(path string) (bool, error) {
+	fi, err := FileStat(path)
+	if err != nil {
+		return false, err
+	}
+
+	return fi.IsDir(), nil
+}
+
+func IsRegular(path string) (bool, error) {
+	fi, err := FileStat(path)
+	if err != nil {
+		return false, err
+	}
+
+	return fi.Mode().IsRegular(), nil
+}
+
+// KeyConfig holds the configuration data for a single key.
+type KeyConfig struct {
+	// Path to the configuration directory.
+	ConfigPath string
+
+	// Paths to the files themselves.
+	keyPath            string
+	allowedClientsPath string
+	allowedHostsPath   string
+	emailToPath        string
+
+	// Allowed certificates.
+	allowedClientCerts []*x509.Certificate
+
+	// Allowed hosts.
+	allowedHosts []string
+}
+
+func NewKeyConfig(configPath string) *KeyConfig {
+	return &KeyConfig{
+		ConfigPath:         configPath,
+		keyPath:            configPath + "/key",
+		allowedClientsPath: configPath + "/allowed_clients",
+		allowedHostsPath:   configPath + "/allowed_hosts",
+		emailToPath:        configPath + "/email_to",
+	}
+}
+
+func (kc *KeyConfig) Exists() (bool, error) {
+	isDir, err := IsDir(kc.ConfigPath)
+	if os.IsNotExist(err) {
+		return false, nil
+	} else if err != nil {
+		return false, err
+	}
+	if !isDir {
+		return false, nil
+	}
+
+	isRegular, err := IsRegular(kc.keyPath)
+	if os.IsNotExist(err) {
+		return false, nil
+	} else if err != nil {
+		return false, err
+	}
+
+	return isRegular, nil
+}
+
+func (kc *KeyConfig) LoadClientCerts() error {
+	rawContents, err := ioutil.ReadFile(kc.allowedClientsPath)
+	if os.IsNotExist(err) {
+		return nil
+	} else if err != nil {
+		return err
+	}
+
+	for len(rawContents) > 0 {
+		var block *pem.Block
+		block, rawContents = pem.Decode(rawContents)
+		if block == nil {
+			return nil
+		}
+
+		if block.Type != "CERTIFICATE" {
+			continue
+		}
+
+		cert, err := x509.ParseCertificate(block.Bytes)
+		if err != nil {
+			return err
+		}
+
+		kc.allowedClientCerts = append(kc.allowedClientCerts, cert)
+	}
+
+	return nil
+}
+
+func (kc *KeyConfig) LoadAllowedHosts() error {
+	contents, err := ioutil.ReadFile(kc.allowedHostsPath)
+	if os.IsNotExist(err) {
+		return nil
+	} else if err != nil {
+		return err
+	}
+
+	// If the file is there, we want our array to exist, even if it's
+	// empty, to avoid authorizing everyone on an empty file (which means
+	// authorize noone).
+	kc.allowedHosts = make([]string, 1)
+	for _, line := range strings.Split(string(contents), "\n") {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+
+		if net.ParseIP(line) != nil {
+			kc.allowedHosts = append(kc.allowedHosts, line)
+		} else {
+			names, err := net.LookupHost(line)
+			if err != nil {
+				continue
+			}
+			kc.allowedHosts = append(kc.allowedHosts, names...)
+		}
+	}
+
+	return nil
+}
+
+func (kc *KeyConfig) IsAnyCertAllowed(
+	certs []*x509.Certificate) *x509.Certificate {
+	for _, cert := range certs {
+		for _, allowedCert := range kc.allowedClientCerts {
+			if cert.Equal(allowedCert) {
+				return cert
+			}
+		}
+	}
+	return nil
+}
+
+func (kc *KeyConfig) IsHostAllowed(addr string) error {
+	if kc.allowedHosts == nil {
+		return nil
+	}
+
+	host, _, err := net.SplitHostPort(addr)
+	if err != nil {
+		return err
+	}
+
+	for _, allowedHost := range kc.allowedHosts {
+		if allowedHost == host {
+			return nil
+		}
+	}
+
+	return fmt.Errorf("Host %q not allowed", host)
+}
+
+func (kc *KeyConfig) Key() (key []byte, err error) {
+	return ioutil.ReadFile(kc.keyPath)
+}
+
+func (kc *KeyConfig) EmailTo() ([]string, error) {
+	contents, err := ioutil.ReadFile(kc.emailToPath)
+	if os.IsNotExist(err) {
+		return nil, nil
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	var emails []string
+	for _, line := range strings.Split(string(contents), "\n") {
+		email := strings.TrimSpace(line)
+		if !strings.Contains(email, "@") {
+			continue
+		}
+		emails = append(emails, email)
+	}
+
+	return emails, nil
+}
diff --git a/kxd/kxd.go b/kxd/kxd.go
new file mode 100644
index 0000000..3f1fd71
--- /dev/null
+++ b/kxd/kxd.go
@@ -0,0 +1,228 @@
+// kxd is a key exchange daemon.
+//
+// It serves blobs of data (keys) over https, authenticating and authorizing
+// the clients using SSL certificates, and notifying upon key accesses.
+//
+// It can be used to get keys remotely instead of using local storage.
+// The main use case is to get keys to open dm-crypt devices automatically,
+// without having to store them on the machine.
+package main
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"log/syslog"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+)
+
+var port = flag.Int(
+	"port", 19840, "Port to listen on")
+var ip_addr = flag.String(
+	"ip_addr", "", "IP address to listen on")
+var data_dir = flag.String(
+	"data_dir", "/etc/kxd/data", "Data directory")
+var certfile = flag.String(
+	"cert", "/etc/kxd/cert.pem", "Certificate")
+var keyfile = flag.String(
+	"key", "/etc/kxd/key.pem", "Private key")
+var smtp_addr = flag.String(
+	"smtp_addr", "", "Address of the SMTP server to use to send emails")
+var email_from = flag.String(
+	"email_from", "", "Email address to send email from")
+var logfile = flag.String(
+	"logfile", "", "File to write logs to, use '-' for stdout")
+
+// Logger we will use to log entries.
+var logging *log.Logger
+
+// Request is our wrap around http.Request, so we can augment it with custom
+// methods.
+type Request struct {
+	*http.Request
+}
+
+func (req *Request) Printf(format string, a ...interface{}) {
+	msg := fmt.Sprintf(format, a...)
+	msg = fmt.Sprintf("%s %s %s", req.RemoteAddr, req.URL.Path, msg)
+	logging.Output(2, msg)
+}
+
+// KeyPath returns the path to the requested key, extracting it from the URL.
+func (req *Request) KeyPath() (string, error) {
+	s := strings.Split(req.URL.Path, "/")
+
+	// We expect the path to be "/v1/path/to/key".
+	if len(s) < 2 || !(s[0] == "" || s[1] == "v1") {
+		return "", fmt.Errorf("Invalid path %q", s)
+	}
+
+	return strings.Join(s[2:], "/"), nil
+}
+
+func CertToString(cert *x509.Certificate) string {
+	return fmt.Sprintf(
+		"<signature:0x%.8s>", fmt.Sprintf("%x", cert.Signature))
+}
+
+// HandlerV1 handles /v1/ key requests.
+func HandlerV1(w http.ResponseWriter, httpreq *http.Request) {
+	req := Request{httpreq}
+	if len(req.TLS.PeerCertificates) <= 0 {
+		req.Printf("Rejecting request without certificate")
+		http.Error(w, "Client certificate not provided",
+			http.StatusNotAcceptable)
+		return
+	}
+
+	keyPath, err := req.KeyPath()
+	if err != nil {
+		req.Printf("Rejecting request with invalid key path: %s", err)
+		http.Error(w, "Invalid key path", http.StatusNotAcceptable)
+		return
+	}
+
+	// Be extra paranoid and reject keys with "..", even if they're valid
+	// (e.g. "/v1/x..y" is valid, but will get rejected anyway).
+	if strings.Contains(keyPath, "..") {
+		req.Printf("Rejecting because requested key %q contained '..'",
+			keyPath)
+		req.Printf("Full request: %+v", *req.Request)
+		http.Error(w, "Invalid key path", http.StatusNotAcceptable)
+		return
+	}
+
+	realKeyPath := path.Clean(*data_dir + "/" + keyPath)
+	keyConf := NewKeyConfig(realKeyPath)
+
+	exists, err := keyConf.Exists()
+	if err != nil {
+		req.Printf("Error checking key path %q: %s", keyPath, err)
+		http.Error(w, "Error checking key",
+			http.StatusInternalServerError)
+		return
+	}
+	if !exists {
+		req.Printf("Unknown key path %q", keyPath)
+		http.Error(w, "Unknown key", http.StatusNotFound)
+		return
+	}
+
+	if err = keyConf.LoadClientCerts(); err != nil {
+		req.Printf("Error loading certs: %s", err)
+		http.Error(w, "Error loading certs",
+			http.StatusInternalServerError)
+		return
+	}
+
+	if err = keyConf.LoadAllowedHosts(); err != nil {
+		req.Printf("Error loading allowed hosts: %s", err)
+		http.Error(w, "Error loading allowed hosts",
+			http.StatusInternalServerError)
+		return
+	}
+
+	err = keyConf.IsHostAllowed(req.RemoteAddr)
+	if err != nil {
+		req.Printf("Host not allowed: %s", err)
+		http.Error(w, "Host not allowed", http.StatusForbidden)
+		return
+	}
+
+	validCert := keyConf.IsAnyCertAllowed(req.TLS.PeerCertificates)
+	if validCert == nil {
+		req.Printf("No allowed certificate found")
+		http.Error(w, "No allowed certificate found",
+			http.StatusForbidden)
+		return
+	}
+
+	keyData, err := keyConf.Key()
+	if err != nil {
+		req.Printf("Error getting key data: %s", err)
+		http.Error(w, "Error getting key data",
+			http.StatusInternalServerError)
+		return
+	}
+
+	req.Printf("Allowing request to cert %s", CertToString(validCert))
+
+	err = SendMail(keyConf, &req, validCert)
+	if err != nil {
+		req.Printf("Error sending notification: %s", err)
+		http.Error(w, "Error sending notification",
+			http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/octet-stream")
+	w.Write(keyData)
+}
+
+func initLog() {
+	var err error
+	var logfd io.Writer
+
+	if *logfile == "-" {
+		logfd = os.Stdout
+	} else if *logfile != "" {
+		logfd, err = os.OpenFile(*logfile,
+			os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
+		if err != nil {
+			log.Fatalf("Error opening log file %s: %s",
+				*logfile, err)
+		}
+	} else {
+		logfd, err = syslog.New(
+			syslog.LOG_INFO|syslog.LOG_DAEMON, "kxd")
+		if err != nil {
+			log.Fatalf("Error opening syslog: %s", err)
+		}
+	}
+
+	logging = log.New(logfd, "",
+		log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile)
+}
+
+func main() {
+	flag.Parse()
+
+	initLog()
+
+	if *smtp_addr == "" {
+		logging.Print(
+			"WARNING: No emails will be sent, use --smtp_addr")
+	}
+
+	if *email_from == "" {
+		// Try to get a sane default if not provided, using
+		// kxd@<smtp host>.
+		*email_from = fmt.Sprintf("kxd@%s",
+			strings.Split(*smtp_addr, ":")[0])
+	}
+
+	listenAddr := fmt.Sprintf("%s:%d", *ip_addr, *port)
+
+	tlsConfig := tls.Config{
+		ClientAuth: tls.RequireAnyClientCert,
+	}
+
+	server := http.Server{
+		Addr:      listenAddr,
+		TLSConfig: &tlsConfig,
+	}
+
+	http.HandleFunc("/v1/", HandlerV1)
+
+	logging.Printf("Listening on %s", listenAddr)
+	err := server.ListenAndServeTLS(*certfile, *keyfile)
+	if err != nil {
+		logging.Fatal(err)
+	}
+}
diff --git a/scripts/create-kxd-config b/scripts/create-kxd-config
new file mode 100755
index 0000000..2513b48
--- /dev/null
+++ b/scripts/create-kxd-config
@@ -0,0 +1,36 @@
+#!/bin/bash
+#
+# Create a basic but functional kxd configuration.
+#
+# This script creates the /etc/kxd directory, and generates a certificate for
+# the server to use.
+#
+# It should be run under the same user as kxd itself.
+
+set -e
+
+# Create the base configuration directory.
+echo "Creating directories (/etc/kxd/)"
+mkdir -p /etc/kxd/
+
+# And the data directory where the keys are stored.
+mkdir -p /etc/kxd/data
+
+# Create a private key for the server.
+if ! [ -e /etc/kxd/key.pem ]; then
+	echo "Generating private key (/etc/kxd/key.pem)"
+	openssl genrsa -out /etc/kxd/key.pem 2048
+	chmod 400 /etc/kxd/key.pem
+else
+	echo "Private key already exists (/etc/kxd/key.pem)"
+fi
+
+# And a self-signed certificate.
+if ! [ -e /etc/kxd/cert.pem ]; then
+	echo "Generating certificate (/etc/kxd/cert.pem)"
+	openssl req -new -x509 -batch \
+		-subj "/organizationalUnitName=kxd@$HOSTNAME/" \
+		-key /etc/kxd/key.pem -out /etc/kxd/cert.pem
+else
+	echo "Certificate already exists (/etc/kxd/cert.pem)"
+fi
diff --git a/scripts/default/kxd b/scripts/default/kxd
new file mode 100644
index 0000000..a54f84e
--- /dev/null
+++ b/scripts/default/kxd
@@ -0,0 +1,11 @@
+# Options for kxd.
+
+# Set this if you don't want the daemon to be started automatically.
+# Note this is only useful for sysv-like init; systemd will ignore it (use
+# "sysctl enable/disable" instead).
+#DISABLE=1
+
+# Set kxd options here.
+# OPTS="--smtp_addr example.org:25"
+OPTS=""
+
diff --git a/scripts/init.d/kxd b/scripts/init.d/kxd
new file mode 100755
index 0000000..0b5b031
--- /dev/null
+++ b/scripts/init.d/kxd
@@ -0,0 +1,82 @@
+#! /bin/sh
+
+### BEGIN INIT INFO
+# Provides:          kxd
+# Required-Start:    $remote_fs $syslog
+# Required-Stop:     $remote_fs $syslog
+# Default-Start:     2 3 4 5
+# Default-Stop:      
+# Short-Description: key exchange daemon
+# Description:       kxd is a program that serves keys to authorized clients.
+### END INIT INFO
+
+DAEMON=/usr/bin/kxd
+DEFAULTS_FILE=/etc/default/kxd
+
+# These variables can be overriden in the defaults file.
+DISABLE=
+OPTS=''
+PID_FILE=/var/run/kxd.pid
+
+test -x $DAEMON || exit 0
+
+. /lib/lsb/init-functions
+
+if [ -s $DEFAULTS_FILE ]; then
+    . $DEFAULTS_FILE
+fi
+
+
+case "$1" in
+  start)
+	if [ "$DISABLE" != "" ]; then
+		log_warning_msg "kxd not enabled in $DEFAULTS_FILE"
+		exit 0
+	fi
+
+	log_daemon_msg "Starting kxd"
+	start-stop-daemon --start --quiet --background \
+		--pidfile $PID_FILE --make-pidfile \
+		--exec $DAEMON -- $OPTS
+	case "$?" in
+	  0)
+		  log_progress_msg "kxd"
+		  log_end_msg 0
+		  exit 0
+		  ;;
+	  1)
+		  log_warning_msg "already running"
+		  exit 0
+		  ;;
+	  *)
+		  log_failure_msg "failed to start daemon"
+		  exit 1
+		  ;;
+	esac
+
+	;;
+
+  stop)
+	log_daemon_msg "Stopping kxd daemon" "kxd"
+	start-stop-daemon --stop --quiet --oknodo --pidfile $PID_FILE
+	log_end_msg $?
+	rm -f $PID_FILE
+	;;
+
+  restart)
+	set +e
+	$0 stop
+	sleep 2
+	$0 start
+	;;
+
+  status)
+	status_of_proc -p $PID_FILE "$DAEMON" kxd
+	exit $?
+	;;
+  *)
+	echo "Usage: /etc/init.d/kxd {start|stop|restart|status}"
+	exit 1
+esac
+
+exit 0
diff --git a/scripts/kxc-add-key b/scripts/kxc-add-key
new file mode 100755
index 0000000..6d57a81
--- /dev/null
+++ b/scripts/kxc-add-key
@@ -0,0 +1,58 @@
+#!/bin/bash
+#
+# Add a new key to kxc's configuration (initializing it if necessary).
+#
+# If /etc/kxc is missing, this script creates it, as well as the required
+# client certificates.
+#
+# Then, it adds configuration for fetching a given key.
+
+set -e
+
+SERVER="$1"
+KEYNAME="$2"
+
+if [ "$SERVER" = "" ] || [ "$KEYNAME" = "" ]; then
+	echo "
+Usage: kxc-add-key <server hostname> <key name>
+
+This command adds a new key to kxc's configuration, initializing it if
+necessary.
+"
+	exit 1
+fi
+
+
+# Create the base configuration directory.
+echo "Creating directories (/etc/kxc/)"
+mkdir -p /etc/kxc/
+
+# Create a private key for the client.
+if ! [ -e /etc/kxc/key.pem ]; then
+	echo "Generating private key (/etc/kxc/key.pem)"
+	openssl genrsa -out /etc/kxc/key.pem 2048
+	chmod 400 /etc/kxc/key.pem
+else
+	echo "Private key already exists (/etc/kxc/key.pem)"
+fi
+
+# And a self-signed certificate.
+if ! [ -e /etc/kxc/cert.pem ]; then
+	echo "Generating certificate (/etc/kxc/cert.pem)"
+	openssl req -new -x509 -batch \
+		-subj "/organizationalUnitName=kxc@$HOSTNAME/" \
+		-key /etc/kxc/key.pem -out /etc/kxc/cert.pem
+else
+	echo "Certificate already exists (/etc/kxc/cert.pem)"
+fi
+
+echo "Setting URL to kxd://$SERVER/$HOSTNAME/$KEYNAME"
+echo "kxd://$SERVER/$HOSTNAME/$KEYNAME" > "/etc/kxc/${KEYNAME}.url"
+
+echo
+echo
+echo "YOU need to copy the server certificate to"
+echo "/etc/kxc/${KEYNAME}.server_cert.pem. For example, using:"
+echo
+echo "  $ scp $SERVER:/etc/kxd/cert.pem /etc/kxc/${KEYNAME}.server_cert.pem"
+echo
diff --git a/scripts/kxd-add-client-key b/scripts/kxd-add-client-key
new file mode 100755
index 0000000..eff0d56
--- /dev/null
+++ b/scripts/kxd-add-client-key
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+set -e
+
+CLIENT="$1"
+KEYNAME="$2"
+
+if [ "$SERVER" = "" ] || [ "$KEYNAME" = "" ]; then
+	echo "
+Usage: kxd-add-client-key <client hostname> <key name>
+
+This command is a helper for adding a new key to kxd's configuration.
+It takes the hostname of the client and the key name, and puts the
+corresponding configuration (including a randomly generated key) in
+/etc/kxd/data/<client hostname>/<key name>/.
+"
+	exit 1
+
+CONFIGPATH="/etc/kxd/data/$SERVER/$KEYNAME"
+
+echo "Creating directory ($CONFIGPATH)"
+mkdir -p "$CONFIGPATH"
+
+echo "Generating random key from /dev/urandom ($CONFIGPATH/key)"
+dd if=/dev/urandom of="$CONFIGPATH/key" bs=1k count=2
+echo
+
+echo "Allowing host $CLIENT"
+echo "$CLIENT" >> "$CONFIGPATH/allowed_hosts"
+
+echo
+echo
+echo "YOU need to copy the client certificate to"
+echo "$CONFIGPATH/allowed_clients. For example, using:"
+echo
+echo " $ scp $CLIENT:/etc/kxc/cert.pem $CONFIGPATH/allowed_clients"
+echo
diff --git a/scripts/systemd/kxd.service b/scripts/systemd/kxd.service
new file mode 100644
index 0000000..42038c3
--- /dev/null
+++ b/scripts/systemd/kxd.service
@@ -0,0 +1,10 @@
+[Unit]
+Description = Key exchange daemon
+
+[Service]
+EnvironmentFile = /etc/default/kxd
+ExecStart = /usr/bin/kxd $OPTS
+Type = simple
+
+[Install]
+WantedBy = multi-user.target
diff --git a/tests/.pylintrc b/tests/.pylintrc
new file mode 100644
index 0000000..4ef871e
--- /dev/null
+++ b/tests/.pylintrc
@@ -0,0 +1,8 @@
+
+[MESSAGES CONTROL]
+disable=missing-docstring, too-many-public-methods, fixme
+
+[REPORTS]
+output-format=colorized
+reports=no
+
diff --git a/tests/run_tests b/tests/run_tests
new file mode 100755
index 0000000..8d7a98b
--- /dev/null
+++ b/tests/run_tests
@@ -0,0 +1,328 @@
+#!/usr/bin/env python
+
+"""
+Tests for kxd and kxc
+---------------------
+
+This file contains various integration and validation tests for kxc and kxd.
+
+It will create different test configurations and run the compiled server and
+client under various conditions, to make sure they behave as intended.
+"""
+
+# NOTE: Please run "pylint --rcfile=.pylintrc run_tests" after making changes,
+# to make sure the file has a reasonably uniform coding style.
+
+
+import httplib
+import os
+import platform
+import shutil
+import socket
+import ssl
+import subprocess
+import tempfile
+import time
+import unittest
+
+
+############################################################
+# Test infrastructure.
+#
+# These functions and classes are used to make the individual tests easier to
+# write.  For the individual test cases, see below.
+
+# Path to our built binaries; used to run the server and client for testing
+# purposes.
+BINS = os.path.abspath(
+        os.path.dirname(os.path.realpath(__file__)) + "/../out")
+
+DEVNULL = open("/dev/null", "w")
+
+TEMPDIR = "/does/not/exist"
+
+
+def setUpModule():    # pylint: disable=invalid-name
+    if not os.path.isfile(BINS + "/kxd"):
+        raise RuntimeError("kxd not found at " + BINS + "/kxd")
+    if not os.path.isfile(BINS + "/kxc"):
+        raise RuntimeError("kxc not found at " + BINS + "/kxc")
+
+    global TEMPDIR    # pylint: disable=global-statement
+    TEMPDIR = tempfile.mkdtemp(prefix="kxdtest-")
+
+
+def tearDownModule():   # pylint: disable=invalid-name
+    # Remove the temporary directory only on success.
+    # Be extra paranoid about removing.
+    # TODO: Only remove on success.
+    if len(TEMPDIR) > 10 and not TEMPDIR.startswith("/home"):
+        shutil.rmtree(TEMPDIR)
+
+
+def gen_certs(path):
+    subprocess.check_call(
+        ["openssl", "genrsa", "-out", "%s/key.pem" % path, "2048"],
+        stdout=DEVNULL, stderr=DEVNULL)
+    subprocess.check_call(
+        ["openssl", "req", "-new", "-x509", "-batch",
+         "-subj", "/organizationalUnitName=kxd-tests:%s@%s" % (
+            os.getlogin(), platform.node()),
+         "-key", "%s/key.pem" % path,
+         "-out", "%s/cert.pem" % path],
+        stdout=DEVNULL, stderr=DEVNULL)
+
+
+class Config(object):
+    def __init__(self):
+        # Note we create this temporary path but never delete it.
+        # This is intentional, as we rather leave a test file around, than
+        # risk a bug doing a harmful recursive delete.
+        self.path = tempfile.mkdtemp(prefix="config-", dir=TEMPDIR)
+        print "Using temporary path", self.path
+
+    def gen_certs(self):
+        gen_certs(self.path)
+
+    def cert_path(self):
+        return self.path + "/cert.pem"
+
+    def key_path(self):
+        return self.path + "/key.pem"
+
+    def cert(self):
+        return open(self.path + "/cert.pem").read()
+
+
+class ServerConfig(Config):
+    def __init__(self):
+        Config.__init__(self)
+        self.keys = {}
+
+    def new_key(self, name, allowed_clients=None, allowed_hosts=None):
+        self.keys[name] = os.urandom(1024)
+        key_path = self.path + "/data/" + name + "/"
+        if not os.path.isdir(key_path):
+            os.makedirs(key_path)
+        open(key_path + "key", "w").write(self.keys[name])
+
+        if allowed_clients is not None:
+            cfd = open(key_path + "/allowed_clients", "a")
+            for cli in allowed_clients:
+                cfd.write(cli)
+
+        if allowed_hosts is not None:
+            hfd = open(key_path + "/allowed_hosts", "a")
+            for host in allowed_hosts:
+                hfd.write(host + "\n")
+
+
+class ClientConfig(Config):
+    def __init__(self):
+        Config.__init__(self)
+        self.gen_certs()
+
+    def call(self, server_cert, url):
+        args = [BINS + "/kxc",
+                "--client_cert=%s/cert.pem" % self.path,
+                "--client_key=%s/key.pem" % self.path,
+                "--server_cert=%s" % server_cert,
+                url]
+        return subprocess.check_output(args, stderr=subprocess.STDOUT)
+
+
+def launch_daemon(cfg):
+    args = [BINS + "/kxd",
+            "--data_dir=%s/data" % cfg,
+            "--key=%s/key.pem" % cfg,
+            "--cert=%s/cert.pem" % cfg,
+            "--logfile=%s/log" % cfg]
+    return subprocess.Popen(args)
+
+
+class TestCase(unittest.TestCase):
+    def setUp(self):
+        self.server = ServerConfig()
+        self.server.gen_certs()
+        self.daemon = launch_daemon(self.server.path)
+
+        self.client = ClientConfig()
+
+        # Wait for the server to start accepting connections.
+        deadline = time.time() + 5
+        while time.time() < deadline:
+            try:
+                socket.create_connection(("localhost", 19840), timeout=5)
+                break
+            except socket.error:
+                continue
+        else:
+            self.fail("Timeout waiting for the server")
+
+    def tearDown(self):
+        self.daemon.kill()
+
+    # pylint: disable=invalid-name
+    def assertClientFails(self, url, regexp, client=None):
+        if client is None:
+            client = self.client
+
+        try:
+            client.call(self.server.cert_path(), url)
+        except subprocess.CalledProcessError as err:
+            self.assertRegexpMatches(err.output, regexp)
+        else:
+            self.fail("Client call did not fail as expected")
+
+
+############################################################
+# Test cases.
+#
+
+class Simple(TestCase):
+    """Simple test cases for common (mis)configurations."""
+
+    def test_simple(self):
+        self.server.new_key("k1",
+                allowed_clients=[self.client.cert()],
+                allowed_hosts=["localhost"])
+        key = self.client.call(self.server.cert_path(), "kxd://localhost/k1")
+        self.assertEquals(key, self.server.keys["k1"])
+
+    def test_404(self):
+        self.assertClientFails("kxd://localhost/k1", "404 Not Found")
+
+    def test_no_client_cert(self):
+        self.server.new_key("k1", allowed_hosts=["localhost"])
+        self.assertClientFails("kxd://localhost/k1",
+                "403 Forbidden.*No allowed certificate found")
+
+    def test_host_not_allowed(self):
+        self.server.new_key("k1",
+                allowed_clients=[self.client.cert()],
+                allowed_hosts=[])
+        self.assertClientFails("kxd://localhost/k1",
+                "403 Forbidden.*Host not allowed")
+
+    def test_not_allowed(self):
+        self.server.new_key("k1")
+        # We don't restrict the reason of failure, that's not defined in this
+        # case, as it could be either the host or the cert that are validated
+        # first.
+        self.assertClientFails("kxd://localhost/k1", "403 Forbidden")
+
+
+class Multiples(TestCase):
+    """Tests for multiple clients and keys."""
+
+    def setUp(self):
+        TestCase.setUp(self)
+        self.client2 = ClientConfig()
+
+    def test_two_clients(self):
+        self.server.new_key("k1",
+                allowed_clients=[self.client.cert(), self.client2.cert()],
+                allowed_hosts=["localhost"])
+        key = self.client.call(self.server.cert_path(), "kxd://localhost/k1")
+        self.assertEquals(key, self.server.keys["k1"])
+
+        key = self.client2.call(self.server.cert_path(), "kxd://localhost/k1")
+        self.assertEquals(key, self.server.keys["k1"])
+
+    def test_one_client_allowed(self):
+        self.server.new_key("k1",
+                allowed_clients=[self.client.cert()],
+                allowed_hosts=["localhost"])
+        key = self.client.call(self.server.cert_path(), "kxd://localhost/k1")
+        self.assertEquals(key, self.server.keys["k1"])
+
+        self.assertClientFails("kxd://localhost/k1",
+                "403 Forbidden.*No allowed certificate found",
+                client=self.client2)
+
+    def test_many_keys(self):
+        keys = ["a", "d/e", "a/b/c", "d/"]
+        for key in keys:
+            self.server.new_key(key,
+                    allowed_clients=[self.client.cert(), self.client2.cert()],
+                    allowed_hosts=["localhost"])
+
+        for key in keys:
+            data = self.client.call(self.server.cert_path(),
+                    "kxd://localhost/%s" % key)
+            self.assertEquals(data, self.server.keys[key])
+
+            data = self.client2.call(self.server.cert_path(),
+                    "kxd://localhost/%s" % key)
+            self.assertEquals(data, self.server.keys[key])
+
+        self.assertClientFails("kxd://localhost/a/b", "404 Not Found")
+
+
+class TrickyRequests(TestCase):
+    """Tests for tricky requests."""
+
+    def test_no_local_cert(self):
+        conn = httplib.HTTPSConnection("localhost", 19840)
+        try:
+            conn.request("GET", "/v1/")
+        except ssl.SSLError as err:
+            self.assertRegexpMatches(str(err), "alert bad certificate")
+        else:
+            self.fail("Client call did not fail as expected")
+
+    def test_dotdot(self):
+        conn = httplib.HTTPSConnection("localhost", 19840,
+                key_file=self.client.key_path(),
+                cert_file=self.client.cert_path())
+        conn.request("GET", "/v1/a/../b")
+        response = conn.getresponse()
+
+        # Go's http server intercepts these and gives us a 301 Moved
+        # Permanently.
+        self.assertEquals(response.status, 301)
+
+    def test_server_cert(self):
+        rawsock = socket.create_connection(("localhost", 19840))
+        sock = ssl.wrap_socket(rawsock,
+                keyfile=self.client.key_path(),
+                certfile=self.client.cert_path())
+
+        # We don't check the cipher itself, as it depends on the environment,
+        # but we should be using > 128 bit secrets.
+        self.assertTrue(sock.cipher()[2] > 128)
+
+        server_cert = ssl.DER_cert_to_PEM_cert(
+                sock.getpeercert(binary_form=True))
+        self.assertEquals(server_cert, self.server.cert())
+
+
+class BrokenServerConfig(TestCase):
+    """Tests for a broken server config."""
+
+    def test_broken_client_certs(self):
+        self.server.new_key("k1",
+                allowed_clients=[self.client.cert()],
+                allowed_hosts=["localhost"])
+
+        # Corrupt the client certificate.
+        cfd = open(self.server.path + "/data/k1/allowed_clients", "r+")
+        for _ in range(4):
+            cfd.readline()
+        cfd.write('+/+BROKEN+/+')
+        cfd.close()
+
+        self.assertClientFails("kxd://localhost/k1",
+                "500 Internal Server Error.*Error loading certs")
+
+    def test_missing_key(self):
+        self.server.new_key("k1",
+                allowed_clients=[self.client.cert()],
+                allowed_hosts=["localhost"])
+
+        os.unlink(self.server.path + "/data/k1/key")
+        self.assertClientFails("kxd://localhost/k1", "404 Not Found")
+
+
+if __name__ == "__main__":
+    unittest.main()