diff options
| author | Carlo Zancanaro <carlo@zancanaro.id.au> | 2024-01-31 11:46:25 +0000 |
|---|---|---|
| committer | Clément Lassieur <clement@lassieur.org> | 2024-01-31 16:54:19 +0100 |
| commit | 023c3e0ac44e7fc35eeebc87535a47df2cd01485 (patch) | |
| tree | c6cff009e27489e5277d47368a1868bb8e121d14 /gnu/services | |
| parent | d4a4b12f0ac52563254d34dc1e26030b354d3f73 (diff) | |
services: certbot: Add one-shot service to renew certificates.
* gnu/services/certbot.scm (certbot-renewal-one-shot): New procedure.
(certbot-service-type)[extensions]: Add it to shepherd-root extension.
(certbot-command): Make connection errors return a different exit code.
(certbot-activation): Remove message with certificate renewal instructions.
Change-Id: I614ac6214a753dba0396e2385a75926c8355caa1
Signed-off-by: Clément Lassieur <clement@lassieur.org>
Diffstat (limited to 'gnu/services')
| -rw-r--r-- | gnu/services/certbot.scm | 89 |
1 files changed, 75 insertions, 14 deletions
diff --git a/gnu/services/certbot.scm b/gnu/services/certbot.scm index cb1be0c0e95..f287c8367f4 100644 --- a/gnu/services/certbot.scm +++ b/gnu/services/certbot.scm | |||
| @@ -180,15 +180,45 @@ deploy." | |||
| 180 | (program-file | 180 | (program-file |
| 181 | "certbot-command" | 181 | "certbot-command" |
| 182 | #~(begin | 182 | #~(begin |
| 183 | (use-modules (ice-9 match)) | 183 | (use-modules (ice-9 match) |
| 184 | (let ((code 0)) | 184 | (ice-9 textual-ports)) |
| 185 | |||
| 186 | (define (log format-string . args) | ||
| 187 | (apply format #t format-string args) | ||
| 188 | (force-output)) | ||
| 189 | |||
| 190 | (define (file-contains? file string) | ||
| 191 | (string-contains (call-with-input-file file | ||
| 192 | get-string-all) | ||
| 193 | string)) | ||
| 194 | |||
| 195 | (define (connection-error?) | ||
| 196 | ;; Certbot errors are always exit code 1, so we need to look at | ||
| 197 | ;; the log file to see if there was a connection error. | ||
| 198 | (file-contains? "/var/log/letsencrypt/letsencrypt.log" | ||
| 199 | "Failed to establish a new connection")) | ||
| 200 | |||
| 201 | (let ((script-code 0)) | ||
| 185 | (for-each | 202 | (for-each |
| 186 | (match-lambda | 203 | (match-lambda |
| 187 | ((name . command) | 204 | ((name . command) |
| 188 | (begin | 205 | (log "Acquiring or renewing certificate: ~a~%" name) |
| 189 | (format #t "Acquiring or renewing certificate: ~a~%" name) | 206 | (cond |
| 190 | (set! code (or (apply system* command) code))))) | 207 | ((zero? (status:exit-val (apply system* command))) |
| 191 | '#$commands) code))))))) | 208 | (log "Certificate successfully acquired: ~a~%" name)) |
| 209 | ((connection-error?) | ||
| 210 | ;; If we have a connection error, then bail early with | ||
| 211 | ;; exit code 2. We don't expect this to resolve within the | ||
| 212 | ;; timespan of this script. | ||
| 213 | (log "Connection error - bailing out~%") | ||
| 214 | (exit 2)) | ||
| 215 | (else | ||
| 216 | ;; If we have any other type of error, then continue but | ||
| 217 | ;; exit with a failing status code in the end. | ||
| 218 | (log "Error: ~a - continuing with other domains~%" name) | ||
| 219 | (set! script-code 1))))) | ||
| 220 | '#$commands) | ||
| 221 | (exit script-code)))))))) | ||
| 192 | 222 | ||
| 193 | (define (certbot-renewal-jobs config) | 223 | (define (certbot-renewal-jobs config) |
| 194 | (list | 224 | (list |
| @@ -197,6 +227,40 @@ deploy." | |||
| 197 | #~(job '(next-minute-from (next-hour '(0 12)) (list (random 60))) | 227 | #~(job '(next-minute-from (next-hour '(0 12)) (list (random 60))) |
| 198 | #$(certbot-command config)))) | 228 | #$(certbot-command config)))) |
| 199 | 229 | ||
| 230 | (define (certbot-renewal-one-shot config) | ||
| 231 | (list | ||
| 232 | ;; Renew certificates when the system first starts. This is a one-shot | ||
| 233 | ;; service, because the mcron configuration will take care of running this | ||
| 234 | ;; periodically. This is most useful the very first time the system starts, | ||
| 235 | ;; to overwrite our self-signed certificates as soon as possible without | ||
| 236 | ;; user intervention. | ||
| 237 | (shepherd-service | ||
| 238 | (provision '(renew-certbot-certificates)) | ||
| 239 | (requirement '(nginx)) | ||
| 240 | (one-shot? #t) | ||
| 241 | (start #~(lambda _ | ||
| 242 | ;; This needs the network, but there's no reliable way to know | ||
| 243 | ;; if the network is up other than trying. If we fail due to a | ||
| 244 | ;; connection error we retry a number of times in the hope that | ||
| 245 | ;; the network comes up soon. | ||
| 246 | (let loop ((attempt 0)) | ||
| 247 | (let ((code (status:exit-val | ||
| 248 | (system* #$(certbot-command config))))) | ||
| 249 | (cond | ||
| 250 | ((and (= code 2) ; Exit code 2 means connection error | ||
| 251 | (< attempt 12)) ; Arbitrarily chosen max attempts | ||
| 252 | (sleep 10) ; Arbitrarily chosen retry delay | ||
| 253 | (loop (1+ attempt))) | ||
| 254 | ((zero? code) | ||
| 255 | ;; Success! | ||
| 256 | #t) | ||
| 257 | (else | ||
| 258 | ;; Failure. | ||
| 259 | #f)))))) | ||
| 260 | (auto-start? #t) | ||
| 261 | (documentation "Call certbot to renew certificates.") | ||
| 262 | (actions (list (shepherd-configuration-action (certbot-command config))))))) | ||
| 263 | |||
| 200 | (define (generate-certificate-gexp certbot-cert-directory rsa-key-size) | 264 | (define (generate-certificate-gexp certbot-cert-directory rsa-key-size) |
| 201 | (match-lambda | 265 | (match-lambda |
| 202 | (($ <certificate-configuration> name (primary-domain other-domains ...) | 266 | (($ <certificate-configuration> name (primary-domain other-domains ...) |
| @@ -240,9 +304,7 @@ deploy." | |||
| 240 | 304 | ||
| 241 | (define (certbot-activation config) | 305 | (define (certbot-activation config) |
| 242 | (let* ((certbot-directory "/var/lib/certbot") | 306 | (let* ((certbot-directory "/var/lib/certbot") |
| 243 | (certbot-cert-directory "/etc/letsencrypt/live") | 307 | (certbot-cert-directory "/etc/letsencrypt/live")) |
| 244 | (script (in-vicinity certbot-directory "renew-certificates")) | ||
| 245 | (message (format #f (G_ "~a may need to be run~%") script))) | ||
| 246 | (match config | 308 | (match config |
| 247 | (($ <certbot-configuration> package webroot certificates email | 309 | (($ <certbot-configuration> package webroot certificates email |
| 248 | server rsa-key-size default-location) | 310 | server rsa-key-size default-location) |
| @@ -258,10 +320,7 @@ deploy." | |||
| 258 | (map (generate-certificate-gexp certbot-cert-directory | 320 | (map (generate-certificate-gexp certbot-cert-directory |
| 259 | rsa-key-size) | 321 | rsa-key-size) |
| 260 | (filter certificate-configuration-start-self-signed? | 322 | (filter certificate-configuration-start-self-signed? |
| 261 | certificates))) | 323 | certificates))))))))) |
| 262 | |||
| 263 | (copy-file #$(certbot-command config) #$script) | ||
| 264 | (display #$message))))))) | ||
| 265 | 324 | ||
| 266 | (define certbot-nginx-server-configurations | 325 | (define certbot-nginx-server-configurations |
| 267 | (match-lambda | 326 | (match-lambda |
| @@ -294,7 +353,9 @@ deploy." | |||
| 294 | (service-extension activation-service-type | 353 | (service-extension activation-service-type |
| 295 | certbot-activation) | 354 | certbot-activation) |
| 296 | (service-extension mcron-service-type | 355 | (service-extension mcron-service-type |
| 297 | certbot-renewal-jobs))) | 356 | certbot-renewal-jobs) |
| 357 | (service-extension shepherd-root-service-type | ||
| 358 | certbot-renewal-one-shot))) | ||
| 298 | (compose concatenate) | 359 | (compose concatenate) |
| 299 | (extend (lambda (config additional-certificates) | 360 | (extend (lambda (config additional-certificates) |
| 300 | (certbot-configuration | 361 | (certbot-configuration |
