make mail system more robus
[distro-setup] / mail-setup
index 5c5ccae88509f5b3f27badec8d96c92eefb94bad..46933160539e29a67fbc258a3c5ae0c208268d82 100755 (executable)
@@ -3,17 +3,6 @@
 # Copyright (C) 2019 Ian Kelling
 # SPDX-License-Identifier: AGPL-3.0-or-later
 
-# todo: sandbox / harden exim:
-#  1. stop it from running as root. how?
-#  https://www.exim.org/exim-html-current/doc/html/spec_html/ch-security_considerations.html
-#  * avoid using .forward files, remove that router
-#  * set deliver_drop_privilege
-#  * set user to run as Debian-exim in systemd
-#  * set port to something like 2500, and forward 25 to 2500 with iptables. same for 587.
-#  https://superuser.com/questions/710253/allow-non-root-process-to-bind-to-port-80-and-443/1334552#1334552
-#  * consider whether other routers like postmaster need modification / removal.
-#  2. restrict its filesystem access from within systemd
-
 #  todo: harden dovecot. need to do some research. one way is for it to only listen on a wireguard vpn interface, so only clients that are on the vpn can access it.
 #  todo: consider hardening cups listening on 0.0.0.0
 #  todo: stop/disable local apache, and rpc.mountd, and kdeconnect when not in use.
@@ -26,8 +15,6 @@
 # todo: emailing info@amnimal.ninja produces a bounce, user doesn't exist
 # instead of a simple rejection like it should.
 
-# todo: auto restart of je on checkrestart
-
 # todo: run mailping test after running, or otherwise
 # clear out terminal alert
 
@@ -149,7 +136,7 @@ fi
 
 [[ $EUID == 0 ]] || exec sudo -E "${BASH_SOURCE[0]}" "$@"
 
-
+# note, this is hardcoded in /etc/exim4/conf.d/main/000_local
 u=$(id -nu 1000)
 
 
@@ -292,7 +279,10 @@ fi
 i() { # install file
   local tmp tmpdir dest="$1"
   local base="${dest##*/}"
-  mkdir -p ${dest%/*}
+  local dir="${dest%/*}"
+  if [[ $dir != "$base" ]]; then
+    mkdir -p ${dest%/*}
+  fi
   ir=false # i result
   tmpdir=$(mktemp -d)
   cat >$tmpdir/"$base"
@@ -378,7 +368,7 @@ esac
 # * Install universal packages
 
 
-# installs epanicclean
+# installs epanicclean iptables-exim ip6tables-exim
 /a/bin/ds/install-my-scripts
 
 if [[ $(debian-codename-compat) == bionic ]]; then
@@ -394,7 +384,7 @@ pi-nostart exim4 exim4-daemon-heavy spamassassin openvpn unbound clamav-daemon w
 
 # note: pyzor debian readme says you need to run some initialization command
 # but its outdated.
-pi spf-tools-perl p0f postgrey pyzor razor jq moreutils certbot
+pi spf-tools-perl p0f postgrey pyzor razor jq moreutils certbot fail2ban
 # bad packages that sometimes get automatically installed
 pu openresolv resolvconf
 
@@ -492,11 +482,15 @@ Requires=mailnn.service
 After=network.target mailnn.service
 JoinsNamespaceOf=mailnn.service
 BindsTo=mailnn.service
+StartLimitIntervalSec=0
 
 [Service]
 PrivateNetwork=true
 # i dont think we need any of these, but it doesnt hurt to stay consistent
 BindPaths=$bindpaths
+
+Restart=on-failure
+RestartSec=20
 EOF
     ;;
 esac
@@ -829,6 +823,121 @@ if $bhost_t && [[ ! -e /etc/exim4/certs/$wghost/privkey.pem ]]; then
           --deploy-hook /a/bin/ds/le-exim-deploy -d $wghost
 fi
 
+# * fail2ban
+
+# todo: test that these configs actually work, eg run
+# s iptables-exim -S
+# and see someone is banned.
+
+sed 's/^ *before *= *iptables-common.conf/before = iptables-common-exim.conf/' \
+    /etc/fail2ban/action.d/iptables-multiport.conf| i /etc/fail2ban/action.d/iptables-exim.conf
+i /etc/fail2ban/action.d/iptables-common-exim.conf <<'EOF'
+# iank: same as iptables-common, except iptables is iptables-exim, ip6tables is ip6tables-exim
+
+# Fail2Ban configuration file
+#
+# Author: Daniel Black
+#
+# This is a included configuration file and includes the definitions for the iptables
+# used in all iptables based actions by default.
+#
+# The user can override the defaults in iptables-common.local
+#
+# Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
+#       made config file IPv6 capable (see new section Init?family=inet6)
+
+[INCLUDES]
+
+after = iptables-blocktype.local
+        iptables-common.local
+# iptables-blocktype.local is obsolete
+
+[Definition]
+
+# Option:  actionflush
+# Notes.:  command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
+# Values:  CMD
+#
+actionflush = <iptables> -F f2b-<name>
+
+
+[Init]
+
+# Option:  chain
+# Notes    specifies the iptables chain to which the Fail2Ban rules should be
+#          added
+# Values:  STRING  Default: INPUT
+chain = INPUT
+
+# Default name of the chain
+#
+name = default
+
+# Option:  port
+# Notes.:  specifies port to monitor
+# Values:  [ NUM | STRING ]  Default:
+#
+port = ssh
+
+# Option:  protocol
+# Notes.:  internally used by config reader for interpolations.
+# Values:  [ tcp | udp | icmp | all ] Default: tcp
+#
+protocol = tcp
+
+# Option:  blocktype
+# Note:    This is what the action does with rules. This can be any jump target
+#          as per the iptables man page (section 8). Common values are DROP
+#          REJECT, REJECT --reject-with icmp-port-unreachable
+# Values:  STRING
+blocktype = REJECT --reject-with icmp-port-unreachable
+
+# Option:  returntype
+# Note:    This is the default rule on "actionstart". This should be RETURN
+#          in all (blocking) actions, except REJECT in allowing actions.
+# Values:  STRING
+returntype = RETURN
+
+# Option:  lockingopt
+# Notes.:  Option was introduced to iptables to prevent multiple instances from
+#          running concurrently and causing irratic behavior.  -w was introduced
+#          in iptables 1.4.20, so might be absent on older systems
+#          See https://github.com/fail2ban/fail2ban/issues/1122
+# Values:  STRING
+lockingopt = -w
+
+# Option:  iptables
+# Notes.:  Actual command to be executed, including common to all calls options
+# Values:  STRING
+iptables = /usr/local/bin/iptables-exim <lockingopt>
+
+
+[Init?family=inet6]
+
+# Option:  blocktype (ipv6)
+# Note:    This is what the action does with rules. This can be any jump target
+#          as per the iptables man page (section 8). Common values are DROP
+#          REJECT, REJECT --reject-with icmp6-port-unreachable
+# Values:  STRING
+blocktype = REJECT --reject-with icmp6-port-unreachable
+
+# Option:  iptables (ipv6)
+# Notes.:  Actual command to be executed, including common to all calls options
+# Values:  STRING
+iptables = /usr/local/bin/ip6tables-exim <lockingopt>
+EOF
+
+i /etc/fail2ban/jail.d/exim.local <<'EOF'
+[exim]
+enabled  = true
+port    = 25,587
+filter   = exim
+banaction = iptables-exim
+EOF
+if $ir; then
+  m systemctl restart fail2ban
+fi
+
 # * common exim4 config
 
 
@@ -897,16 +1006,24 @@ if (( ${#files[@]} )); then
     ${files[@]} /etc/exim4
 fi
 
-# by default, only 10 days of logs are kept. increase that.
-m sed -ri 's/^(\s*rotate\s).*/\11000/' /etc/logrotate.d/exim4-base
-
+# By default, only 10 days of logs are kept. increase that.
+# And dont compress, I look back at logs too often and
+# dont need the annoyance of decompressing them all the time.
+m sed -ri '/^\s*compress\s*$/d;s/^(\s*rotate\s).*/\11000/' /etc/logrotate.d/exim4-base
+files=(/var/log/exim4/*.gz)
+if (( ${#files[@]} )); then
+  gunzip ${files[@]}
+fi
 
-## https://blog.dhampir.no/content/make-exim4-on-debian-respect-forward-and-etcaliases-when-using-a-smarthost
-# i only need .forwards, so just doing that one.
-cd /etc/exim4/conf.d/router
-b=userforward_higher_priority
-# replace the router name so it is unique
-sed -r s/^\\S+:/$b:/ 600_exim4-config_userforward >175_$b
+## disabled. not using .forward files, but this is still interesting
+## for reference.
+# ## https://blog.dhampir.no/content/make-exim4-on-debian-respect-forward-and-etcaliases-when-using-a-smarthost
+# # i only need .forwards, so just doing that one.
+# cd /etc/exim4/conf.d/router
+# b=userforward_higher_priority
+# # replace the router name so it is unique
+# sed -r s/^\\S+:/$b:/ 600_exim4-config_userforward >175_$b
+rm -fv /etc/exim4/conf.d/router/175_userforward_higher_priority
 
 # todo, consider 'separate' in etc/exim4.conf, could it help on busy systems?
 
@@ -935,7 +1052,16 @@ EOF
 
 
 rm -vf /etc/exim4/conf.d/main/000_localmacros # old filename
-cat >/etc/exim4/conf.d/main/000_local <<EOF
+
+# separate file so without quoted EOF for convenience
+cat >/etc/exim4/conf.d/main/000_local2 <<EOF
+# normally empty, I set this so I can set the envelope address
+# when doing mail redelivery to invoke filters. Also allows
+# me exiqgrep and stuff.
+MAIN_TRUSTED_GROUPS = $u
+EOF
+
+cat >/etc/exim4/conf.d/main/000_local <<'EOF'
 MAIN_TLS_ENABLE = true
 
 # require tls connections for all smarthosts
@@ -957,11 +1083,6 @@ MAIN_LOG_SELECTOR = +all
 # Based on spec, seems like a good idea to be nice.
 smtp_return_error_details = true
 
-# normally empty, I set this so I can set the envelope address
-# when doing mail redelivery to invoke filters. Also allows
-# me exiqgrep and stuff.
-MAIN_TRUSTED_GROUPS = $u
-
 # default is 10. when exim has been down for a bit, fsf mailserver
 # will do a big send in one connection, then exim decides to put
 # the messages in the queue instead of delivering them, to avoid
@@ -980,10 +1101,10 @@ DKIM_SELECTOR = li
 # There could be some circumstance when the
 # from: isnt our domain, but the envelope sender is
 # and so still want to sign, but I cant think of any case.
-DKIM_DOMAIN = \${lc:\${domain:\$rh_from:}}
+DKIM_DOMAIN = ${lc:${domain:$rh_from:}}
 # The file is based on the outgoing domain-name in the from-header.
 # sign if key exists
-DKIM_PRIVATE_KEY = \${if exists{/etc/exim4/\${dkim_domain}-private.pem} {/etc/exim4/\${dkim_domain}-private.pem}}
+DKIM_PRIVATE_KEY = ${if exists{/etc/exim4/${dkim_domain}-private.pem} {/etc/exim4/${dkim_domain}-private.pem}}
 
 # most of the ones that gmail seems to use.
 # Exim has horrible default of signing unincluded
@@ -1007,6 +1128,19 @@ hostlist iank_trusted = <; \\
 85.119.82.128 ; 2001:ba8:1f1:f09d::2 ; \\
 # fsf_mit_net fsf_mit_net_ip6 fsf_net fsf_net_ip6 fsf_office_net
 18.4.89.0/24 ; 2603:3005:71a:2e00::/64 ; 209.51.188.0/24 ; 2001:470:142::/48 ; 74.94.156.208/28
+
+
+# this is the default delay_warning_condition, plus matching on local_domains.
+# If I have some problem with my local system that causes delayed delivery,
+# I dont want to send warnings out to non-local domains.
+delay_warning_condition = ${if or {\
+  { !eq{$h_list-id:$h_list-post:$h_list-subscribe:}{} }\
+  { match{$h_precedence:}{(?i)bulk|list|junk} }\
+  { match{$h_auto-submitted:}{(?i)auto-generated|auto-replied} }\
+  { match_domain{$domain}{+local_domains} }\
+  } {no}{yes}}
+
+
 EOF
 
 rm -fv /etc/exim4/rcpt_local_acl # old path
@@ -1066,14 +1200,14 @@ warn
 
 EOF
 
-i /etc/exim4/conf.d/router/900_exim4-config_local_user <<EOF
+i /etc/exim4/conf.d/router/900_exim4-config_local_user <<'EOF'
 ### router/900_exim4-config_local_user
 #################################
 
 # This router matches local user mailboxes. If the router fails, the error
 # message is "Unknown user".
 local_user:
-  debug_print = "R: local_user for \$local_part@\$domain"
+  debug_print = "R: local_user for $local_part@$domain"
   driver = accept
   domains = +local_domains
 # ian: default file except where mentioned.
@@ -1390,8 +1524,8 @@ case $HOSTNAME in
         break
       fi
     done
-    for f in /p/c/subdir_files/sieve/*sieve /a/c/subdir_files/sieve/*sieve; do
-      m sudo -u $u /a/exe/lnf -T $f $uhome/sieve/${f##*/}
+    for f in /p/c/subdir_files/sieve/*sieve /a/bin/ds/subdir_files/sieve/*sieve; do
+      m sudo -u $u /a/exe/lnf -v -T $f $uhome/sieve/${f##*/}
     done
 
     # https://wiki.dovecot.org/SSL/DovecotConfiguration
@@ -1417,7 +1551,7 @@ ssl_cert = </etc/exim4/exim.crt
 ssl_key = </etc/exim4/exim.key
 EOF
       fi
-      cat <<EOF
+      cat <<'EOF'
 # https://ssl-config.mozilla.org
 ssl = required
 # this is the same as the certbot list, in my cert cronjob, I check if that has changed upstream.
@@ -1427,8 +1561,8 @@ ssl_prefer_server_ciphers = no
 
 protocol lmtp {
 #per https://wiki2.dovecot.org/Pigeonhole/Sieve/Configuration
-# default is just \$mail_plugins
-  mail_plugins = \$mail_plugins sieve
+# default is just $mail_plugins
+  mail_plugins = $mail_plugins sieve
 }
 EOF
       if dpkg --compare-versions $(dpkg-query -f='${Version}\n' --show dovecot-core) ge 1:2.3; then
@@ -1867,7 +2001,6 @@ EOF
     m chown -R www-data.www-data $rctmpdir /m/rc
     m chmod 750 $rctmpdir
     # Ensure the log file monitored by fail2ban exists, or else fail2ban can't start.
-    # todo: setup fail2ban
     # todo: check for other mailinabox things
     m sudo -u www-data touch $rclogdir/errors.log
 
@@ -2168,7 +2301,7 @@ if (( ${#files[@]} )); then
 fi
 
 
-# ** auth
+# ** exim: auth
 
 case $HOSTNAME in
   bk|je)
@@ -2204,7 +2337,7 @@ server_advertise_condition = ${if eq{$tls_in_cipher}{}{}{*}}
 EOF
 fi
 
-# ** main daemon use non-default config file
+# ** exim: main daemon use non-default config file
 case $HOSTNAME in
   bk|$MAIL_HOST)
     # to see the default comments in /etc/default/exim4:
@@ -2215,8 +2348,11 @@ QUEUERUNNER='combined'
 QUEUEINTERVAL='30m'
 COMMONOPTIONS='-C /etc/exim4/my.conf'
 UPEX4OPTS='-o /etc/exim4/my.conf'
-#E4BCD_PANICLOG_NOISE='malware acl condition: clamd /var/run/clamav/clamd\.ctl : unable to connect to UNIX socket'
+#E4BCD_PANICLOG_NOISE='exim user lost privilege for using -C option'
 EOF
+    chown Debian-exim:Debian-exim /usr/sbin/exim4
+    # needs guid set in order to become Debian-exim
+    chmod g+s,u+s /usr/sbin/exim4
     i /etc/exim4/trusted_configs <<'EOF'
 /etc/exim4/my.conf
 EOF
@@ -2230,7 +2366,51 @@ EOF
     ;;
 esac
 
+# ** exim non-root
+
 case $HOSTNAME in
+  bk|je|li)
+    # no reason to expect it to ever be there.
+    rm -fv /etc/systemd/system/exim4.service.d/nonroot.conf
+    ;;
+  *)
+    i /etc/systemd/system/exim4.service.d/nonroot.conf <<'EOF'
+[Service]
+# see 56.2 Root privilege in exim spec
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+# https://www.redhat.com/sysadmin/mastering-systemd
+# things that seem good and reasonabl.e
+PrivateTmp=yes
+ProtectHome=yes
+# note, in t10 systemd, if one of these is an sshfs mountpoint,
+# this whole setting doesnt work. tried it with a newer systemd 250 though
+# an nspawn, and it worked there.
+InaccessiblePaths=d m media mnt nocow o p q
+NoNewPrivileges=yes
+ProtectSystem=yes
+
+# when we get newer systemd
+#ProtectDevices=yes
+EOF
+    i /etc/exim4/conf.d/main/000_local-noroot <<'EOF'
+# see 56.2 Root privilege in exim spec
+deliver_drop_privilege = true
+EOF
+    files=(
+      300_exim4-config_real_local
+      600_exim4-config_userforward
+      700_exim4-config_procmail
+      800_exim4-config_maildrop
+      mmm_mail4root
+    )
+    for f in ${files[@]}; do
+      echo "# iank: removed due to running nonroot"|i /etc/exim4/conf.d/router/$f
+    done
+    ;;
+esac
+
+case $HOSTNAME in
+
   # ** $MAIL_HOST|bk|je)
   $MAIL_HOST|bk|je)
 
@@ -2652,12 +2832,12 @@ backup_maildir:
   user = $u
 EOF
 
-      i /etc/exim4/conf.d/router/870_backup_local <<EOF
+      i /etc/exim4/conf.d/router/870_backup_local <<'EOF'
 ### router/900_exim4-config_local_user
 #################################
 
 backup_local:
-  debug_print = "R: local_user for \$local_part@\$domain"
+  debug_print = "R: local_user for $local_part@$domain"
   driver = accept
   domains = eximbackup.b8.nz
   transport = backup_maildir
@@ -2921,6 +3101,8 @@ MAILTO=alerts@iankelling.org
 #5-59/5 * * * *   root mailtest-check |& log-once -1 mailtest-check
 #0 * * * *   root mailtest-check slow |& log-once -1 mailtest-slow
 */5 * * * *   root timeout 290 mailtest-check slow |& log-once -12 mailtest-check
+# if a bounce happened yesterday, dont let it slip through the cracks
+8   1 * * *   root awk '$5 == "**"' /var/log/exim4/mainlog.1
 EOF
     m sudo rsync -ahhi --chown=root:root --chmod=0755 \
       /b/ds/mailtest-check /b/ds/check-remote-mailqs /usr/local/bin/