summaryrefslogtreecommitdiffstats
path: root/docs/tls-alpn.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tls-alpn.md')
-rw-r--r--docs/tls-alpn.md124
1 files changed, 124 insertions, 0 deletions
diff --git a/docs/tls-alpn.md b/docs/tls-alpn.md
new file mode 100644
index 0000000..fc19698
--- /dev/null
+++ b/docs/tls-alpn.md
@@ -0,0 +1,124 @@
+# TLS-ALPN-01
+
+With `tls-alpn-01`-type verification Let's Encrypt (or the ACME-protocol in general) is checking if you are in control of a domain by accessing
+your webserver using a custom ALPN and expecting a specially crafted TLS certificate containing a verification token.
+It will do that for any (sub-)domain you want to sign a certificate for.
+
+Dehydrated generates the required verification certificates, but the delivery is out of its scope.
+
+### Example lighttpd config
+
+lighttpd can be configured to recognize ALPN `acme-tls/1` and to respond to such
+requests using the specially crafted TLS certificates generated by dehydrated.
+Configure lighttpd and dehydrated to use the same path for these certificates.
+(Be sure to allow read access to the user account under which the lighttpd
+server is running.) `mkdir -p /etc/dehydrated/alpn-certs`
+
+lighttpd.conf:
+```
+ssl.acme-tls-1 = "/etc/dehydrated/alpn-certs"
+```
+
+When renewing certificates, specify `-t tls-alpn-01` and `--alpn /etc/dehydrated/alpn-certs` to dehydrated, e.g.
+```
+dehydrated -t tls-alpn-01 --alpn /etc/dehydrated/alpn-certs -c --out /etc/lighttpd/certs -d www.example.com
+# gracefully reload lighttpd to use the new certificates by sending lighttpd pid SIGUSR1
+systemctl reload lighttpd
+```
+
+### Example nginx config
+
+On an nginx tcp load-balancer you can use the `ssl_preread` module to map a different port for acme-tls
+requests than for e.g. HTTP/2 or HTTP/1.1 requests.
+
+Your config should look something like this:
+
+```nginx
+stream {
+ map $ssl_preread_alpn_protocols $tls_port {
+ ~\bacme-tls/1\b 10443;
+ default 443;
+ }
+
+ server {
+ listen 443;
+ listen [::]:443;
+ proxy_pass 10.13.37.42:$tls_port;
+ ssl_preread on;
+ }
+}
+```
+
+That way https requests are forwarded to port 443 on the backend server, and acme-tls/1 requests are
+forwarded to port 10443.
+
+In the future nginx might support internal routing based on custom ALPNs, but for now you'll have to
+use a custom responder for the alpn verification certificates (see below).
+
+### Example responder
+
+I hacked together a simple responder in Python, it might not be the best, but it works for me:
+
+```python
+#!/usr/bin/env python3
+
+import ssl
+import socketserver
+import threading
+import re
+import os
+
+ALPNDIR="/etc/dehydrated/alpn-certs"
+PROXY_PROTOCOL=False
+
+FALLBACK_CERTIFICATE="/etc/ssl/certs/ssl-cert-snakeoil.pem"
+FALLBACK_KEY="/etc/ssl/private/ssl-cert-snakeoil.key"
+
+class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
+ pass
+
+class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
+ def create_context(self, certfile, keyfile, first=False):
+ ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ ssl_context.set_ciphers('ECDHE+AESGCM')
+ ssl_context.set_alpn_protocols(["acme-tls/1"])
+ ssl_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+ if first:
+ ssl_context.set_servername_callback(self.load_certificate)
+ ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
+ return ssl_context
+
+ def load_certificate(self, sslsocket, sni_name, sslcontext):
+ print("Got request for %s" % sni_name)
+ if not re.match(r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$', sni_name):
+ return
+
+ certfile = os.path.join(ALPNDIR, "%s.crt.pem" % sni_name)
+ keyfile = os.path.join(ALPNDIR, "%s.key.pem" % sni_name)
+
+ if not os.path.exists(certfile) or not os.path.exists(keyfile):
+ return
+
+ sslsocket.context = self.create_context(certfile, keyfile)
+
+ def handle(self):
+ if PROXY_PROTOCOL:
+ buf = b""
+ while b"\r\n" not in buf:
+ buf += self.request.recv(1)
+
+ ssl_context = self.create_context(FALLBACK_CERTIFICATE, FALLBACK_KEY, True)
+ newsock = ssl_context.wrap_socket(self.request, server_side=True)
+
+if __name__ == "__main__":
+ HOST, PORT = "0.0.0.0", 10443
+
+ server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler, bind_and_activate=False)
+ server.allow_reuse_address = True
+ try:
+ server.server_bind()
+ server.server_activate()
+ server.serve_forever()
+ except:
+ server.shutdown()
+```