summaryrefslogtreecommitdiff
path: root/gemfeed/2022-07-30-lets-encrypt-with-openbsd-and-rex.html
blob: 65c153ceb9356ce5466b2b6866c093c985016d9a (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
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Let's Encrypt with OpenBSD and Rex</title>
<link rel="shortcut icon" type="image/gif" href="/favicon.ico" />
<link rel="stylesheet" href="../style.css" />
<link rel="stylesheet" href="style-override.css" />
</head>
<body>
<div class="rfx-overlay-grid"></div>
<div class="rfx-overlay-scanlines"></div>
<div id="rfx-stars"></div>
<div class="rfx-vignette"></div>
<p class="header">
<a href="https://foo.zone">Home</a> | <a href="https://codeberg.org/snonux/foo.zone/src/branch/content-md/gemfeed/2022-07-30-lets-encrypt-with-openbsd-and-rex.md">Markdown</a> | <a href="gemini://foo.zone/gemfeed/2022-07-30-lets-encrypt-with-openbsd-and-rex.gmi">Gemini</a>
</p>
<h1 style='display: inline' id='let-s-encrypt-with-openbsd-and-rex'>Let&#39;s Encrypt with OpenBSD and Rex</h1><br />
<br />
<span class='quote'>Published at 2022-07-30T12:14:31+01:00</span><br />
<br />
<span>I was amazed at how easy it is to automatically generate and update Let&#39;s Encrypt certificates with OpenBSD.</span><br />
<br />
<pre>
                                               /    _    \
  The Hebern Machine                            \ ." ". /
                                  ___            /     \
                              ..""   ""..       |   O   |
                             /           \      |       |
                            /             \     |       |
                          ---------------------------------
                        _/  o     (O)     o   _            |
                      _/                    ." ".          |
                    I/    _________________/     \         |
                  _/I   ."                        |        |
          =====  /  I  /                         /         |
     =====  | | |   \ |       _________________."          |
=====  | |  | | |   /  \     /  _|_|__|_|_          __     |
  | |  | |  | | |   \   "._."  /  o    o  \       ."  ".   |
  |  --|  --|  -|   /          \         _/      /      \  |
   \____\____\__|   \  ______   |       /       |        | |
               --------      ---       /        |        | |
              ( )        (O)          /          \      /  |
               -----------------------            ".__."   |
               _|__________________________________________|_
              /                                              \
             /________________________________________________\
                                 ASCII Art by John Savard
</pre>
<br />
<h2 style='display: inline' id='table-of-contents'>Table of Contents</h2><br />
<br />
<ul>
<li><a href='#let-s-encrypt-with-openbsd-and-rex'>Let&#39;s Encrypt with OpenBSD and Rex</a></li>
<li>⇢ ⇢ <a href='#what-s-let-s-encrypt'>What&#39;s Let&#39;s Encrypt?</a></li>
<li>⇢ <a href='#meet-acme-client'>Meet <span class='inlinecode'>acme-client</span></a></li>
<li>⇢ <a href='#configuration'>Configuration</a></li>
<li>⇢ ⇢ <a href='#acme-clientconf'>acme-client.conf</a></li>
<li>⇢ ⇢ <a href='#httpdconf'>httpd.conf</a></li>
<li>⇢ ⇢ <a href='#cron-job'>CRON job</a></li>
<li>⇢ <a href='#relaydconf-and-smtpdconf'>relayd.conf and smtpd.conf</a></li>
<li>⇢ <a href='#rexification'>Rexification</a></li>
<li>⇢ ⇢ <a href='#general-acme-client-configuration'>General ACME client configuration</a></li>
<li>⇢ ⇢ <a href='#service-rexification-'>Service rexification </a></li>
<li>⇢ <a href='#all-pieces-together'>All pieces together</a></li>
<li>⇢ <a href='#conclusion'>Conclusion</a></li>
</ul><br />
<h3 style='display: inline' id='what-s-let-s-encrypt'>What&#39;s Let&#39;s Encrypt?</h3><br />
<br />
<span class='quote'>Let&#39;s Encrypt is a non-profit certificate authority run by Internet Security Research Group that provides X.509 certificates for Transport Layer Security (TLS) encryption at no charge. It is the world&#39;s largest certificate authority, used by more than 265 million websites, with the goal of all websites being secure and using HTTPS.</span><br />
<br />
<a class='textlink' href='https://en.wikipedia.org/wiki/Let&#39;s_Encrypt'>Source: Wikipedia</a><br />
<br />
<span>In short, it gives away TLS certificates for your website - for free! The catch is, that the certificates are only valid for three months. So it is better to automate certificate generation and renewals.</span><br />
<br />
<h2 style='display: inline' id='meet-acme-client'>Meet <span class='inlinecode'>acme-client</span></h2><br />
<br />
<span><span class='inlinecode'>acme-client</span> is the default Automatic Certifcate Management Environment (ACME) client on OpenBSD and part of the OpenBSD base system. </span><br />
<br />
<span>When invoked, the client first checks whether certificates actually require to be generated.</span><br />
<br />
<ul>
<li>It first checks whether a certificate already exists; if not, it will attempt to generate a new one.</li>
<li>If the certificate already exists but expires within the next 30 days, it will renew it.</li>
<li>Otherwise, <span class='inlinecode'>acme-client</span> won&#39;t do anything.</li>
</ul><br />
<span>Oversimplified, the following steps are undertaken by <span class='inlinecode'>acme-client</span> for generating a new certificate:</span><br />
<br />
<ul>
<li>Reading its config file <span class='inlinecode'>/etc/acme-client.conf</span> for a list of hosts (and their alternative names) to generate certificates. So it means you can also have certificates for arbitrary subdomains!</li>
<li>Automatic generation of the private certificate part (the certificate key) and the certificate signing request (CSR) to <span class='inlinecode'>/etc/ssl/...</span>.</li>
<li>Requesting Let&#39;s Encrypt to sign the certificate. This also includes providing a set of temporary files requested by Let&#39;s Encrypt in the next step for verification.</li>
<li>Let&#39;s Encrypt then will contact the hostname for the certificate through a particular URL (e.g. <span class='inlinecode'>http://foo.zone/.well-known/acme-challenge/...</span>) to verify that the requester is the valid owner of the host.</li>
<li>Let&#39;s Encrypt generates a certificate, which then is downloaded to <span class='inlinecode'>/etc/ssl/...</span>.</li>
</ul><br />
<h2 style='display: inline' id='configuration'>Configuration</h2><br />
<br />
<span>There is some (but easy) configuration required to make that all work on OpenBSD.</span><br />
<br />
<h3 style='display: inline' id='acme-clientconf'>acme-client.conf</h3><br />
<br />
<span>This is how my <span class='inlinecode'>/etc/acme-client.conf</span> looks like (I copied a template from <span class='inlinecode'>/etc/examples/acme-client.conf</span> to <span class='inlinecode'>/etc/acme-client.conf</span> and added my domains to the bottom:</span><br />
<br />
<pre>
#
# $OpenBSD: acme-client.conf,v 1.4 2020/09/17 09:13:06 florian Exp $
#
authority letsencrypt {
    api url "https://acme-v02.api.letsencrypt.org/directory"
    account key "/etc/acme/letsencrypt-privkey.pem"
}

authority letsencrypt-staging {
    api url "https://acme-staging-v02.api.letsencrypt.org/directory"
    account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

authority buypass {
    api url "https://api.buypass.com/acme/directory"
    account key "/etc/acme/buypass-privkey.pem"
    contact "mailto:me@example.com"
}

authority buypass-test {
    api url "https://api.test4.buypass.no/acme/directory"
    account key "/etc/acme/buypass-test-privkey.pem"
    contact "mailto:me@example.com"
}

domain buetow.org {
    alternative names { www.buetow.org paul.buetow.org }
    domain key "/etc/ssl/private/buetow.org.key"
    domain full chain certificate "/etc/ssl/buetow.org.fullchain.pem"
    sign with letsencrypt
}

domain dtail.dev {
    alternative names { www.dtail.dev }
    domain key "/etc/ssl/private/dtail.dev.key"
    domain full chain certificate "/etc/ssl/dtail.dev.fullchain.pem"
    sign with letsencrypt
}

domain foo.zone {
    alternative names { www.foo.zone }
    domain key "/etc/ssl/private/foo.zone.key"
    domain full chain certificate "/etc/ssl/foo.zone.fullchain.pem"
    sign with letsencrypt
}

domain irregular.ninja {
    alternative names { www.irregular.ninja }
    domain key "/etc/ssl/private/irregular.ninja.key"
    domain full chain certificate "/etc/ssl/irregular.ninja.fullchain.pem"
    sign with letsencrypt
}

domain snonux.land {
    alternative names { www.snonux.land }
    domain key "/etc/ssl/private/snonux.land.key"
    domain full chain certificate "/etc/ssl/snonux.land.fullchain.pem"
    sign with letsencrypt
}
</pre>
<br />
<h3 style='display: inline' id='httpdconf'>httpd.conf</h3><br />
<br />
<span>For ACME to work, you will need to configure the HTTP daemon so that the "special" ACME requests from Let&#39;s Encrypt are served correctly. I am using the standard OpenBSD <span class='inlinecode'>httpd</span> here. These are the snippets I use for the <span class='inlinecode'>foo.zone</span> host in <span class='inlinecode'>/etc/httpd.conf</span> (of course, you need a similar setup for all other hosts as well):</span><br />
<br />
<pre>
server "foo.zone" {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location * {
    block return 302 "https://$HTTP_HOST$REQUEST_URI"
  }
}

server "foo.zone" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/foo.zone.fullchain.pem"
    key "/etc/ssl/private/foo.zone.key"
  }
  location * {
    root "/htdocs/gemtexter/foo.zone"
    directory auto index
  }
}
</pre>
<br />
<span>As you see, plain HTTP only serves the ACME challenge path. Otherwise, it redirects the requests to TLS. The TLS section then attempts to use the Let&#39;s Encrypt certificates.</span><br />
<br />
<span>It is worth noticing that <span class='inlinecode'>httpd</span> will start without the certificates being present. This will cause a certificate error when you try to reach the HTTPS endpoint, but it helps to bootstrap Let&#39;s Encrypt. As you saw in the config snippet above, Let&#39;s Encrypt only requests the plain HTTP endpoint for the verification process, so HTTPS doesn&#39;t need to be operational yet at this stage. But once the certificates are generated, you will have to reload or restart <span class='inlinecode'>httpd</span> to use any new certificate.</span><br />
<br />
<h3 style='display: inline' id='cron-job'>CRON job</h3><br />
<br />
<span>You could now run <span class='inlinecode'>doas acme-client foo.zone</span> to generate the certificate or to renew it. Or you could automate it with CRON.</span><br />
<br />
<span>I have created a script <span class='inlinecode'>/usr/local/bin/acme.sh</span> for that for all of my domains:</span><br />
<br />
<pre>
#!/bin/sh

function handle_cert {
    host=$1
    # Create symlink, so that relayd also can read it.
    crt_path=/etc/ssl/$host
    if [ -e $crt_path.crt ]; then
        rm $crt_path.crt
    fi
    ln -s $crt_path.fullchain.pem $crt_path.crt
    # Requesting and renewing certificate.
    /usr/sbin/acme-client -v $host
}

has_update=no
handle_cert www.buetow.org
if [ $? -eq 0 ]; then
    has_update=yes
fi
handle_cert www.paul.buetow.org
if [ $? -eq 0 ]; then
    has_update=yes
fi
handle_cert www.tmp.buetow.org
if [ $? -eq 0 ]; then
    has_update=yes
fi
handle_cert www.dtail.dev
if [ $? -eq 0 ]; then
    has_update=yes
fi
handle_cert www.foo.zone
if [ $? -eq 0 ]; then
    has_update=yes
fi
handle_cert www.irregular.ninja
if [ $? -eq 0 ]; then
    has_update=yes
fi
handle_cert www.snonux.land
if [ $? -eq 0 ]; then
    has_update=yes
fi

# Pick up the new certs.
if [ $has_update = yes ]; then
    /usr/sbin/rcctl reload httpd
    /usr/sbin/rcctl reload relayd
    /usr/sbin/rcctl restart smtpd
fi
</pre>
<br />
<span>And added the following line to <span class='inlinecode'>/etc/daily.local</span> to run the script once daily so that certificates will be renewed fully automatically:</span><br />
<br />
<pre>
/usr/local/bin/acme.sh
</pre>
<br />
<span>I am receiving a daily output via E-Mail like this now:</span><br />
<br />
<pre>
Running daily.local:
acme-client: /etc/ssl/buetow.org.fullchain.pem: certificate valid: 80 days left
acme-client: /etc/ssl/paul.buetow.org.fullchain.pem: certificate valid: 80 days left
acme-client: /etc/ssl/tmp.buetow.org.fullchain.pem: certificate valid: 80 days left
acme-client: /etc/ssl/dtail.dev.fullchain.pem: certificate valid: 80 days left
acme-client: /etc/ssl/foo.zone.fullchain.pem: certificate valid: 80 days left
acme-client: /etc/ssl/irregular.ninja.fullchain.pem: certificate valid: 80 days left
acme-client: /etc/ssl/snonux.land.fullchain.pem: certificate valid: 79 days left
</pre>
<br />
<h2 style='display: inline' id='relaydconf-and-smtpdconf'>relayd.conf and smtpd.conf</h2><br />
<br />
<span>Besides <span class='inlinecode'>httpd</span>, <span class='inlinecode'>relayd</span> (mainly for Gemini) and <span class='inlinecode'>smtpd</span> (for mail, of course) also use TLS certificates. And as you can see in <span class='inlinecode'>acme.sh</span>, the services are reloaded or restarted (<span class='inlinecode'>smtpd</span> doesn&#39;t support reload) whenever a certificate is generated or updated.</span><br />
<br />
<h2 style='display: inline' id='rexification'>Rexification</h2><br />
<br />
<span>I didn&#39;t write all these configuration files by hand. As a matter of fact, everything is automated with the Rex configuration management system.</span><br />
<br />
<a class='textlink' href='https://www.rexify.org'>https://www.rexify.org</a><br />
<br />
<span>At the top of the <span class='inlinecode'>Rexfile</span> I define all my hosts:</span><br />
<br />
<pre>
our @acme_hosts = qw/buetow.org paul.buetow.org tmp.buetow.org dtail.dev foo.zone irregular.ninja snonux.land/;
</pre>
<br />
<h3 style='display: inline' id='general-acme-client-configuration'>General ACME client configuration</h3><br />
<br />
<span>ACME will be installed into the frontend group of hosts. Here, blowfish is the primary, and twofish is the secondary OpenBSD box.</span><br />
<br />
<pre>
group frontends =&gt; &#39;blowfish.buetow.org&#39;, &#39;twofish.buetow.org&#39;;
</pre>
<br />
<span>This is my Rex task for the general ACME configuration:</span><br />
<br />
<pre>
desc &#39;Configure ACME client&#39;;
task &#39;acme&#39;, group =&gt; &#39;frontends&#39;,
  sub {
    file &#39;/etc/acme-client.conf&#39;,
      content =&gt; template(&#39;./etc/acme-client.conf.tpl&#39;,
        acme_hosts =&gt; \@acme_hosts,
        is_primary =&gt; $is_primary),
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;644&#39;;

    file &#39;/usr/local/bin/acme.sh&#39;,
      content =&gt; template(&#39;./scripts/acme.sh.tpl&#39;,
        acme_hosts =&gt; \@acme_hosts,
        is_primary =&gt; $is_primary),
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;744&#39;;

    file &#39;/etc/daily.local&#39;,
      ensure =&gt; &#39;present&#39;,
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;644&#39;;

    append_if_no_such_line &#39;/etc/daily.local&#39;, &#39;/usr/local/bin/acme.sh&#39;;
  };
</pre>
<br />
<span>And there is also a Rex task just to run the ACME script remotely:</span><br />
<br />
<pre>
desc &#39;Invoke ACME client&#39;;
task &#39;acme_invoke&#39;, group =&gt; &#39;frontends&#39;,
  sub {
    say run &#39;/usr/local/bin/acme.sh&#39;;
  };

</pre>
<br />
<span>Furthermore, this snippet (also at the top of the Rexfile) helps to determine whether the current server is the primary server (all hosts will be without the <span class='inlinecode'>www.</span> prefix) or the secondary server (all hosts will be with the <span class='inlinecode'>www.</span> prefix):</span><br />
<br />
<pre>
# Bootstrapping the FQDN based on the server IP as the hostname and domain
# facts aren&#39;t set yet due to the myname file in the first place.
our $fqdns = sub {
  my $ipv4 = shift;
  return &#39;blowfish.buetow.org&#39; if $ipv4 eq &#39;23.88.35.144&#39;;
  return &#39;twofish.buetow.org&#39; if $ipv4 eq &#39;108.160.134.135&#39;;
  Rex::Logger::info("Unable to determine hostname for $ipv4", &#39;error&#39;);
  return &#39;HOSTNAME-UNKNOWN.buetow.org&#39;;
};

# To determine whether the server is the primary or the secondary.
our $is_primary = sub {
  my $ipv4 = shift;
  $fqdns-&gt;($ipv4) eq &#39;blowfish.buetow.org&#39;;
};
</pre>
<br />
<span>The following is the <span class='inlinecode'>acme-client.conf.tpl</span> Rex template file used for the automation. You see that the <span class='inlinecode'>www.</span> prefix isn&#39;t sent for the primary server. E.g. <span class='inlinecode'>foo.zone</span> will be served by the primary server (in my case, a server located in Germany) and <span class='inlinecode'>www.foo.zone</span> by the secondary server (in my case, a server located in Japan):</span><br />
<br />
<pre>
#
# $OpenBSD: acme-client.conf,v 1.4 2020/09/17 09:13:06 florian Exp $
#
authority letsencrypt {
	api url "https://acme-v02.api.letsencrypt.org/directory"
	account key "/etc/acme/letsencrypt-privkey.pem"
}

authority letsencrypt-staging {
	api url "https://acme-staging-v02.api.letsencrypt.org/directory"
	account key "/etc/acme/letsencrypt-staging-privkey.pem"
}

authority buypass {
	api url "https://api.buypass.com/acme/directory"
	account key "/etc/acme/buypass-privkey.pem"
	contact "mailto:me@example.com"
}

authority buypass-test {
	api url "https://api.test4.buypass.no/acme/directory"
	account key "/etc/acme/buypass-test-privkey.pem"
	contact "mailto:me@example.com"
}

&lt;%
  our $primary = $is_primary-&gt;($vio0_ip);
  our $prefix = $primary ? &#39;&#39; : &#39;www.&#39;;
%&gt;

&lt;% for my $host (@$acme_hosts) { %&gt;
domain &lt;%= $prefix.$host %&gt; {
	domain key "/etc/ssl/private/&lt;%= $prefix.$host %&gt;.key"
	domain full chain certificate "/etc/ssl/&lt;%= $prefix.$host %&gt;.fullchain.pem"
	sign with letsencrypt
}
&lt;% } %&gt;

</pre>
<br />
<span>And this is the <span class='inlinecode'>acme.sh.tpl</span>:</span><br />
<br />
<pre>
#!/bin/sh

&lt;%
  our $primary = $is_primary-&gt;($vio0_ip);
  our $prefix = $primary ? &#39;&#39; : &#39;www.&#39;;
-%&gt;

function handle_cert {
    host=$1
    # Create symlink, so that relayd also can read it.
    crt_path=/etc/ssl/$host
    if [ -e $crt_path.crt ]; then
        rm $crt_path.crt
    fi
    ln -s $crt_path.fullchain.pem $crt_path.crt
    # Requesting and renewing certificate.
    /usr/sbin/acme-client -v $host
}

has_update=no
&lt;% for my $host (@$acme_hosts) { -%&gt;
handle_cert &lt;%= $prefix.$host %&gt;
if [ $? -eq 0 ]; then
    has_update=yes
fi
&lt;% } -%&gt;

# Pick up the new certs.
if [ $has_update = yes ]; then
    /usr/sbin/rcctl reload httpd
    /usr/sbin/rcctl reload relayd
    /usr/sbin/rcctl restart smtpd
fi
</pre>
<br />
<h3 style='display: inline' id='service-rexification-'>Service rexification </h3><br />
<br />
<span>These are the Rex tasks setting up <span class='inlinecode'>httpd</span>, <span class='inlinecode'>relayd</span> and <span class='inlinecode'>smtpd</span> services:</span><br />
<br />
<pre>
desc &#39;Setup httpd&#39;;
task &#39;httpd&#39;, group =&gt; &#39;frontends&#39;,
  sub {
    append_if_no_such_line &#39;/etc/rc.conf.local&#39;, &#39;httpd_flags=&#39;;

    file &#39;/etc/httpd.conf&#39;,
      content =&gt; template(&#39;./etc/httpd.conf.tpl&#39;,
        acme_hosts =&gt; \@acme_hosts,
        is_primary =&gt; $is_primary),
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;644&#39;,
      on_change =&gt; sub { service &#39;httpd&#39; =&gt; &#39;restart&#39; };

    service &#39;httpd&#39;, ensure =&gt; &#39;started&#39;;
  };

desc &#39;Setup relayd&#39;;
task &#39;relayd&#39;, group =&gt; &#39;frontends&#39;,
  sub {
    append_if_no_such_line &#39;/etc/rc.conf.local&#39;, &#39;relayd_flags=&#39;;

    file &#39;/etc/relayd.conf&#39;,
      content =&gt; template(&#39;./etc/relayd.conf.tpl&#39;,
        ipv6address =&gt; $ipv6address,
        is_primary =&gt; $is_primary),
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;600&#39;,
      on_change =&gt; sub { service &#39;relayd&#39; =&gt; &#39;restart&#39; };

    service &#39;relayd&#39;, ensure =&gt; &#39;started&#39;;
  };

desc &#39;Setup OpenSMTPD&#39;;
task &#39;smtpd&#39;, group =&gt; &#39;frontends&#39;,
  sub {
    Rex::Logger::info(&#39;Dealing with mail aliases&#39;);
    file &#39;/etc/mail/aliases&#39;,
      source =&gt; &#39;./etc/mail/aliases&#39;,
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;644&#39;,
      on_change =&gt; sub { say run &#39;newaliases&#39; };

    Rex::Logger::info(&#39;Dealing with mail virtual domains&#39;);
    file &#39;/etc/mail/virtualdomains&#39;,
      source =&gt; &#39;./etc/mail/virtualdomains&#39;,
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;644&#39;,
      on_change =&gt; sub { service &#39;smtpd&#39; =&gt; &#39;restart&#39; };

    Rex::Logger::info(&#39;Dealing with mail virtual users&#39;);
    file &#39;/etc/mail/virtualusers&#39;,
      source =&gt; &#39;./etc/mail/virtualusers&#39;,
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;644&#39;,
      on_change =&gt; sub { service &#39;smtpd&#39; =&gt; &#39;restart&#39; };

    Rex::Logger::info(&#39;Dealing with smtpd.conf&#39;);
    file &#39;/etc/mail/smtpd.conf&#39;,
      content =&gt; template(&#39;./etc/mail/smtpd.conf.tpl&#39;,
        is_primary =&gt; $is_primary),
      owner =&gt; &#39;root&#39;,
      group =&gt; &#39;wheel&#39;,
      mode =&gt; &#39;644&#39;,
      on_change =&gt; sub { service &#39;smtpd&#39; =&gt; &#39;restart&#39; };

    service &#39;smtpd&#39;, ensure =&gt; &#39;started&#39;;
  };

</pre>
<br />
<span>This is the <span class='inlinecode'>httpd.conf.tpl</span>:</span><br />
<br />
<pre>
&lt;%
  our $primary = $is_primary-&gt;($vio0_ip);
  our $prefix = $primary ? &#39;&#39; : &#39;www.&#39;;
%&gt;

# Plain HTTP for ACME and HTTPS redirect
&lt;% for my $host (@$acme_hosts) { %&gt;
server "&lt;%= $prefix.$host %&gt;" {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location * {
    block return 302 "https://$HTTP_HOST$REQUEST_URI"
  }
}
&lt;% } %&gt;

# Gemtexter hosts
&lt;% for my $host (qw/foo.zone snonux.land/) { %&gt;
server "&lt;%= $prefix.$host %&gt;" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/&lt;%= $prefix.$host %&gt;.fullchain.pem"
    key "/etc/ssl/private/&lt;%= $prefix.$host %&gt;.key"
  }
  location * {
    root "/htdocs/gemtexter/&lt;%= $host %&gt;"
    directory auto index
  }
}
&lt;% } %&gt;

# DTail special host
server "&lt;%= $prefix %&gt;dtail.dev" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/&lt;%= $prefix %&gt;dtail.dev.fullchain.pem"
    key "/etc/ssl/private/&lt;%= $prefix %&gt;dtail.dev.key"
  }
  location * {
    block return 302 "https://github.dtail.dev$REQUEST_URI"
  }
}

# Irregular Ninja special host
server "&lt;%= $prefix %&gt;irregular.ninja" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/&lt;%= $prefix %&gt;irregular.ninja.fullchain.pem"
    key "/etc/ssl/private/&lt;%= $prefix %&gt;irregular.ninja.key"
  }
  location * {
    root "/htdocs/irregular.ninja"
    directory auto index
  }
}

# buetow.org special host.
server "&lt;%= $prefix %&gt;buetow.org" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/&lt;%= $prefix %&gt;buetow.org.fullchain.pem"
    key "/etc/ssl/private/&lt;%= $prefix %&gt;buetow.org.key"
  }
  block return 302 "https://paul.buetow.org"
}

server "&lt;%= $prefix %&gt;paul.buetow.org" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/&lt;%= $prefix %&gt;paul.buetow.org.fullchain.pem"
    key "/etc/ssl/private/&lt;%= $prefix %&gt;paul.buetow.org.key"
  }
  block return 302 "https://foo.zone/contact-information.html"
}

server "&lt;%= $prefix %&gt;tmp.buetow.org" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/&lt;%= $prefix %&gt;tmp.buetow.org.fullchain.pem"
    key "/etc/ssl/private/&lt;%= $prefix %&gt;tmp.buetow.org.key"
  }
  root "/htdocs/buetow.org/tmp"
  directory auto index
}
</pre>
<br />
<span>and this the <span class='inlinecode'>relayd.conf.tpl</span>:</span><br />
<br />
<pre>
&lt;%
  our $primary = $is_primary-&gt;($vio0_ip);
  our $prefix = $primary ? &#39;&#39; : &#39;www.&#39;;
%&gt;

log connection

tcp protocol "gemini" {
    tls keypair &lt;%= $prefix %&gt;foo.zone
    tls keypair &lt;%= $prefix %&gt;buetow.org
}

relay "gemini4" {
    listen on &lt;%= $vio0_ip %&gt; port 1965 tls
    protocol "gemini"
    forward to 127.0.0.1 port 11965
}

relay "gemini6" {
    listen on &lt;%= $ipv6address-&gt;($hostname) %&gt; port 1965 tls
    protocol "gemini"
    forward to 127.0.0.1 port 11965
}
</pre>
<br />
<span>And last but not least, this is the <span class='inlinecode'>smtpd.conf.tpl</span>:</span><br />
<br />
<pre>
&lt;%
  our $primary = $is_primary-&gt;($vio0_ip);
  our $prefix = $primary ? &#39;&#39; : &#39;www.&#39;;
%&gt;

pki "buetow_org_tls" cert "/etc/ssl/&lt;%= $prefix %&gt;buetow.org.fullchain.pem"
pki "buetow_org_tls" key "/etc/ssl/private/&lt;%= $prefix %&gt;buetow.org.key"

table aliases file:/etc/mail/aliases
table virtualdomains file:/etc/mail/virtualdomains
table virtualusers file:/etc/mail/virtualusers

listen on socket
listen on all tls pki "buetow_org_tls" hostname "&lt;%= $prefix %&gt;buetow.org"
#listen on all

action localmail mbox alias &lt;aliases&gt;
action receive mbox virtual &lt;virtualusers&gt;
action outbound relay

match from any for domain &lt;virtualdomains&gt; action receive
match from local for local action localmail
match from local for any action outbound
</pre>
<br />
<h2 style='display: inline' id='all-pieces-together'>All pieces together</h2><br />
<br />
<span>For the complete <span class='inlinecode'>Rexfile</span> example and all the templates, please look at the Git repository:</span><br />
<br />
<a class='textlink' href='https://codeberg.org/snonux/conf'>https://codeberg.org/snonux/conf</a><br />
<br />
<span>Besides ACME, other things, such as DNS servers, are also rexified. The following command will run all the Rex tasks and configure everything on my frontend machines automatically:</span><br />
<br />
<pre>
rex commons
</pre>
<br />
<span>The <span class='inlinecode'>commons</span> is a group of tasks I specified which combines a set of common tasks I always want to execute on all frontend machines. This also includes the ACME tasks mentioned in this article!</span><br />
<br />
<h2 style='display: inline' id='conclusion'>Conclusion</h2><br />
<br />
<span>ACME and Let&#39;s Encrypt greatly help reduce recurring manual maintenance work (creating and renewing certificates). Furthermore, all the certificates are free of cost! I love to use OpenBSD and Rex to automate all of this.</span><br />
<br />
<span>OpenBSD suits perfectly here as all the tools are already part of the base installation. But I like underdogs. Rex is not as powerful and popular as other configuration management systems (e.g. Puppet, Chef, SALT or even Ansible). It is more of an underdog, and the community is small.</span><br />
<br />
<span>Why re-inventing the wheel? I love that a <span class='inlinecode'>Rexfile</span> is just a Perl DSL. Also, OpenBSD comes with Perl in the base system. So no new programming language had to be added to my mix for the configuration management system. Also, the <span class='inlinecode'>acme.sh</span> shell script is not a Bash but a standard Bourne shell script, so I didn&#39;t have to install an additional shell as OpenBSD does not come with the Bash pre-installed.</span><br />
<br />
<span>E-Mail your comments to <span class='inlinecode'>paul@nospam.buetow.org</span> :-)</span><br />
<br />
<span>Other *BSD related posts are:</span><br />
<br />
<a class='textlink' href='./2025-12-07-f3s-kubernetes-with-freebsd-part-8.html'>2025-12-07 f3s: Kubernetes with FreeBSD - Part 8: Observability</a><br />
<a class='textlink' href='./2025-10-02-f3s-kubernetes-with-freebsd-part-7.html'>2025-10-02 f3s: Kubernetes with FreeBSD - Part 7: k3s and first pod deployments</a><br />
<a class='textlink' href='./2025-07-14-f3s-kubernetes-with-freebsd-part-6.html'>2025-07-14 f3s: Kubernetes with FreeBSD - Part 6: Storage</a><br />
<a class='textlink' href='./2025-05-11-f3s-kubernetes-with-freebsd-part-5.html'>2025-05-11 f3s: Kubernetes with FreeBSD - Part 5: WireGuard mesh network</a><br />
<a class='textlink' href='./2025-04-05-f3s-kubernetes-with-freebsd-part-4.html'>2025-04-05 f3s: Kubernetes with FreeBSD - Part 4: Rocky Linux Bhyve VMs</a><br />
<a class='textlink' href='./2025-02-01-f3s-kubernetes-with-freebsd-part-3.html'>2025-02-01 f3s: Kubernetes with FreeBSD - Part 3: Protecting from power cuts</a><br />
<a class='textlink' href='./2024-12-03-f3s-kubernetes-with-freebsd-part-2.html'>2024-12-03 f3s: Kubernetes with FreeBSD - Part 2: Hardware and base installation</a><br />
<a class='textlink' href='./2024-11-17-f3s-kubernetes-with-freebsd-part-1.html'>2024-11-17 f3s: Kubernetes with FreeBSD - Part 1: Setting the stage</a><br />
<a class='textlink' href='./2024-04-01-KISS-high-availability-with-OpenBSD.html'>2024-04-01 KISS high-availability with OpenBSD</a><br />
<a class='textlink' href='./2024-01-13-one-reason-why-i-love-openbsd.html'>2024-01-13 One reason why I love OpenBSD</a><br />
<a class='textlink' href='./2022-10-30-installing-dtail-on-openbsd.html'>2022-10-30 Installing DTail on OpenBSD</a><br />
<a class='textlink' href='./2022-07-30-lets-encrypt-with-openbsd-and-rex.html'>2022-07-30 Let&#39;s Encrypt with OpenBSD and Rex (You are currently reading this)</a><br />
<a class='textlink' href='./2016-04-09-jails-and-zfs-on-freebsd-with-puppet.html'>2016-04-09 Jails and ZFS with Puppet on FreeBSD</a><br />
<br />
<a class='textlink' href='../'>Back to the main site</a><br />
<p class="footer">
    Generated with <a href="https://codeberg.org/snonux/gemtexter">Gemtexter 3.0.1-develop</a> |
    served by <a href="https://www.OpenBSD.org">OpenBSD</a>/<a href="https://man.openbsd.org/relayd.8">relayd(8)</a>+<a href="https://man.openbsd.org/httpd.8">httpd(8)</a> |
    <a href="https://foo.zone/site-mirrors.html">Site Mirrors</a>
    <br />
    Webring: <a href="https://shring.sh/foo.zone/previous">previous</a> | <a href="https://shring.sh">shring</a> | <a href="https://shring.sh/foo.zone/next">next</a>
</p>
<script type="text/javascript" src="../retrofuturistic.js"></script>
</body>
</html>