aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2023-08-30 11:40:19 +0200
committerWojtek Kosior <koszko@koszko.org>2023-08-30 11:40:19 +0200
commit9e71165dd3fa31accbcce8d5875aa774ab8b8fe1 (patch)
tree6de9d1246e80fd8ad391840c5da47aca24999592
parent41d988a7ae42eb5038be051d208a6164b576e189 (diff)
downloadkoszko-org-server-9e71165dd3fa31accbcce8d5875aa774ab8b8fe1.tar.gz
koszko-org-server-9e71165dd3fa31accbcce8d5875aa774ab8b8fe1.zip
run Exim in container
-rw-r--r--.gitignore1
-rw-r--r--Makefile55
-rw-r--r--container.scm106
-rw-r--r--exim.conf (renamed from exim4.conf)11
-rwxr-xr-xguix-container.sh101
5 files changed, 217 insertions, 57 deletions
diff --git a/.gitignore b/.gitignore
index 2b7dbc1..67a94e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
container-runner
log
+test-root
pidfile
hosts
*.touchfile
diff --git a/Makefile b/Makefile
index 1acdba5..0bea659 100644
--- a/Makefile
+++ b/Makefile
@@ -34,9 +34,10 @@ ALL_EGG_INFOS = \
$(HYDRILLA_WEBSITE_INFO) \
$(HYDRILLA_INFO)
-CONTAINER_PREREQUISITES = container.scm $(ALL_EGG_INFOS) hydrilla-wsgi.py
+CONTAINER_PREREQUISITES = container.scm $(ALL_EGG_INFOS) hydrilla-wsgi.py \
+ exim.conf
-TEST_ROOT_DIR = "/tmp/$$(pwd | sed 's|/|!|g')"'!!test-root'
+TEST_ROOT_DIR = "/tmp/$$(pwd | sed 's|/|!|g')"'!!test-root/current'
all: | container-runner.touchfile log sample-malcontent
@@ -59,7 +60,7 @@ hosts: hosts-extra /etc/hosts
cat $^ > $@
log:
- ln -sf $(TEST_ROOT_DIR)/var/log/guix-container $@
+ ln -sf test-root/var/log/guix-container $@
sample-malcontent:
mkdir $@
@@ -68,12 +69,31 @@ sample-malcontent:
make -C subrepos/hydrilla shell-with-hydrilla-only
LETSENCRYPT_ETC_DIR = $(TEST_ROOT_DIR)/etc/letsencrypt
+EXIM_ETC_DIR = $(TEST_ROOT_DIR)/etc/exim
HYDRILLA_WEBSITE_ETC_DIR = $(TEST_ROOT_DIR)/etc/guix-container/hydrilla-website
MALCONTENT_DIR = $(TEST_ROOT_DIR)/var/lib/hydrilla/malcontent_dirs
GITOLITE_DIR = $(TEST_ROOT_DIR)/var/lib/gitolite3
+EXIM_SPOOL_DIR = $(TEST_ROOT_DIR)/var/spool/exim
+
+test-root: sample-malcontent Makefile
+ rm -f $@
+ ln -sf $(TEST_ROOT_DIR) $@
+ $(MAKE) prepare-test-root
+
+.PHONY: ensure-test-root
+ensure-test-root:
+ $(MAKE) test-root
+ if [ \! -e $(TEST_ROOT_DIR) ]; then \
+ $(MAKE) prepare-test-root; \
+ fi
+.PHONY: prepare-test-root
prepare-test-root: sample-malcontent
- rm -rf $(TEST_ROOT_DIR)
+ @# Move the old test root
+ if [ -e $(TEST_ROOT_DIR) ]; then \
+ mv $(TEST_ROOT_DIR) \
+ $(TEST_ROOT_DIR)/../"old-$$(date --iso-8601=seconds)"; \
+ fi
@# Prepare replacement `/var/www`
for WWW_SUBDIR in \
koszko.org/html \
@@ -92,6 +112,17 @@ prepare-test-root: sample-malcontent
printf 'test secret\n' > $(LETSENCRYPT_ETC_DIR)/dummy-keys-and-stuff.txt
chmod 540 $(LETSENCRYPT_ETC_DIR)/dummy-keys-and-stuff.txt
chgrp 1001 $(LETSENCRYPT_ETC_DIR)/dummy-keys-and-stuff.txt
+ @# Prepare replacement `/etc/exim`
+ mkdir --mode=755 -p $(EXIM_ETC_DIR)
+ guix shell openssl -- openssl genrsa -out $(EXIM_ETC_DIR)/dkim.pem 2048
+ chmod 640 $(EXIM_ETC_DIR)/dkim.pem
+ chown 106:113 $(EXIM_ETC_DIR)/dkim.pem
+ printf koszko: > $(EXIM_ETC_DIR)/passwd
+ printf silnehaslo | guix shell whois -- mkpasswd --method=sha-256 -s \
+ >> $(EXIM_ETC_DIR)/passwd
+ echo >> $(EXIM_ETC_DIR)/passwd
+ chmod 640 $(EXIM_ETC_DIR)/passwd
+ chown 106:113 $(EXIM_ETC_DIR)/passwd
@# Prepare replacement `/etc`
mkdir --mode=750 -p $(HYDRILLA_WEBSITE_ETC_DIR)
printf 'test non-secret\n' > $(HYDRILLA_WEBSITE_ETC_DIR)/secret.txt
@@ -108,18 +139,23 @@ prepare-test-root: sample-malcontent
printf "sheets-websites.git\n" > $(GITOLITE_DIR)/projects.list
chmod -R o-rwx,g-w $(GITOLITE_DIR)
chgrp -R 118 $(GITOLITE_DIR)
+ @# Prepare replacement `/var/spool/exim`
+ mkdir -p $(EXIM_SPOOL_DIR)
+ chmod 750 $(EXIM_SPOOL_DIR)
+ chown 106:113 $(EXIM_SPOOL_DIR)
-GUIX_CONTAINER_FLAGS = -e ./container-runner -p ./pidfile -r $(TEST_ROOT_DIR)
+GUIX_CONTAINER_FLAGS = -e ./container-runner -p ./pidfile \
+ -r "$$(realpath test-root)"
-start-container: guix-container.sh container-runner.touchfile \
- prepare-test-root | log
+start-container: guix-container.sh container-runner.touchfile ensure-test-root \
+ | log
./$< start $(GUIX_CONTAINER_FLAGS)
stop-container: guix-container.sh
./$< stop $(GUIX_CONTAINER_FLAGS)
restart-container: guix-container.sh container-runner.touchfile \
- prepare-test-root | log
+ ensure-test-root | log
./$< restart $(GUIX_CONTAINER_FLAGS)
enter-container: pidfile
@@ -152,11 +188,10 @@ clean: clean-runner
for SUBREPO in $(SUBREPOS_WITH_MAKEFILE); do \
$(MAKE) -C subrepos/"$$SUBREPO" clean; \
done
- rm -rf log hosts schemas sample-malcontent
+ rm -rf log test-root hosts schemas sample-malcontent
.PHONY: all \
clean-runner clean \
- prepare-test-root \
start-container stop-container restart-container \
enter-container fake-client \
install \
diff --git a/container.scm b/container.scm
index 7caf4fe..a513dda 100644
--- a/container.scm
+++ b/container.scm
@@ -4,15 +4,14 @@
;;
;; Available under the terms of Creative Commons Zero v1.0 Universal.
-(use-modules (gnu)
+(use-modules ((srfi srfi-1) #:select (append-map filter-map))
+ (gnu)
(koszko-org-website)
(sheets-websites)
(hydrilla-website)
(hydrilla-json-schemas)
(hydrilla-base)
(ice-9 match)
- ;; srfi-1 provides `append-map`.
- (srfi srfi-1)
;; srfi-26 provides `cut`.
(srfi srfi-26)
(guix records)
@@ -24,14 +23,17 @@
(guix packages)
(guix search-paths)
(guix modules)
+ ((guix utils) #:select (substitute-keyword-arguments))
;; The following exports account-service-type.
(gnu system shadow))
(use-package-modules web
python
- version-control)
+ version-control
+ mail)
(use-service-modules web
shepherd
- certbot)
+ certbot
+ mail)
(define %here
(getcwd))
@@ -376,6 +378,10 @@
"CustomLog /var/log/httpd/access.log combined" "\n"
"ScriptSock /var/run/cgid.sock" "\n")))))))
+(define (extension-of-type? ext type)
+ (eq? (service-type-name (service-extension-target ext))
+ (service-type-name type)))
+
(define %koszko-httpd-deploy-hook
(program-file
"httpd-deploy-hook"
@@ -416,8 +422,7 @@
;; Prevent certbot from pulling in Nginx — we use Apache here.
(extensions (filter
(lambda (ext)
- (not (eq? (service-type-name (service-extension-target ext))
- (service-type-name nginx-service-type))))
+ (not (extension-of-type? ext nginx-service-type)))
(service-type-extensions certbot-service-type))))
(certbot-configuration
(email "koszko@koszko.org")
@@ -437,6 +442,66 @@
(deploy-hook %koszko-httpd-deploy-hook)))))
%all-site-confs)))))
+(define koszko-exim-service-type
+ (service-type
+ (inherit exim-service-type)
+ (extensions (filter-map
+ (lambda (ext)
+ (cond
+ ((extension-of-type? ext account-service-type)
+ ;; Avoid double declaration of "exim" user and group.
+ #f)
+ ((extension-of-type? ext activation-service-type)
+ ;; Make exim logs accessible under /var/log
+ (let ((old-activation (service-extension-compute ext)))
+ (define (new-activation exim-config)
+ #~(begin
+ (symlink "../spool/exim/log" "/var/log/exim")
+ #$(old-activation exim-config)))
+
+ (service-extension activation-service-type
+ new-activation)))
+ (else
+ ext)))
+ (service-type-extensions exim-service-type)))))
+
+(define %koszko-exim-service
+ (service koszko-exim-service-type
+ (exim-configuration
+ (package (package/inherit exim
+ (arguments
+ (substitute-keyword-arguments
+ (package-arguments exim)
+ ((#:phases phases)
+ #~(modify-phases #$phases
+ (add-after 'configure 'configure-enable-maildir
+ (lambda _
+ (substitute* "Local/Makefile"
+ (("# (SUPPORT_MAILDIR=yes)" all line)
+ line))))))))))
+ (config-file (local-file "./exim.conf")))))
+
+(define %koszko-mail-aliases-service
+ (service mail-aliases-service-type
+ '(("mailer-daemon" "postmaster")
+ ("postmaster" "root")
+ ("nobody" "root")
+ ("hostmaster" "root")
+ ("usenet" "root")
+ ("news" "root")
+ ("webmaster" "root")
+ ("www" "root")
+ ("ftp" "root")
+ ("abuse" "root")
+ ("noc" "root")
+ ("security" "root")
+ ("root" "urz")
+ ("dmarc" "urz")
+ ("admin" "urz")
+ ("wk" "urz")
+ ("koszko" "urz")
+ ("my-contribution-is-licensed-cc0" "urz"))))
+
(operating-system
(host-name "koszko")
(timezone "Europe/Warsaw")
@@ -445,6 +510,10 @@
;; files that are readable by certain daemons and not readable by the
;; world.
(user-group
+ (name "exim")
+ (id 113)
+ (system? #t))
+ (user-group
(name "httpd")
(id 133)
(system? #t))
@@ -456,9 +525,21 @@
(name "certsaccess")
(id 1001)
(system? #t))
+ (user-group
+ (name "urz")
+ (id 1000))
+ (user-group
+ (name "joanna")
+ (id 1003))
%base-groups))
(users (cons*
(user-account
+ (name "exim")
+ (group "exim")
+ (supplementary-groups '("certsaccess"))
+ (uid 106)
+ (system? #t))
+ (user-account
(name "httpd")
(group "httpd")
(supplementary-groups '("gitolite3" "certsaccess"))
@@ -472,6 +553,15 @@
(group "gitolite3")
(uid 110)
(system? #t))
+ (user-account
+ (name "urz")
+ (group "urz")
+ (supplementary-groups '("cdrom" "floppy" "audio" "video" "netdev"))
+ (uid 1000))
+ (user-account
+ (name "joanna")
+ (group "joanna")
+ (uid 1001))
%base-user-accounts))
(file-systems (cons (file-system
(device (file-system-label "does-not-matter"))
@@ -498,4 +588,6 @@
(description "Make other services assume network is there."))
#f)
%koszko-certbot-service
+ %koszko-exim-service
+ %koszko-mail-aliases-service
%base-services)))
diff --git a/exim4.conf b/exim.conf
index ac0261b..a5b2ec2 100644
--- a/exim4.conf
+++ b/exim.conf
@@ -7,7 +7,8 @@
# Adapted from
# https://git.exim.org/exim.git/blob/3e6d406e8ae9681a8cc1b404e7f5d1bd6d65d201:/src/src/configure.default
-log_file_path = /var/log/exim/${s}log
+spool_directory = /var/spool/exim
+log_file_path = $spool_directory/log/%slog
log_selector = +smtp_protocol_error +smtp_syntax_error \
+tls_certificate_verified +tls_peerdn
@@ -63,8 +64,6 @@ timeout_frozen_after = 7d
freeze_tell = admin
-spool_directory = /var/spool/exim4
-
check_rfc2047_length = false
accept_8bitmime = false
@@ -180,7 +179,7 @@ remote_smtp:
dkim_domain = koszko.org
dkim_selector = mail
-dkim_private_key = /etc/exim4/dkim.pem
+dkim_private_key = /etc/exim/dkim.pem
.ifdef _HAVE_DANE
dnssec_request_domains = *
@@ -235,7 +234,7 @@ PLAIN:
driver = plaintext
server_set_id = $auth2
server_prompts = :
- server_condition = "${if crypteq{$auth3}{${extract{1}{:}{${lookup{$auth2}lsearch{/etc/exim4/passwd}{$value}{*:*}}}}}{1}{0}}"
+ server_condition = "${if crypteq{$auth3}{${extract{1}{:}{${lookup{$auth2}lsearch{/etc/exim/passwd}{$value}{*:*}}}}}{1}{0}}"
server_advertise_condition = ${if def:tls_in_cipher }
# LOGIN authentication has traditional prompts and responses. There is no
@@ -247,7 +246,7 @@ LOGIN:
driver = plaintext
server_set_id = $auth1
server_prompts = <| Username: | Password:
- server_condition = "${if crypteq{$auth2}{${extract{1}{:}{${lookup{$auth1}lsearch{/etc/exim4/passwd}{$value}{*:*}}}}}{1}{0}}"
+ server_condition = "${if crypteq{$auth2}{${extract{1}{:}{${lookup{$auth1}lsearch{/etc/exim/passwd}{$value}{*:*}}}}}{1}{0}}"
server_advertise_condition = ${if def:tls_in_cipher }
# Hehe
diff --git a/guix-container.sh b/guix-container.sh
index da8b765..d117ae3 100755
--- a/guix-container.sh
+++ b/guix-container.sh
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: CC0-1.0
-# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
+# Copyright (C) 2022-2023 Wojtek Kosior <koszko@koszko.org>
#
# Available under the terms of Creative Commons Zero v1.0 Universal.
@@ -87,6 +87,49 @@ is_running() {
return $?
}
+network_setup() {
+ SHEPHERD_PID="$1"
+
+ ip link add veth-guix-out type veth peer name veth-guix-in
+ ip link set veth-guix-in netns "$SHEPHERD_PID"
+
+ ip link set veth-guix-out up
+ ip addr add 10.207.87.1/24 dev veth-guix-out
+
+ nsenter --target "$SHEPHERD_PID" --net ip link set lo up
+ nsenter --target "$SHEPHERD_PID" --net ip link set veth-guix-in up
+ nsenter --target "$SHEPHERD_PID" --net ip addr add \
+ 10.207.87.2/24 dev veth-guix-in
+ nsenter --target "$SHEPHERD_PID" --net ip route add \
+ default via 10.207.87.1 dev veth-guix-in
+
+ if [ -n "$HOST_SYSTEM_ROOT" ]; then
+ # Don't connect to the real net when running in a test environment.
+ return
+ fi
+
+ for LINKNAME in $(ip route | grep default | awk '{print $5}'); do
+ iptables -t nat -A POSTROUTING \
+ -s 10.207.87.1/24 -o "$LINKNAME" -j MASQUERADE
+ iptables -t nat -A PREROUTING \
+ -i "$LINKNAME" -p tcp \
+ -m multiport --dports 25,12525,465,587 \
+ -j DNAT --to-destination 10.207.87.2
+ done
+
+ cat /etc/resolv.conf |
+ nsenter --target "$SHEPHERD_PID" --all \
+ /run/current-system/profile/bin/tee /etc/resolv.conf > /dev/null
+
+ echo 1 > /proc/sys/net/ipv4/ip_forward
+}
+
+iptables_rip_rule() {
+ while iptables "$@" 2>/dev/null; do
+ true
+ done
+}
+
network_rip() {
ip link delete veth-guix-out 2>/dev/null || true
@@ -99,18 +142,26 @@ network_rip() {
echo 0 > /proc/sys/net/ipv4/ip_forward
for LINKNAME in $(ip route | grep default | awk '{print $5}'); do
- iptables -t nat -D POSTROUTING \
- -s 10.207.87.1/24 -o "$LINKNAME" -j MASQUERADE 2>/dev/null \
- || true
+ iptables_rip_rule -t nat -D PREROUTING \
+ -i "$LINKNAME" -p tcp \
+ -m multiport --dports 25,12525,465,587 \
+ -j DNAT --to-destination 10.207.87.2
+ iptables_rip_rule -t nat -D POSTROUTING \
+ -s 10.207.87.1/24 -o "$LINKNAME" \
+ -j MASQUERADE
done
}
stop() {
network_rip
+ if ! is_running; then
+ return
+ fi
+
if [ -x /sbin/start-stop-daemon ]; then
- /sbin/start-stop-daemon \
- --stop --signal TERM --pidfile "$PIDFILE" --remove-pidfile --quiet \
+ /sbin/start-stop-daemon \
+ --stop --signal TERM --pidfile "$PIDFILE" --remove-pidfile --quiet \
--retry 60 2>/dev/null || true
else
DAEMON_PID="$(cat "$PIDFILE")"
@@ -153,18 +204,24 @@ start() {
HYDRILLAREPOS_HTTP_REAL="$HOST_SYSTEM_ROOT"/var/www/hydrillarepos.koszko.org/html
LOG_REAL="$LOG_DIR"/container
ETC_LETSENCRYPT_REAL="$HOST_SYSTEM_ROOT"/etc/letsencrypt
+ ETC_EXIM_REAL="$HOST_SYSTEM_ROOT"/etc/exim
ETC_REAL="$HOST_SYSTEM_ROOT"/etc/guix-container
+ VAR_SPOOL_EXIM_REAL="$HOST_SYSTEM_ROOT"/var/spool/exim
VAR_HYDRILLA_REAL="$HOST_SYSTEM_ROOT"/var/lib/hydrilla
VAR_GITOLITE_REAL="$HOST_SYSTEM_ROOT"/var/lib/gitolite3
+ HOME_REAL="$HOST_SYSTEM_ROOT"/home
KOSZKO_SIDELOAD_DIR_SHARE_OPT=--share="$KOSZKO_SIDELOAD_REAL"=/srv/http/koszko.org
HYDRILLA_HTTP_DIR_SHARE_OPT=--share="$HYDRILLA_HTTP_REAL"=/srv/http/hydrilla.koszko.org
HYDRILLAREPOS_HTTP_DIR_SHARE_OPT=--share="$HYDRILLAREPOS_HTTP_REAL"=/srv/http/hydrillarepos.koszko.org
LOG_DIR_SHARE_OPT=--share="$LOG_REAL"=/var/log
ETC_LETSENCRYPT_DIR_SHARE_OPT=--share="$ETC_LETSENCRYPT_REAL"=/etc/letsencrypt
+ ETC_EXIM_DIR_SHARE_OPT=--share="$ETC_EXIM_REAL"=/etc/exim
ETC_DIR_SHARE_OPT=--share="$ETC_REAL"=/etc
+ VAR_SPOOL_EXIM_DIR_SHARE_OPT=--share="$VAR_SPOOL_EXIM_REAL"=/var/spool/exim
VAR_HYDRILLA_DIR_SHARE_OPT=--share="$VAR_HYDRILLA_REAL"=/var/lib/hydrilla
VAR_GITOLITE_DIR_SHARE_OPT=--share="$VAR_GITOLITE_REAL"=/var/lib/gitolite3
+ HOME_DIR_SHARE_OPT=--share="$HOME_REAL"=/home
mkdir --mode=700 -p "$LOG_DIR"
mkdir --mode=700 -p "$LOG_DIR"/container
@@ -174,9 +231,12 @@ start() {
"$HYDRILLAREPOS_HTTP_DIR_SHARE_OPT" \
"$LOG_DIR_SHARE_OPT" \
"$ETC_LETSENCRYPT_DIR_SHARE_OPT" \
+ "$ETC_EXIM_DIR_SHARE_OPT" \
"$ETC_DIR_SHARE_OPT" \
+ "$VAR_SPOOL_EXIM_DIR_SHARE_OPT" \
"$VAR_HYDRILLA_DIR_SHARE_OPT" \
"$VAR_GITOLITE_DIR_SHARE_OPT" \
+ "$HOME_DIR_SHARE_OPT" \
>> "$LOG_DIR"/stdout.log 2>> "$LOG_DIR"/stderr.log &
GUILE_PID=$!
@@ -200,34 +260,7 @@ start() {
network_rip
- ip link add veth-guix-out type veth peer name veth-guix-in
- ip link set veth-guix-in netns "$SHEPHERD_PID"
-
- ip link set veth-guix-out up
- ip addr add 10.207.87.1/24 dev veth-guix-out
-
- nsenter --target "$SHEPHERD_PID" --net ip link set lo up
- nsenter --target "$SHEPHERD_PID" --net ip link set veth-guix-in up
- nsenter --target "$SHEPHERD_PID" --net ip addr add \
- 10.207.87.2/24 dev veth-guix-in
- nsenter --target "$SHEPHERD_PID" --net ip route add \
- default via 10.207.87.1 dev veth-guix-in
-
- if [ -n "$HOST_SYSTEM_ROOT" ]; then
- # Don't connect to the real net when running in a test environment.
- return
- fi
-
- for LINKNAME in $(ip route | grep default | awk '{print $5}'); do
- iptables -t nat -A POSTROUTING \
- -s 10.207.87.1/24 -o "$LINKNAME" -j MASQUERADE
- done
-
- cat /etc/resolv.conf |
- nsenter --target "$SHEPHERD_PID" --all \
- /run/current-system/profile/bin/tee /etc/resolv.conf > /dev/null
-
- echo 1 > /proc/sys/net/ipv4/ip_forward
+ network_setup "$SHEPHERD_PID"
}
trap onexit EXIT