summaryrefslogtreecommitdiff
path: root/posts/2021-03-19-how-to-automate-certbot-renewal-with-haproxy.html
blob: 634530b1cca12250f588f66415e1c185fe47b87d (plain) (blame)
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="/includes/stylesheet.css" />
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta
      property="og:description"
      content="The World Wide Web pages of Adam Carpenter"
    />
    <meta
      property="og:image"
      content="https://nextcloud.53hor.net/index.php/s/Nx9e7iHbw4t99wo/preview"
    />
    <meta property="og:site_name" content="53hor.net" />
    <meta
      property="og:title"
      content="How to Automate Certbot Renewal with HAProxy"
    />
    <meta property="og:type" content="website" />
    <meta property="og:url" content="https://www.53hor.net" />
    <title>53hornet ➙ How to Automate Certbot Renewal with HAProxy</title>
  </head>

  <body>
    <nav>
      <ul>
        <li>
          <a href="/">
            <img src="/includes/icons/home-roof.svg" />
            Home
          </a>
        </li>
        <li>
          <a href="/info.html">
            <img src="/includes/icons/information-variant.svg" />
            Info
          </a>
        </li>
        <li>
          <a href="https://git.53hor.net">
            <img src="/includes/icons/git.svg" />
            Repos
          </a>
        </li>
        <li>
          <a href="/hosted.html">
            <img src="/includes/icons/desktop-tower.svg" />
            Hosted
          </a>
        </li>
        <li>
          <a type="application/rss+xml" href="/rss.xml">
            <img src="/includes/icons/rss.svg" />
            RSS
          </a>
        </li>
      </ul>
    </nav>

    <article>
      <h1>How to Automate Certbot Renewal with HAProxy</h1>

      <p>
        So this is specifically for HAProxy on FreeBSD, but it should apply to
        other *nix systems as well. Basically, I use HAProxy as a reverse proxy
        to a bunch of servers I administer. I use Let's Encrypt for a
        certificate and I used <code>certbot</code> to generate that
        certificate. Generating the certificate for the first time is easy and
        has lots of documentation, but it wasn't initially clear on how I could
        easily set up auto-renewal. Here's how I did it.
      </p>

      <p>
        If you've already set up TLS termination with HAProxy and
        <code>certbot</code>, you know you need to combine your Let's Encrypt
        fullchain and private key to get a combined certificate that HAProxy can
        use. You can do this by <code>cat</code>-ing the chain and key together
        like so:
      </p>

      <pre>
<code>
cat /usr/local/etc/letsencrypt/live/$SITE/fullchain.pem /usr/local/etc/letsencrypt/live/$SITE/privkey.pem > /usr/local/etc/ssl/haproxy.pem
</code>
	  </pre>

      <p>
        In this example, <code>$SITE</code> is your domain name that you fed
        HAProxy when you created the certificate and <code>haproxy.pem</code> is
        wherever you're storing HAProxy's combined certificate. Your HAProxy
        config then points to that certificate like this:
      </p>

      <pre>
<code>
macon% grep crt /usr/local/etc/haproxy.conf
        bind *:443 ssl crt /usr/local/etc/ssl/haproxy.pem
</code>
	  </pre>

      <p>
        And that was the end of the first-time setup. Then a few months later
        you probably had to do it again because Let's Encrypt certs are only
        good for 90 days in between renewals. To renew the certificate, you
        usually run <code>certbot renew</code>, it detects which certificates
        are present, and uses either the webroot or standlone server renewal
        process. Then you have to <code>cat</code> the fullchain and privkey
        together and restart HAProxy so it starts using the new certificate.
      </p>

      <p>
        To automate those steps, newer versions of
        <code>certbot</code> will run any post renewal hooks (read: scripts)
        that you want. You can also configure HAProxy and
        <code>certbot</code> to perform the ACME challenge dance for renewal so
        that you don't have to use it interactively.
      </p>

      <p>
        First, if you haven't already done it, change your HAProxy config so
        there's a frontend+backend for responding to ACME challenges. In a
        frontend listening for requests, create an access control list for any
        request looking for <code>/.well-known/acme-challenge/</code>. Send
        those requests to a backend server with an unused local port.
      </p>

      <pre>
<code>
frontend http-in
		acl letsencrypt-acl path_beg /.well-known/acme-challenge/
        use_backend letsencrypt-backend if letsencrypt-acl
		...
backend letsencrypt-backend
		server letsencrypt 127.0.0.1:54321
</code>
	  </pre>

      <p>
        What this will do is allow <code>certbot</code> and Let's Encrypt to
        renew your server in standalone mode via your reverse proxy. As an added
        bonus it prevents you from having to open up an additional port on your
        firewall.
      </p>

      <p>
        Now you've gotta configure <code>certbot</code> to do just that. A
        config file was created in <code>certbot</code>'s
        <code>renew</code> directory for your site. All you need to do in that
        file is add a line to the <code>[renewalparams]</code> section
        specifying the port you're using in your HAProxy config.
      </p>

      <pre>
<code>
macon% echo "http01_port = 54321" >> /usr/local/etc/letsencrypt/renewal/$SITE.conf
</code>
	  </pre>

      <p>
        Now you need the post-renewal hooks. I dropped two separate scripts into
        the <code>renewal-hooks</code> directory: one does the job of combining
        the certificate chain and private key and the other just restarts
        HAProxy.
      </p>

      <pre>
<code>
macon% cat /usr/local/etc/letsencrypt/renewal-hooks/post/001-catcerts.sh
#!/bin/sh

SITE=(your site of course)

cd /usr/local/etc/letsencrypt/live/$SITE
cat fullchain.pem privkey.pem > /usr/local/etc/ssl/haproxy.pem
macon% cat /usr/local/etc/letsencrypt/renewal-hooks/post/002-haproxy.sh
#!/bin/sh
service haproxy restart
</code>
	  </pre>

      <p>
        When <code>certbot renew</code> is run, <code>certbot</code> checks the
        <code>renewal-hooks/post</code> directory and runs any executable things
        in it after it's renewed the certificate(s). As a side note,
        <em>make sure you hit those scripts with <code>chmod +x</code></em> or
        they probably won't run.
      </p>

      <p>
        Now all that's left is dropping a job into <code>cron</code> or
        <code>periodic</code> to run <code>certbot renew</code> at least once or
        twice within the renewal period.
      </p>

      <pre>
<code>
macon% doas crontab -l|grep certbot
# certbot renewal
@monthly certbot renew
</code>
	  </pre>

      <p>
        You can always test that your scripts are working with
        <code>certbot renew --dry-run</code> just to be safe.
      </p>

      <pre>
<code>
macon% doas certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /usr/local/etc/letsencrypt/renewal/53hor.net.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not due for renewal, but simulating renewal for dry run
Plugins selected: Authenticator standalone, Installer None
Simulating renewal of an existing certificate for 53hor.net and 7 more domains
Performing the following challenges:
http-01 challenge for 53hor.net
http-01 challenge for carpentertutoring.com
http-01 challenge for git.53hor.net
http-01 challenge for nextcloud.53hor.net
http-01 challenge for pkg.53hor.net
http-01 challenge for plex.53hor.net
http-01 challenge for theglassyladies.com
http-01 challenge for www.53hor.net
Waiting for verification...
Cleaning up challenges

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed without reload, fullchain is
/usr/local/etc/letsencrypt/live/53hor.net/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
  /usr/local/etc/letsencrypt/live/53hor.net/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Running post-hook command: /usr/local/etc/letsencrypt/renewal-hooks/post/001-catcerts.sh
Running post-hook command: /usr/local/etc/letsencrypt/renewal-hooks/post/002-haproxy.sh
Output from post-hook command 002-haproxy.sh:
Waiting for PIDS: 15191.
Starting haproxy.

</code>
		</pre>

      <p>
        And there it is. Automated Let's Encrypt certificate renewal on FreeBSD
        with HAProxy.
      </p>
    </article>
  </body>
</html>