diff options
Diffstat (limited to 'docs/tls-alpn.md')
-rw-r--r-- | docs/tls-alpn.md | 124 |
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() +``` |