projects/guix·blob java-team · refs

385 lines · 17.8 KiB

1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2016 Nikita <[email protected]>
3;;; Copyright © 2016 Sou Bunnbu <[email protected]>
4;;; Copyright © 2017, 2018 Clément Lassieur <[email protected]>
5;;; Copyright © 2019 Julien Lepiller <[email protected]>
6;;; Copyright © 2020 Jack Hill <[email protected]>
7;;; Copyright © 2020 Tobias Geerinckx-Rice <[email protected]>
8;;; Copyright © 2021 Raghav Gururajan <[email protected]>
9;;; Copyright © 2021 Pierre Langlois <[email protected]>
10;;; Copyright © 2024 Carlo Zancanaro <[email protected]>
11;;;
12;;; This file is part of GNU Guix.
13;;;
14;;; GNU Guix is free software; you can redistribute it and/or modify it
15;;; under the terms of the GNU General Public License as published by
16;;; the Free Software Foundation; either version 3 of the License, or (at
17;;; your option) any later version.
18;;;
19;;; GNU Guix is distributed in the hope that it will be useful, but
20;;; WITHOUT ANY WARRANTY; without even the implied warranty of
21;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22;;; GNU General Public License for more details.
23;;;
24;;; You should have received a copy of the GNU General Public License
25;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
26
27(define-module (gnu services certbot)
28 #:use-module (gnu services)
29 #:use-module (gnu services base)
30 #:use-module (gnu services shepherd)
31 #:use-module (gnu services web)
32 #:use-module (gnu system shadow)
33 #:use-module (gnu packages tls)
34 #:use-module (guix i18n)
35 #:use-module (guix records)
36 #:use-module (guix gexp)
37 #:use-module (srfi srfi-1)
38 #:use-module (ice-9 format)
39 #:use-module (ice-9 match)
40 #:export (certbot-service-type
41 certbot-configuration
42 certbot-configuration?
43 certificate-configuration))
44
45;;; Commentary:
46;;;
47;;; Automatically obtaining TLS certificates from Let's Encrypt.
48;;;
49;;; Code:
50
51
52(define-record-type* <certificate-configuration>
53 certificate-configuration make-certificate-configuration
54 certificate-configuration?
55 (name certificate-configuration-name
56 (default #f))
57 (domains certificate-configuration-domains
58 (default '()))
59 (challenge certificate-configuration-challenge
60 (default #f))
61 (csr certificate-configuration-csr
62 (default #f))
63 (authentication-hook certificate-authentication-hook
64 (default #f))
65 (cleanup-hook certificate-cleanup-hook
66 (default #f))
67 (deploy-hook certificate-configuration-deploy-hook
68 (default #f))
69 (dry-run? certbot-configuration-dry-run?
70 (default #f))
71 (start-self-signed? certificate-configuration-start-self-signed?
72 (default #t)))
73
74(define-record-type* <certbot-configuration>
75 certbot-configuration make-certbot-configuration
76 certbot-configuration?
77 (package certbot-configuration-package
78 (default certbot))
79 (webroot certbot-configuration-webroot
80 (default "/var/www"))
81 (certificates certbot-configuration-certificates
82 (default '()))
83 (email certbot-configuration-email
84 (default #f))
85 (server certbot-configuration-server
86 (default #f))
87 (rsa-key-size certbot-configuration-rsa-key-size
88 (default #f))
89 (default-location certbot-configuration-default-location
90 (default
91 (nginx-location-configuration
92 (uri "/")
93 (body
94 (list "return 301 https://$host$request_uri;"))))))
95
96(define (certbot-deploy-hook name deploy-hook-script)
97 "Returns a gexp which creates symlinks for privkey.pem and fullchain.pem
98from /etc/certs/NAME to /etc/letsenctypt/live/NAME. If DEPLOY-HOOK-SCRIPT is
99not #f then it is run after the symlinks have been created. This wrapping is
100necessary for certificates with start-self-signed? set to #t, as it will
101overwrite the initial self-signed certificates upon the first successful
102deploy."
103 (program-file
104 (string-append name "-deploy-hook")
105 (with-imported-modules '((gnu services herd)
106 (guix build utils))
107 #~(begin
108 (use-modules (gnu services herd)
109 (guix build utils))
110 (mkdir-p #$(string-append "/etc/certs/" name))
111 (chmod #$(string-append "/etc/certs/" name) #o755)
112
113 ;; Create new symlinks
114 (symlink #$(string-append
115 "/etc/letsencrypt/live/" name "/privkey.pem")
116 #$(string-append "/etc/certs/" name "/privkey.pem.new"))
117 (symlink #$(string-append
118 "/etc/letsencrypt/live/" name "/fullchain.pem")
119 #$(string-append "/etc/certs/" name "/fullchain.pem.new"))
120
121 ;; Rename over the top of the old ones, just in case they were the
122 ;; original self-signed certificates.
123 (rename-file #$(string-append "/etc/certs/" name "/privkey.pem.new")
124 #$(string-append "/etc/certs/" name "/privkey.pem"))
125 (rename-file #$(string-append "/etc/certs/" name "/fullchain.pem.new")
126 #$(string-append "/etc/certs/" name "/fullchain.pem"))
127
128 ;; With the new certificates in place, tell nginx to reload them.
129 (with-shepherd-action 'nginx ('reload) result result)
130
131 #$@(if deploy-hook-script
132 (list #~(invoke #$deploy-hook-script))
133 '())))))
134
135(define certbot-command
136 (match-lambda
137 (($ <certbot-configuration> package webroot certificates email
138 server rsa-key-size default-location)
139 (let* ((certbot (file-append package "/bin/certbot"))
140 (rsa-key-size (and rsa-key-size (number->string rsa-key-size)))
141 (commands
142 (map
143 (match-lambda
144 (($ <certificate-configuration> custom-name domains challenge
145 csr authentication-hook
146 cleanup-hook deploy-hook
147 dry-run?)
148 (append
149 (let ((name (or custom-name (car domains))))
150 (if challenge
151 (append
152 (list name certbot "certonly" "-n" "--agree-tos"
153 "--manual"
154 (string-append "--preferred-challenges=" challenge)
155 "--cert-name" name
156 "-d" (string-join domains ","))
157 (if csr `("--csr" ,csr) '())
158 (if email
159 `("--email" ,email)
160 '("--register-unsafely-without-email"))
161 (if server `("--server" ,server) '())
162 (if rsa-key-size `("--rsa-key-size" ,rsa-key-size) '())
163 (if authentication-hook
164 `("--manual-auth-hook" ,authentication-hook)
165 '())
166 (if cleanup-hook `("--manual-cleanup-hook" ,cleanup-hook) '())
167 (list "--deploy-hook"
168 (certbot-deploy-hook name deploy-hook)))
169 (append
170 (list name certbot "certonly" "-n" "--agree-tos"
171 "--webroot" "-w" webroot
172 "--cert-name" name
173 "-d" (string-join domains ","))
174 (if csr `("--csr" ,csr) '())
175 (if email
176 `("--email" ,email)
177 '("--register-unsafely-without-email"))
178 (if server `("--server" ,server) '())
179 (if rsa-key-size `("--rsa-key-size" ,rsa-key-size) '())
180 (list "--deploy-hook"
181 (certbot-deploy-hook name deploy-hook)))))
182 ;; Common options.
183 (if dry-run? '("--dry-run") '()))))
184 certificates)))
185 (program-file
186 "certbot-command"
187 #~(begin
188 (use-modules (ice-9 match)
189 (ice-9 textual-ports))
190
191 (define (log format-string . args)
192 (apply format #t format-string args)
193 (force-output))
194
195 (define (file-contains? file string)
196 (string-contains (call-with-input-file file
197 get-string-all)
198 string))
199
200 (define (connection-error?)
201 ;; Certbot errors are always exit code 1, so we need to look at
202 ;; the log file to see if there was a connection error.
203 (file-contains? "/var/log/letsencrypt/letsencrypt.log"
204 "Failed to establish a new connection"))
205
206 (let ((script-code 0))
207 (for-each
208 (match-lambda
209 ((name . command)
210 (log "Acquiring or renewing certificate: ~a~%" name)
211 (cond
212 ((zero? (status:exit-val (apply system* command)))
213 (log "Certificate successfully acquired: ~a~%" name))
214 ((connection-error?)
215 ;; If we have a connection error, then bail early with
216 ;; exit code 2. We don't expect this to resolve within the
217 ;; timespan of this script.
218 (log "Connection error - bailing out~%")
219 (exit 2))
220 (else
221 ;; If we have any other type of error, then continue but
222 ;; exit with a failing status code in the end.
223 (log "Error: ~a - continuing with other domains~%" name)
224 (set! script-code 1)))))
225 '#$commands)
226 (exit script-code))))))))
227
228(define (certbot-renewal-shepherd-services config)
229 (list (shepherd-service
230 (provision '(certbot-certificate-renewal))
231 (requirement '(user-processes nginx))
232 (modules '((shepherd service timer)))
233 (start #~(make-timer-constructor
234 ;; Attempt to renew the certificates twice per day. See
235 ;; https://eff-certbot.readthedocs.io/.
236 (calendar-event #:minutes '(22) #:hours '(0 12))
237 (command '(#$(certbot-command config)))
238 #:wait-for-termination? #t))
239 (stop #~(make-timer-destructor))
240 (documentation "Periodically run the 'certbot' command to renew X.509
241certificates.")
242 (actions
243 (list shepherd-trigger-action
244 (shepherd-configuration-action (certbot-command config)))))
245
246 ;; Renew certificates when the system first starts. This is a one-shot
247 ;; service, because the timer above takes care of running this
248 ;; periodically. This is most useful the very first time the system
249 ;; starts, to overwrite our self-signed certificates as soon as
250 ;; possible without user intervention.
251 (shepherd-service
252 (provision '(renew-certbot-certificates))
253 (requirement '(user-processes nginx))
254 (one-shot? #t)
255 (start #~(lambda _
256 ;; This needs the network, but there's no reliable way to know
257 ;; if the network is up other than trying. If we fail due to a
258 ;; connection error we retry a number of times in the hope that
259 ;; the network comes up soon.
260 (let loop ((attempt 0))
261 (let ((code (status:exit-val
262 (system* #$(certbot-command config)))))
263 (cond
264 ((and (= code 2) ; Exit code 2 means connection error
265 (< attempt 12)) ; Arbitrarily chosen max attempts
266 (sleep 10) ; Arbitrarily chosen retry delay
267 (loop (1+ attempt)))
268 ((zero? code)
269 ;; Success!
270 #t)
271 (else
272 ;; Failure.
273 #f))))))
274 (auto-start? #t)
275 (documentation "Run 'certbot' to renew certificates at boot time.")
276 (actions
277 (list (shepherd-configuration-action (certbot-command config)))))))
278
279(define (generate-certificate-gexp certbot-cert-directory rsa-key-size)
280 (match-lambda
281 (($ <certificate-configuration> name (primary-domain other-domains ...)
282 challenge
283 csr authentication-hook
284 cleanup-hook deploy-hook)
285 (let (;; Arbitrary default subject, with just the
286 ;; right domain filled in. These values don't
287 ;; have any real significance.
288 (subject (string-append
289 "/C=US/ST=Oregon/L=Portland/O=Company Name/OU=Org/CN="
290 primary-domain))
291 (alt-names (if (null? other-domains)
292 #f
293 (format #f "subjectAltName=~{DNS:~a~^,~}"
294 other-domains)))
295 (directory (string-append "/etc/certs/" (or name primary-domain))))
296 #~(when (not (file-exists? #$directory))
297 ;; We generate self-signed certificates in /etc/certs/{domain},
298 ;; because certbot is very sensitive to its directory
299 ;; structure. It refuses to write over the top of existing files,
300 ;; so we need to use a directory outside of its control.
301 ;;
302 ;; These certificates are overwritten by the certbot deploy hook
303 ;; the first time it successfully obtains a letsencrypt-signed
304 ;; certificate.
305 (mkdir-p #$directory)
306 (chmod #$directory #o755)
307 (invoke #$(file-append openssl "/bin/openssl")
308 "req" "-x509"
309 "-newkey" #$(string-append "rsa:" (or rsa-key-size "4096"))
310 "-keyout" #$(string-append directory "/privkey.pem")
311 "-out" #$(string-append directory "/fullchain.pem")
312 "-sha256"
313 "-days" "1" ; Only one day, because we expect certbot to run
314 "-nodes"
315 "-subj" #$subject
316 #$@(if alt-names
317 (list "-addext" alt-names)
318 (list))))))))
319
320(define (certbot-activation config)
321 (let* ((certbot-directory "/var/lib/certbot")
322 (certbot-cert-directory "/etc/letsencrypt/live"))
323 (match config
324 (($ <certbot-configuration> package webroot certificates email
325 server rsa-key-size default-location)
326 (with-imported-modules '((guix build utils))
327 #~(begin
328 (use-modules (guix build utils))
329 (mkdir-p #$webroot)
330 (mkdir-p #$certbot-directory)
331 (mkdir-p #$certbot-cert-directory)
332
333 #$@(let ((rsa-key-size (and rsa-key-size
334 (number->string rsa-key-size))))
335 (map (generate-certificate-gexp certbot-cert-directory
336 rsa-key-size)
337 (filter certificate-configuration-start-self-signed?
338 certificates)))))))))
339
340(define certbot-nginx-server-configurations
341 (match-lambda
342 (($ <certbot-configuration> package webroot certificates email
343 server rsa-key-size default-location)
344 (define (certificate->nginx-server certificate-configuration)
345 (match-record certificate-configuration <certificate-configuration>
346 (domains challenge)
347 (nginx-server-configuration
348 (listen '("80" "[::]:80"))
349 (ssl-certificate #f)
350 (ssl-certificate-key #f)
351 (server-name domains)
352 (locations
353 (filter identity
354 (append
355 (if challenge
356 '()
357 (list (nginx-location-configuration
358 (uri "/.well-known")
359 (body (list (list "root " webroot ";"))))))
360 (list default-location)))))))
361 (map certificate->nginx-server certificates))))
362
363(define certbot-service-type
364 (service-type (name 'certbot)
365 (extensions
366 (list (service-extension nginx-service-type
367 certbot-nginx-server-configurations)
368 (service-extension profile-service-type
369 (compose list certbot-configuration-package))
370 (service-extension activation-service-type
371 certbot-activation)
372 (service-extension shepherd-root-service-type
373 certbot-renewal-shepherd-services)))
374 (compose concatenate)
375 (extend (lambda (config additional-certificates)
376 (certbot-configuration
377 (inherit config)
378 (certificates
379 (append
380 (certbot-configuration-certificates config)
381 additional-certificates)))))
382 (description
383 "Automatically renew @url{https://letsencrypt.org, Let's
384Encrypt} HTTPS certificates by adjusting the nginx web server configuration
385and periodically invoking @command{certbot}.")))