From 7276d8e6ae494bb7ab85c1cdfa2917e3b889468d Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Fri, 8 Sep 2023 17:04:38 +0200 Subject: Make TLS functional Enable letsencrypt certificates in httpd while making all daemons fall back to self-issued certs when /etc/letsencrypt/live is unpopulated. --- container.scm | 284 ++++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 225 insertions(+), 59 deletions(-) (limited to 'container.scm') diff --git a/container.scm b/container.scm index d57b565..ef97dc1 100644 --- a/container.scm +++ b/container.scm @@ -31,7 +31,8 @@ python version-control mail - admin) + admin + tls) (use-service-modules web shepherd certbot @@ -47,6 +48,14 @@ (define* (g-string-append #:rest args) #~(string-append #$@args)) +(define g-symlink-exists? + #~(lambda (path) + (false-if-exception + (with-input-from-file (dirname path) + (lambda _ + (statat (current-input-port) + (basename path) AT_SYMLINK_NOFOLLOW)))))) + (define (httpd-conf-token arg) (match arg ((? string?) @@ -88,32 +97,45 @@ (auto-www-aliases koszko-httpd-site-conf-auto-www-aliases (default #t))) -(define make-virtualhost-directives - (match-lambda - (($ name-and-aliases body auto-www-aliases) - (let ((name (car name-and-aliases)) - (aliases (cdr name-and-aliases))) - `(,(httpd-directive 'ServerName name) - ,@(map (cut httpd-directive 'ServerAlias <>) - aliases) - ,@(if auto-www-aliases - (map (lambda (alias-or-name) - (httpd-directive - 'ServerAlias (string-append "www." alias-or-name))) - name-and-aliases) - '()) - ,(httpd-directive 'ServerAdmin "koszko@koszko.org") - ,(httpd-tag 'If (list (format #f "%{HTTP_HOST} != '~a'" name)) - (httpd-directive 'Redirect 'permanent "/" - (format #f "https://~a/" name))) - ,(httpd-directive - 'Alias "/.well-known/acme-challenge" "/srv/http/acme-challenge/") - ,@body))))) +(define* (make-virtualhost-directives site-conf #:key (tls? #f)) + (match-record site-conf + (name-and-aliases body auto-www-aliases) + (let ((name (car name-and-aliases)) + (aliases (cdr name-and-aliases))) + `(,(httpd-directive 'ServerName name) + ,@(map (cut httpd-directive 'ServerAlias <>) + aliases) + ,@(if auto-www-aliases + (map (lambda (alias-or-name) + (httpd-directive + 'ServerAlias (string-append "www." alias-or-name))) + name-and-aliases) + '()) + ,(httpd-directive 'ServerAdmin "koszko@koszko.org") + ,(httpd-tag 'If (list (format #f "%{HTTP_HOST} != '~a'" name)) + (httpd-directive 'Redirect 'permanent "/" + (format #f "https://~a/" name))) + ,(httpd-directive + 'Alias "/.well-known/acme-challenge" "/srv/http/acme-challenge/") + ,@body + ,@(if tls? + (let ((cert-base (format #f "/etc/cert-links/guixbot_~a" name))) + (list (httpd-directive 'SSLEngine 'on) + (httpd-directive 'SSLCertificateFile + (string-append cert-base "/cert.pem")) + (httpd-directive 'SSLCertificateKeyFile + (string-append cert-base "/privkey.pem")) + (httpd-directive 'SSLCertificateChainFile + (string-append cert-base "/chain.pem")))) + '()))))) (define (make-virtualhosts koszko-site-conf-record) (list (httpd-virtualhost "*:80" - (make-virtualhost-directives koszko-site-conf-record)))) + (make-virtualhost-directives koszko-site-conf-record)) + (httpd-virtualhost + "*:443" + (make-virtualhost-directives koszko-site-conf-record #:tls? #t)))) (define %all-site-confs (list)) @@ -331,6 +353,11 @@ ,(httpd-directive 'Options '+Indexes))))) +(define %ssl-module + (httpd-module + (name "ssl_module") + (file (file-append httpd "/modules/mod_ssl.so")))) + (define %cgid-module (httpd-module (name "cgid_module") @@ -379,8 +406,10 @@ (config (httpd-config-file (server-name "koszko.org") + (listen '("80" "443")) (error-log "/var/log/httpd/error.log") - (modules `(,%cgid-module + (modules `(,%ssl-module + ,%cgid-module ,%wsgi-module ,@%proxy-http-modules ,%rewrite-module @@ -396,16 +425,147 @@ (eq? (service-type-name (service-extension-target ext)) (service-type-name type))) -(define %koszko-httpd-deploy-hook +(define %all-site-domains + (map (match-record-lambda + (name-and-aliases auto-www-aliases) + (let ((www-aliases (map (cut string-append "www." <>) + (if auto-www-aliases name-and-aliases '())))) + (append name-and-aliases www-aliases))) + %all-site-confs)) + +(define %all-site-cert-names + (map (lambda (domains) + (format #f "guixbot_~a" (car domains))) + %all-site-domains)) + +(define %g-certsaccess-gid + #~(group:gid (getgrnam "certsaccess"))) + +(define %setup-ca-gexp + #~(begin + (use-modules (guix build utils)) + + (define initial-umask + (umask)) + + (mkdir-p "/etc/self-issued/") + + (umask #o027) + + (for-each (lambda (path) + (mkdir-p path) + (chown path 0 #$%g-certsaccess-gid)) + '("/etc/cert-links" "/etc/self-issued/certs")) + + (umask #o266) + + (unless (file-exists? "/etc/self-issued/ca-key.pem") + (copy-file #$(local-file "./test-ca-key.pem") + "/etc/self-issued/ca-key.pem")) + + (umask #o022) + + (unless (file-exists? "/etc/self-issued/ca-cert.pem") + (copy-file #$(local-file "./test-ca-cert.pem") + "/etc/self-issued/ca-cert.pem")))) + +(define g-activate-certs + #~(lambda (cert-names domain-lists) + (use-modules (guix build utils) + (ice-9 format) + (ice-9 textual-ports)) + + (define (issue-cert cert-name domains) + (let* ((cert-dir (format #f "/etc/self-issued/certs/~a" cert-name)) + (cert-path (format #f "~a/cert.pem" cert-dir)) + (chain-path (format #f "~a/chain.pem" cert-dir)) + (fullchain-path (format #f "~a/fullchain.pem" cert-dir)) + (privkey-path (format #f "~a/privkey.pem" cert-dir)) + (csr-path (format #f "~a/csr.pem" cert-dir)) + (openssl (string-append #$openssl "/bin/openssl")) + (initial-umask (umask))) + (unless (false-if-exception (> (stat:gid (stat cert-path)) 0)) + ;; Prepare the directory and private key. + (umask #o027) + (mkdir-p cert-dir) + (system* openssl "genrsa" "-out" privkey-path "2048") + + ;; Make cert.pem and chain.pem. + (with-output-to-file "/tmp/sth" + (lambda _ + (display (format #f "subjectAltName=~{DNS:~a~^,~}" + domains)))) + (system* openssl "req" + "-new" "-nodes" "-out" csr-path "-key" privkey-path + "-subj" "/CN=My Server/C=PL/ST=PL/L=Krakow/O=Koszko" + "-addext" (format #f "subjectAltName=~{DNS:~a~^,~}" + domains)) + (system* openssl "x509" + "-req" "-in" csr-path "-copy_extensions=copyall" + "-CA" "/etc/self-issued/ca-cert.pem" + "-CAkey" "/etc/self-issued/ca-key.pem" "-CAcreateserial" + "-out" cert-path "-days" "3650" "-sha256") + (copy-file "/etc/self-issued/ca-cert.pem" chain-path) + + ;; Concatenate cert.pem and chain.pem into fullchain.pem. + (with-output-to-file fullchain-path + (lambda _ + (for-each (lambda (part) + (call-with-input-file part + (lambda (port) + (display (get-string-all port))))) + (list cert-path chain-path)))) + + ;; Correct permissions. + (chmod privkey-path #o640) + (for-each (lambda (path) + (chown path 0 #$%g-certsaccess-gid)) + (list cert-dir + privkey-path + cert-path + chain-path + fullchain-path)) + (umask initial-umask)))) + + (for-each (lambda (cert-name domains) + (let* ((link-name (format #f "/etc/cert-links/~a" cert-name)) + (tmp-link-name (format #f "~a.newlink" link-name)) + (letsencrypt-target-name + (format #f "/etc/letsencrypt/live/~a" cert-name)) + (self-issued-target-name + (format #f "/etc/self-issued/certs/~a" cert-name))) + (when (#$g-symlink-exists? tmp-link-name) + (delete-file tmp-link-name)) + (if (file-exists? letsencrypt-target-name) + (symlink letsencrypt-target-name tmp-link-name) + (begin + (issue-cert cert-name domains) + (symlink self-issued-target-name tmp-link-name))) + (rename-file tmp-link-name link-name))) + cert-names domain-lists))) + +(define (make-httpd-deploy-hook cert-name domains) (program-file "httpd-deploy-hook" #~(begin + (define %cert-name + #$cert-name) + + (define %domains + '#$domains) + (for-each (lambda (subdir) (system* "chgrp" "-R" "certsaccess" - (string-append "/etc/letsencrypt/" subdir))) + (format #f "/etc/letsencrypt/~a/~a" + subdir %cert-name))) '("live" "archive")) - (system* "find" "/etc/letsencrypt/archive/" "-name" "privkey1.pem" + + (system* "find" (format #f "/etc/letsencrypt/archive/~a" %cert-name) + "-name" "privkey1.pem" "-exec" "chmod" "640" "{}" ";") + + (#$g-activate-certs (list %cert-name) (list %domains)) + (kill (call-with-input-file "/var/run/httpd" read) SIGHUP)))) @@ -428,33 +588,41 @@ "cert-cleanup-hook" #~(delete-file #$%certbot-token-filename-gexp))) +(define koszko-certbot-service-type + (service-type + (inherit certbot-service-type) + (name 'koszko-certbot) + ;; Prevent certbot from pulling in Nginx — we use Apache here. + (extensions (filter + (lambda (ext) + (not (extension-of-type? ext nginx-service-type))) + (service-type-extensions certbot-service-type))))) + +(define %koszko-certificate-configurations + (map (lambda (cert-name domains) + (certificate-configuration + (name cert-name) + (domains domains) + (challenge "http") + (authentication-hook %koszko-certbot-auth-hook) + (cleanup-hook %koszko-certbot-cleanup-hook) + (deploy-hook (make-httpd-deploy-hook cert-name domains)))) + %all-site-cert-names %all-site-domains)) + (define %koszko-certbot-service - (service - (service-type - (inherit certbot-service-type) - (name 'koszko-certbot) - ;; Prevent certbot from pulling in Nginx — we use Apache here. - (extensions (filter - (lambda (ext) - (not (extension-of-type? ext nginx-service-type))) - (service-type-extensions certbot-service-type)))) - (certbot-configuration - (email "koszko@koszko.org") - (rsa-key-size 4096) - (certificates - (map - (match-lambda - (($ name-and-aliases auto-www-aliases) - (let ((www-aliases (map (cut string-append "www." <>) - (if auto-www-aliases name-and-aliases '())))) - (certificate-configuration - (name (string-append "guixbot_" (car name-and-aliases))) - (domains (append name-and-aliases www-aliases)) - (challenge "http") - (authentication-hook %koszko-certbot-auth-hook) - (cleanup-hook %koszko-certbot-cleanup-hook) - (deploy-hook %koszko-httpd-deploy-hook))))) - %all-site-confs))))) + (service koszko-certbot-service-type + (certbot-configuration + (email "koszko@koszko.org") + (rsa-key-size 4096) + (certificates %koszko-certificate-configurations)))) + +(define %koszko-certs-activation-service + (simple-service 'koszko-certs-activation-service + activation-service-type + #~(begin + #$%setup-ca-gexp + (#$g-activate-certs '#$%all-site-cert-names + '#$%all-site-domains)))) (define exim-configuration-config-file (@@ (gnu services mail) exim-configuration-config-file)) @@ -470,10 +638,7 @@ #~(begin ;; There's unfortunately no option to tell file-exist? or stat not to ;; follow symlinks, hence we use statat... - (unless (with-input-from-file "/var/log" - (lambda _ - (false-if-exception (statat (current-input-port) - "exim" AT_SYMLINK_NOFOLLOW)))) + (unless (#$g-symlink-exists? "/var/log/exim") (symlink "../spool/exim/log" "/var/log/exim")) ;; Exim often rereads its config file. Let's substitute it ;; atomiacally. @@ -607,8 +772,8 @@ exim_path = /run/setuid-programs/exim ;; Dovecot requires process-limit to be 1 here. (service-count 0) (process-limit 1)))) - (ssl-cert "