diff options
author | Wojtek Kosior <koszko@koszko.org> | 2023-09-08 17:04:38 +0200 |
---|---|---|
committer | Wojtek Kosior <koszko@koszko.org> | 2023-09-09 15:54:48 +0200 |
commit | 7276d8e6ae494bb7ab85c1cdfa2917e3b889468d (patch) | |
tree | 6fea20b60ce604555335f977713d9ac0b3bd6299 | |
parent | 18da556c5cb0234a4c1ad3df0b263a452db2ebf1 (diff) | |
download | koszko-org-server-7276d8e6ae494bb7ab85c1cdfa2917e3b889468d.tar.gz koszko-org-server-7276d8e6ae494bb7ab85c1cdfa2917e3b889468d.zip |
Make TLS functional
Enable letsencrypt certificates in httpd while making all daemons fall back to
self-issued certs when /etc/letsencrypt/live is unpopulated.
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Makefile | 20 | ||||
-rw-r--r-- | container.scm | 284 | ||||
-rw-r--r-- | exim.conf | 4 | ||||
-rwxr-xr-x | fake-client-setup-mounts.sh | 3 |
5 files changed, 249 insertions, 65 deletions
@@ -10,6 +10,9 @@ test-root Makefile.local pidfile hosts +test-ca-key.pem +test-ca-cert.pem +test-ca-certificates.crt *.touchfile schemas/ sample-malcontent/ @@ -28,6 +28,8 @@ GUIX_SYS_CONTAINER = $(GUIX_TM) system container $(GUIX_LOAD_PATHS) GUIX_SHELL = $(GUIX_TM) shell +GUIX_OPENSSL = $(GUIX_SHELL) openssl -- openssl + KOSZKO_ORG_WEBSITE_INFO = \ subrepos/koszko-org-website/src/koszko_org_website.egg-info/PKG-INFO HYDRILLA_WEBSITE_INFO = \ @@ -40,7 +42,7 @@ ALL_EGG_INFOS = \ $(HYDRILLA_INFO) CONTAINER_PREREQUISITES = container.scm $(ALL_EGG_INFOS) hydrilla-wsgi.py \ - exim.conf Makefile.local + exim.conf Makefile.local test-ca-key.pem test-ca-cert.pem PWD_DERIVED_DIRECTORY_NAME != pwd | sed 's|[/'\'']|!|g' TEST_ROOT_DIR = '/tmp/$(PWD_DERIVED_DIRECTORY_NAME)!!test-root/current' @@ -57,6 +59,17 @@ $(HYDRILLA_INFO): Makefile.local: touch $@ +test-ca-key.pem: + $(GUIX_OPENSSL) genrsa -out $@ 4096 + +test-ca-cert.pem: test-ca-key.pem + $(GUIX_OPENSSL) req -x509 -new -nodes -key $< -sha256 -days 3650 \ + -out $@ -subj '/CN=Self-signed CA/C=PL/ST=PL/L=Krakow/O=Koszko' + +test-ca-certificates.crt: /etc/ssl/certs/ca-certificates.crt \ + test-ca-cert.pem + cat $^ > $@ + container-runner: | $(CONTAINER_PREREQUISITES) container-runner.touchfile: $(CONTAINER_PREREQUISITES) @@ -197,7 +210,7 @@ enter-container: pidfile nsenter -a -t "$$(cat pidfile)" \ /run/current-system/profile/bin/bash --login -fake-client: fake-client-setup-mounts.sh hosts +fake-client: fake-client-setup-mounts.sh hosts test-ca-certificates.crt unshare --map-root-user --mount ./$< "$${SHELL:-/bin/sh}" install: $(CONTAINER_PREREQUISITES) @@ -223,7 +236,8 @@ clean: clean-runner for SUBREPO in $(SUBREPOS_WITH_MAKEFILE); do \ $(MAKE) -C subrepos/"$$SUBREPO" clean; \ done - rm -rf log test-root hosts schemas sample-malcontent + rm -rf log test-root hosts test-ca-key.pem test-ca-cert.pem \ + test-ca-certificates.crt schemas sample-malcontent .PHONY: all \ clean-runner clean \ 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 - (($ <koszko-httpd-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))))) +(define* (make-virtualhost-directives site-conf #:key (tls? #f)) + (match-record site-conf <koszko-httpd-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 <koszko-httpd-site-conf> + (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 - (($ <koszko-httpd-site-conf> 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 "</etc/letsencrypt/live/guixbot_koszko.org/fullchain.pem") - (ssl-key "</etc/letsencrypt/live/guixbot_koszko.org/privkey.pem") + (ssl-cert "</etc/cert-links/guixbot_koszko.org/fullchain.pem") + (ssl-key "</etc/cert-links/guixbot_koszko.org/privkey.pem") (auth-mechanisms '("plain" "login")) (passdbs (list (passdb-configuration (driver "passwd-file") @@ -864,6 +1029,7 @@ log: (description "Make other services assume network is there.")) #f) %koszko-certbot-service + %koszko-certs-activation-service %koszko-exim-service %koszko-mail-aliases-service %koszko-dovecot-service @@ -23,8 +23,8 @@ acl_smtp_data_prdr = acl_check_prdr .endif acl_smtp_data = acl_check_data -tls_certificate = /etc/letsencrypt/live/guixbot_koszko.org/fullchain.pem -tls_privatekey = /etc/letsencrypt/live/guixbot_koszko.org/privkey.pem +tls_certificate = /etc/cert-links/guixbot_koszko.org/fullchain.pem +tls_privatekey = /etc/cert-links/guixbot_koszko.org/privkey.pem tls_verify_certificates = ${if exists{/etc/ssl/certs/ca-certificates.crt}\ {/etc/ssl/certs/ca-certificates.crt}\ diff --git a/fake-client-setup-mounts.sh b/fake-client-setup-mounts.sh index 9ad1e76..f2e5866 100755 --- a/fake-client-setup-mounts.sh +++ b/fake-client-setup-mounts.sh @@ -10,6 +10,7 @@ set -e SHELL_TO_USE="$1" -mount --bind hosts /etc/hosts; +mount --bind hosts /etc/hosts +mount --bind test-ca-certificates.crt /etc/ssl/certs/ca-certificates.crt mount -t tmpfs dummy /var/run/nscd 2>/dev/null || true; unshare "$SHELL_TO_USE" |