aboutsummaryrefslogtreecommitdiff
path: root/src/property/lets-encrypt.lisp
blob: b87224cc598e7a33e3469f099ddd66b836c5a3a2 (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
;;; Consfigurator -- Lisp declarative configuration management system

;;; Copyright (C) 2021  Sean Whitton <spwhitton@spwhitton.name>

;;; This file is free software; you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3, or (at your option)
;;; any later version.

;;; This file is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.

;;; You should have received a copy of the GNU General Public License
;;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

(in-package :consfigurator.property.lets-encrypt)
(named-readtables:in-readtable :consfigurator)

(defproplist installed :posix ()
  (:desc "Let's Encrypt client installed")
  (os:etypecase
    (debianlike (apt:installed "certbot"))))

(defclass agree-tos ()
  ((email-address :initarg :email-address))
  (:documentation
   "Object representing your agreement with the Let's Encrypt Subscriber
Agreement; you will need to pass this to properties which will invoke the
Let's Encrypt client.  Supply an e-mail address so that Let's Encrypt can
contact you for things like certificate expiry, planned outage notifications
etc."))

(define-print-object-for-structlike agree-tos)

(defmacro agree-tos (&key (email-address nil email-address-supplied-p))
  `(make-instance 'agree-tos ,@(and email-address-supplied-p
                                    `(:email-address ,email-address))))

;; Based on Propellor's LetsEncrypt.letsEncrypt' property.
(defprop %obtained :posix (agree-tos htdocs domains)
  (:apply
   (check-type agree-tos agree-tos)
   (let ((dir (ensure-directory-pathname
               (merge-pathnames (car domains) #P"/etc/letsencrypt/live/"))))
     (with-change-if-changes-files ((merge-pathnames "cert.pem" dir)
                                    (merge-pathnames "chain.pem" dir)
                                    (merge-pathnames "privkey.pem" dir)
                                    (merge-pathnames "fullchain.pem" dir))
       (mrun "letsencrypt" "certonly" "--agree-tos"
             (if (slot-boundp agree-tos 'email-address)
                 (strcat "--email=" (slot-value agree-tos 'email-address))
                 "--register-unsafely-without-email")
             "--webroot" "--webroot-path" htdocs
             "--text" "--noninteractive" "--keep-until-expiring"
             ;; Always request expansion in case DOMAINS has changed.
             "--expand"
             (loop for domain in domains
                   when (and (stringp domain) (plusp (length domain)))
                     collect (strcat "--domain=" domain)))))))

(defproplist certificate-obtained :posix (agree-tos htdocs &rest domains)
  "Obtains, and renews as necessary, an SSL certificate for DOMAINS.
The first element of DOMAINS, after flattening, is the Common Name of the
certificate.  Use of this property implies agreement with the Let's Encrypt
Subscriber Agreement; AGREE-TOS is an instance of LETS-ENCRYPT:AGREE-TOS.
HTDOCS is the web root for DOMAINS, which must be writeable, and publically
available over plain HTTP.

This property does nothing to ensure that your web server will actually use
the obtained certificate.  Typically you'll want to combine this property with
web server-specific properties in a DEFPROPLIST/DEFPROPSPEC."
  (:desc (format nil "Let's Encrypt for ~{~A~^, ~}" domains))
  (installed)
  (%obtained agree-tos htdocs (flatten domains)))

(defun dir-for (domain)
  (ensure-directory-pathname
   (merge-pathnames domain #P"/etc/letsencrypt/live/")))

(defun fullchain-for (domain)
  (merge-pathnames "fullchain.pem" (dir-for domain)))

(defun chain-for (domain)
  (merge-pathnames "chain.pem" (dir-for domain)))

(defun certificate-for (domain)
  (merge-pathnames "cert.pem" (dir-for domain)))

(defun privkey-for (domain)
  (merge-pathnames "privkey.pem" (dir-for domain)))