aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2023-09-08 17:04:38 +0200
committerWojtek Kosior <koszko@koszko.org>2023-09-09 15:54:48 +0200
commit7276d8e6ae494bb7ab85c1cdfa2917e3b889468d (patch)
tree6fea20b60ce604555335f977713d9ac0b3bd6299
parent18da556c5cb0234a4c1ad3df0b263a452db2ebf1 (diff)
downloadkoszko-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--.gitignore3
-rw-r--r--Makefile20
-rw-r--r--container.scm284
-rw-r--r--exim.conf4
-rwxr-xr-xfake-client-setup-mounts.sh3
5 files changed, 249 insertions, 65 deletions
diff --git a/.gitignore b/.gitignore
index 9d5463c..fb3a4de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/Makefile b/Makefile
index 4f5ef58..9b91cfa 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/exim.conf b/exim.conf
index a5b2ec2..f237306 100644
--- a/exim.conf
+++ b/exim.conf
@@ -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"