# 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() ```