Skip to content

Latest commit

 

History

History
124 lines (95 loc) · 4.27 KB

tls-alpn.md

File metadata and controls

124 lines (95 loc) · 4.27 KB

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:

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:

#!/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()