summaryrefslogtreecommitdiffstats
path: root/docs/tls-alpn.md
blob: fc19698e91a69bb8f5f754ecef9deaefee441211 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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()
```