(use-modules ((guix licenses) #:prefix license:) ((srfi srfi-1) #:prefix srfi-1:) ((xyz euandreh heredoc) #:prefix heredoc:) (gnu) (gnu build linux-container) (gnu services mail) (gnu services shepherd) (gnu packages admin) (gnu packages cyrus-sasl) (gnu packages dbm) (gnu packages m4) (gnu packages mail) (gnu packages messaging) (gnu packages onc-rpc) (gnu packages perl) (gnu packages tls) (gnu packages version-control) (gnu system setuid) (guix build-system gnu) (guix build-system trivial) (guix download) (guix git-download) (guix least-authority) (guix packages) (guix records) (guix utils)) (use-package-modules lua web) (use-service-modules certbot cgit mcron messaging networking security ssh version-control virtualization vpn web) (heredoc:enable-syntax) (define ssh-pubkey "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDnUv7iWOejQNa3fZ6v4lkHT6qFRp2+NuzIpFJ2Vy7eP58XZoiz6HJPcCU8Hf95JXwaXEwS4S7mXdw1x60hd8JIe058Ek6MZSSVQmlLfocGsAYj1wTrLmnQ8+PV0IeQlNj1aytBI1fL+v3IPt+JdLt6b+g3vwcEUU9efzxx2E0KZ5GIpb2meiCQ6ha+tcd7XqegB53eQj/h/coE2zLJodpaJ3xbj894pE/OJCNC0+4d0Sv7oHhY7QoLYldTQbSgPyhyfl4iZpJf6OEPZxK2cJaB+cbeoBB6aGNyU+CIJToM+uAJJ7H7EpvxfcnfJQ1PuY5szTdvFbW820euiUEKEW69mW4uaFNPSc6D4Z8tZ5hXQIqBD40irULhF0CYNkIILmyNV/KJIZ5HkbQ1q+UrCFHJyvuH/3aCTjj9OSfE7xHPQ3xd3Xw8vvj0Mjie09xFbbcklBTw5WRzH7cw8c+Q0O69kZZ8b+ykcdzWTeZeWNdnzptNqnMjfheig90rUIJ7DN0c+53jCUcGpWJxJhcYF9Uk1RNHmSE5+VzK1y+20t0grVFX90nApm4Tl35QPrX7Qxp9C81cWiUB8xCAE6jYrmd4x+P/3wSQfc1Xg0Eg3QjJB+6JD7cbyDJpzDR3ja+CLZCAr9I0B4rDKD2d6et/z67iXPnZUWMyZ8RVVZPFbBMOTw== openpgp:0xF727046D") (define rc.sh (plain-file "rc.sh" #"- #!/bin/sh # shellcheck source=/dev/null . /etc/profile export XDG_PREFIX=~/.usr export XDG_CACHE_HOME="$XDG_PREFIX"/var/cache export XDG_CONFIG_HOME="$XDG_PREFIX"/etc export XDG_DATA_HOME="$XDG_PREFIX"/share export XDG_STATE_HOME="$XDG_PREFIX"/state export XDG_LOG_HOME="$XDG_PREFIX"/var/log mkdir -p \ "$XDG_CONFIG_HOME" \ "$XDG_CACHE_HOME" \ "$XDG_DATA_HOME" \ "$XDG_STATE_HOME"/ssh/conn \ "$XDG_LOG_HOME" GUIX_PROFILE="$XDG_CONFIG_HOME"/guix/current if [ -r "$GUIX_PROFILE"/etc/profile ]; then # shellcheck source=/dev/null . "$GUIX_PROFILE"/etc/profile fi export ENV=~/.profile export HISTSIZE=-1 export HISTCONTROL=ignorespace:ignoredups export EDITOR=vi export VISUAL="$EDITOR" export PAGER='less -R' export EXINIT=' " set number " set autoindent set ruler set showmode set showmatch ' export HISTFILE="$XDG_STATE_HOME"/bash-history export LESSHISTFILE="$XDG_STATE_HOME"/lesshst export RLWRAP_HOME="$XDG_CACHE_HOME"/rlwrap export GUILE_HISTORY="$XDG_STATE_HOME"/guile-history HOSTNAME="$(hostname)" export BORG_REPO="16686@ch-s010.rsync.net:borg/$HOSTNAME" export BORG_REMOTE_PATH='borg1' export BORG_PASSCOMMAND='cat /var/lib/borg-passphrase.txt' export GIT_CONFIG_GLOBAL=/etc/gitconfig unalias -a alias l='ls -lahF --color' alias grep='grep --color=auto' alias diff='diff --color=auto' alias watch='watch --color ' alias man='MANWIDTH=$((COLUMNS > 80 ? 80 : COLUMNS)) man' alias less='less -R' alias tree='tree -aC' alias mv='mv -i' alias e='vi' alias sqlite='rlwrap sqlite3' alias guile='guile -l /etc/init.scm' error_marker() { STATUS=$? if [ "$STATUS" != 0 ]; then printf ' (!! %s !!) ' "$STATUS" fi } export PS1='`error_marker`\T \w/ \u@\H\$ ' "#)) (define ssh.conf (plain-file "ssh.conf" #"- Host * ServerAliveInterval 30 ServerAliveCountMax 20 ControlMaster auto ControlPath ${XDG_STATE_HOME}/ssh/conn/%r@%h:%p ControlPersist 1h "#)) (define init.scm (plain-file "init.scm" #"- (use-modules (ice-9 colorized) (ice-9 readline)) (activate-colorized) (activate-readline) "#)) (define r.sh #"- #!/bin/sh set -eu # FIXME: what about /etc/login.defs? usage() { cat <<-'EOF' Usage: r COMMAND... r -h EOF } help() { cat <<-'EOF' Options: -h, --help show this message COMMAND the command to be executed Execute the given command, with a proper login environment loaded. Examples: Run a backup via SSH: $ ssh toph r backup -q cron EOF } for flag in "$@"; do case "$flag" in --) break ;; --help) usage help exit ;; *) ;; esac done while getopts 'h' flag; do case "$flag" in h) usage help exit ;; *) usage >&2 exit 2 ;; esac done shift $((OPTIND - 1)) set +eu # shellcheck source=/dev/null . /etc/rc set -eu exec "$@" "#) (define backup.sh #"- #!/bin/sh set -eu usage() { cat <<-'EOF' Usage: backup [-q] [-C COMMENT] [-r REPO] [ARCHIVE_TAG] backup -h EOF } help() { cat <<-'EOF' Options: -q disable verbose move, useful for for batch sessions -C COMMENT the comment text to be attached to the archive -r REPO operate on REPO instead of :: -h, --help show this message ARCHIVE_TAG the tag used to create the new backup (default: "manual") The repository is expected to have been created with: $ borg init -e repokey-blake2 The following environment variables are expected to be exported: $BORG_PASSCOMMAND $BORG_REPO $BORG_REMOTE_PATH Password-less SSH access is required, usually done via adding ~/.ssh/id_rsa.pub to the-ssh-remote:.ssh/authorized_keys. Root permission is also required. Examples: Run backup from cronjob: $ backup -q cronjob Create backup with a comment, a tag, and verbose mode active: $ backup -C 'The backup has a comment' EOF } for flag in "$@"; do case "$flag" in --) break ;; --help) usage help exit ;; *) ;; esac done VERBOSE_FLAGS='--verbose --progress' COMMENT='' REPO='' while getopts 'qC:r:h' flag; do case "$flag" in q) VERBOSE_FLAGS='' ;; C) COMMENT="$OPTARG" ;; r) REPO="$OPTARG" ;; h) usage help exit ;; *) usage >&2 exit 2 ;; esac done shift $((OPTIND - 1)) ARCHIVE_TAG="${1:-manual}" run() { set -x # shellcheck disable=2086 sudo -i borg create \ $VERBOSE_FLAGS \ --comment " $COMMENT" \ --stats \ --compression lzma,9 \ "$REPO::$(hostname)-{now}-$ARCHIVE_TAG" \ /root/ \ /home/ \ /etc/ \ /var/ \ /srv/ STATUS=$? set +x if [ "$STATUS" = 0 ]; then return 0 elif [ "$STATUS" = 1 ]; then printf 'WARNING, but no ERROR.\n' >&2 return 0 else return "$STATUS" fi } run || exit $? sudo -i borg check --verify-data --verbose "${REPO:-::}" "#) (define cronjob.sh #"- #!/bin/sh set -eu usage() { cat <<-'EOF' Usage: cronjob COMMAND... cronjob -h EOF } help() { cat <<-'EOF' Options: -h, --help show this message COMMAND the command to be executed Execute the given command, and send the output to email, with special treatment to the status code. It loads the appropriate files, so that the actual cron declaration is smaller. Examples: Run a backup: $ cronjob backup -q cron EOF } for flag in "$@"; do case "$flag" in --) break ;; --help) usage help exit ;; *) ;; esac done while getopts 'h' flag; do case "$flag" in h) usage help exit ;; *) usage >&2 exit 2 ;; esac done shift $((OPTIND - 1)) CMD="$*" r with-email -s "$(hostname): $CMD" -- "$@" 1>> /var/log/cronjob.log 2>&1 "#) (define reconfigure.sh #"- #!/bin/sh set -eu usage() { cat <<-'EOF' Usage: reconfigure [-U] reconfigure -h EOF } help() { cat <<-'EOF' Options: -U pull the latest channels before reconfiguring -h, --help show this message Run a "guix system reconfigure". If the -U flag is given, perform a "guix pull" prior to the reconfigure. Examples: Just do the deploy: $ reconfigure Update and upgrade: $ reconfigure -U EOF } for flag in "$@"; do case "$flag" in --) break ;; --help) usage help exit ;; *) ;; esac done UPDATE=false while getopts 'Uh' flag; do case "$flag" in U) UPDATE=true ;; h) usage help exit ;; *) usage >&2 exit 2 ;; esac done shift $((OPTIND - 1)) if [ "$UPDATE" = true ]; then sudo -i guix pull -v3 fi sudo -i guix system -v3 reconfigure /etc/guix/system.scm "#) (define with-email.sh #"- #!/bin/sh set -eu usage() { cat <<-'EOF' Usage: with-email [-s SUBJECT] COMMAND... with-email -h EOF } help() { cat <<-'EOF' Options: -s SUBJECT set the subject of the email -h, --help show this message COMMAND the command to be wrapped Examples: Send email with default subject: $ with-email echo 123 Use custom subject and explicit separation of command: $ with-email -s 'Something' -- do-something.sh EOF } now() { date '+%Y-%m-%dT%H:%M:%S%Z' } uuid() { od -xN20 /dev/urandom | head -n1 | awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}' } mkstemp() { name="${TMPDIR:-/tmp}/uuid-tmpname with spaces.$(uuid)" touch "$name" echo "$name" } for flag in "$@"; do case "$flag" in --) break ;; --help) usage help exit ;; *) ;; esac done SUBJECT='NO SUBJECT' while getopts 's:h' flag; do case "$flag" in s) SUBJECT="$OPTARG" ;; h) usage help exit ;; *) usage >&2 exit 2 ;; esac done shift $((OPTIND - 1)) if [ -z "$1" ]; then printf 'Missing COMMAND.\n\n' >&2 usage >&2 exit 2 fi STATUS=0 OUT="$(mkstemp)" { printf 'Running command: %s\n' "$*" printf 'Starting at: %s\n' "$(now)" printf '\n' "$@" || STATUS=$? printf '\n' printf 'Finished at: %s\n' "$(now)" } 1>"$OUT" 2>&1 HOSTNAME="$(hostname)" mail \ -a 'Content-Type: text/plain; charset=UTF-8' \ -s "(exit status: $STATUS) - $SUBJECT" \ root < "$OUT" || cat "$OUT" "#) (define (script name content) (package (name name) (version "latest") (source #f) (build-system trivial-build-system) (arguments (list #:modules '((guix build utils)) #:builder #~(begin (use-modules (guix build utils)) (let* ((bin (string-append %output "/bin")) (prog (string-append bin "/" #$name))) (mkdir-p bin) (call-with-output-file prog (lambda (port) (format port "~a" #$content))) (chmod prog #o755))))) (home-page #f) (synopsis #f) (description #f) (license #f))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Cyrus SASL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-public lua-cyrussasl (package (name "lua-cyrussasl") (version "1.1.0") (source (origin (method git-fetch) (uri (git-reference (url "https://github.com/JorjBauer/lua-cyrussasl") (commit (string-append "v" version)))) (file-name (git-file-name name version)) (sha256 (base32 "14kzm3vk96k2i1m9f5zvpvq4pnzaf7s91h5g4h4x2bq1mynzw2s1")))) (build-system gnu-build-system) (arguments (list #:tests? #f #:make-flags #~(list (string-append "CC=" #$(cc-for-target)) (string-append "PREFIX=" %output)) #:phases #~(modify-phases %standard-phases (delete 'configure)))) (inputs (list cyrus-sasl git-minimal lua)) (home-page "https://github.com/JorjBauer/lua-cyrussasl") (synopsis " Cyrus SASL library for Lua 5.1+") (description #"- Bugs: @itemize @item Prompts aren't implemented in the client functions. @item Server/Client first is essentially hard-coded (it's server-first). @item It's not clear that encode/decode are useful as implemented (and tests do not cover encode/decode). @end itemize"#) (license license:bsd-1))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Postfix ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-record-type* postfix-configuration make-postfix-configuration postfix-configuration? (postfix postfix-configuration-postfix (default postfix)) (set-sendmail? postfix-configuration-set-sendmail? (default #t)) (master.cf-file postfix-configuration-master.cf-file (default #f)) (main.cf-file postfix-configuration-main.cf-file (default #f)) (master.cf-extra postfix-configuration-master.cf-extra (default "")) (main.cf-extra postfix-configuration-main.cf-extra (default "")) (config-dirname postfix-configuration-config-dirname (default "postfix")) (data-directory postfix-configuration-data-directory (default "/var/lib/postfix")) (queue-directory postfix-configuration-queue-directory (default "/var/spool/postfix")) (user postfix-configuration-user (default "postfix")) (group postfix-configuration-group (default "postfix")) (setgid-group postfix-configuration-setgid-group (default "postdrop")) (root-aliases postfix-configuration-root-aliases (default '("abuse" "admin" "hostmaster" "postmaster"))) (cert-file postfix-configuration-cert-file (default #f)) (key-file postfix-configuration-key-file (default #f)) (hostname postfix-configuration-hostname (default (gethostname))) (run-in-container? postfix-configuration-run-in-container? (default #f)) (container-name postfix-configuration-container-name (default "postfix")) (container-namespaces postfix-configuration-container-namespaces (default (srfi-1:fold delq %namespaces '(net)))) (extra-mappings postfix-configuration-extra-mappings (default '()))) ; FIXME: hardcoded value of dkimproxy listen and relay (define (generate-master.cf config) (match-record config (master.cf-extra) (format #f #"- # ============================================================================================================ # service type private unpriv chroot wakeup maxproc command + args # (yes) (yes) (no) (never) (100) # ============================================================================================================= anvil unix - - n - 1 anvil bounce unix - - n - 0 bounce cleanup unix n - n - 0 cleanup defer unix - - n - 0 bounce discard unix - - n - - discard error unix - - n - - error flush unix n - n 1000? 0 flush lmtp unix - - n - - lmtp local unix - n n - - local # FIXME: replace 127.0.0.1 with localhost pickup unix n - n 60 1 pickup -o content_filter=dksign:[127.0.0.1]:10027 proxymap unix - - n - - proxymap proxywrite unix - - n - 1 proxymap qmgr unix n - n 300 1 qmgr relay unix - - n - - smtp -o syslog_name=postfix/relay retry unix - - n - - error rewrite unix - - n - - trivial-rewrite scache unix - - n - 1 scache showq unix n - n - - showq smtp inet n - n - - smtpd -o syslog_name=postfix/smtp smtp unix - - n - - smtp submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_tls_security_level=encrypt -o content_filter=dksign:[127.0.0.1]:10027 tlsmgr unix - - n 1000? 1 tlsmgr trace unix - - n - 0 bounce verify unix - - n - 1 verify virtual unix - n n - - virtual postlog unix-dgram n - n - 1 postlogd # FIXME: doesn't work for sendmail -t in localhost dksign unix - - n - - smtp -o syslog_name=postfix/dkimproxyout-listen -o smtp_send_xforward_command=yes -o smtp_discard_ehlo_keywords=8bitmime,starttls 127.0.0.1:10028 inet n - n - - smtpd -o syslog_name=postfix/dkimproxyout-relay -o content_filter= -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks -o smtpd_helo_restrictions= -o smtpd_client_restrictions= -o smtpd_sender_restrictions= -o smtpd_recipient_restrictions=permit_mynetworks,reject -o mynetworks=127.0.0.0/8 -o smtpd_authorized_xforward_hosts=127.0.0.0/8 ~a "# master.cf-extra))) (define (generate-main.cf config) (match-record config (postfix queue-directory data-directory user setgid-group cert-file key-file hostname main.cf-extra) (format #f #"- compatibility_level = 3.6 queue_directory = ~a data_directory = ~a mail_owner = ~a setgid_group = ~a myhostname = ~a mydestination = $myhostname, $mydomain, localhost.$mydomain, localhost alias_maps = hash:/etc/aliases # alias_maps = hash:/etc/aliases, static:andreh home_mailbox = Mail/Inbox/ header_checks = regexp:{ { /^Received:.*/ IGNORE }, { /^X-Originating-IP:.*/ IGNORE } } smtpd_use_tls = yes smtpd_tls_cert_file = ~a smtpd_tls_key_file = ~a smtp_use_tls = $smtpd_use_tls smtp_tls_cert_file = $smtpd_tls_cert_file smtp_tls_key_file = $smtpd_tls_key_file smtp_tls_security_level = may recipient_delimiter = + # smtpd_sasl_security_options = FIXME: deny all smtpd_sasl_tls_security_options = noanonymous # FIXME: shouldn't this be "encrypt"? smtpd_tls_security_level = may smtpd_tls_auth_only = yes smtpd_relay_restrictions = $smtpd_recipient_restrictions smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination smtpd_sasl_auth_enable = yes cyrus_sasl_config_path = /etc/sasl2 debug_peer_list = 127.0.0.1 milter_default_action = accept # smtpd_milters = FIXME ~a "# queue-directory data-directory user setgid-group hostname (or cert-file (format #f "/etc/letsencrypt/live/~a/fullchain.pem" hostname)) (or key-file (format #f "/etc/letsencrypt/live/~a/privkey.pem" hostname)) main.cf-extra))) (define (postfix-etc-files config) (match-record config (master.cf-file main.cf-file config-dirname) `((,config-dirname ,(file-union config-dirname `(("master.cf" ,(plain-file "master.cf" (or master.cf-file (generate-master.cf config)))) ("main.cf" ,(plain-file "main.cf" (or main.cf-file (generate-main.cf config)))))))))) (define (postfix-accounts config) (match-record config (user group setgid-group) (list (user-account (name user) (group group) (comment "Postfix system user") (home-directory "/var/empty") (create-home-directory? #f) (shell (file-append shadow "/sbin/nologin")) (system? #t)) (user-group (name group) (system? #t)) (user-group (name setgid-group) (system? #t))))) (define (postfix-setuid-programs config) (match-record config (postfix setgid-group set-sendmail?) (append (list (setuid-program (program (file-append postfix "/sbin/postdrop")) (setuid? #f) (setgid? #t) (group setgid-group)) (setuid-program (program (file-append postfix "/sbin/postqueue")) (setuid? #f) (setgid? #t) (group setgid-group))) (if set-sendmail? (list (setuid-program (program (file-append postfix "/sbin/sendmail")) (setuid? #f) (setgid? #t) (group setgid-group))) '())))) (define (postfix-activation config) (match-record config (queue-directory) #~(begin (use-modules (guix build utils)) (let ((user (getpwnam "root"))) (format (current-error-port) "Creating Postfix queue directory: \"~a\".~%" #$queue-directory) (mkdir-p #$queue-directory) (chown #$queue-directory (passwd:uid user) (passwd:gid user)) (chmod #$queue-directory #o755) (format (current-error-port) "Creating email spool director: \"/var/mail\".~%") (mkdir-p "/var/mail") (format (current-error-port) "Updating /etc/aliases: FIXME.~%") ;; FIXME: add -c option #; (invoke #$(file-append postfix "/sbin/postalias") "/etc/aliases"))))) (define (postfix-shepherd-service config) (match-record config (postfix config-dirname data-directory queue-directory run-in-container? container-name container-namespaces extra-mappings) (let* ((config-dir (string-append "/etc/" config-dirname)) (bin (file-append postfix "/sbin/postfix")) (cmd (if (not run-in-container?) bin (least-authority-wrapper bin #:name container-name #:mappings (append (list (file-system-mapping (source data-directory) (target source) (writable? #t)) (file-system-mapping (source queue-directory) (target source) (writable? #t))) extra-mappings) #:namespaces container-namespaces)))) (list (shepherd-service (provision '(postfix)) (documentation #"- Run the Postfix MTA. This is the entrypoint for starting the "master" process. Then the "master" process itself takes responsability of starting all the required daemons and commands."#) (start #~(make-forkexec-constructor (list #$(file-append postfix "/sbin/postfix") "-c" #$config-dir "start-fg") #:pid-file "/var/lib/postfix/master.lock")) (stop #~(make-kill-destructor SIGKILL)) (actions (list (shepherd-action (name 'configuration) (documentation #"- FIXME:DOCUMENTATION "#) (procedure #~(lambda _ (format #t "~a/master.cf~%" #$config-dir) (format #t "~a/main.cf~%" #$config-dir)))) (shepherd-action (name 'reload) (documentation #"- Re-read the "master.cf" and "main.cf" configuration files. Daemon processes terminate when possible, and when restarted use the values of the new configuration files. This live-reload option is usually preferable over a stop/start cycle, as it incurs in no interruption of the running service."#) (procedure #~(lambda _ (invoke #$(file-append postfix "/sbin/postfix") "-c" #$config-dir "reload"))))))))))) (define (postfix-aliases config) (match-record config (root-aliases) (map (lambda (alias) `(,alias "root")) root-aliases))) (define postfix-service-type (service-type (name 'postfix) (extensions (list (service-extension etc-service-type postfix-etc-files) (service-extension account-service-type postfix-accounts) (service-extension setuid-program-service-type postfix-setuid-programs) (service-extension activation-service-type postfix-activation) (service-extension mail-aliases-service-type postfix-aliases) (service-extension profile-service-type (compose list postfix-configuration-postfix)) (service-extension shepherd-root-service-type postfix-shepherd-service))) (default-value (postfix-configuration)) (description #"- Run the Postfix MTA. This is the top-level system service for Postfix. It includes: - populating /etc/postfix/ with read-only configuration files; - the user and groups used by Postfix when handling email delivery; - the special setgid binaries for daily usage, such as "sendmail"; - the Shepherd service for starting, stopping and *reloading* the service without restarting it; - the activation script for creating the required directories and configuring them with the correct permissions; - the binaries in the system profile so that one doesn't need to explicilty include the package when the service is already enabled. An extension to the log-rotation service isn't included: the default rottlog configuration already includes /var/log/maillog in its routine, so it is kept out. The defaults of provide sane default values for most things, such as group names, data and queue directories, etc. When used as-is, it creates a Postfix server that sends email from local users of the domain provided by "/etc/hostname"."#))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Dovecot ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-record-type* dovecot2-configuration make-dovecot2-configuration dovecot2-configuration? (dovecot2 dovecot2-configuration-dovecot2 (default dovecot)) (raw-file dovecot2-configuration-raw-file (default #f)) (extra-content dovecot2-configuration-extra-content (default "")) (config-name dovecot2-configuration-config-name (default "dovecot2.conf")) (user dovecot2-configuration-user (default "dovecot2")) (group dovecot2-configuration-group (default "dovecot2")) (auth-worker-group dovecot2-configuration-auth-worker-group (default "etc-shadow")) (untrusted-user dovecot2-configuration-untrusted-user (default "dovenull2")) (untrusted-group dovecot2-configuration-untrusted-group (default "dovenull2")) (base-dir dovecot2-configuration-base-dir (default "/var/run/dovecot2")) (state-dir dovecot2-configuration-state-dir (default "/var/lib/dovecot2")) (hostname dovecot2-configuration-hostname (default (gethostname)))) (define (generate-dovecot-config config) (match-record config (user group auth-worker-group untrusted-user hostname base-dir state-dir extra-content) (format #f #"- protocols = imap default_internal_user = ~a default_internal_group = ~a default_login_user = ~a auth_mechanisms = plain login auth_username_format = %n passdb { driver = shadow } userdb { driver = passwd } service auth-worker { group = ~a } ssl = required ssl_cert = (raw-file config-name) `((,config-name ,(plain-file config-name (or raw-file (generate-dovecot-config config))))))) (define (dovecot2-accounts config) (match-record config (user group untrusted-user untrusted-group) (list (user-account (name user) (group group) (comment "Dovecot system user") (home-directory "/var/empty") (create-home-directory? #f) (shell (file-append shadow "/sbin/nologin")) (system? #t)) (user-account (name untrusted-user) (group untrusted-group) (comment "Dovecot user for untrusted logins") (home-directory "/var/empty") (create-home-directory? #f) (shell (file-append shadow "/sbin/nologin")) (system? #t)) (user-group (name group) (system? #t)) (user-group (name untrusted-group) (system? #t))))) (define (dovecot2-activation config) (match-record config (base-dir state-dir) #~(begin (use-modules (guix build utils)) (let ((user (getpwnam "root"))) (format (current-error-port) "Creating Dovecot base_dir directory: \"~a\".~%" #$base-dir) (mkdir-p #$base-dir) (let ((dhparam.pem (string-append #$state-dir "/dhparam.pem"))) (unless (file-exists? dhparam.pem) (cond ((zero? (system* (string-append #$openssl "/bin/openssl") "dhparam" "-out" dhparam.pem "2048")) (format (current-error-port) "Dovecot2 dhparam.pem file created: \"~a\".~%" dhparam.pem)) (else (format (current-error-port) "Failed to create dhparam.pem file: \"~a\".~%" dhparam.pem))))))))) (define (dovecot2-shepherd-service config) (match-record config (dovecot2 config-name) (let ((config-file (string-append "/etc/" config-name))) (list (shepherd-service (provision '(dovecot2)) (documentation "FIXME:DOCUMENTATION: heredoc syntax") (start #~(make-forkexec-constructor (list #$(file-append dovecot2 "/sbin/dovecot") "-F" "-c" #$config-file))) (stop #~(make-kill-destructor)) (actions (list (shepherd-action (name 'configuration) (documentation "FIXME:DOCUMENTATION: heredoc syntax") (procedure #~(lambda _ (format #t "~a~%" #$config-file)))) (shepherd-action (name 'reload) (documentation "FIXME:DOCUMENTATION: heredoc syntax") (procedure #~(lambda _ (invoke #$(file-append dovecot "/bin/doveadm") "-c" #$config-file "reload"))))))))))) (define dovecot2-service-type (service-type (name 'dovecot2) (extensions (list (service-extension etc-service-type dovecot2-etc-files) (service-extension account-service-type dovecot2-accounts) (service-extension activation-service-type dovecot2-activation) (service-extension profile-service-type (compose list dovecot2-configuration-dovecot2)) (service-extension shepherd-root-service-type dovecot2-shepherd-service))) (default-value (dovecot2-configuration)) (description "FIXME:DOCUMENTATION: heredoc syntax"))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;; OS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-record-type* shadow-group-configuration make-shadow-group-configuration shadow-group-configuration? (group shadow-group-configuration-group (default "etc-shadow"))) (define (shadow-group-activation config) (match-record config (group) #~(begin (use-modules (guix build utils)) (format (current-error-port) "Setting ownership and permission for \"/etc/passwd\".~%") (chown "/etc/shadow" (passwd:uid (getpwnam "root")) (group:gid (getgrnam #$group))) (chmod "/etc/shadow" #o640)))) (define (shadow-group-accounts config) (match-record config (group) (list (user-group (name group) (system? #t))))) (define shadow-group-service-type (service-type (name 'shadow-group) (extensions (list (service-extension activation-service-type shadow-group-activation) (service-extension account-service-type shadow-group-accounts))) (default-value (shadow-group-configuration)) (description "FIXME:DOCUMENTATION"))) (define-record-type* cyrus-service-configuration make-cyrus-service-configuration cyrus-service-configuration? (name cyrus-service-configuration-name) (authmech cyrus-service-configuration-authmech (default "saslauthd")) (log-level cyrus-service-configuration-log-level (default 7))) (define-record-type* cyrus-sasl-configuration make-cyrus-sasl-configuration cyrus-sasl-configuration? (cyrus-sasl cyrus-sasl-configuration-cyrus-sasl (default cyrus-sasl)) (user cyrus-sasl-configuration-user (default "cyrus-sasl")) (group cyrus-sasl-configuration-group (default "cyrus-sasl")) (supplementary-groups cyrus-sasl-configuration-supplementary-groups (default '("etc-shadow"))) (authmech cyrus-sasl-configuration-authmech (default "shadow")) (services cyrus-sasl-configuration-services (default '())) (config-dirname cyrus-sasl-configuration-config-dirname (default "sasl2")) (run-directory cyrus-sasl-configuration-run-directory (default "/var/run/saslauthd")) (run-in-container? cyrus-sasl-configuration-run-in-container? (default #t)) (container-name cyrus-sasl-configuration-container-name (default "saslauthd")) (container-namespaces cyrus-sasl-configuration-container-namespaces (default %namespaces)) (extra-mappings cyrus-sasl-configuration-extra-mappings (default '()))) (define (cyrus-sasl-etc-files config) ;; FIXME: support opaque files ;; FIXME: extend this with postfix instead of making postfix add here (match-record config (services config-dirname run-directory) `((,config-dirname ,(file-union config-dirname (map (lambda (service-config) (match-record service-config (name authmech log-level) `(,name ,(plain-file name (format #f "pwcheck_method: ~a~%saslauthd_path: ~a/mux~%log_level: ~a~%" authmech run-directory log-level))))) services)))))) (define (cyrus-sasl-activation config) (match-record config (user run-directory) #~(begin (use-modules (guix build utils)) (let ((user (getpwnam #$user))) (format (current-error-port) "Creating Cyrus SASL socket directory: \"~a\".~%" #$run-directory) (mkdir-p #$run-directory) (chown #$run-directory (passwd:uid user) (passwd:gid user)) (chmod #$run-directory #o755))))) (define (cyrus-sasl-accounts config) (match-record config (user group supplementary-groups) (list (user-account (name user) (group group) (supplementary-groups supplementary-groups) (comment "Cyrus SASL system user") (home-directory "/var/empty") (create-home-directory? #f) (shell (file-append shadow "/sbin/nologin")) (system? #t)) (user-group (name group) (system? #t))))) (define (cyrus-sasl-shepherd-service config) (match-record config (cyrus-sasl user group supplementary-groups authmech config-dirname run-directory run-in-container? container-name container-namespaces extra-mappings) (let* ((config-dir (string-append "/etc/" config-dirname)) (bin (file-append cyrus-sasl "/sbin/saslauthd")) (cmd (if (not run-in-container?) bin (least-authority-wrapper bin #:name container-name #:mappings (append (list (file-system-mapping (source run-directory) (target source) (writable? #t)) (file-system-mapping (source "/etc/passwd") (target source)) (file-system-mapping (source "/etc/shadow") (target source))) extra-mappings) #:namespaces container-namespaces)))) (list (shepherd-service (provision '(cyrus-sasl)) (documentation "FIXME:DOCUMENTATION") (start #~(make-forkexec-constructor (list #$cmd "-a" #$authmech "-d" "-m" #$run-directory) #:user #$user #:group #$group #:supplementary-groups '(#$@supplementary-groups))) (stop #~(make-kill-destructor)) (actions (list (shepherd-action (name 'configuration) (documentation "FIXME:DOCUMENTATION") (procedure #~(lambda _ (format #t "~a~%" #$config-dir))))))))))) (define cyrus-sasl-service-type (service-type (name 'cyrus-sasl) (extensions (list (service-extension etc-service-type cyrus-sasl-etc-files) (service-extension activation-service-type cyrus-sasl-activation) (service-extension profile-service-type (compose list cyrus-sasl-configuration-cyrus-sasl)) (service-extension account-service-type cyrus-sasl-accounts) (service-extension shepherd-root-service-type cyrus-sasl-shepherd-service))) (compose srfi-1:concatenate) (extend (lambda (config services) (cyrus-sasl-configuration (inherit config) (services (append (cyrus-sasl-configuration-services config) services))))) (default-value (cyrus-sasl-configuration)) (description "FIXME:DOCUMENTATION"))) (define-record-type* dkimproxyout-configuration make-dkimproxyout-configuration dkimproxyout-configuration? (dkimproxy dkimproxyout-configuration-dkimproxy (default dkimproxy)) (user dkimproxyout-configuration-user (default "dkimproxyout")) (group dkimproxyout-configuration-group (default "dkimproxyout")) (config-name dkimproxyout-configuration-config-name (default "dkimproxyout.conf")) (listen dkimproxyout-configuration-listen (default "127.0.0.1:10027")) (relay dkimproxyout-configuration-relay (default "127.0.0.1:10028")) (domains dkimproxyout-configuration-domains (default (list (gethostname)))) (selector dkimproxyout-configuration-selector (default "dkimproxyout")) (key-size dkimproxyout-configuration-key-size (default 2048)) (data-directory dkimproxyout-configuration-data-directory (default "/var/lib/dkimproxyout")) (run-in-container? dkimproxyout-configuration-run-in-container? (default #f)) (container-name dkimproxyout-configuration-container-name (default "dkimproxyout")) (container-namespaces dkimproxyout-configuration-container-namespaces (default (srfi-1:fold delq %namespaces '(net)))) (extra-mappings dkimproxyout-configuration-extra-mappings (default '()))) (define (generate-out.cf config) (match-record config (listen relay domains selector data-directory) (format #f " listen ~a relay ~a domain ~a selector ~a signature dkim(c=relaxed/relaxed) # FIXME:DOCUMENTATION add this to the service documentation # the corresponding public key is available at: # ~a/public.key keyfile ~a/private.key " listen relay (string-join domains ",") selector data-directory data-directory))) (define (dkimproxyout-etc-files config) (match-record config (config-name) `((,config-name ,(plain-file config-name (generate-out.cf config)))))) (define (dkimproxyout-accounts config) (match-record config (user group) (list (user-account (name user) (group group) (comment "DKIMproxy.out signing system user") (home-directory "/var/empty") (create-home-directory? #f) (shell (file-append shadow "/sbin/nologin")) (system? #t)) (user-group (name group) (system? #t))))) (define (dkimproxyout-activation config) (match-record config (user group data-directory key-size) #~(begin (use-modules (guix build utils)) (let ((uid (passwd:uid (getpwnam #$user))) (gid (group:gid (getgrnam #$group)))) (format (current-error-port) "Creating DKIMproxy.out data directory: \"~a\".~%" #$data-directory) (mkdir-p #$data-directory) (chown #$data-directory uid gid) (chmod #$data-directory #o755) (let ((private-key (string-append #$data-directory "/private.key")) (public-key (string-append #$data-directory "/public.key"))) (unless (file-exists? private-key) (cond ((zero? (system* #$(file-append openssl "/bin/openssl") "genrsa" "-out" private-key (number->string #$key-size))) (format (current-error-port) "DKIMproxy.out private key file created: \"~a\".~%" private-key)) (else (format (current-error-port) "Failed to create DKIMproxy.out private key file: \"~a\".~%" private-key)))) (invoke #$(file-append openssl "/bin/openssl") "rsa" "-in" private-key "-pubout" "-out" public-key) (format (current-error-port) "Setting permissions for the public/private DKIMproxy.out keypair: \"~a/{public,private}.key\".~%" #$data-directory) (chown private-key uid gid) (chown public-key uid gid) (chmod private-key #o400) (chmod public-key #o644)))))) (define (dkimproxyout-shepherd-service config) (match-record config (dkimproxy user group config-name data-directory run-in-container? container-name container-namespaces extra-mappings) (let* ((config-file (string-append "/etc/" config-name)) (bin (file-append dkimproxy "/bin/dkimproxy.out")) (cmd (if (not run-in-container?) bin (least-authority-wrapper bin #:name container-name #:mappings (append (list (file-system-mapping (source config-file) (target source)) (file-system-mapping (source (string-append data-directory "/private.key")) (target source))) extra-mappings) #:namespaces container-namespaces)))) (list (shepherd-service (provision '(dkimproxyout)) (documentation "FIXME:DOCUMENTATION") (start #~(make-forkexec-constructor (list #$cmd "--conf_file" #$config-file) #:user #$user #:group #$group)) (stop #~(make-kill-destructor)) (actions (list (shepherd-action (name 'configuration) (documentation "FIXME:DOCUMENTATION") (procedure #~(lambda _ (format #t "~a~%" #$config-file))))))))))) (define-public dkimproxyout-service-type (service-type (name 'dkimproxyout) (extensions (list (service-extension etc-service-type dkimproxyout-etc-files) (service-extension account-service-type dkimproxyout-accounts) (service-extension activation-service-type dkimproxyout-activation) (service-extension profile-service-type (compose list dkimproxyout-configuration-dkimproxy)) (service-extension shepherd-root-service-type dkimproxyout-shepherd-service))) (default-value (dkimproxyout-configuration)) (description "FIXME:DOCUMENTATION"))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;; OS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define tld "euandre.org") (define mta-sts.tld (string-append "mta-sts." tld)) (define whoami "andreh") (define me "eu") (define public-me (format #f "~a@~a" me tld)) (define git-dir "/srv/git/git") ;; FIXME: get git-root from config (define www-dir "/srv/www") ;; FIXME: get nginx-root from config (define gitconfig (plain-file "gitconfig" (format #f #"- [init] defaultBranch = main [user] email = ci@~a name = "~a CI" [advice] detachedHead = false "# tld tld))) (operating-system (locale "fr_FR.UTF-8") (timezone "America/Sao_Paulo") (host-name tld) (hosts-file (plain-file "hosts" (format #f #"- 127.0.0.1 localhost ~a ::1 localhost ~a 10.0.0.0 toph 10.0.0.1 velhinho 10.0.0.2 azula "# tld tld))) (users (append (list (user-account (name whoami) (comment "EuAndreh") (group "users") (supplementary-groups '("wheel"))) (user-account (name "git") (group "git") (comment "Public Git user") (home-directory git-dir) (shell (file-append git "/bin/git-shell")))) %base-user-accounts)) (groups (append (list (user-group (name "git"))) %base-groups)) (sudoers-file (plain-file "sudoers" #"- root ALL=(ALL) ALL %wheel ALL=NOPASSWD: ALL "#)) (packages (append (map (compose list specification->package+output symbol->string) '(nss-certs borg btop curl entr git-minimal guile-heredoc-latest htop lsof mailutils ranger rlwrap rsync strace trash-cli tree prosody)) (list ;; lua-cyrussasl (script "r" r.sh) (script "backup" backup.sh) (script "cronjob" cronjob.sh) (script "reconfigure" reconfigure.sh) (script "with-email" with-email.sh)) %base-packages)) (services (append (list (service dhcp-client-service-type) (service ntp-service-type) (service libvirt-service-type) (service virtlog-service-type) (service shadow-group-service-type) (service fail2ban-service-type) (service mcron-service-type (mcron-configuration (jobs (list #~(job "0 0 * * *" "cronjob backup -q -r /mnt/backup/borg cron") #~(job "0 1 * * *" "cronjob backup -q cron") #~(job "0 2 * * *" "cronjob reconfigure -U"))))) (service dkimproxyout-service-type) (service git-daemon-service-type (git-daemon-configuration (export-all? #t))) (service cgit-service-type (cgit-configuration (nginx '()) (source-filter (file-append cgit "/lib/cgit/filters/syntax-highlighting.py")) (about-filter (file-append cgit "/lib/cgit/filters/about-formatting.sh")) (repository-directory git-dir) (virtual-root "/git/") (remove-suffix? #t) (nocache? #t) (enable-commit-graph? #t) (enable-follow-links? #t) (enable-index-owner? #t) (enable-log-filecount? #t) (enable-log-linecount? #t) (enable-remote-branches? #t) (enable-subject-links? #t) (snapshots '("tar.gz" "tar.xz")) (root-desc "Patches welcome!") (root-title "EuAndreh repositories") (logo "/git/static/cgit.png") (favicon "/git/static/favicon.ico") (css "/git/static/cgit.css") (extra-options '(#"- enable-blame=1 readme=:README.md readme=:README "#)))) (service nginx-service-type (nginx-configuration (server-blocks (list (nginx-server-configuration (server-name (list mta-sts.tld)) (listen '("[::]:443 ssl http2" "443 ssl http2")) (ssl-certificate (format #f "/etc/letsencrypt/live/~a/fullchain.pem" tld)) (ssl-certificate-key (format #f "/etc/letsencrypt/live/~a/privkey.pem" tld)) (locations (list (nginx-location-configuration (uri "= /.well-known/mta-sts.txt") (body (list (list "alias " (plain-file "mta-sts.txt" #"- version: STSv1 mode: enforce mx: euandre.org max_age: 604800 "#) ";"))))))) (nginx-server-configuration (server-name (list tld)) (listen '("[::]:443 ssl http2" "443 ssl http2")) (root www-dir) (ssl-certificate (format #f "/etc/letsencrypt/live/~a/fullchain.pem" tld)) (ssl-certificate-key (format #f "/etc/letsencrypt/live/~a/privkey.pem" tld)) (locations (list (nginx-location-configuration (uri "/git/static/") (body (list (list "alias " cgit "/share/cgit/;")))) (nginx-location-configuration (uri "/git/") (body (list (list "fastcgi_param SCRIPT_FILENAME " cgit "/lib/cgit/cgit.cgi;") #"- fastcgi_param SCRIPT_FILENAME ~a/lib/cgit/cgit.cgi; fastcgi_param PATH_INFO $uri; fastcgi_param QUERY_STRING $args; fastcgi_param HTTP_HOST $server_name; fastcgi_pass localhost:9000; rewrite /git(.*) $1 break; "#))) (nginx-location-configuration (uri "/r/velhinho/") (body '(#"- rewrite /r/velhinho(.*) $1 break; proxy_pass http://velhinho:4219; "#)))))))))) (simple-service 'create-/srv/* activation-service-type #~(begin (use-modules (guix build utils)) (let ((user (getpwnam #$whoami)) (git (getpwnam "git"))) (format (current-error-port) "Creating \"~a\".~%" #$www-dir) (mkdir-p #$www-dir) (chown #$www-dir (passwd:uid user) (passwd:gid user)) (chmod #$www-dir #o755) (format (current-error-port) "Setting permissions for \"~a\".~%" #$git-dir) (mkdir-p #$git-dir) (chown #$git-dir (passwd:uid git) (passwd:gid git)) (chmod #$git-dir #o755)))) (service prosody-service-type (prosody-configuration (modules-enabled (append '("groups" "mam") %default-modules-enabled)) (admins (list public-me)) (c2s-require-encryption? #t) (s2s-require-encryption? #t) (s2s-secure-auth? #t) ;; (authentication "cyrus") (authentication "internal_hashed") (virtualhosts (list (virtualhost-configuration (domain tld)))))) (service wireguard-service-type (wireguard-configuration (addresses '("10.0.0.0/32")) (peers (list (wireguard-peer (name "velhinho") (public-key "Mhv8KxB/QXQpNKNtqD57PoFv43TXJ1lg52PJd6TmtwI=") (allowed-ips '("10.0.0.1/32")) (keep-alive 25)) (wireguard-peer (name "azula") (public-key "8IxSFlJoFuTzLtIkoKZH4CkUbIxd6++E0lBOin/7rT8=") (allowed-ips '("10.0.0.2/32")) (keep-alive 25)))))) (service dovecot2-service-type) (service certbot-service-type (certbot-configuration (email public-me) (certificates (list (certificate-configuration (domains (list tld mta-sts.tld)) (deploy-hook (program-file (string-append tld "-deploy-hook") #~(begin (format (current-error-port) "Importing new TLS certificates for \"~a\" to Prosody via prosodyctl(8).~%" #$tld) (invoke #$(file-append prosody "/bin/prosodyctl") "--root" "cert" "import" "/etc/letsencrypt/live"))))))))) (service cyrus-sasl-service-type (cyrus-sasl-configuration (services (list (cyrus-service-configuration (name "smtpd.conf")) (cyrus-service-configuration (name "prosody.conf")))))) (service postfix-service-type (postfix-configuration (main.cf-extra (format #f #"- canonical_maps = inline:{ ~a=~a } virtual_alias_database = static:~a "# whoami public-me public-me)))) (service mail-aliases-service-type `(("root" ,whoami) (,me ,whoami) ("mailing-list" ,whoami))) (simple-service 'extra-etc-file etc-service-type `(("rc" ,rc.sh) ("ssh.conf" ,ssh.conf) ("init.scm" ,init.scm) ("gitconfig" ,gitconfig))) (service openssh-service-type (openssh-configuration (password-authentication? #f) (authorized-keys `((,whoami ,(plain-file "id_rsa.pub" ssh-pubkey)) ("git" ,(plain-file "id_rsa.pub" ssh-pubkey)))) (extra-content #"- ClientAliveInterval 30 ClientAliveCountMax 20 MaxSessions 20 "#)))) %base-services)) (bootloader (bootloader-configuration (bootloader grub-bootloader) (targets '("/dev/vda")) )) (swap-devices (list (swap-space (target (uuid "94b47d91-3542-438a-84a9-859fe347ce09"))))) (file-systems (append (list (file-system (mount-point "/") (device (uuid "4c36d5ad-f996-413e-a55c-c05b7e1876f2" 'btrfs)) (type "btrfs")) (file-system (mount-point "/mnt/backup") (device (uuid "6632849d-f180-4740-86e6-a519d43ab75a" 'btrfs)) (type "btrfs"))) %base-file-systems)))