Config Library¶
Diese Datei enthält alle relevanten Konfigurationsdateien und Scripts des Setups. Platzhalter wie {{DOMAIN}} sind in [Variablen und Platzhalter](../00_Einleitung/00_variablen.md) definiert.
Sensible Werte wie Passwörter sind durch {{SECRET_...}} Platzhalter ersetzt.
Heimserver¶
/etc/postfix/main.cf¶
# Basisinfos
myhostname = {{HOME_SMTP}}
myorigin = /etc/mailname
mydestination = $myhostname, localhost.$mydomain, localhost
local_recipient_maps =
# Virtuelle Mailboxen via PostfixAdmin/MySQL
mailbox_transport = dovecot
virtual_alias_maps = mysql:/etc/postfix/mysql_virtual_alias_maps.cf
virtual_mailbox_domains = mysql:/etc/postfix/mysql_virtual_domains_maps.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql_virtual_mailbox_maps.cf
virtual_mailbox_base = /var/vmail
virtual_mailbox_limit = 0
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
inet_interfaces = all
inet_protocols = ipv4
# Mailweiterleitung über Relay (Port 587 mit SASL)
relayhost = [{{RELAY_HOSTNAME}}]:587
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_CApath = /etc/letsencrypt/live/{{HOME_SMTP}}
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
# Erlaubte Absender (LAN + Relay-IP fest)
mynetworks = 127.0.0.0/8 192.168.1.0/24 {{RELAY_IP}}
# SASL Authentifizierung über Dovecot (für Mailclients)
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth_dovecot
smtpd_sasl_auth_enable = yes
# TLS für eingehende Verbindungen
smtpd_tls_cert_file = /etc/letsencrypt/live/{{HOME_SMTP}}/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/{{HOME_SMTP}}/privkey.pem
smtpd_tls_CAfile = /etc/letsencrypt/live/{{HOME_SMTP}}/chain.pem
smtpd_tls_security_level = may
smtpd_tls_mandatory_protocols = TLSv1.2 TLSv1.3
smtpd_tls_mandatory_ciphers = high
smtpd_tls_exclude_ciphers = aNULL, MD5
smtpd_tls_dh1024_param_file = /etc/ssl/certs/dh4096.pem
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# Empfangsregeln
smtpd_recipient_restrictions =
permit_sasl_authenticated,
permit_mynetworks,
reject_unauth_destination
# Keine lokalen Filterdienste
content_filter =
receive_override_options =
mailbox_command =
mailbox_size_limit = 0
# OpenDKIM Milter
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:12301
non_smtpd_milters = $smtpd_milters
# Allgemein
message_size_limit = 52428800
recipient_delimiter = +
compatibility_level = 3.6
/etc/postfix/master.cf¶
smtp inet n - y - - smtpd
-o smtpd_tls_security_level=may
-o smtpd_recipient_restrictions=permit_mynetworks,reject
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_tls_cert_file=/etc/letsencrypt/live/{{HOME_SMTP}}/fullchain.pem
-o smtpd_tls_key_file=/etc/letsencrypt/live/{{HOME_SMTP}}/privkey.pem
-o smtpd_tls_CAfile=/etc/letsencrypt/live/{{HOME_SMTP}}/chain.pem
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
2525 inet n - y - - smtpd
-o syslog_name=postfix/2525
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
pickup fifo n - y 60 1 pickup
-o content_filter=
-o receive_override_options=no_header_body_checks
cleanup unix n - y - 0 cleanup
qmgr fifo n - n 300 1 qmgr
tlsmgr unix - - y 1000? 1 tlsmgr
rewrite unix - - y - - trivial-rewrite
bounce unix - - y - 0 bounce
defer unix - - y - 0 bounce
trace unix - - y - 0 bounce
verify unix - - y - 1 verify
flush unix n - y 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - y - - smtp
relay unix - - y - - smtp
showq unix n - y - - showq
error unix - - y - - error
retry unix - - y - - error
discard unix - - y - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - y - - lmtp
anvil unix - - y - 1 anvil
scache unix - - y - 1 scache
dovecot unix - n n - - pipe
flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -d ${recipient}
/etc/postfix/sasl_passwd¶
Nach Änderung:
postmap hash:/etc/postfix/sasl_passwd && chmod 600 /etc/postfix/sasl_passwd
/etc/postfix/mysql_virtual_domains_maps.cf¶
hosts = 127.0.0.1
user = postfixadmin
password = {{SECRET_DB_PASSWORD}}
dbname = postfixadmin
query = SELECT domain FROM domain WHERE domain='%s' AND active = '1'
/etc/postfix/mysql_virtual_mailbox_maps.cf¶
hosts = 127.0.0.1
user = postfixadmin
password = {{SECRET_DB_PASSWORD}}
dbname = postfixadmin
query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1'
/etc/postfix/mysql_virtual_alias_maps.cf¶
hosts = 127.0.0.1
user = postfixadmin
password = {{SECRET_DB_PASSWORD}}
dbname = postfixadmin
query = SELECT goto FROM alias WHERE address='%s' AND active = '1'
/etc/postfix/mysql_sender_login_maps.cf¶
hosts = 127.0.0.1
user = postfixadmin
password = {{SECRET_DB_PASSWORD}}
dbname = postfixadmin
query = SELECT username AS allowedUser FROM mailbox WHERE username='%s' AND active = 1
UNION SELECT goto FROM alias WHERE address='%s' AND active = 1
/etc/postfix/mysql_virtual_relayhosts_maps.cf¶
hosts = 127.0.0.1
user = postfixadmin
password = {{SECRET_DB_PASSWORD}}
dbname = postfixadmin
query = SELECT description FROM domain WHERE domain='%d' AND active = '1'
/etc/opendkim.conf¶
Syslog yes
UMask 002
Canonicalization relaxed/simple
Mode sv
AutoRestart yes
AutoRestartRate 10/1h
Background yes
DNSTimeout 5
SignatureAlgorithm rsa-sha256
KeyTable refile:/etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
ExternalIgnoreList refile:/etc/opendkim/trusted.hosts
InternalHosts refile:/etc/opendkim/trusted.hosts
Socket inet:12301@localhost
UserID opendkim:opendkim
PidFile /run/opendkim/opendkim.pid
/etc/opendkim/key.table¶
{{DKIM_SELECTOR}}._domainkey.{{DOMAIN}} {{DOMAIN}}:{{DKIM_SELECTOR}}:/etc/opendkim/keys/{{DOMAIN}}/{{DKIM_SELECTOR}}.private
/etc/opendkim/signing.table¶
/etc/opendkim/trusted.hosts¶
{{HOME_HOSTNAME}}.ddns.netist der No-IP-DynDNS-Name des Heimservers. Er wird parallel zu{{HOME_SMTP}}(A-Record via deSEC) gepflegt.
Certbot Hooks¶
Pre-Hook /etc/letsencrypt/renewal-hooks/pre/001-open-ports.sh:
#!/bin/bash
echo "[Certbot pre-hook] Öffne Ports 80 und 443 für Let's Encrypt Validierung..."
iptables -I INPUT -p tcp --dport 80 -j ACCEPT
iptables -I INPUT -p tcp --dport 443 -j ACCEPT
Post-Hook /etc/letsencrypt/renewal-hooks/post/001-close-ports.sh:
#!/bin/bash
echo "[Certbot post-hook] Entferne temporäre Portfreigaben für 80 und 443..."
iptables -D INPUT -p tcp --dport 80 -j ACCEPT
iptables -D INPUT -p tcp --dport 443 -j ACCEPT
Deploy-Hook /etc/letsencrypt/renewal-hooks/deploy/001-dovecot-apache-restart.sh:
#!/bin/bash
echo "ssl certs updated in dovecot" && service dovecot restart
echo "ssl certs updated in apache" && service apache2 restart
# Zertifikate in Postfix-Chroot aktualisieren
cp /etc/letsencrypt/live/{{HOME_SMTP}}/fullchain.pem /var/spool/postfix/etc/letsencrypt/live/{{HOME_SMTP}}/
cp /etc/letsencrypt/live/{{HOME_SMTP}}/chain.pem /var/spool/postfix/etc/letsencrypt/live/{{HOME_SMTP}}/
cp /etc/letsencrypt/live/{{HOME_SMTP}}/cert.pem /var/spool/postfix/etc/letsencrypt/live/{{HOME_SMTP}}/
cp /etc/letsencrypt/live/{{HOME_SMTP}}/privkey.pem /var/spool/postfix/etc/letsencrypt/live/{{HOME_SMTP}}/
cp /etc/hosts /var/spool/postfix/etc/hosts
echo "ssl certs updated in postfix chroot" && postfix reload
Deploy-Hook /etc/letsencrypt/renewal-hooks/deploy/update-tlsa.sh:
#!/bin/bash
TLSA_HASH=$(openssl x509 -in /etc/letsencrypt/live/{{HOME_SMTP}}/fullchain.pem -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| xxd -p -c 64)
curl -X PATCH https://desec.io/api/v1/domains/{{DOMAIN}}/rrsets/_465._tcp.smtp/TLSA/ \
-H "Authorization: Token $(cat /root/.dedyn-token)" \
-H "Content-Type: application/json" \
-d "{\"records\": [\"3 1 1 $TLSA_HASH\"]}"
Dieser Hook aktualisiert den TLSA-Record für DANE nach jeder Zertifikatserneuerung. Siehe TLS für IMAP und SMTP.
/usr/local/bin/update-dedyn.sh¶
#!/bin/bash
TOKEN_FILE="/root/.dedyn-token"
LOG_FILE="/var/log/dedyn-update.log"
IP_FILE="/var/lib/dedyn/last_ip"
DOMAIN="{{DOMAIN}}"
TOKEN=$(cat "$TOKEN_FILE")
mkdir -p /var/lib/dedyn
CURRENT_IP=$(curl -s --max-time 10 https://ifconfig.me)
if [ -z "$CURRENT_IP" ]( -z "$CURRENT_IP" .md); then
echo "$(date) - ❌ Fehler: Konnte IP nicht ermitteln." >> "$LOG_FILE"
exit 1
fi
LAST_IP=$(cat "$IP_FILE" 2>/dev/null || echo "")
if [ "$CURRENT_IP" != "$LAST_IP" ]( "$CURRENT_IP" != "$LAST_IP" .md); then
update_record() {
local RR_NAME="$1"
local RRSET_URL="https://desec.io/api/v1/domains/${DOMAIN}/rrsets/${RR_NAME}/A"
local RESPONSE=$(curl -s --max-time 15 --retry 3 --retry-delay 5 \
-X PATCH "$RRSET_URL/" \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"records\": [\"$CURRENT_IP\"]}")
if echo "$RESPONSE" | grep -q "$CURRENT_IP"; then
echo "$(date) - ✅ Updated ${RR_NAME}.${DOMAIN} to $CURRENT_IP" >> "$LOG_FILE"
else
echo "$(date) - ❌ Fehler beim Update ${RR_NAME}.${DOMAIN} – Antwort: $RESPONSE" >> "$LOG_FILE"
fi
}
update_record "smtp" # {{HOME_SMTP}}
update_record "%40" # @ = Root-Domain ({{DOMAIN}})
else
echo "$(date) - ℹ️ IP unverändert: $CURRENT_IP" >> "$LOG_FILE"
fi
echo "$CURRENT_IP" > "$IP_FILE"
/etc/dovecot/dovecot.conf¶
auth_mechanisms = plain login
log_timestamp = "%Y-%m-%d %H:%M:%S "
passdb {
args = /etc/dovecot/dovecot-mysql.conf
driver = sql
}
userdb {
args = /etc/dovecot/dovecot-mysql.conf
driver = sql
}
plugin {
sieve_before = /var/vmail/sieve/spam-global.sieve
sieve_dir = /var/vmail/%d/%n/sieve/scripts/
sieve = /var/vmail/%d/%n/sieve/active-script.sieve
}
protocols = imap pop3 sieve
service auth {
unix_listener /var/spool/postfix/private/auth_dovecot {
group = postfix
mode = 0660
user = postfix
}
unix_listener auth-master {
mode = 0600
user = vmail
}
user = root
}
service managesieve-login {
executable = /usr/lib/dovecot/managesieve-login
}
service managesieve {
executable = /usr/lib/dovecot/managesieve
}
ssl = required
ssl_cert = </etc/letsencrypt/live/{{HOME_IMAP}}/fullchain.pem
ssl_key = </etc/letsencrypt/live/{{HOME_IMAP}}/privkey.pem
ssl_dh = </etc/dovecot/dh.pem
protocol pop3 {
pop3_uidl_format = %08Xu%08Xv
}
protocol lda {
auth_socket_path = /var/run/dovecot/auth-master
mail_plugin_dir = /usr/lib/dovecot/modules
mail_plugins = sieve
postmaster_address = {{ADMIN_MAIL}}
}
protocol sieve {
managesieve_implementation_string = dovecot
managesieve_logout_format = bytes=%i/%o
managesieve_max_line_length = 65536
}
mail_home = /var/vmail/%d/%n
ssl_dh = </etc/dovecot/dh.pem
!include /etc/dovecot/conf.d/*.conf
Hinweis:
ssl_dhist in dieser Datei doppelt definiert (Zeile ~15 und ~25). Dovecot verwendet den letzten Wert – das ist identisch, hat also keine Auswirkung. Beim nächsten Bearbeiten der Datei kann das Duplikat entfernt werden.
/etc/dovecot/dovecot-mysql.conf¶
driver = mysql
connect = host=localhost dbname=postfixadmin user=postfixadmin password={{SECRET_DB_PASSWORD}}
default_pass_scheme = PLAIN-MD5
password_query = SELECT password FROM mailbox WHERE username = '%u'
user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, 5000 AS gid FROM mailbox WHERE username = '%u'
Die
user_queryliefert den vollständigen Maildir-Pfad direkt aus der PostfixAdmin-Datenbank.mail_locationin10-mail.confwird dadurch überschrieben.
/etc/dovecot/conf.d/10-master.conf (relevante Abschnitte)¶
default_internal_user = vmail
service imap-login {
inet_listener imaps {
port = 993
ssl = yes
}
}
service auth {
unix_listener /var/spool/postfix/private/auth_dovecot {
mode = 0660
user = postfix
group = postfix
}
unix_listener auth-userdb {
mode = 0600
user = vmail
group = vmail
}
user = root
}
/etc/dovecot/conf.d/10-auth.conf (relevante Abschnitte)¶
auth_mechanisms = plain
# Passwdfile für einzelne Admin-User (Fallback)
!include auth-passwdfile.conf.ext
# SQL-Backend wird in dovecot.conf direkt definiert und hat Vorrang
#!include auth-sql.conf.ext
/etc/dovecot/conf.d/10-ssl.conf¶
TLS wird in dovecot.conf direkt konfiguriert – diese Datei enthält nur den Standardwert:
DH-Parameter erzeugen¶
Dovecot und Postfix nutzen separate DH-Dateien:
# Für Dovecot
openssl dhparam -out /etc/dovecot/dh.pem 4096
# Für Postfix
openssl dhparam -out /etc/ssl/certs/dh4096.pem 4096
Beide Befehle dauern mehrere Minuten. Sie können parallel auf zwei Terminals ausgeführt werden.
Relay-Server¶
/etc/postfix/main.cf¶
smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
message_size_limit = 52428800
# TLS
smtpd_tls_cert_file = /etc/letsencrypt/live/{{RELAY_HOSTNAME}}/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/{{RELAY_HOSTNAME}}/privkey.pem
smtpd_tls_security_level = may
smtp_tls_security_level = may
smtp_tls_CApath = /etc/ssl/certs
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtpd_tls_auth_only = yes
smtpd_use_tls = yes
# SASL (Dovecot) – für Mailclients und Heimserver
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
# Empfangsregeln
smtpd_recipient_restrictions =
permit_sasl_authenticated,
permit_mynetworks,
reject_unauth_destination
# Netzwerke – Relay-IP ist vertrauenswürdig für Port 2525
mynetworks = 127.0.0.0/8 [::1]/128 {{RELAY_IP}}
# Hostname
myhostname = {{RELAY_HOSTNAME}}
myorigin = /etc/mailname
mydestination = localhost
relay_domains = {{DOMAIN}}
transport_maps = hash:/etc/postfix/transport
# kein relayhost – ausgehende Mails direkt ins Internet
relayhost =
# Sonstiges
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = ipv4
compatibility_level = 3.6
readme_directory = no
disable_vrfy_command = yes
# HELO-Prüfungen
smtpd_helo_required = yes
smtpd_helo_restrictions =
permit_mynetworks,
reject_non_fqdn_helo_hostname,
reject_invalid_helo_hostname,
reject_unknown_helo_hostname,
permit
# Daten-Prüfungen
smtpd_data_restrictions =
reject_unauth_pipelining,
reject_multi_recipient_bounce,
permit
# RBL-Prüfungen
smtpd_client_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
check_client_access hash:/etc/postfix/rbl_override,
reject_rbl_client dnsbl.sorbs.net,
reject_rbl_client bl.spamcop.net,
reject_rbl_client b.barracudacentral.org,
reject_rbl_client psbl.surriel.com,
permit
# Rspamd Milter (Port 11332 – nicht 12301, der ist von OpenDKIM auf dem Heimserver belegt)
milter_protocol = 6
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
smtpd_milters = inet:localhost:11332
non_smtpd_milters = inet:localhost:11332
milter_default_action = accept
/etc/postfix/transport¶
/etc/postfix/sasl_passwd¶
Leer – der Relay sendet ausgehende Mails direkt ins Internet ohne Smarthost.
/etc/rspamd/local.d/worker-proxy.inc¶
# Milter-Dienst für Postfix
milter = true;
# Port 11332 – nicht 12301 (von OpenDKIM auf dem Heimserver belegt)
bind_socket = "127.0.0.1:11332";
milter_mail_macros = "i, mail_addr, rcpt_addr, from_addr, rcpt_user, rcpt_domain";
/etc/rspamd/local.d/worker-controller.inc¶
/etc/rspamd/local.d/dkim_signing.conf¶
domain {
{{DOMAIN}} {
selector = "{{DKIM_SELECTOR}}";
path = "/etc/rspamd/dkim/{{DKIM_SELECTOR}}.{{DOMAIN}}.key";
}
}
sign_local = true;
sign_authenticated = true;
use_domain = "header";
/etc/rspamd/local.d/antivirus.conf¶
clamav {
type = "clamav";
symbol = "CLAM_VIRUS";
servers = "/var/run/clamav/clamd.ctl";
action = "quarantine";
}
/etc/rspamd/local.d/spf.conf¶
/etc/rspamd/local.d/actions.conf¶
/etc/rspamd/local.d/history_redis.conf¶
/etc/rspamd/local.d/options.inc¶
/etc/rspamd/local.d/neural.conf¶
/etc/systemd/system/rspamd.service.d/override.conf¶
Wichtig: Datei muss mit Newline enden – fehlendes Newline verhindert den Start.
/etc/tmpfiles.d/rspamd.conf¶
Stellt sicher dass
/run/rspamd/nach Reboots automatisch angelegt wird.
/etc/iptables/drop_tor.sh¶
Blockt Tor-Exit-Nodes per iptables. Wird manuell oder per Cronjob ausgeführt – die Regeln überleben keinen Reboot (nicht in rules.v4 gespeichert).
#!/bin/bash
# Block Tor Exit nodes
IPTABLES_TARGET="DROP"
IPTABLES_CHAINNAME="TOR"
if ! iptables -L TOR -n >/dev/null 2>&1; then
iptables -N TOR >/dev/null 2>&1
iptables -A INPUT -p tcp -j TOR 2>&1
fi
cd /tmp/
wget -q -O - "https://www.dan.me.uk/torlist/" -U SXTorBlocker/1.0 > /tmp/full.tor
sed -i 's|^#.*$||g' /tmp/full.tor
iptables -F TOR
CMD=$(cat /tmp/full.tor | uniq | sort)
for IP in $CMD; do
iptables -A TOR -s $IP -j DROP
done
iptables -A TOR -j RETURN
rm /tmp/full.tor
Empfohlener Cronjob (/etc/cron.d/drop-tor):
Relay-Server – Certbot Hooks¶
Pre-Hook /etc/letsencrypt/renewal-hooks/pre/fw-open.sh:
Post-Hook /etc/letsencrypt/renewal-hooks/post/fw-close.sh:
Post-Hook /etc/letsencrypt/renewal-hooks/post/reload-services.sh:
# /etc/letsencrypt/renewal-hooks/post/reload-services.sh
#!/bin/bash
echo "[+] Reloading Postfix and Dovecot after cert renewal..."
systemctl reload postfix
systemctl reload dovecot
/usr/local/bin/fw-open-http.sh:
# /usr/local/bin/fw-open-http.sh
#!/bin/bash
iptables -I INPUT -p tcp --dport 80 -j ACCEPT
ip6tables -I INPUT -p tcp --dport 80 -j ACCEPT
/usr/local/bin/fw-close-http.sh:
# /usr/local/bin/fw-close-http.sh
#!/bin/bash
iptables -D INPUT -p tcp --dport 80 -j ACCEPT
ip6tables -D INPUT -p tcp --dport 80 -j ACCEPT
Port 443 muss nicht temporär geöffnet werden – er ist in
update_relay_ip.shdauerhaft freigegeben.
Relay-Server – Firewall-Skript¶
/usr/local/bin/update_relay_ip.sh¶
Setzt die gesamte iptables/ip6tables-Policy des Relay dynamisch. Ermittelt die aktuelle IP des Heimservers per DNS, setzt alle Regeln neu, speichert sie persistent und aktualisiert die Unbound-access-control.
#!/bin/bash
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
DYNDNS_NAME="{{HOME_HOSTNAME}}.ddns.net"
UNBOUND_CONF="/etc/unbound/unbound.conf.d/local.conf"
# WAN-Interface dynamisch bestimmen
WAN_IF=$(ip route | awk '/default/ {print $5; exit}')
: "${WAN_IF:?Konnte WAN-Interface nicht bestimmen}"
HOMESERVER_IPV4=$(dig +short A "$DYNDNS_NAME" | head -n 1 || true)
HOMESERVER_IPV6=$(dig +short AAAA "$DYNDNS_NAME" | head -n 1 || true)
if [ -z "${HOMESERVER_IPV4}" && -z "${HOMESERVER_IPV6}" ]( -z "${HOMESERVER_IPV4}" && -z "${HOMESERVER_IPV6}" .md); then
echo "❌ Fehler: Konnte IP-Adresse für $DYNDNS_NAME nicht auflösen."
exit 1
fi
# IPv4
iptables -F INPUT
iptables -F OUTPUT
iptables -P INPUT DROP
iptables -P OUTPUT ACCEPT
iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
if ip link show docker0 >/dev/null 2>&1; then
iptables -A FORWARD -i docker0 -o "$WAN_IF" -j ACCEPT
iptables -A FORWARD -i "$WAN_IF" -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i docker0 -o docker0 -j ACCEPT
fi
if [ -n "${HOMESERVER_IPV4}" ]( -n "${HOMESERVER_IPV4}" .md); then
iptables -A INPUT -p tcp -s "$HOMESERVER_IPV4" --dport 22 -j ACCEPT
iptables -A INPUT -p tcp -s "$HOMESERVER_IPV4" --dport 2525 -j ACCEPT
iptables -A INPUT -p tcp -s "$HOMESERVER_IPV4" --dport 587 -j ACCEPT
iptables -A INPUT -p udp -s "$HOMESERVER_IPV4" --dport 53 -j ACCEPT
iptables -A INPUT -p tcp -s "$HOMESERVER_IPV4" --dport 53 -j ACCEPT
fi
iptables -A INPUT -p tcp --dport 25 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p icmp -j ACCEPT
# IPv6
ip6tables -F INPUT
ip6tables -F OUTPUT
ip6tables -P INPUT DROP
ip6tables -P OUTPUT ACCEPT
ip6tables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
if [ -n "${HOMESERVER_IPV6}" ]( -n "${HOMESERVER_IPV6}" .md); then
ip6tables -A INPUT -p tcp -s "$HOMESERVER_IPV6" --dport 22 -j ACCEPT
ip6tables -A INPUT -p tcp -s "$HOMESERVER_IPV6" --dport 2525 -j ACCEPT
ip6tables -A INPUT -p tcp -s "$HOMESERVER_IPV6" --dport 587 -j ACCEPT
ip6tables -A INPUT -p tcp -s "$HOMESERVER_IPV6" --dport 53 -j ACCEPT
ip6tables -A INPUT -p udp -s "$HOMESERVER_IPV6" --dport 53 -j ACCEPT
fi
ip6tables -A INPUT -p tcp --dport 25 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
ip6tables -A INPUT -p icmpv6 -j ACCEPT
# Regeln speichern
iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6
# Unbound access-control aktualisieren
cp "$UNBOUND_CONF" "${UNBOUND_CONF}.bak"
sed -i '/# BEGIN HOMESERVER/,/# END HOMESERVER/d' "$UNBOUND_CONF"
{
echo " # BEGIN HOMESERVER (automatisch generiert)"
[ -n "${HOMESERVER_IPV4}" ]( -n "${HOMESERVER_IPV4}" .md) && echo " access-control: ${HOMESERVER_IPV4}/32 allow"
[ -n "${HOMESERVER_IPV6}" ]( -n "${HOMESERVER_IPV6}" .md) && echo " access-control: ${HOMESERVER_IPV6}/128 allow"
echo " # END HOMESERVER"
} >> "$UNBOUND_CONF"
systemctl is-active --quiet unbound && systemctl reload unbound || systemctl restart unbound
Das Skript wird per Cronjob alle 15 Minuten ausgeführt (
/etc/cron.d/update-relay-ip), damit Firewall und Unbound bei IP-Wechsel des Heimservers automatisch aktualisiert werden.
Relay-Server – Backup¶
/usr/local/bin/backup_relay.sh¶
Sichert die Relay-Konfiguration (Postfix, Dovecot, Let's Encrypt, Scripts) per scp auf den Heimserver. Wird auf dem Relay ausgeführt.
#!/bin/bash
set -euo pipefail
DATE=$(date +"%Y-%m-%d_%H-%M")
TARGET="relaybackup@{{HOME_HOSTNAME}}.ddns.net:/mnt/lvm/cloud/relay"
ARCHIVENAME="relay-backup-$DATE.tar.gz"
ARCHIVPFAD="/tmp/$ARCHIVENAME"
TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/etc" "$TMPDIR/usr-local-bin"
cp -r /etc/postfix "$TMPDIR/etc/" || echo "⚠️ /etc/postfix fehlt"
cp -r /etc/dovecot "$TMPDIR/etc/" || echo "⚠️ /etc/dovecot fehlt"
cp -r /etc/letsencrypt "$TMPDIR/etc/" 2>/dev/null || true
cp -r /usr/local/bin "$TMPDIR/usr-local-bin/" 2>/dev/null || true
cp /etc/aliases "$TMPDIR/etc/" 2>/dev/null || true
cp /etc/hostname "$TMPDIR/etc/" || true
cp /etc/hosts "$TMPDIR/etc/" || true
tar czf "$ARCHIVPFAD" -C "$TMPDIR" .
scp -O -i ~/.ssh/id_relaybackup "$ARCHIVPFAD" "$TARGET" || {
echo "❌ Fehler beim Übertragen!"
exit 2
}
rm -rf "$TMPDIR" "$ARCHIVPFAD"
echo "✅ Backup erfolgreich nach $TARGET übertragen!"
Voraussetzung: SSH-Key
~/.ssh/id_relaybackupist auf dem Heimserver für den Userrelaybackupautorisiert. Backup-Ziel:/mnt/lvm/cloud/relay/auf dem Heimserver.
Heimserver – Scripts¶
/usr/local/bin/update-dedyn.sh¶
Bereits dokumentiert weiter oben – aktualisiert smtp und @ A-Records bei deSEC per API.
/usr/local/bin/update-tlsa.sh¶
Aktualisiert den TLSA-Record für DANE (Port 465, SMTPS) nach manuellen Zertifikatsoperationen. Für automatische Erneuerung wird der Deploy-Hook genutzt.
#!/bin/bash
CERT_PATH="/etc/letsencrypt/live/{{HOME_SMTP}}/fullchain.pem"
TOKEN=$(cat /root/.dedyn-token)
HASH=$(openssl x509 -in "$CERT_PATH" -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 | awk '{print $2}')
curl -X PATCH https://desec.io/api/v1/domains/{{DOMAIN}}/rrsets/_465._tcp.smtp/TLSA/ \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"records\": [\"3 1 1 $HASH\"]}"
/usr/local/bin/mailcheck.sh¶
Diagnoseskript – prüft SPF, DKIM, DMARC, PTR, MTA-STS und Spamhaus-Listing für die Domain. Nützlich nach Konfigurationsänderungen.
#!/bin/bash
DOMAIN="{{DOMAIN}}"
IP="{{RELAY_IP}}"
DKIM_SELECTOR="{{DKIM_SELECTOR}}"
echo "📧 Mailserver-Sicherheitscheck für: $DOMAIN"
echo "-----------------------------------------"
echo -e "\n🔍 SPF:"
dig +short TXT $DOMAIN | grep spf || echo "❌ Kein SPF-Eintrag."
echo -e "\n🔍 DKIM:"
dig +short TXT $DKIM_SELECTOR._domainkey.$DOMAIN || echo "❌ Kein DKIM-Eintrag."
echo -e "\n🔍 DMARC:"
dig +short TXT _dmarc.$DOMAIN || echo "❌ Kein DMARC-Eintrag."
echo -e "\n🔍 Reverse DNS:"
PTR=$(dig +short -x $IP)
[ $PTR == *$DOMAIN* ]( $PTR == *$DOMAIN* .md) && echo "✅ PTR: $PTR" || echo "❌ PTR: $PTR – passt nicht zur Domain!"
echo -e "\n🔍 MTA-STS DNS:"
dig +short TXT _mta-sts.$DOMAIN || echo "❌ Kein _mta-sts Eintrag."
echo -e "\n🔍 MTA-STS Policy:"
curl -s --max-time 5 https://mta-sts.$DOMAIN/.well-known/mta-sts.txt || echo "❌ Keine Policy abrufbar!"
echo -e "\n🔍 TLS-RPT:"
dig +short TXT _smtp._tls.$DOMAIN || echo "❌ Kein TLS-RPT-Eintrag."
echo -e "\n🔍 Spamhaus ZEN:"
REVERSE_IP=$(echo $IP | awk -F. '{print $4"."$3"."$2"."$1}')
BL=$(dig +short ${REVERSE_IP}.zen.spamhaus.org)
[ $BL == 127.* ]( $BL == 127.* .md) && echo "❌ GELISTET: $BL" || echo "✅ Nicht gelistet."
/usr/local/bin/mail-status.sh¶
Schnellcheck ob Postfix, Dovecot und MariaDB laufen, plus Queue-Übersicht.
#!/bin/bash
services=("postfix" "dovecot" "mariadb")
GREEN="\033[0;32m"; RED="\033[0;31m"; NC="\033[0m"
echo "📡 Mailserver-Statusprüfung:"
for svc in "${services[@]}"; do
systemctl is-active --quiet "$svc" \
&& echo -e "$svc: ${GREEN}läuft${NC}" \
|| echo -e "$svc: ${RED}gestoppt${NC}"
done
echo -e "\n📬 Postfix-Queue:"
postqueue -p | head -n 10
/usr/local/bin/init_blacklist.sh¶
Einmaliges Setup-Skript – initialisiert die ipset-Blacklist, lädt externe Blacklists, setzt GeoIP-Blocking und erstellt die iptables-Chain GEOIP_REJECT. Nur beim ersten Einrichten oder nach einem vollständigen Reset ausführen.
#!/bin/bash
IP_BLACKLIST=/etc/ip-blacklist.conf
IP_BLACKLIST_TMP=/tmp/ip-blacklist.tmp
IP_BLACKLIST_LOCAL=/etc/ip-blacklist-local.conf
GEOIPLIST="/usr/local/bin/geoip.lst"
BLACKLISTS=(
"http://check.torproject.org/cgi-bin/TorBulkExitList.py?ip=1.1.1.1"
"http://rules.emergingthreats.net/blockrules/rbn-ips.txt"
"http://www.spamhaus.org/drop/drop.lasso"
"http://cinsscore.com/list/ci-badguys.txt"
"http://lists.blocklist.de/lists/all.txt"
)
for url in "${BLACKLISTS[@]}"; do
curl -s "$url" | grep -Po '(?:\d{1,3}\.){3}\d{1,3}(?:/\d{1,2})?' >> $IP_BLACKLIST_TMP
done
iptables -N GEOIP_REJECT
while read -r line; do
[ "$line" =~ ^#.*$ ]( "$line" =~ ^#.*$ .md) && continue
blockcode=${line%#*}
iptables -I GEOIP_REJECT -m geoip --src-cc $blockcode -j DROP
done < "$GEOIPLIST"
iptables -A INPUT -j GEOIP_REJECT
/usr/local/bin/cpfail2ban2bl.sh
cat $IP_BLACKLIST_LOCAL >> $IP_BLACKLIST_TMP
sort -n -t . -k1,1 -k2,2 -k3,3 -k4,4 $IP_BLACKLIST_TMP | uniq > $IP_BLACKLIST
rm $IP_BLACKLIST_TMP
ipset create blacklist hash:net -exist
ipset flush blacklist
iptables -A INPUT -m set --match-set blacklist src -j DROP
egrep -v "^#|^$" $IP_BLACKLIST | while IFS= read -r ip; do
ipset add blacklist $ip
done
Voraussetzung:
iptables-mod-geoipundxt_geoip-Daten müssen installiert sein. GeoIP-Daten mitupdate_xt_geoip.shaktualisieren.
/usr/local/bin/update-blacklist.sh¶
Aktualisiert die laufende Blacklist ohne Reset (Cronjob, täglich empfohlen).
#!/bin/bash
IP_BLACKLIST=/etc/ip-blacklist.conf
IP_BLACKLIST_TMP=/tmp/ip-blacklist.tmp
IP_BLACKLIST_LOCAL=/etc/ip-blacklist-local.conf
GEOIPLIST="/usr/local/bin/geoip.lst"
BLACKLISTS=(
"http://check.torproject.org/cgi-bin/TorBulkExitList.py?ip=1.1.1.1"
"http://rules.emergingthreats.net/blockrules/rbn-ips.txt"
"http://www.spamhaus.org/drop/drop.lasso"
"http://cinsscore.com/list/ci-badguys.txt"
"http://lists.blocklist.de/lists/all.txt"
)
for url in "${BLACKLISTS[@]}"; do
curl -s "$url" | grep -Po '(?:\d{1,3}\.){3}\d{1,3}(?:/\d{1,2})?' >> $IP_BLACKLIST_TMP
done
iptables -F GEOIP_REJECT
while read -r line; do
[ "$line" =~ ^#.*$ ]( "$line" =~ ^#.*$ .md) && continue
blockcode=${line%#*}
iptables -I GEOIP_REJECT -m geoip --src-cc $blockcode -j DROP
done < "$GEOIPLIST"
/usr/local/bin/cpfail2ban2bl.sh
cat $IP_BLACKLIST_LOCAL >> $IP_BLACKLIST_TMP
sort -n -t . -k1,1 -k2,2 -k3,3 -k4,4 $IP_BLACKLIST_TMP | uniq > $IP_BLACKLIST
rm $IP_BLACKLIST_TMP
ipset flush blacklist
egrep -v "^#|^$" $IP_BLACKLIST | while IFS= read -r ip; do
ipset add blacklist $ip
done
Empfohlener Cronjob (/etc/cron.d/update-blacklist):
/usr/local/bin/cpfail2ban2bl.sh¶
Abhängigkeit von init_blacklist.sh und update-blacklist.sh. Überträgt gebannte IPs aus dem Fail2Ban-Log in die lokale Blacklist.
#!/bin/bash
grep "Ban" /var/log/fail2ban.log \
| grep -Po '(?:\d{1,3}\.){3}\d{1,3}(?:/\d{1,2})?' \
>> /etc/ip-blacklist-local.conf
sort -n -t . -k1,1 -k2,2 -k3,3 -k4,4 /etc/ip-blacklist-local.conf \
| uniq > /etc/ip-blacklist-local.tmp \
&& mv /etc/ip-blacklist-local.tmp /etc/ip-blacklist-local.conf
/usr/local/bin/update_xt_geoip.sh¶
Aktualisiert die MaxMind GeoIP-Datenbank für xt_geoip. Benötigt einen gültigen MaxMind-Lizenzkey.
#!/bin/bash
cd /usr/share/xt_geoip
URL="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country-CSV&license_key={{SECRET_MAXMIND_KEY}}&suffix=zip"
wget "$URL" -O geoip.zip
unzip -o geoip.zip
dir=$(ls -d */)
cd "$dir"
/usr/libexec/xtables-addons/xt_geoip_build_maxmind -D /usr/share/xt_geoip *.csv
cd ..
modprobe xt_geoip
MaxMind-Lizenzkey in
/etc/environmentoder als Variable in00_variablen.mdunter{{SECRET_MAXMIND_KEY}}dokumentieren. Kostenloser Account auf maxmind.com.
/usr/local/bin/geoip.lst¶
Länderliste für GeoIP-Blocking. Länder ohne # am Anfang werden geblockt. Erlaubte Länder (DE, AT, CH, US etc.) sind auskommentiert.
# Auskommentiert = erlaubt:
# DE # Germany
# AT # Austria
# CH # Switzerland
# US # United States
# ...
# Aktiv = geblockt (Beispiele):
CN # China
RU # Russia
KP # North Korea
Die vollständige Liste liegt auf dem Heimserver unter
/usr/local/bin/geoip.lst.
Historisch: dovecot-lda-wrapper.sh¶
Dieses Skript wurde früher als Wrapper für die LDA-Zustellung verwendet. Es ist durch die direkte Pipe-Konfiguration in Postfix
master.cf(dovecot unix - n n - - pipe flags=DRhu user=vmail:vmail ...) ersetzt worden und wird nicht mehr benötigt.
Platzhalter-Referenz¶
| Platzhalter | Bedeutung |
|---|---|
{{DOMAIN}} |
Primäre Domain |
{{RELAY_HOSTNAME}} |
FQDN Relay-Server |
{{RELAY_IP}} |
Statische IP des Relay-Servers |
{{HOME_SMTP}} |
FQDN Heimserver Submission |
{{HOME_IMAP}} |
FQDN Heimserver IMAP |
{{HOME_HOSTNAME}} |
Interner Hostname des Heimservers |
{{DKIM_SELECTOR}} |
DKIM-Selektor |
{{ADMIN_MAIL}} |
Administrative Mailadresse |
{{SECRET_DB_PASSWORD}} |
Passwort des PostfixAdmin DB-Users |
{{SECRET_RELAY_SASL_PASSWORD}} |
SASL-Passwort für Heimserver → Relay |
{{SECRET_RSPAMD_CONTROLLER_PASSWORD}} |
Passwort für Rspamd Controller |
{{SECRET_MAXMIND_KEY}} |
MaxMind GeoLite2 Lizenzkey |
Alle Platzhalter sind in Variablen und Platzhalter definiert.